blob: 902d5c81318f3678f7d9993ce140a13d02c57ac6 [file] [log] [blame]
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
-module(nouveau_httpd).
-include_lib("couch/include/couch_db.hrl").
-export([
handle_analyze_req/1,
handle_search_req/3,
handle_info_req/3,
handle_cleanup_req/2
]).
-import(chttpd, [
send_method_not_allowed/2,
send_json/2, send_json/3,
send_error/2
]).
-define(RETRY_LIMIT, 20).
-define(RETRY_SLEEP, 500).
handle_analyze_req(#httpd{method = 'POST'} = Req) ->
check_if_enabled(),
couch_httpd:validate_ctype(Req, "application/json"),
{Fields} = chttpd:json_body_obj(Req),
LuceneMajor = couch_util:get_value(<<"lucene_major">>, Fields),
Analyzer = couch_util:get_value(<<"analyzer">>, Fields),
Text = couch_util:get_value(<<"text">>, Fields),
case nouveau_api:analyze(LuceneMajor, Text, Analyzer) of
{ok, Tokens} ->
send_json(Req, 200, {[{<<"tokens">>, Tokens}]});
{error, Reason} ->
send_error(Req, Reason)
end;
handle_analyze_req(Req) ->
send_method_not_allowed(Req, "POST").
handle_search_req(Req, Db, DDoc) ->
check_if_enabled(),
couch_stats:increment_counter([nouveau, active_searches]),
T0 = erlang:monotonic_time(),
try
handle_search_req_int(Req, Db, DDoc)
after
T1 = erlang:monotonic_time(),
couch_stats:decrement_counter([nouveau, active_searches]),
RequestTime = erlang:convert_time_unit(T1 - T0, native, millisecond),
couch_stats:update_histogram([nouveau, search_latency], RequestTime)
end.
handle_search_req_int(#httpd{method = 'GET', path_parts = [_, _, _, _, IndexName]} = Req, Db, DDoc) ->
DbName = couch_db:name(Db),
QueryArgs = #{
query => ?l2b(chttpd:qs_value(Req, "q")),
limit => list_to_integer(chttpd:qs_value(Req, "limit", "25")),
sort => ?JSON_DECODE(chttpd:qs_value(Req, "sort", "null")),
ranges => ?JSON_DECODE(chttpd:qs_value(Req, "ranges", "null")),
counts => ?JSON_DECODE(chttpd:qs_value(Req, "counts", "null")),
update => ?JSON_DECODE(chttpd:qs_value(Req, "update", "true")),
bookmark => chttpd:qs_value(Req, "bookmark"),
include_docs => parse_bool_param(
"include_docs", chttpd:qs_value(Req, "include_docs", "false")
)
},
handle_search_req(Req, DbName, DDoc, IndexName, QueryArgs, ?RETRY_LIMIT);
handle_search_req_int(#httpd{method = 'POST', path_parts = [_, _, _, _, IndexName]} = Req, Db, DDoc) ->
couch_httpd:validate_ctype(Req, "application/json"),
DbName = couch_db:name(Db),
ReqBody = chttpd:json_body(Req, [return_maps]),
QueryArgs = #{
query => maps:get(<<"q">>, ReqBody, undefined),
limit => maps:get(<<"limit">>, ReqBody, 25),
sort => maps:get(<<"sort">>, ReqBody, null),
ranges => maps:get(<<"ranges">>, ReqBody, null),
counts => maps:get(<<"counts">>, ReqBody, null),
update => maps:get(<<"update">>, ReqBody, true),
bookmark => maps:get(<<"bookmark">>, ReqBody, undefined),
include_docs => maps:get(<<"include_docs">>, ReqBody, false)
},
handle_search_req(Req, DbName, DDoc, IndexName, QueryArgs, ?RETRY_LIMIT);
handle_search_req_int(Req, _Db, _DDoc) ->
send_method_not_allowed(Req, "GET, POST").
handle_search_req(#httpd{} = Req, DbName, DDoc, IndexName, QueryArgs, Retry) ->
IncludeDocs = maps:get(include_docs, QueryArgs, false),
case nouveau_fabric_search:go(DbName, DDoc, IndexName, QueryArgs) of
{ok, SearchResults} ->
RespBody = #{
<<"bookmark">> => nouveau_bookmark:pack(maps:get(bookmark, SearchResults)),
<<"total_hits">> => maps:get(<<"total_hits">>, SearchResults),
<<"total_hits_relation">> => maps:get(<<"total_hits_relation">>, SearchResults),
<<"hits">> => include_docs(
DbName, maps:get(<<"hits">>, SearchResults), IncludeDocs
),
<<"counts">> => maps:get(<<"counts">>, SearchResults, null),
<<"ranges">> => maps:get(<<"ranges">>, SearchResults, null)
},
HitCount = length(maps:get(<<"hits">>, RespBody)),
incr_stats(HitCount, IncludeDocs),
send_json(Req, 200, RespBody);
{error, {service_unavailable, _}} when Retry > 1 ->
timer:sleep(?RETRY_SLEEP),
handle_search_req(Req, DbName, DDoc, IndexName, QueryArgs, Retry - 1);
{error, Reason} ->
send_error(Req, Reason)
end.
handle_info_req(
#httpd{method = 'GET', path_parts = [_, _, _, _, IndexName]} = Req,
Db,
#doc{id = Id} = DDoc
) ->
check_if_enabled(),
DbName = couch_db:name(Db),
case nouveau_fabric_info:go(DbName, DDoc, IndexName) of
{ok, IndexInfo} ->
send_json(
Req,
200,
{[
{name, <<Id/binary, "/", IndexName/binary>>},
{search_index, IndexInfo}
]}
);
{error, Reason} ->
send_error(Req, Reason)
end;
handle_info_req(#httpd{path_parts = [_, _, _, _, _]} = Req, _Db, _DDoc) ->
check_if_enabled(),
send_method_not_allowed(Req, "GET");
handle_info_req(Req, _Db, _DDoc) ->
check_if_enabled(),
send_error(Req, {bad_request, "path not recognized"}).
handle_cleanup_req(#httpd{method = 'POST'} = Req, Db) ->
couch_httpd:validate_ctype(Req, "application/json"),
ok = nouveau_fabric_cleanup:go(couch_db:name(Db)),
send_json(Req, 202, {[{ok, true}]});
handle_cleanup_req(Req, _Db) ->
send_method_not_allowed(Req, "POST").
include_docs(_DbName, Hits, false) ->
Hits;
include_docs(DbName, Hits, true) ->
Ids = [maps:get(<<"id">>, Hit) || Hit <- Hits],
{ok, Docs} = nouveau_fabric:get_json_docs(DbName, Ids),
lists:zipwith(fun(Hit, Doc) -> Hit#{<<"doc">> => Doc} end, Hits, Docs).
incr_stats(HitCount, false) ->
chttpd_stats:incr_rows(HitCount);
incr_stats(HitCount, true) ->
chttpd_stats:incr_reads(HitCount),
incr_stats(HitCount, false).
parse_bool_param(_, Val) when is_boolean(Val) ->
Val;
parse_bool_param(_, "true") ->
true;
parse_bool_param(_, "false") ->
false;
parse_bool_param(Name, Val) ->
Msg = io_lib:format("Invalid value for ~s: ~p", [Name, Val]),
throw({query_parse_error, ?l2b(Msg)}).
check_if_enabled() ->
case nouveau:enabled() of
true ->
ok;
false ->
throw(not_found)
end.