fix(search_family): Fix LOAD fields parsing in the FT.AGGREGATE and FT.SEARCH commands (#4012)

* fix(search_family): Fix LOAD fields parsing in the FT.AGGREGATE and FT.SEARCH commands

fixes dragonflydb#3989

Signed-off-by: Stsiapan Bahrytsevich <stefan@dragonflydb.io>

* refactor: address comments

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>

* refactor(search_family): Address comments 2

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>

* refactor(search_family): address comments 3

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>

* refactor(search_family): address comments 4

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>

* refactor(search_family): address comments 5

Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>

---------

Signed-off-by: Stsiapan Bahrytsevich <stefan@dragonflydb.io>
Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>
This commit is contained in:
Stepan Bagritsevich 2024-11-25 13:50:31 +04:00 committed by GitHub
parent 3d68c9c99e
commit 2b3c182cc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 410 additions and 87 deletions

View file

@ -501,6 +501,12 @@ string_view Schema::LookupAlias(string_view alias) const {
return alias;
}
string_view Schema::LookupIdentifier(string_view identifier) const {
if (auto it = fields.find(identifier); it != fields.end())
return it->second.short_name;
return identifier;
}
IndicesOptions::IndicesOptions() {
static absl::flat_hash_set<std::string> kDefaultStopwords{
"a", "is", "the", "an", "and", "are", "as", "at", "be", "but", "by",
@ -646,21 +652,10 @@ const Schema& FieldIndices::GetSchema() const {
return schema_;
}
vector<pair<string, SortableValue>> FieldIndices::ExtractStoredValues(DocId doc) const {
vector<pair<string, SortableValue>> out;
for (const auto& [ident, index] : sort_indices_) {
out.emplace_back(ident, index->Lookup(doc));
}
return out;
}
absl::flat_hash_set<std::string_view> FieldIndices::GetSortIndiciesFields() const {
absl::flat_hash_set<std::string_view> fields_idents;
fields_idents.reserve(sort_indices_.size());
for (const auto& [ident, _] : sort_indices_) {
fields_idents.insert(ident);
}
return fields_idents;
SortableValue FieldIndices::GetSortIndexValue(DocId doc, std::string_view field_identifier) const {
auto it = sort_indices_.find(field_identifier);
DCHECK(it != sort_indices_.end());
return it->second->Lookup(doc);
}
SearchAlgorithm::SearchAlgorithm() = default;

View file

@ -60,6 +60,9 @@ struct Schema {
// Return identifier for alias if found, otherwise return passed value
std::string_view LookupAlias(std::string_view alias) const;
// Return alias for identifier if found, otherwise return passed value
std::string_view LookupIdentifier(std::string_view identifier) const;
};
struct IndicesOptions {
@ -88,10 +91,7 @@ class FieldIndices {
const std::vector<DocId>& GetAllDocs() const;
const Schema& GetSchema() const;
// Extract values stored in sort indices
std::vector<std::pair<std::string, SortableValue>> ExtractStoredValues(DocId doc) const;
absl::flat_hash_set<std::string_view> GetSortIndiciesFields() const;
SortableValue GetSortIndexValue(DocId doc, std::string_view field_identifier) const;
private:
void CreateIndices(PMR_NS::memory_resource* mr);
@ -100,8 +100,8 @@ class FieldIndices {
const Schema& schema_;
const IndicesOptions& options_;
std::vector<DocId> all_ids_;
absl::flat_hash_map<std::string, std::unique_ptr<BaseIndex>> indices_;
absl::flat_hash_map<std::string, std::unique_ptr<BaseSortIndex>> sort_indices_;
absl::flat_hash_map<std::string_view, std::unique_ptr<BaseIndex>> indices_;
absl::flat_hash_map<std::string_view, std::unique_ptr<BaseSortIndex>> sort_indices_;
};
struct AlgorithmProfile {

View file

@ -65,6 +65,10 @@ class StringOrView {
val_ = std::string{std::get<std::string_view>(val_)};
}
bool empty() const {
return visit([](const auto& s) { return s.empty(); }, val_);
}
private:
std::variant<std::string_view, std::string> val_;
};

View file

@ -80,10 +80,13 @@ FieldValue ExtractSortableValueFromJson(const search::Schema& schema, string_vie
} // namespace
SearchDocData BaseAccessor::Serialize(
const search::Schema& schema, absl::Span<const SearchField<std::string_view>> fields) const {
SearchDocData BaseAccessor::Serialize(const search::Schema& schema,
absl::Span<const SearchField> fields) const {
SearchDocData out{};
for (const auto& [fident, fname] : fields) {
for (const auto& field : fields) {
const auto& fident = field.GetIdentifier(schema, false);
const auto& fname = field.GetShortName(schema);
auto field_value =
ExtractSortableValue(schema, fident, absl::StrJoin(GetStrings(fident).value(), ","));
if (field_value) {
@ -348,14 +351,16 @@ JsonAccessor::JsonPathContainer* JsonAccessor::GetPath(std::string_view field) c
SearchDocData JsonAccessor::Serialize(const search::Schema& schema) const {
SearchFieldsList fields{};
for (const auto& [fname, fident] : schema.field_names)
fields.emplace_back(fident, fname);
fields.emplace_back(StringOrView::FromView(fident), false, StringOrView::FromView(fname));
return Serialize(schema, fields);
}
SearchDocData JsonAccessor::Serialize(
const search::Schema& schema, absl::Span<const SearchField<std::string_view>> fields) const {
SearchDocData JsonAccessor::Serialize(const search::Schema& schema,
absl::Span<const SearchField> fields) const {
SearchDocData out{};
for (const auto& [ident, name] : fields) {
for (const auto& field : fields) {
const auto& ident = field.GetIdentifier(schema, true);
const auto& name = field.GetShortName(schema);
if (auto* path = GetPath(ident); path) {
if (auto res = path->Evaluate(json_); !res.empty()) {
auto field_value = ExtractSortableValueFromJson(schema, ident, res[0]);

View file

@ -30,7 +30,7 @@ struct BaseAccessor : public search::DocumentAccessor {
// Serialize selected fields
virtual SearchDocData Serialize(const search::Schema& schema,
absl::Span<const SearchField<std::string_view>> fields) const;
absl::Span<const SearchField> fields) const;
/*
Serialize the whole type, the default implementation is to serialize all fields.
@ -84,7 +84,7 @@ struct JsonAccessor : public BaseAccessor {
// The JsonAccessor works with structured types and not plain strings, so an overload is needed
SearchDocData Serialize(const search::Schema& schema,
absl::Span<const SearchField<std::string_view>> fields) const override;
absl::Span<const SearchField> fields) const override;
SearchDocData Serialize(const search::Schema& schema) const override;
SearchDocData SerializeDocument(const search::Schema& schema) const override;

View file

@ -60,8 +60,8 @@ bool SerializedSearchDoc::operator>=(const SerializedSearchDoc& other) const {
return this->score >= other.score;
}
bool SearchParams::ShouldReturnField(std::string_view field) const {
auto cb = [field](const auto& entry) { return entry.first == field; };
bool SearchParams::ShouldReturnField(std::string_view alias) const {
auto cb = [alias](const auto& entry) { return entry.GetShortName() == alias; };
return !return_fields || any_of(return_fields->begin(), return_fields->end(), cb);
}
@ -224,12 +224,12 @@ bool ShardDocIndex::Matches(string_view key, unsigned obj_code) const {
return base_->Matches(key, obj_code);
}
SearchFieldsList ToSV(const std::optional<OwnedSearchFieldsList>& fields) {
SearchFieldsList ToSV(const search::Schema& schema, const std::optional<SearchFieldsList>& fields) {
SearchFieldsList sv_fields;
if (fields) {
sv_fields.reserve(fields->size());
for (const auto& [fident, fname] : fields.value()) {
sv_fields.emplace_back(fident, fname);
for (const auto& field : fields.value()) {
sv_fields.push_back(field.View());
}
}
return sv_fields;
@ -243,8 +243,8 @@ SearchResult ShardDocIndex::Search(const OpArgs& op_args, const SearchParams& pa
if (!search_results.error.empty())
return SearchResult{facade::ErrorReply{std::move(search_results.error)}};
SearchFieldsList fields_to_load =
ToSV(params.ShouldReturnAllFields() ? params.load_fields : params.return_fields);
SearchFieldsList fields_to_load = ToSV(
base_->schema, params.ShouldReturnAllFields() ? params.load_fields : params.return_fields);
vector<SerializedSearchDoc> out;
out.reserve(search_results.ids.size());
@ -285,6 +285,57 @@ SearchResult ShardDocIndex::Search(const OpArgs& op_args, const SearchParams& pa
std::move(search_results.profile)};
}
using SortIndiciesFieldsList =
std::vector<std::pair<string_view /*identifier*/, string_view /*alias*/>>;
std::pair<SearchFieldsList, SortIndiciesFieldsList> PreprocessAggregateFields(
const search::Schema& schema, const AggregateParams& params,
const std::optional<SearchFieldsList>& load_fields) {
auto is_sortable = [&schema](std::string_view fident) {
auto it = schema.fields.find(fident);
return it != schema.fields.end() && (it->second.flags & search::SchemaField::SORTABLE);
};
absl::flat_hash_map<std::string_view, SearchField> fields_by_identifier;
absl::flat_hash_map<std::string_view, std::string_view> sort_indicies_aliases;
fields_by_identifier.reserve(schema.field_names.size());
sort_indicies_aliases.reserve(schema.field_names.size());
for (const auto& [fname, fident] : schema.field_names) {
if (!is_sortable(fident)) {
fields_by_identifier[fident] = {StringOrView::FromView(fident), true,
StringOrView::FromView(fname)};
} else {
sort_indicies_aliases[fident] = fname;
}
}
if (load_fields) {
for (const auto& field : load_fields.value()) {
const auto& fident = field.GetIdentifier(schema, false);
if (!is_sortable(fident)) {
fields_by_identifier[fident] = field.View();
} else {
sort_indicies_aliases[fident] = field.GetShortName();
}
}
}
SearchFieldsList fields;
fields.reserve(fields_by_identifier.size());
for (auto& [_, field] : fields_by_identifier) {
fields.emplace_back(std::move(field));
}
SortIndiciesFieldsList sort_fields;
sort_fields.reserve(sort_indicies_aliases.size());
for (auto& [fident, fname] : sort_indicies_aliases) {
sort_fields.emplace_back(fident, fname);
}
return {std::move(fields), std::move(sort_fields)};
}
vector<SearchDocData> ShardDocIndex::SearchForAggregator(
const OpArgs& op_args, const AggregateParams& params,
search::SearchAlgorithm* search_algo) const {
@ -294,8 +345,8 @@ vector<SearchDocData> ShardDocIndex::SearchForAggregator(
if (!search_results.error.empty())
return {};
SearchFieldsList fields_to_load =
GetFieldsToLoad(params.load_fields, indices_->GetSortIndiciesFields());
auto [fields_to_load, sort_indicies] =
PreprocessAggregateFields(base_->schema, params, params.load_fields);
vector<absl::flat_hash_map<string, search::SortableValue>> out;
for (DocId doc : search_results.ids) {
@ -306,41 +357,23 @@ vector<SearchDocData> ShardDocIndex::SearchForAggregator(
continue;
auto accessor = GetAccessor(op_args.db_cntx, (*it)->second);
auto extracted = indices_->ExtractStoredValues(doc);
SearchDocData extracted_sort_indicies;
extracted_sort_indicies.reserve(sort_indicies.size());
for (const auto& [fident, fname] : sort_indicies) {
extracted_sort_indicies[fname] = indices_->GetSortIndexValue(doc, fident);
}
SearchDocData loaded = accessor->Serialize(base_->schema, fields_to_load);
out.emplace_back(make_move_iterator(extracted.begin()), make_move_iterator(extracted.end()));
out.emplace_back(make_move_iterator(extracted_sort_indicies.begin()),
make_move_iterator(extracted_sort_indicies.end()));
out.back().insert(make_move_iterator(loaded.begin()), make_move_iterator(loaded.end()));
}
return out;
}
SearchFieldsList ShardDocIndex::GetFieldsToLoad(
const std::optional<OwnedSearchFieldsList>& load_fields,
const absl::flat_hash_set<std::string_view>& skip_fields) const {
// identifier to short name
absl::flat_hash_map<std::string_view, std::string_view> unique_fields;
unique_fields.reserve(base_->schema.field_names.size());
for (const auto& [fname, fident] : base_->schema.field_names) {
if (!skip_fields.contains(fident)) {
unique_fields[fident] = fname;
}
}
if (load_fields) {
for (const auto& [fident, fname] : load_fields.value()) {
if (!skip_fields.contains(fident)) {
unique_fields[fident] = fname;
}
}
}
return {unique_fields.begin(), unique_fields.end()};
}
DocIndexInfo ShardDocIndex::GetInfo() const {
return {*base_, key_index_.Size()};
}

View file

@ -52,10 +52,82 @@ struct SearchResult {
std::optional<facade::ErrorReply> error;
};
template <typename T> using SearchField = std::pair<T /*identifier*/, T /*short name*/>;
/* SearchField represents a field that can store combinations of identifiers and aliases in various
forms: [identifier and alias], [alias and new_alias], [new identifier and alias] (used for JSON
data) This class provides methods to retrieve the actual identifier and alias for a field,
handling different naming conventions and resolving names based on the schema. */
class SearchField {
private:
static bool IsJsonPath(std::string_view name) {
if (name.size() < 2) {
return false;
}
return name.front() == '$' && (name[1] == '.' || name[1] == '[');
}
using SearchFieldsList = std::vector<SearchField<std::string_view>>;
using OwnedSearchFieldsList = std::vector<SearchField<std::string>>;
public:
SearchField() = default;
SearchField(StringOrView name, bool is_short_name)
: name_(std::move(name)), is_short_name_(is_short_name) {
}
SearchField(StringOrView name, bool is_short_name, StringOrView new_alias)
: name_(std::move(name)), is_short_name_(is_short_name), new_alias_(std::move(new_alias)) {
}
std::string_view GetIdentifier(const search::Schema& schema, bool is_json_field) const {
auto as_view = NameView();
if (!is_short_name_ || (is_json_field && IsJsonPath(as_view))) {
return as_view;
}
return schema.LookupAlias(as_view);
}
std::string_view GetShortName() const {
if (HasNewAlias()) {
return AliasView();
}
return NameView();
}
std::string_view GetShortName(const search::Schema& schema) const {
if (HasNewAlias()) {
return AliasView();
}
return is_short_name_ ? NameView() : schema.LookupIdentifier(NameView());
}
/* Returns a new SearchField instance with name and alias stored as views to the values in this
* SearchField */
SearchField View() const {
if (HasNewAlias()) {
return SearchField{StringOrView::FromView(NameView()), is_short_name_,
StringOrView::FromView(AliasView())};
}
return SearchField{StringOrView::FromView(NameView()), is_short_name_};
}
private:
bool HasNewAlias() const {
return !new_alias_.empty();
}
std::string_view NameView() const {
return name_.view();
}
std::string_view AliasView() const {
return new_alias_.view();
}
private:
StringOrView name_;
bool is_short_name_;
StringOrView new_alias_;
};
using SearchFieldsList = std::vector<SearchField>;
struct SearchParams {
// Parameters for "LIMIT offset total": select total amount documents with a specific offset from
@ -68,14 +140,14 @@ struct SearchParams {
2. If set but empty -> no fields should be returned
3. If set and not empty -> return only these fields
*/
std::optional<OwnedSearchFieldsList> return_fields;
std::optional<SearchFieldsList> return_fields;
/*
Fields that should be also loaded from the document.
Only one of load_fields and return_fields should be set.
*/
std::optional<OwnedSearchFieldsList> load_fields;
std::optional<SearchFieldsList> load_fields;
std::optional<search::SortOption> sort_option;
search::QueryParams query_params;
@ -88,14 +160,14 @@ struct SearchParams {
return return_fields && return_fields->empty();
}
bool ShouldReturnField(std::string_view field) const;
bool ShouldReturnField(std::string_view alias) const;
};
struct AggregateParams {
std::string_view index, query;
search::QueryParams params;
std::optional<OwnedSearchFieldsList> load_fields;
std::optional<SearchFieldsList> load_fields;
std::vector<aggregate::PipelineStep> steps;
};
@ -169,11 +241,6 @@ class ShardDocIndex {
io::Result<StringVec, facade::ErrorReply> GetTagVals(std::string_view field) const;
private:
// Returns the fields that are the union of the already indexed fields and load_fields, excluding
// skip_fields Load_fields should not be destroyed while the result of this function is being used
SearchFieldsList GetFieldsToLoad(const std::optional<OwnedSearchFieldsList>& load_fields,
const absl::flat_hash_set<std::string_view>& skip_fields) const;
// Clears internal data. Traverses all matching documents and assigns ids.
void Rebuild(const OpArgs& op_args, PMR_NS::memory_resource* mr);

View file

@ -185,7 +185,7 @@ optional<search::Schema> ParseSchemaOrReply(DocIndex::DataType type, CmdArgParse
std::string_view ParseField(CmdArgParser* parser) {
std::string_view field = parser->Next();
if (!field.empty() && field.front() == '@') {
if (absl::StartsWith(field, "@"sv)) {
field.remove_prefix(1); // remove leading @ if exists
}
return field;
@ -193,7 +193,7 @@ std::string_view ParseField(CmdArgParser* parser) {
std::string_view ParseFieldWithAtSign(CmdArgParser* parser) {
std::string_view field = parser->Next();
if (!field.empty() && field.front() == '@') {
if (absl::StartsWith(field, "@"sv)) {
field.remove_prefix(1); // remove leading @
} else {
// Temporary warning until we can throw an error
@ -203,16 +203,27 @@ std::string_view ParseFieldWithAtSign(CmdArgParser* parser) {
return field;
}
void ParseLoadFields(CmdArgParser* parser, std::optional<OwnedSearchFieldsList>* load_fields) {
void ParseLoadFields(CmdArgParser* parser, std::optional<SearchFieldsList>* load_fields) {
// TODO: Change to num_strings. In Redis strings number is expected. For example: LOAD 3 $.a AS a
size_t num_fields = parser->Next<size_t>();
if (!load_fields->has_value()) {
load_fields->emplace();
}
while (num_fields--) {
string_view field = ParseField(parser);
string_view alias = parser->Check("AS") ? parser->Next() : field;
load_fields->value().emplace_back(field, alias);
string_view str = parser->Next();
if (absl::StartsWith(str, "@"sv)) {
str.remove_prefix(1); // remove leading @
}
StringOrView name = StringOrView::FromString(std::string{str});
if (parser->Check("AS")) {
load_fields->value().emplace_back(name, true,
StringOrView::FromString(parser->Next<std::string>()));
} else {
load_fields->value().emplace_back(name, true);
}
}
}
@ -248,12 +259,19 @@ optional<SearchParams> ParseSearchParamsOrReply(CmdArgParser* parser, SinkReplyB
}
// RETURN {num} [{ident} AS {name}...]
/* TODO: Change to num_strings. In Redis strings number is expected. For example: RETURN 3 $.a
* AS a */
size_t num_fields = parser->Next<size_t>();
params.return_fields.emplace();
while (params.return_fields->size() < num_fields) {
string_view ident = parser->Next();
string_view alias = parser->Check("AS") ? parser->Next() : ident;
params.return_fields->emplace_back(ident, alias);
StringOrView name = StringOrView::FromString(parser->Next<std::string>());
if (parser->Check("AS")) {
params.return_fields->emplace_back(std::move(name), true,
StringOrView::FromString(parser->Next<std::string>()));
} else {
params.return_fields->emplace_back(std::move(name), true);
}
}
} else if (parser->Check("NOCONTENT")) { // NOCONTENT
params.load_fields.emplace();
@ -261,7 +279,8 @@ optional<SearchParams> ParseSearchParamsOrReply(CmdArgParser* parser, SinkReplyB
} else if (parser->Check("PARAMS")) { // [PARAMS num(ignored) name(ignored) knn_vector]
params.query_params = ParseQueryParams(parser);
} else if (parser->Check("SORTBY")) {
params.sort_option = search::SortOption{string{parser->Next()}, bool(parser->Check("DESC"))};
params.sort_option =
search::SortOption{parser->Next<std::string>(), bool(parser->Check("DESC"))};
} else {
// Unsupported parameters are ignored for now
parser->Skip(1);

View file

@ -1425,4 +1425,204 @@ TEST_F(SearchFamilyTest, WrongVectorFieldType) {
EXPECT_THAT(resp, AreDocIds("j6", "j7", "j1", "j4"));
}
TEST_F(SearchFamilyTest, SearchLoadReturnJson) {
Run({"JSON.SET", "j1", ".", R"({"a":"one"})"});
Run({"JSON.SET", "j2", ".", R"({"a":"two"})"});
auto resp = Run({"FT.CREATE", "i1", "ON", "JSON", "SCHEMA", "$.a", "AS", "a", "TEXT"});
EXPECT_EQ(resp, "OK");
// Search with RETURN $.a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "$.a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("$.a", "\"one\""), "j2", IsMap("$.a", "\"two\"")));
// Search with RETURN a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("a", "\"one\""), "j2", IsMap("a", "\"two\"")));
// Search with RETURN @a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap(), "j2", IsMap()));
// Search with RETURN $.a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "$.a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("vvv", "\"one\""), "j2", IsMap("vvv", "\"two\"")));
// Search with RETURN a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("vvv", "\"one\""), "j2", IsMap("vvv", "\"two\"")));
// Search with RETURN @a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "@a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap(), "j2", IsMap()));
// Search with LOAD $.a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "$.a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("$.a", "\"one\"", "$", R"({"a":"one"})"), "j2",
IsMap("$.a", "\"two\"", "$", R"({"a":"two"})")));
// Search with LOAD a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("a", "\"one\"", "$", R"({"a":"one"})"), "j2",
IsMap("a", "\"two\"", "$", R"({"a":"two"})")));
// Search with LOAD @a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("a", "\"one\"", "$", R"({"a":"one"})"), "j2",
IsMap("a", "\"two\"", "$", R"({"a":"two"})")));
// Search with LOAD $.a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "$.a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("$", R"({"a":"one"})", "vvv", "\"one\""), "j2",
IsMap("$", R"({"a":"two"})", "vvv", "\"two\"")));
// Search with LOAD a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("$", R"({"a":"one"})", "vvv", "\"one\""), "j2",
IsMap("$", R"({"a":"two"})", "vvv", "\"two\"")));
// Search with LOAD @a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "@a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("$", R"({"a":"one"})", "vvv", "\"one\""), "j2",
IsMap("$", R"({"a":"two"})", "vvv", "\"two\"")));
/* Test another name */
resp = Run({"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.a", "AS", "nnn", "TEXT"});
EXPECT_EQ(resp, "OK");
// Search with RETURN nnn
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "nnn"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("nnn", "\"one\""), "j2", IsMap("nnn", "\"two\"")));
// Search with RETURN @nnn
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "@nnn"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap(), "j2", IsMap()));
// Search with RETURN a
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap(), "j2", IsMap()));
// Search with RETURN @a
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap(), "j2", IsMap()));
// Search with LOAD nnn
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "nnn"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("nnn", "\"one\"", "$", R"({"a":"one"})"), "j2",
IsMap("nnn", "\"two\"", "$", R"({"a":"two"})")));
// Search with LOAD @nnn
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "@nnn"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("nnn", "\"one\"", "$", R"({"a":"one"})"), "j2",
IsMap("nnn", "\"two\"", "$", R"({"a":"two"})")));
// Search with LOAD a
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "a"});
EXPECT_THAT(
resp, IsMapWithSize("j1", IsMap("$", R"({"a":"one"})"), "j2", IsMap("$", R"({"a":"two"})")));
// Search with LOAD @a
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "@a"});
EXPECT_THAT(
resp, IsMapWithSize("j1", IsMap("$", R"({"a":"one"})"), "j2", IsMap("$", R"({"a":"two"})")));
}
TEST_F(SearchFamilyTest, SearchLoadReturnHash) {
Run({"HSET", "h1", "a", "one"});
Run({"HSET", "h2", "a", "two"});
auto resp = Run({"FT.CREATE", "i1", "ON", "HASH", "SCHEMA", "a", "TEXT"});
EXPECT_EQ(resp, "OK");
// Search with RETURN $.a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "$.a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap(), "h1", IsMap()));
// Search with RETURN a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with RETURN @a
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap(), "h1", IsMap()));
// Search with RETURN $.a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "$.a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap(), "h1", IsMap()));
// Search with RETURN a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("vvv", "two"), "h1", IsMap("vvv", "one")));
// Search with RETURN @a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "RETURN", "1", "@a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap(), "h1", IsMap()));
// Search with LOAD $.a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "$.a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with LOAD a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with LOAD @a
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with LOAD $.a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "$.a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with LOAD a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("vvv", "two", "a", "two"), "h1",
IsMap("vvv", "one", "a", "one")));
// Search with LOAD @a AS vvv
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "@a", "AS", "vvv"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("vvv", "two", "a", "two"), "h1",
IsMap("vvv", "one", "a", "one")));
/* Test another name */
resp = Run({"FT.CREATE", "i2", "ON", "HASH", "SCHEMA", "a", "AS", "nnn", "TEXT"});
EXPECT_EQ(resp, "OK");
// Search with RETURN nnn
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "nnn"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("nnn", "two"), "h1", IsMap("nnn", "one")));
// Search with RETURN @nnn
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "@nnn"});
EXPECT_THAT(resp, IsMapWithSize("h1", IsMap(), "h2", IsMap()));
// Search with RETURN a
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with RETURN @a
resp = Run({"FT.SEARCH", "i2", "*", "RETURN", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("h1", IsMap(), "h2", IsMap()));
// Search with LOAD nnn
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "nnn"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("nnn", "two", "a", "two"), "h1",
IsMap("nnn", "one", "a", "one")));
// Search with LOAD @nnn
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "@nnn"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("nnn", "two", "a", "two"), "h1",
IsMap("nnn", "one", "a", "one")));
// Search with LOAD a
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
// Search with LOAD @a
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "@a"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("a", "two"), "h1", IsMap("a", "one")));
}
} // namespace dfly