@@ -599,6 +599,111 @@ void SearchReply(const SearchParams& params, std::optional<search::AggregationIn
599
599
}
600
600
}
601
601
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
+
602
707
} // namespace
603
708
604
709
void SearchFamily::FtCreate (CmdArgList args, const CommandContext& cmd_cntx) {
@@ -815,6 +920,13 @@ void SearchFamily::FtSearch(CmdArgList args, const CommandContext& cmd_cntx) {
815
920
if (!search_algo.Init (query_str, ¶ms->query_params , sort_opt))
816
921
return builder->SendError (" Query syntax error" );
817
922
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
+
818
930
// Because our coordinator thread may not have a shard, we can't check ahead if the index exists.
819
931
atomic<bool > index_not_found{false };
820
932
vector<SearchResult> docs (shard_set->size ());
@@ -966,6 +1078,31 @@ void SearchFamily::FtProfile(CmdArgList args, const CommandContext& cmd_cntx) {
966
1078
}
967
1079
}
968
1080
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
+
969
1106
void SearchFamily::FtTagVals (CmdArgList args, const CommandContext& cmd_cntx) {
970
1107
string_view index_name = ArgS (args, 0 );
971
1108
string_view field_name = ArgS (args, 1 );
@@ -1052,11 +1189,24 @@ void SearchFamily::FtAggregate(CmdArgList args, const CommandContext& cmd_cntx)
1052
1189
auto * rb = static_cast <RedisReplyBuilder*>(cmd_cntx.rb );
1053
1190
auto sortable_value_sender = SortableValueSender (rb);
1054
1191
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
+
1056
1201
rb->StartArray (result_size + 1 );
1057
1202
rb->SendLong (result_size);
1058
1203
1059
1204
for (const auto & value : agg_results.values ) {
1205
+ if (result_size == 0 ) {
1206
+ break ;
1207
+ }
1208
+ result_size--;
1209
+
1060
1210
size_t fields_count = 0 ;
1061
1211
for (const auto & field : agg_results.fields_to_print ) {
1062
1212
if (value.find (field) != value.end ()) {
@@ -1089,18 +1239,19 @@ void SearchFamily::Register(CommandRegistry* registry) {
1089
1239
CO::NO_KEY_TRANSACTIONAL | CO::NO_KEY_TX_SPAN_ALL | CO::NO_AUTOJOURNAL;
1090
1240
1091
1241
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);
1104
1255
}
1105
1256
1106
1257
} // namespace dfly
0 commit comments