feat: discovery tops for empty GET /v1/search. Refs EventHub/EventHubBack#50
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,7 +24,7 @@ trails() ->
|
|||||||
#{
|
#{
|
||||||
path => <<"/v1/search">>,
|
path => <<"/v1/search">>,
|
||||||
method => <<"GET">>,
|
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">>],
|
tags => [<<"Search">>],
|
||||||
parameters => [
|
parameters => [
|
||||||
#{name => <<"type">>, in => <<"query">>, schema => #{type => string, enum => [<<"calendar">>, <<"event">>]}, description => <<"Type of entities to search">>},
|
#{name => <<"type">>, in => <<"query">>, schema => #{type => string, enum => [<<"calendar">>, <<"event">>]}, description => <<"Type of entities to search">>},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
%% ─────────────────────────────────────────────────────────────────
|
%% ─────────────────────────────────────────────────────────────────
|
||||||
-define(DEFAULT_LIMIT, 20).
|
-define(DEFAULT_LIMIT, 20).
|
||||||
-define(MAX_LIMIT, 100).
|
-define(MAX_LIMIT, 100).
|
||||||
|
-define(DISCOVERY_FETCH, 200).
|
||||||
-define(EARTH_RADIUS_KM, 6371.0).
|
-define(EARTH_RADIUS_KM, 6371.0).
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
@@ -36,6 +37,14 @@
|
|||||||
search(Type, Query, UserId, Params) ->
|
search(Type, Query, UserId, Params) ->
|
||||||
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
|
Limit = min(maps:get(limit, Params, ?DEFAULT_LIMIT), ?MAX_LIMIT),
|
||||||
Offset = maps:get(offset, Params, 0),
|
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
|
case Type of
|
||||||
<<"event">> ->
|
<<"event">> ->
|
||||||
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
|
{ok, Total, Events} = search_events(Query, UserId, Params, Limit, Offset),
|
||||||
@@ -52,6 +61,58 @@ search(Type, Query, UserId, Params) ->
|
|||||||
}}
|
}}
|
||||||
end.
|
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,
|
-spec search_events(Query :: binary() | undefined,
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ logic_search_test_() ->
|
|||||||
{"Pagination", fun test_pagination/0},
|
{"Pagination", fun test_pagination/0},
|
||||||
{"Sorting", fun test_sorting/0},
|
{"Sorting", fun test_sorting/0},
|
||||||
{"Access control in search", fun test_access_control/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, #{})),
|
{Total, Results} = events_from(logic_search:search(<<"event">>, <<"nonexistent">>, OwnerId, #{})),
|
||||||
?assertEqual(0, Total),
|
?assertEqual(0, Total),
|
||||||
?assertEqual([], Results).
|
?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).
|
||||||
|
|||||||
Reference in New Issue
Block a user