%%%------------------------------------------------------------------- %%% @doc Обработчик полнотекстового поиска (клиентский API). %%% %%% GET – выполняет поиск по календарям и событиям. %%% @end %%%------------------------------------------------------------------- -module(handler_search). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). %%% cowboy_handler callback -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, Opts) -> handle(Req, Opts). %%% Swagger metadata -spec trails() -> [map()]. trails() -> [ #{ path => <<"/v1/search">>, method => <<"GET">>, 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">>}, #{name => <<"q">>, in => <<"query">>, schema => #{type => string}, description => <<"Search query">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Maximum results per page">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset for pagination">>}, #{name => <<"tags">>, in => <<"query">>, schema => #{type => string}, description => <<"Comma-separated tags">>}, #{name => <<"sort">>, in => <<"query">>, schema => #{type => string, enum => [<<"start_time">>, <<"created_at">>, <<"title">>]}, description => <<"Field to sort by">>}, #{name => <<"order">>, in => <<"query">>, schema => #{type => string, enum => [<<"asc">>, <<"desc">>]}, description => <<"Sort order">>}, #{name => <<"lat">>, in => <<"query">>, schema => #{type => number, format => float}, description => <<"Latitude for geo search">>}, #{name => <<"lon">>, in => <<"query">>, schema => #{type => number, format => float}, description => <<"Longitude for geo search">>}, #{name => <<"radius">>, in => <<"query">>, schema => #{type => integer}, description => <<"Radius in km for geo search">>}, #{name => <<"from">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"Start datetime (ISO8601)">>}, #{name => <<"to">>, in => <<"query">>, schema => #{type => string, format => <<"date-time">>}, description => <<"End datetime (ISO8601)">>} ], responses => #{ 200 => #{ description => <<"Search results with pagination">>, content => #{<<"application/json">> => #{schema => #{ type => object, properties => #{ total => #{type => integer}, limit => #{type => integer}, offset => #{type => integer}, results => #{type => array, items => #{type => object}} } }}} }, 400 => #{description => <<"Invalid parameters">>}, 500 => #{description => <<"Search failed">>} } } ]. %%%=================================================================== %%% HTTP-методы %%%=================================================================== %% @private -spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> search(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %% @doc GET /v1/search — полнотекстовый поиск с фильтрами. -spec search(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. search(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> Qs = cowboy_req:parse_qs(Req1), Type = proplists:get_value(<<"type">>, Qs, undefined), Query = proplists:get_value(<<"q">>, Qs, undefined), Params = parse_params(Qs), case logic_search:search(Type, Query, UserId, Params) of {ok, Total, Results} -> Response = #{ total => Total, limit => maps:get(limit, Params, 20), offset => maps:get(offset, Params, 0), results => Results }, handler_utils:send_json(Req1, 200, Response); {error, _} -> handler_utils:send_error(Req1, 500, <<"Search failed">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %%%=================================================================== %%% Внутренние функции %%%=================================================================== %% @private Собирает карту параметров для поискового движка. %% Не кладёт sort/tags/geo/даты, если их нет в QS — иначе %% logic_search:is_discovery_request/2 никогда не сработает (Back#50). -spec parse_params(cowboy_req:qs()) -> map(). parse_params(Qs) -> Params0 = #{ limit => parse_int_param(Qs, <<"limit">>, 20), offset => parse_int_param(Qs, <<"offset">>, 0) }, Params1 = case proplists:get_value(<<"tags">>, Qs) of undefined -> Params0; <<>> -> Params0; Tags -> Params0#{tags => Tags} end, Params2 = case proplists:get_value(<<"sort">>, Qs) of undefined -> Params1; <<>> -> Params1; Sort -> Order = case proplists:get_value(<<"order">>, Qs) of undefined -> <<"asc">>; <<>> -> <<"asc">>; O -> O end, Params1#{sort => Sort, order => Order} end, Params3 = case {parse_float_param(Qs, <<"lat">>), parse_float_param(Qs, <<"lon">>)} of {{ok, Lat}, {ok, Lon}} -> Radius = parse_int_param(Qs, <<"radius">>, 10), Params2#{lat => Lat, lon => Lon, radius => Radius}; _ -> Params2 end, case {parse_datetime_param(Qs, <<"from">>), parse_datetime_param(Qs, <<"to">>)} of {{ok, From}, {ok, To}} -> Params3#{from => From, to => To}; {{ok, From}, error} -> Params3#{from => From}; {error, {ok, To}} -> Params3#{to => To}; _ -> Params3 end. -spec parse_int_param(cowboy_req:qs(), binary(), integer()) -> integer(). parse_int_param(Qs, Key, Default) -> handler_utils:parse_int_qs(proplists:get_value(Key, Qs), Default). -spec parse_float_param(cowboy_req:qs(), binary()) -> {ok, float()} | error. parse_float_param(Qs, Key) -> case proplists:get_value(Key, Qs) of undefined -> error; Val -> {ok, binary_to_float(Val)} end. -spec parse_datetime_param(cowboy_req:qs(), binary()) -> {ok, calendar:datetime()} | error. parse_datetime_param(Qs, Key) -> case proplists:get_value(Key, Qs) of undefined -> error; Val -> handler_utils:parse_datetime(Val) end.