Skip to content

Commit fee09ba

Browse files
feat(search_family): Add basic support for the FT.CONFIG
fixes dragonflydb#4352 Signed-off-by: Stepan Bagritsevich <[email protected]>
1 parent 753c25e commit fee09ba

File tree

3 files changed

+249
-15
lines changed

3 files changed

+249
-15
lines changed

src/server/search/search_family.cc

+164-13
Original file line numberDiff line numberDiff line change
@@ -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

604709
void 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, &params->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+
9691106
void 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

src/server/search/search_family.h

+13-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ class CommandRegistry;
1818
struct CommandContext;
1919

2020
class SearchFamily {
21+
public:
22+
static void Register(CommandRegistry* registry);
23+
24+
static inline constexpr size_t kOptionsCount = 2;
25+
26+
template <typename V>
27+
using ConfigOptionsMap = std::array<std::pair<std::string_view, V>, kOptionsCount>;
28+
using Config = ConfigOptionsMap<uint64_t>;
29+
30+
private:
2131
using SinkReplyBuilder = facade::SinkReplyBuilder;
2232

2333
static void FtCreate(CmdArgList args, const CommandContext& cmd_cntx);
@@ -27,11 +37,12 @@ class SearchFamily {
2737
static void FtList(CmdArgList args, const CommandContext& cmd_cntx);
2838
static void FtSearch(CmdArgList args, const CommandContext& cmd_cntx);
2939
static void FtProfile(CmdArgList args, const CommandContext& cmd_cntx);
40+
static void FtConfig(CmdArgList args, const CommandContext& cmd_cntx);
3041
static void FtAggregate(CmdArgList args, const CommandContext& cmd_cntx);
3142
static void FtTagVals(CmdArgList args, const CommandContext& cmd_cntx);
3243

33-
public:
34-
static void Register(CommandRegistry* registry);
44+
static util::fb2::Mutex config_mu_;
45+
static Config config_; // guarded by config_mu_
3546
};
3647

3748
} // namespace dfly

src/server/search/search_family_test.cc

+72
Original file line numberDiff line numberDiff line change
@@ -2014,4 +2014,76 @@ TEST_F(SearchFamilyTest, InvalidCreateOptions) {
20142014
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
20152015
}
20162016

2017+
TEST_F(SearchFamilyTest, ConfigMaxSearchResults) {
2018+
Run({"HSET", "doc1", "title", "hello world1"});
2019+
Run({"HSET", "doc2", "title", "hello world2"});
2020+
Run({"FT.CREATE", "index", "ON", "HASH", "SCHEMA", "title", "TEXT"});
2021+
2022+
auto resp = Run({"FT.SEARCH", "index", "*", "NOCONTENT"});
2023+
EXPECT_THAT(resp, IsUnordArray(IntArg(2), "doc1", "doc2"));
2024+
2025+
resp = Run({"FT.CONFIG", "GET", "MAXSEARCHRESULTS"});
2026+
EXPECT_THAT(resp, IsArray("MAXSEARCHRESULTS", IntArg(10000)));
2027+
2028+
resp = Run({"FT.CONFIG", "SET", "MAXSEARCHRESULTS", "1"});
2029+
EXPECT_EQ(resp, "OK");
2030+
2031+
resp = Run({"FT.SEARCH", "index", "*", "NOCONTENT"});
2032+
EXPECT_THAT(resp, IsUnordArray(IntArg(2), AnyOf("doc1", "doc2")));
2033+
2034+
resp = Run({"FT.CONFIG", "GET", "MAXSEARCHRESULTS"});
2035+
EXPECT_THAT(resp, IsArray("MAXSEARCHRESULTS", IntArg(1)));
2036+
}
2037+
2038+
TEST_F(SearchFamilyTest, ConfigMaxAggregateResult) {
2039+
Run({"HSET", "doc1", "title", "hello world1"});
2040+
Run({"HSET", "doc2", "title", "hello world2"});
2041+
Run({"FT.CREATE", "index", "ON", "HASH", "SCHEMA", "title", "TEXT"});
2042+
2043+
auto resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "@title", "REDUCE", "COUNT", "0",
2044+
"AS", "count"});
2045+
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("title", "hello world1", "count", "1"),
2046+
IsMap("title", "hello world2", "count", "1")));
2047+
2048+
resp = Run({"FT.CONFIG", "GET", "MAXAGGREGATERESULTS"});
2049+
EXPECT_THAT(resp, IsArray("MAXAGGREGATERESULTS", IntArg(10000)));
2050+
2051+
resp = Run({"FT.CONFIG", "SET", "MAXAGGREGATERESULTS", "1"});
2052+
EXPECT_EQ(resp, "OK");
2053+
2054+
resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "@title", "REDUCE", "COUNT", "0", "AS",
2055+
"count"});
2056+
EXPECT_THAT(resp, IsUnordArrayWithSize(AnyOf(IsMap("title", "hello world1", "count", "1"),
2057+
IsMap("title", "hello world2", "count", "1"))));
2058+
2059+
resp = Run({"FT.CONFIG", "GET", "MAXAGGREGATERESULTS"});
2060+
EXPECT_THAT(resp, IsArray("MAXAGGREGATERESULTS", IntArg(1)));
2061+
}
2062+
2063+
TEST_F(SearchFamilyTest, InvalidConfigOptions) {
2064+
// Test with an invalid argument
2065+
auto resp = Run({"FT.CONFIG", "INVALIDARG", "INVLIDARG"});
2066+
EXPECT_THAT(resp, ErrArg("Unknown subcommand"));
2067+
2068+
// Test with an invalid argument
2069+
resp = Run({"FT.CONFIG", "GET", "INVALIDARG"});
2070+
EXPECT_THAT(resp, IsArray());
2071+
2072+
// Test with an invalid argument
2073+
resp = Run({"FT.CONFIG", "SET", "INVALIDARG"});
2074+
EXPECT_THAT(resp, ErrArg(kSyntaxErr));
2075+
2076+
// Test with an invalid argument
2077+
resp = Run({"FT.CONFIG", "SET", "INVALIDARG", "5"});
2078+
EXPECT_THAT(resp, ErrArg("Invalid option"));
2079+
2080+
// Test with an invalid value
2081+
resp = Run({"FT.CONFIG", "SET", "MAXSEARCHRESULTS", "not_a_number"});
2082+
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
2083+
2084+
// Test with an invalid argument
2085+
resp = Run({"FT.CONFIG", "HELP", "INVALIDARG"});
2086+
EXPECT_THAT(resp, IsArray());
2087+
}
2088+
20172089
} // namespace dfly

0 commit comments

Comments
 (0)