mirror of
https://github.com/dragonflydb/dragonfly.git
synced 2025-05-10 18:05:44 +02:00
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:
parent
3d68c9c99e
commit
2b3c182cc9
9 changed files with 410 additions and 87 deletions
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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_;
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue