diff --git a/src/handlers/handler_search.erl b/src/handlers/handler_search.erl index 89c5ed2..022a6ce 100644 --- a/src/handlers/handler_search.erl +++ b/src/handlers/handler_search.erl @@ -24,7 +24,7 @@ trails() -> #{ path => <<"/v1/search">>, method => <<"GET">>, - description => <<"Search calendars and events">>, + description => <<"Search calendars and events. Empty query (auth only) returns discovery tops by rating; use q/tags/geo/from/to for filtered search.">>, tags => [<<"Search">>], parameters => [ #{name => <<"type">>, in => <<"query">>, schema => #{type => string, enum => [<<"calendar">>, <<"event">>]}, description => <<"Type of entities to search">>}, diff --git a/src/logic/logic_search.erl b/src/logic/logic_search.erl index cfa57b9..c50406f 100644 --- a/src/logic/logic_search.erl +++ b/src/logic/logic_search.erl @@ -16,6 +16,7 @@ %% ───────────────────────────────────────────────────────────────── -define(DEFAULT_LIMIT, 20). -define(MAX_LIMIT, 100). +-define(DISCOVERY_FETCH, 200). -define(EARTH_RADIUS_KM, 6371.0). %%%------------------------------------------------------------------- @@ -36,6 +37,14 @@ search(Type, Query, UserId, Params) -> Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT), Offset = maps:get(offset, Params, 0), + case is_discovery_request(Query, Params) of + true -> + discovery_search(Type, UserId, Params, Limit, Offset); + false -> + filtered_search(Type, Query, UserId, Params, Limit, Offset) + end. + +filtered_search(Type, Query, UserId, Params, Limit, Offset) -> case Type of <<"event">> -> {ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset), @@ -52,6 +61,58 @@ search(Type, Query, UserId, Params) -> }} end. +%% Пустой search (страница «Главная»): tops из stats_tops; иначе — полный scan. +is_discovery_request(Query, Params) -> + QueryEmpty = Query =:= undefined orelse Query =:= <<>>, + QueryEmpty andalso + not maps:is_key(tags, Params) andalso + not maps:is_key(from, Params) andalso + not maps:is_key(to, Params) andalso + not maps:is_key(lat, Params) andalso + not maps:is_key(lon, Params) andalso + not maps:is_key(sort, Params). + +discovery_search(Type, UserId, Params, Limit, Offset) -> + case Type of + <<"event">> -> + {ok, Total, Events} = discovery_events(UserId, Params, Limit, Offset), + {ok, Total, #{<<"events">> => Events}}; + <<"calendar">> -> + {ok, Total, Calendars} = discovery_calendars(UserId, Params, Limit, Offset), + {ok, Total, #{<<"calendars">> => Calendars}}; + _ -> + {ok, EventsTotal, Events} = discovery_events(UserId, Params, Limit, Offset), + {ok, CalendarsTotal, Calendars} = discovery_calendars(UserId, Params, Limit, Offset), + {ok, EventsTotal + CalendarsTotal, #{ + <<"events">> => Events, + <<"calendars">> => Calendars + }} + end. + +discovery_events(UserId, Params, Limit, Offset) -> + FetchN = max(Limit + Offset, ?DISCOVERY_FETCH), + Tops = core_event:get_top_events_by_rating(FetchN), + Accessible = filter_accessible_events(Tops, UserId), + case Accessible of + [] -> + search_events(undefined, UserId, Params, Limit, Offset); + Items -> + Total = length(Items), + {ok, Total, format_events(paginate(Items, Limit, Offset))} + end. + +discovery_calendars(UserId, Params, Limit, Offset) -> + FetchN = max(Limit + Offset, ?DISCOVERY_FETCH), + Tops = core_calendar:get_top_calendars_by_rating(FetchN), + Accessible = filter_accessible_calendars(Tops, UserId), + case Accessible of + [] -> + search_calendars(undefined, UserId, Params, Limit, Offset); + Items -> + Total = length(Items), + {ok, Total, format_calendars(paginate(Items, Limit, Offset))} + end. + %% ============ Поиск событий ============ -spec search_events(Query :: binary() | undefined, diff --git a/test/unit/logic_search_tests.erl b/test/unit/logic_search_tests.erl index 1a4a4a0..648e8cc 100644 --- a/test/unit/logic_search_tests.erl +++ b/test/unit/logic_search_tests.erl @@ -29,7 +29,9 @@ logic_search_test_() -> {"Pagination", fun test_pagination/0}, {"Sorting", fun test_sorting/0}, {"Access control in search", fun test_access_control/0}, - {"Empty search results", fun test_empty_search/0} + {"Empty search results", fun test_empty_search/0}, + {"Discovery tops without filters", fun test_discovery_returns_tops/0}, + {"Query switches to filtered search", fun test_discovery_with_q_uses_filter/0} ]}. %% Вспомогательные функции @@ -239,3 +241,34 @@ test_empty_search() -> {Total, Results} = events_from(logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{})), ?assertEqual(0, Total), ?assertEqual([], Results). + +test_discovery_returns_tops() -> + OwnerId = create_test_user(user), + ViewerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, commercial, []), + StartTime = eh_test_support:future_start(), + LowId = create_test_event(CalendarId, <<"Low Rated">>, <<"">>, StartTime, [], undefined), + HighId = create_test_event(CalendarId, <<"High Rated">>, <<"">>, StartTime, [], undefined), + {ok, _} = core_event:update(LowId, [{rating_avg, 1.0}, {rating_count, 1}]), + {ok, _} = core_event:update(HighId, [{rating_avg, 5.0}, {rating_count, 10}]), + stats_tops:init_tables(), + stats_tops:rebuild(), + + {Total, [First | _]} = events_from(logic_search:search(<<"event">>, undefined, ViewerId, #{})), + ?assertEqual(2, Total), + ?assertMatch(#{title := <<"High Rated">>}, First). + +test_discovery_with_q_uses_filter() -> + OwnerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, personal, []), + StartTime = eh_test_support:future_start(), + LowId = create_test_event(CalendarId, <<"Alpha">>, <<"">>, StartTime, [], undefined), + HighId = create_test_event(CalendarId, <<"Beta">>, <<"">>, StartTime, [], undefined), + {ok, _} = core_event:update(LowId, [{rating_avg, 1.0}]), + {ok, _} = core_event:update(HighId, [{rating_avg, 5.0}]), + stats_tops:init_tables(), + stats_tops:rebuild(), + + {Total, Results} = events_from(logic_search:search(<<"event">>, <<"Alpha">>, OwnerId, #{})), + ?assertEqual(1, Total), + ?assertMatch([#{title := <<"Alpha">>}], Results).