@@ -599,6 +599,111 @@ void SearchReply(const SearchParams& params, std::optional<search::AggregationIn
599599 }
600600}
601601
602+ constexpr std::string_view kSearchLimit = " MAXSEARCHRESULTS" sv;
603+ constexpr std::string_view kAggregateLimit = " MAXAGGREGATERESULTS" sv;
604+
605+ // Do not forget to update SearchFamily::config_values_ after adding new option
606+ constexpr SearchFamily::ConfigOptionsMap<std::string_view> kConfigOptionsHelp = {{
607+ {kSearchLimit , " Maximum number of results from ft.search command" sv},
608+ {kAggregateLimit , " Maximum number of results from ft.aggregate command" sv},
609+ }};
610+
611+ template <typename V>
612+ std::optional<V> FindOptionsMapValue (const SearchFamily::ConfigOptionsMap<V>& options,
613+ std::string_view name) {
614+ auto it = std::find_if (options.begin (), options.end (), [name](const auto & opt) {
615+ return absl::EqualsIgnoreCase (opt.first , name);
616+ });
617+ if (it != options.end ()) {
618+ return it->second ;
619+ }
620+ return std::nullopt ;
621+ }
622+
623+ void FtConfigHelp (CmdArgParser* parser, RedisReplyBuilder* rb, SearchFamily::Config* config) {
624+ string_view option = parser->Next ();
625+
626+ auto send_value = [&](string_view option_name) {
627+ auto value = FindOptionsMapValue (*config, option_name);
628+ DCHECK (value.has_value ());
629+ rb->SendLong (value.value ());
630+ };
631+
632+ if (option == " *" sv) {
633+ rb->StartArray (kConfigOptionsHelp .size ());
634+ for (const auto & option_help : kConfigOptionsHelp ) {
635+ rb->StartArray (5 );
636+ rb->SendBulkString (option_help.first );
637+ rb->SendBulkString (" Description" sv);
638+ rb->SendBulkString (option_help.second );
639+ rb->SendBulkString (" Value" sv);
640+ send_value (option_help.first );
641+ }
642+ return ;
643+ }
644+
645+ auto option_description = FindOptionsMapValue (kConfigOptionsHelp , option);
646+ if (option_description) {
647+ rb->StartArray (1 );
648+ rb->StartArray (5 );
649+ rb->SendBulkString (absl::AsciiStrToUpper (option));
650+ rb->SendBulkString (" Description" sv);
651+ rb->SendBulkString (option_description.value ());
652+ rb->SendBulkString (" Value" sv);
653+ send_value (option);
654+ return ;
655+ }
656+
657+ LOG (WARNING) << " Unknown configuration option: " << option;
658+ rb->SendEmptyArray ();
659+ }
660+
661+ void FtConfigGet (CmdArgParser* parser, RedisReplyBuilder* rb, SearchFamily::Config* config) {
662+ string_view option = parser->Next ();
663+
664+ if (option == " *" sv) {
665+ rb->StartArray (config->size ());
666+ for (const auto & option_help : *config) {
667+ rb->StartArray (2 );
668+ rb->SendBulkString (option_help.first );
669+ rb->SendLong (option_help.second );
670+ }
671+ return ;
672+ }
673+
674+ auto option_value = FindOptionsMapValue (*config, option);
675+ if (option_value) {
676+ rb->StartArray (1 );
677+ rb->StartArray (2 );
678+ rb->SendBulkString (absl::AsciiStrToUpper (option));
679+ rb->SendLong (option_value.value ());
680+ return ;
681+ }
682+
683+ LOG (WARNING) << " Unknown configuration option: " << option;
684+ rb->SendEmptyArray ();
685+ }
686+
687+ void FtConfigSet (CmdArgParser* parser, RedisReplyBuilder* rb, SearchFamily::Config* config) {
688+ string_view option = parser->Next ();
689+ uint64_t value = parser->Next <uint64_t >();
690+
691+ if (auto err = parser->Error (); err)
692+ return rb->SendError (err->MakeReply ());
693+
694+ auto it = std::find_if (config->begin (), config->end (), [option_name = option](const auto & opt) {
695+ return absl::EqualsIgnoreCase (opt.first , option_name);
696+ });
697+
698+ if (it != config->end ()) {
699+ LOG (INFO) << " Setting " << option << " to " << value;
700+ it->second = value;
701+ rb->SendOk ();
702+ } else {
703+ rb->SendError (" Invalid option" sv);
704+ }
705+ }
706+
602707} // namespace
603708
604709void SearchFamily::FtCreate (CmdArgList args, const CommandContext& cmd_cntx) {
@@ -815,6 +920,13 @@ void SearchFamily::FtSearch(CmdArgList args, const CommandContext& cmd_cntx) {
815920 if (!search_algo.Init (query_str, ¶ms->query_params , sort_opt))
816921 return builder->SendError (" Query syntax error" );
817922
923+ {
924+ util::fb2::LockGuard lg{config_mu_};
925+ auto search_limit = FindOptionsMapValue (config_, kSearchLimit );
926+ DCHECK (search_limit.has_value ());
927+ params->limit_total = std::min (params->limit_total , search_limit.value ());
928+ }
929+
818930 // Because our coordinator thread may not have a shard, we can't check ahead if the index exists.
819931 atomic<bool > index_not_found{false };
820932 vector<SearchResult> docs (shard_set->size ());
@@ -966,6 +1078,31 @@ void SearchFamily::FtProfile(CmdArgList args, const CommandContext& cmd_cntx) {
9661078 }
9671079}
9681080
1081+ // Do not forget to update kConfigOptionsHelp after adding new option
1082+ SearchFamily::Config SearchFamily::config_ = {{
1083+ {kSearchLimit , 10000 },
1084+ {kAggregateLimit , 10000 },
1085+ }};
1086+
1087+ util::fb2::Mutex SearchFamily::config_mu_;
1088+
1089+ void SearchFamily::FtConfig (CmdArgList args, const CommandContext& cmd_cntx) {
1090+ CmdArgParser parser{args};
1091+ auto * rb = static_cast <RedisReplyBuilder*>(cmd_cntx.rb );
1092+
1093+ auto func = parser.TryMapNext (" GET" , &FtConfigGet, " SET" , &FtConfigSet, " HELP" , &FtConfigHelp);
1094+
1095+ if (func) {
1096+ util::fb2::LockGuard lg{config_mu_};
1097+ return func.value ()(&parser, rb, &config_);
1098+ } else {
1099+ return rb->SendError (" Unknown subcommand" );
1100+ }
1101+
1102+ static_assert (config_.size () == kConfigOptionsHelp .size (),
1103+ " SearchFamily::config_values_ and kConfigOptionsHelp must have the same size." );
1104+ }
1105+
9691106void SearchFamily::FtTagVals (CmdArgList args, const CommandContext& cmd_cntx) {
9701107 string_view index_name = ArgS (args, 0 );
9711108 string_view field_name = ArgS (args, 1 );
@@ -1052,11 +1189,24 @@ void SearchFamily::FtAggregate(CmdArgList args, const CommandContext& cmd_cntx)
10521189 auto * rb = static_cast <RedisReplyBuilder*>(cmd_cntx.rb );
10531190 auto sortable_value_sender = SortableValueSender (rb);
10541191
1055- const size_t result_size = agg_results.values .size ();
1192+ size_t result_size = agg_results.values .size ();
1193+
1194+ {
1195+ util::fb2::LockGuard lg{config_mu_};
1196+ auto aggregate_limit = FindOptionsMapValue (config_, kAggregateLimit );
1197+ DCHECK (aggregate_limit.has_value ());
1198+ result_size = std::min (result_size, aggregate_limit.value ());
1199+ }
1200+
10561201 rb->StartArray (result_size + 1 );
10571202 rb->SendLong (result_size);
10581203
10591204 for (const auto & value : agg_results.values ) {
1205+ if (result_size == 0 ) {
1206+ break ;
1207+ }
1208+ result_size--;
1209+
10601210 size_t fields_count = 0 ;
10611211 for (const auto & field : agg_results.fields_to_print ) {
10621212 if (value.find (field) != value.end ()) {
@@ -1089,18 +1239,19 @@ void SearchFamily::Register(CommandRegistry* registry) {
10891239 CO::NO_KEY_TRANSACTIONAL | CO::NO_KEY_TX_SPAN_ALL | CO::NO_AUTOJOURNAL;
10901240
10911241 registry->StartFamily ();
1092- *registry << CI{" FT.CREATE" , CO::WRITE | CO::GLOBAL_TRANS, -2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (
1093- FtCreate)
1094- << CI{" FT.ALTER" , CO::WRITE | CO::GLOBAL_TRANS, -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtAlter)
1095- << CI{" FT.DROPINDEX" , CO::WRITE | CO::GLOBAL_TRANS, -2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (
1096- FtDropIndex)
1097- << CI{" FT.INFO" , kReadOnlyMask , 2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtInfo)
1098- // Underscore same as in RediSearch because it's "temporary" (long time already)
1099- << CI{" FT._LIST" , kReadOnlyMask , 1 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtList)
1100- << CI{" FT.SEARCH" , kReadOnlyMask , -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtSearch)
1101- << CI{" FT.AGGREGATE" , kReadOnlyMask , -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtAggregate)
1102- << CI{" FT.PROFILE" , kReadOnlyMask , -4 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtProfile)
1103- << CI{" FT.TAGVALS" , kReadOnlyMask , 3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtTagVals);
1242+ *registry
1243+ << CI{" FT.CREATE" , CO::WRITE | CO::GLOBAL_TRANS, -2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtCreate)
1244+ << CI{" FT.ALTER" , CO::WRITE | CO::GLOBAL_TRANS, -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtAlter)
1245+ << CI{" FT.DROPINDEX" , CO::WRITE | CO::GLOBAL_TRANS, -2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (
1246+ FtDropIndex)
1247+ << CI{" FT.CONFIG" , CO::WRITE | CO::GLOBAL_TRANS, -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtConfig)
1248+ << CI{" FT.INFO" , kReadOnlyMask , 2 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtInfo)
1249+ // Underscore same as in RediSearch because it's "temporary" (long time already)
1250+ << CI{" FT._LIST" , kReadOnlyMask , 1 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtList)
1251+ << CI{" FT.SEARCH" , kReadOnlyMask , -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtSearch)
1252+ << CI{" FT.AGGREGATE" , kReadOnlyMask , -3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtAggregate)
1253+ << CI{" FT.PROFILE" , kReadOnlyMask , -4 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtProfile)
1254+ << CI{" FT.TAGVALS" , kReadOnlyMask , 3 , 0 , 0 , acl::FT_SEARCH}.HFUNC (FtTagVals);
11041255}
11051256
11061257} // namespace dfly
0 commit comments