dragonfly/src/server/search/search_family_test.cc
Stepan Bagritsevich 83569aca28
fix(search_family): Add options test for the FT.SEARCH command (#4478)
Signed-off-by: Stepan Bagritsevich <stefan@dragonflydb.io>
2025-01-30 09:43:01 +00:00

1887 lines
80 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2023, DragonflyDB authors. All rights reserved.
// See LICENSE for licensing terms.
//
#include "server/search/search_family.h"
#include "base/gtest.h"
#include "base/logging.h"
#include "facade/error.h"
#include "facade/facade_test.h"
#include "server/command_registry.h"
#include "server/test_utils.h"
using namespace testing;
using namespace std;
using namespace util;
using namespace facade;
namespace dfly {
class SearchFamilyTest : public BaseFamilyTest {
protected:
};
const auto kNoResults = IntArg(0); // tests auto destruct single element arrays
/* Asserts that response is array of two arrays. Used to test FT.PROFILE response */
::testing::AssertionResult AssertArrayOfTwoArrays(const RespExpr& resp) {
if (resp.GetVec().size() != 2) {
return ::testing::AssertionFailure()
<< "Expected response array length to be 2, but was " << resp.GetVec().size();
}
const auto& vec = resp.GetVec();
if (vec[0].type != RespExpr::ARRAY) {
return ::testing::AssertionFailure()
<< "Expected resp[0] to be an array, but was " << vec[0].type;
}
if (vec[1].type != RespExpr::ARRAY) {
return ::testing::AssertionFailure()
<< "Expected resp[1] to be an array, but was " << vec[1].type;
}
return ::testing::AssertionSuccess();
}
#define ASSERT_ARRAY_OF_TWO_ARRAYS(resp) ASSERT_PRED1(AssertArrayOfTwoArrays, resp)
MATCHER_P2(DocIds, total, arg_ids, "") {
if (arg_ids.empty()) {
if (auto res = arg.GetInt(); !res || *res != 0) {
*result_listener << "Expected single zero";
return false;
}
return true;
}
if (arg.type != RespExpr::ARRAY) {
*result_listener << "Wrong response type: " << int(arg.type);
return false;
}
auto results = arg.GetVec();
if (results.size() != arg_ids.size() * 2 + 1) {
*result_listener << "Wrong resp vec size: " << results.size();
return false;
}
if (auto num_results = results[0].GetInt(); !num_results || size_t(*num_results) != total) {
*result_listener << "Bad total count in reply: " << num_results.value_or(-1);
return false;
}
vector<string> received_ids;
for (size_t i = 1; i < results.size(); i += 2)
received_ids.push_back(results[i].GetString());
vector<string> expected_ids = arg_ids;
sort(received_ids.begin(), received_ids.end());
sort(expected_ids.begin(), expected_ids.end());
return expected_ids == received_ids;
}
template <typename... Args> auto AreDocIds(Args... args) {
return DocIds(sizeof...(args), vector<string>{args...});
}
template <typename... Args> auto IsArray(Args... args) {
return RespArray(ElementsAre(std::forward<Args>(args)...));
}
template <typename... Args> auto IsUnordArray(Args... args) {
return RespArray(UnorderedElementsAre(std::forward<Args>(args)...));
}
template <typename Expected, size_t... Is>
void BuildKvMatchers(std::vector<Matcher<std::pair<std::string, RespExpr>>>& kv_matchers,
const Expected& expected, std::index_sequence<Is...>) {
(kv_matchers.emplace_back(Pair(std::get<Is * 2>(expected), std::get<Is * 2 + 1>(expected))), ...);
}
MATCHER_P(IsMapMatcher, expected, "") {
if (arg.type != RespExpr::ARRAY) {
*result_listener << "Wrong response type: " << arg.type;
return false;
}
constexpr size_t expected_size = std::tuple_size<decltype(expected)>::value;
constexpr size_t exprected_pairs_number = expected_size / 2;
auto result = arg.GetVec();
if (result.size() != expected_size) {
*result_listener << "Wrong resp array size: " << result.size();
return false;
}
std::vector<std::pair<std::string, RespExpr>> received_pairs;
for (size_t i = 0; i < result.size(); i += 2) {
received_pairs.emplace_back(result[i].GetString(), result[i + 1]);
}
std::vector<Matcher<std::pair<std::string, RespExpr>>> kv_matchers;
BuildKvMatchers(kv_matchers, expected, std::make_index_sequence<exprected_pairs_number>{});
return ExplainMatchResult(UnorderedElementsAreArray(kv_matchers), received_pairs,
result_listener);
}
template <typename... Args> auto IsMap(Args... args) {
return IsMapMatcher(std::make_tuple(args...));
}
MATCHER_P(IsMapWithSizeMatcher, expected, "") {
if (arg.type != RespExpr::ARRAY) {
*result_listener << "Wrong response type: " << arg.type;
return false;
}
constexpr size_t expected_size = std::tuple_size<decltype(expected)>::value;
constexpr size_t exprected_pairs_number = expected_size / 2;
auto result = arg.GetVec();
if (result.size() != expected_size + 1 || result.size() % 2 != 1) {
*result_listener << "Wrong resp array size: " << result.size();
return false;
}
if (result[0].GetInt() != exprected_pairs_number) {
*result_listener << "Wrong pairs count: " << result[0].GetInt().value_or(-1);
return false;
}
std::vector<std::pair<std::string, RespExpr>> received_pairs;
for (size_t i = 1; i < result.size(); i += 2) {
received_pairs.emplace_back(result[i].GetString(), result[i + 1]);
}
std::vector<Matcher<std::pair<std::string, RespExpr>>> kv_matchers;
BuildKvMatchers(kv_matchers, expected, std::make_index_sequence<exprected_pairs_number>{});
return ExplainMatchResult(UnorderedElementsAreArray(kv_matchers), received_pairs,
result_listener);
}
template <typename... Args> auto IsMapWithSize(Args... args) {
return IsMapWithSizeMatcher(std::make_tuple(args...));
}
MATCHER_P(IsUnordArrayWithSizeMatcher, expected, "") {
if (arg.type != RespExpr::ARRAY) {
*result_listener << "Wrong response type: " << arg.type;
return false;
}
auto result = arg.GetVec();
size_t expected_size = std::tuple_size<decltype(expected)>::value;
if (result.size() != expected_size + 1) {
*result_listener << "Wrong resp array size: " << result.size();
return false;
}
if (result[0].GetInt() != expected_size) {
*result_listener << "Wrong elements count: " << result[0].GetInt().value_or(-1);
return false;
}
std::vector<RespExpr> received_elements(result.begin() + 1, result.end());
// Create a vector of matchers from the tuple
std::vector<Matcher<RespExpr>> matchers;
std::apply([&matchers](auto&&... args) { ((matchers.push_back(args)), ...); }, expected);
return ExplainMatchResult(UnorderedElementsAreArray(matchers), received_elements,
result_listener);
}
template <typename... Matchers> auto IsUnordArrayWithSize(Matchers... matchers) {
return IsUnordArrayWithSizeMatcher(std::make_tuple(matchers...));
}
TEST_F(SearchFamilyTest, CreateDropListIndex) {
EXPECT_EQ(Run({"ft.create", "idx-1", "ON", "HASH", "PREFIX", "1", "prefix-1"}), "OK");
EXPECT_EQ(Run({"ft.create", "idx-2", "ON", "JSON", "PREFIX", "1", "prefix-2"}), "OK");
EXPECT_EQ(Run({"ft.create", "idx-3", "ON", "JSON", "PREFIX", "1", "prefix-3"}), "OK");
EXPECT_THAT(Run({"ft._list"}).GetVec(), testing::UnorderedElementsAre("idx-1", "idx-2", "idx-3"));
EXPECT_EQ(Run({"ft.dropindex", "idx-2"}), "OK");
EXPECT_THAT(Run({"ft._list"}).GetVec(), testing::UnorderedElementsAre("idx-1", "idx-3"));
EXPECT_THAT(Run({"ft.create", "idx-1"}), ErrArg("Index already exists"));
EXPECT_THAT(Run({"ft.dropindex", "idx-100"}), ErrArg("Unknown Index name"));
EXPECT_EQ(Run({"ft.dropindex", "idx-1"}), "OK");
EXPECT_EQ(Run({"ft._list"}), "idx-3");
}
TEST_F(SearchFamilyTest, CreateDropDifferentDatabases) {
// Create index on db 0
auto resp =
Run({"ft.create", "idx-1", "ON", "HASH", "PREFIX", "1", "doc-", "SCHEMA", "name", "TEXT"});
EXPECT_EQ(resp, "OK");
EXPECT_EQ(Run({"select", "1"}), "OK"); // change database
// Creating an index on non zero database must fail
resp = Run({"ft.create", "idx-2", "ON", "JSON", "PREFIX", "1", "prefix-2"});
EXPECT_THAT(resp, ErrArg("ERR Cannot create index on db != 0"));
// Add some data to the index
Run({"hset", "doc-0", "name", "Name of 0"});
// ft.search must work on the another database
resp = Run({"ft.search", "idx-1", "*"});
EXPECT_THAT(resp, IsMapWithSize("doc-0", IsMap("name", "Name of 0")));
// ft.dropindex must work on the another database
EXPECT_EQ(Run({"ft.dropindex", "idx-1"}), "OK");
EXPECT_THAT(Run({"ft.info", "idx-1"}), ErrArg("ERR Unknown Index name"));
EXPECT_EQ(Run({"select", "0"}), "OK");
EXPECT_THAT(Run({"ft.info", "idx-1"}), ErrArg("ERR Unknown Index name"));
}
TEST_F(SearchFamilyTest, AlterIndex) {
Run({"hset", "d:1", "color", "blue", "cost", "150"});
Run({"hset", "d:2", "color", "green", "cost", "200"});
Run({"ft.create", "idx-1", "ON", "HASH"});
EXPECT_EQ(Run({"ft.alter", "idx-1", "schema", "add", "color", "tag"}), "OK");
EXPECT_THAT(Run({"ft.search", "idx-1", "@color:{blue}"}), AreDocIds("d:1"));
EXPECT_THAT(Run({"ft.search", "idx-1", "@color:{green}"}), AreDocIds("d:2"));
EXPECT_EQ(Run({"ft.alter", "idx-1", "schema", "add", "cost", "numeric"}), "OK");
EXPECT_THAT(Run({"ft.search", "idx-1", "@cost:[0 100]"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "idx-1", "@cost:[100 300]"}), AreDocIds("d:1", "d:2"));
EXPECT_THAT(Run({"ft.alter", "idx-2", "schema", "add", "price", "numeric"}),
ErrArg("Index not found"));
}
TEST_F(SearchFamilyTest, InfoIndex) {
EXPECT_EQ(
Run({"ft.create", "idx-1", "ON", "HASH", "PREFIX", "1", "doc-", "SCHEMA", "name", "TEXT"}),
"OK");
for (size_t i = 0; i < 15; i++) {
Run({"hset", absl::StrCat("doc-", i), "name", absl::StrCat("Name of", i)});
}
auto info = Run({"ft.info", "idx-1"});
EXPECT_THAT(info,
IsArray(_, _, _, IsArray("key_type", "HASH", "prefix", "doc-"), "attributes",
IsArray(IsArray("identifier", "name", "attribute", "name", "type", "TEXT")),
"num_docs", IntArg(15)));
}
TEST_F(SearchFamilyTest, Stats) {
EXPECT_EQ(
Run({"ft.create", "idx-1", "ON", "HASH", "PREFIX", "1", "doc1-", "SCHEMA", "name", "TEXT"}),
"OK");
EXPECT_EQ(
Run({"ft.create", "idx-2", "ON", "HASH", "PREFIX", "1", "doc2-", "SCHEMA", "name", "TEXT"}),
"OK");
for (size_t i = 0; i < 50; i++) {
Run({"hset", absl::StrCat("doc1-", i), "name", absl::StrCat("Name of", i)});
Run({"hset", absl::StrCat("doc2-", i), "name", absl::StrCat("Name of", i)});
}
auto metrics = GetMetrics();
EXPECT_EQ(metrics.search_stats.num_indices, 2);
EXPECT_EQ(metrics.search_stats.num_entries, 50 * 2);
size_t expected_usage = 2 * (50 + 3 /* number of distinct words*/) * (24 + 48 /* kv size */) +
50 * 2 * 1 /* posting list entries */;
EXPECT_GE(metrics.search_stats.used_memory, expected_usage);
EXPECT_LE(metrics.search_stats.used_memory, 3 * expected_usage);
}
// todo: ASAN fails heres on arm
#ifndef SANITIZERS
TEST_F(SearchFamilyTest, Simple) {
Run({"hset", "d:1", "foo", "baz", "k", "v"});
Run({"hset", "d:2", "foo", "bar", "k", "v"});
Run({"hset", "d:3", "foo", "bad", "k", "v"});
EXPECT_EQ(Run({"ft.create", "i1", "PREFIX", "1", "d:", "SCHEMA", "foo", "TEXT", "k", "TEXT"}),
"OK");
EXPECT_THAT(Run({"ft.search", "i1", "@foo:bar"}), AreDocIds("d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@foo:bar | @foo:baz"}), AreDocIds("d:1", "d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@foo:(bar|baz|bad)"}), AreDocIds("d:1", "d:2", "d:3"));
EXPECT_THAT(Run({"ft.search", "i1", "@foo:none"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "iNone", "@foo:bar"}), ErrArg("iNone: no such index"));
EXPECT_THAT(Run({"ft.search", "i1", "@@NOTAQUERY@@"}), ErrArg("Query syntax error"));
// w: prefix is not part of index
Run({"hset", "w:2", "foo", "this", "k", "v"});
EXPECT_THAT(Run({"ft.search", "i1", "@foo:this"}), kNoResults);
}
#endif
TEST_F(SearchFamilyTest, Errors) {
Run({"ft.create", "i1", "PREFIX", "1", "d:", "SCHEMA", "foo", "TAG", "bar", "TEXT"});
// Wrong field
EXPECT_THAT(Run({"ft.search", "i1", "@whoami:lol"}), ErrArg("Invalid field: whoami"));
// Wrong field type
EXPECT_THAT(Run({"ft.search", "i1", "@foo:lol"}), ErrArg("Wrong access type for field: foo"));
// ft.create index on json schema $.sometag AS sometag TAG SEPARATOR
EXPECT_THAT(Run({"ft.create", "i2", "ON", "JSON", "SCHEMA", "$.sometag", "AS", "sometag", "TAG",
"SEPARATOR"}),
ErrArg("Tag separator must be a single character. Got ``"));
}
TEST_F(SearchFamilyTest, NoPrefix) {
Run({"hset", "d:1", "a", "one", "k", "v"});
Run({"hset", "d:2", "a", "two", "k", "v"});
Run({"hset", "d:3", "a", "three", "k", "v"});
EXPECT_EQ(Run({"ft.create", "i1", "schema", "a", "text", "k", "text"}), "OK");
EXPECT_THAT(Run({"ft.search", "i1", "one | three"}), AreDocIds("d:1", "d:3"));
}
TEST_F(SearchFamilyTest, Json) {
Run({"json.set", "k1", ".", R"({"a": "small test", "b": "some details"})"});
Run({"json.set", "k2", ".", R"({"a": "another test", "b": "more details"})"});
Run({"json.set", "k3", ".", R"({"a": "last test", "b": "secret details"})"});
EXPECT_EQ(Run({"ft.create", "i1", "on", "json", "schema", "$.a", "as", "a", "text", "$.b", "as",
"b", "text"}),
"OK");
EXPECT_THAT(Run({"ft.search", "i1", "some|more"}), AreDocIds("k1", "k2"));
EXPECT_THAT(Run({"ft.search", "i1", "some|more|secret"}), AreDocIds("k1", "k2", "k3"));
EXPECT_THAT(Run({"ft.search", "i1", "@a:last @b:details"}), AreDocIds("k3"));
EXPECT_THAT(Run({"ft.search", "i1", "@a:(another|small)"}), AreDocIds("k1", "k2"));
EXPECT_THAT(Run({"ft.search", "i1", "@a:(another|small|secret)"}), AreDocIds("k1", "k2"));
EXPECT_THAT(Run({"ft.search", "i1", "none"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "@a:small @b:secret"}), kNoResults);
}
TEST_F(SearchFamilyTest, JsonAttributesPaths) {
Run({"json.set", "k1", ".", R"( {"nested": {"value": "no"}} )"});
Run({"json.set", "k2", ".", R"( {"nested": {"value": "yes"}} )"});
Run({"json.set", "k3", ".", R"( {"nested": {"value": "maybe"}} )"});
EXPECT_EQ(
Run({"ft.create", "i1", "on", "json", "schema", "$.nested.value", "as", "value", "text"}),
"OK");
EXPECT_THAT(Run({"ft.search", "i1", "yes"}), AreDocIds("k2"));
}
TEST_F(SearchFamilyTest, JsonIdentifierWithBrackets) {
Run({"json.set", "k1", ".", R"({"name":"London","population":8.8,"continent":"Europe"})"});
Run({"json.set", "k2", ".", R"({"name":"Athens","population":3.1,"continent":"Europe"})"});
Run({"json.set", "k3", ".", R"({"name":"Tel-Aviv","population":1.3,"continent":"Asia"})"});
Run({"json.set", "k4", ".", R"({"name":"Hyderabad","population":9.8,"continent":"Asia"})"});
EXPECT_EQ(Run({"ft.create", "i1", "on", "json", "schema", "$[\"name\"]", "as", "name", "tag",
"$[\"population\"]", "as", "population", "numeric", "sortable", "$[\"continent\"]",
"as", "continent", "tag"}),
"OK");
EXPECT_THAT(Run({"ft.search", "i1", "(@continent:{Europe})"}), AreDocIds("k1", "k2"));
}
// todo: fails on arm build
#ifndef SANITIZERS
TEST_F(SearchFamilyTest, JsonArrayValues) {
string_view D1 = R"(
{
"name": "Alex",
"plays" : [
{"game": "Pacman", "score": 10},
{"game": "Tetris", "score": 15}
],
"areas": ["EU-west", "EU-central"]
}
)";
string_view D2 = R"(
{
"name": "Bob",
"plays" : [
{"game": "Pacman", "score": 15},
{"game": "Mario", "score": 7}
],
"areas": ["US-central"]
}
)";
string_view D3 = R"(
{
"name": "Caren",
"plays" : [
{"game": "Mario", "score": 9},
{"game": "Doom", "score": 20}
],
"areas": ["EU-central", "EU-east"]
}
)";
Run({"json.set", "k1", ".", D1});
Run({"json.set", "k2", ".", D2});
Run({"json.set", "k3", ".", D3});
Run({"ft.create", "i1",
"on", "json",
"schema", "$.name",
"as", "name",
"text", "$.plays[*].game",
"as", "games",
"tag", "$.plays[*].score",
"as", "scores",
"numeric", "$.areas[*]",
"as", "areas",
"tag"});
EXPECT_THAT(Run({"ft.search", "i1", "*"}), AreDocIds("k1", "k2", "k3"));
// Find players by games
EXPECT_THAT(Run({"ft.search", "i1", "@games:{Tetris | Mario | Doom}"}),
AreDocIds("k1", "k2", "k3"));
EXPECT_THAT(Run({"ft.search", "i1", "@games:{Pacman}"}), AreDocIds("k1", "k2"));
EXPECT_THAT(Run({"ft.search", "i1", "@games:{Mario}"}), AreDocIds("k2", "k3"));
// Find players by scores
EXPECT_THAT(Run({"ft.search", "i1", "@scores:[15 15]"}), AreDocIds("k1", "k2"));
EXPECT_THAT(Run({"ft.search", "i1", "@scores:[0 (10]"}), AreDocIds("k2", "k3"));
EXPECT_THAT(Run({"ft.search", "i1", "@scores:[(15 20]"}), AreDocIds("k3"));
// Find platers by areas
EXPECT_THAT(Run({"ft.search", "i1", "@areas:{'EU-central'}"}), AreDocIds("k1", "k3"));
EXPECT_THAT(Run({"ft.search", "i1", "@areas:{'US-central'}"}), AreDocIds("k2"));
// Test complicated RETURN expression
auto res = Run(
{"ft.search", "i1", "@name:bob", "return", "1", "max($.plays[*].score)", "as", "max-score"});
EXPECT_THAT(res, IsMapWithSize("k2", IsMap("max-score", "15")));
// Test invalid json path expression omits that field
res = Run({"ft.search", "i1", "@name:alex", "return", "1", "::??INVALID??::", "as", "retval"});
EXPECT_THAT(res, IsMapWithSize("k1", IsMap()));
}
#endif
TEST_F(SearchFamilyTest, Tags) {
Run({"hset", "d:1", "color", "red, green"});
Run({"hset", "d:2", "color", "green, blue"});
Run({"hset", "d:3", "color", "blue, red"});
Run({"hset", "d:4", "color", "red"});
Run({"hset", "d:5", "color", "green"});
Run({"hset", "d:6", "color", "blue"});
EXPECT_EQ(Run({"ft.create", "i1", "on", "hash", "schema", "color", "tag", "dummy", "numeric"}),
"OK");
EXPECT_THAT(Run({"ft.tagvals", "i2", "color"}), ErrArg("Unknown Index name"));
EXPECT_THAT(Run({"ft.tagvals", "i1", "foo"}), ErrArg("No such field"));
EXPECT_THAT(Run({"ft.tagvals", "i1", "dummy"}), ErrArg("Not a tag field"));
auto resp = Run({"ft.tagvals", "i1", "color"});
ASSERT_THAT(resp, IsUnordArray("red", "blue", "green"));
// Tags don't participate in full text search
EXPECT_THAT(Run({"ft.search", "i1", "red"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "@color:{ red }"}), AreDocIds("d:1", "d:3", "d:4"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{green}"}), AreDocIds("d:1", "d:2", "d:5"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), AreDocIds("d:2", "d:3", "d:6"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{red | green}"}),
AreDocIds("d:1", "d:2", "d:3", "d:4", "d:5"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue | green}"}),
AreDocIds("d:1", "d:2", "d:3", "d:5", "d:6"));
EXPECT_EQ(Run({"ft.create", "i2", "on", "hash", "schema", "c1", "as", "c2", "tag"}), "OK");
// TODO: there is a discrepancy here between redis stack and Dragonfly,
// we accept the original field when it has alias, while redis stack does not.
//
// EXPECT_THAT(Run({"ft.tagvals", "i2", "c1"}), ErrArg("No such field"));
EXPECT_THAT(Run({"ft.tagvals", "i2", "c2"}), ArrLen(0));
}
TEST_F(SearchFamilyTest, TagOptions) {
Run({"hset", "d:1", "color", " red/ green // bLUe "});
Run({"hset", "d:2", "color", "blue /// GReeN "});
Run({"hset", "d:3", "color", "grEEn // yellow //"});
Run({"hset", "d:4", "color", " /blue/green/ "});
EXPECT_EQ(Run({"ft.create", "i1", "on", "hash", "schema", "color", "tag", "casesensitive",
"separator", "/"}),
"OK");
EXPECT_THAT(Run({"ft.search", "i1", "@color:{green}"}), AreDocIds("d:1", "d:4"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{GReeN}"}), AreDocIds("d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), AreDocIds("d:2", "d:4"));
}
TEST_F(SearchFamilyTest, TagNumbers) {
Run({"hset", "d:1", "number", "1"});
Run({"hset", "d:2", "number", "2"});
Run({"hset", "d:3", "number", "3"});
EXPECT_EQ(Run({"ft.create", "i1", "on", "hash", "schema", "number", "tag"}), "OK");
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1}"}), AreDocIds("d:1"));
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1|2}"}), AreDocIds("d:1", "d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1|2|3}"}), AreDocIds("d:1", "d:2", "d:3"));
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1.0|2|3.0}"}), AreDocIds("d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1|2|3.0}"}), AreDocIds("d:1", "d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "@number:{1|hello|2}"}), AreDocIds("d:1", "d:2"));
}
TEST_F(SearchFamilyTest, Numbers) {
for (unsigned i = 0; i <= 10; i++) {
for (unsigned j = 0; j <= 10; j++) {
auto key = absl::StrCat("i", i, "j", j);
Run({"hset", key, "i", absl::StrCat(i), "j", absl::StrCat(j)});
}
}
EXPECT_EQ(Run({"ft.create", "i1", "schema", "i", "numeric", "j", "numeric"}), "OK");
// Test simple ranges:
EXPECT_THAT(Run({"ft.search", "i1", "@i:[5 5] @j:[5 5]"}), AreDocIds("i5j5"));
EXPECT_THAT(Run({"ft.search", "i1", "@i:[0 1] @j:[9 10]"}),
AreDocIds("i0j9", "i0j10", "i1j9", "i1j10"));
EXPECT_THAT(Run({"ft.search", "i1", "@i:[7 8] @j:[2 3]"}),
AreDocIds("i7j2", "i7j3", "i8j2", "i8j3"));
// Test union of ranges:
EXPECT_THAT(Run({"ft.search", "i1", "(@i:[1 2] | @i:[6 6]) @j:[7 7]"}),
AreDocIds("i1j7", "i2j7", "i6j7"));
EXPECT_THAT(Run({"ft.search", "i1", "(@i:[1 5] | @i:[1 3] | @i:[3 5]) @j:[7 7]"}),
AreDocIds("i1j7", "i2j7", "i3j7", "i4j7", "i5j7"));
// Test intersection of ranges:
EXPECT_THAT(Run({"ft.search", "i1", "(@i:[9 9]) (@j:[5 7] @j:[6 8])"}),
AreDocIds("i9j6", "i9j7"));
EXPECT_THAT(Run({"ft.search", "i1", "@i:[9 9] (@j:[4 6] @j:[1 5] @j:[5 10])"}),
AreDocIds("i9j5"));
EXPECT_THAT(Run({"ft.search", "i1", "@i:[9 9] (@j:[4 6] @j:[1 5] @j:[5 10])"}),
AreDocIds("i9j5"));
// Test negation of ranges:
EXPECT_THAT(Run({"ft.search", "i1", "@i:[9 9] -@j:[1 10]"}), AreDocIds("i9j0"));
EXPECT_THAT(Run({"ft.search", "i1", "-@i:[0 9] -@j:[1 10]"}), AreDocIds("i10j0"));
// Test empty range
EXPECT_THAT(Run({"ft.search", "i1", "@i:[9 1]"}), AreDocIds());
EXPECT_THAT(Run({"ft.search", "i1", "@j:[5 0]"}), AreDocIds());
EXPECT_THAT(Run({"ft.search", "i1", "@i:[7 1] @j:[6 2]"}), AreDocIds());
}
TEST_F(SearchFamilyTest, TestLimit) {
for (unsigned i = 0; i < 20; i++)
Run({"hset", to_string(i), "match", "all"});
Run({"ft.create", "i1", "SCHEMA", "match", "text"});
// Default limit is 10
auto resp = Run({"ft.search", "i1", "all"});
EXPECT_THAT(resp, ArrLen(10 * 2 + 1));
resp = Run({"ft.search", "i1", "all", "limit", "0", "0"});
EXPECT_THAT(resp, IntArg(20));
resp = Run({"ft.search", "i1", "all", "limit", "0", "5"});
EXPECT_THAT(resp, ArrLen(5 * 2 + 1));
resp = Run({"ft.search", "i1", "all", "limit", "17", "5"});
EXPECT_THAT(resp, ArrLen(3 * 2 + 1));
}
TEST_F(SearchFamilyTest, TestReturn) {
auto floatsv = [](const float* f) -> string_view {
return {reinterpret_cast<const char*>(f), sizeof(float)};
};
for (unsigned i = 0; i < 20; i++) {
const float score = i;
Run({"hset", "k"s + to_string(i), "longA", to_string(i), "longB", to_string(i + 1), "longC",
to_string(i + 2), "secret", to_string(i + 3), "vector", floatsv(&score)});
}
Run({"ft.create", "i1", "SCHEMA", "longA", "AS", "justA", "TEXT",
"longB", "AS", "justB", "NUMERIC", "longC", "AS", "justC",
"NUMERIC", "vector", "VECTOR", "FLAT", "2", "DIM", "1"});
auto MatchEntry = [](string key, auto... fields) { return IsMapWithSize(key, IsMap(fields...)); };
// Check all fields are returned
auto resp = Run({"ft.search", "i1", "@justA:0"});
EXPECT_THAT(resp, MatchEntry("k0", "longA", "0", "longB", "1", "longC", "2", "secret", "3",
"vector", "[0]"));
// Check no fields are returned
resp = Run({"ft.search", "i1", "@justA:0", "return", "0"});
EXPECT_THAT(resp, IsArray(IntArg(1), "k0"));
resp = Run({"ft.search", "i1", "@justA:0", "nocontent"});
EXPECT_THAT(resp, IsArray(IntArg(1), "k0"));
// Check only one field is returned (and with original identifier)
resp = Run({"ft.search", "i1", "@justA:0", "return", "1", "longA"});
EXPECT_THAT(resp, MatchEntry("k0", "longA", "0"));
// Check only one field is returned with right alias
resp = Run({"ft.search", "i1", "@justA:0", "return", "1", "longB", "as", "madeupname"});
EXPECT_THAT(resp, MatchEntry("k0", "madeupname", "1"));
// Check two fields
resp = Run({"ft.search", "i1", "@justA:0", "return", "2", "longB", "as", "madeupname", "longC"});
EXPECT_THAT(resp, MatchEntry("k0", "madeupname", "1", "longC", "2"));
// Check non-existing field
resp = Run({"ft.search", "i1", "@justA:0", "return", "1", "nothere"});
EXPECT_THAT(resp, MatchEntry("k0"));
// Checl implcit __vector_score is provided
float score = 20;
resp = Run({"ft.search", "i1", "@justA:0 => [KNN 20 @vector $vector]", "SORTBY", "__vector_score",
"DESC", "RETURN", "1", "longA", "PARAMS", "2", "vector", floatsv(&score)});
EXPECT_THAT(resp, MatchEntry("k0", "longA", "0"));
// Check sort doesn't shadow knn return alias
score = 20;
resp = Run({"ft.search", "i1", "@justA:0 => [KNN 20 @vector $vector AS vec_return]", "SORTBY",
"vec_return", "DESC", "RETURN", "1", "vec_return", "PARAMS", "2", "vector",
floatsv(&score)});
EXPECT_THAT(resp, MatchEntry("k0", "vec_return", "20"));
}
TEST_F(SearchFamilyTest, TestStopWords) {
Run({"ft.create", "i1", "STOPWORDS", "3", "red", "green", "blue", "SCHEMA", "title", "TEXT"});
Run({"hset", "d:1", "title", "ReD? parrot flies away"});
Run({"hset", "d:2", "title", "GrEEn crocodile eats you"});
Run({"hset", "d:3", "title", "BLUe. Whale surfes the sea"});
EXPECT_THAT(Run({"ft.search", "i1", "red"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "green"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "blue"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "parrot"}), AreDocIds("d:1"));
EXPECT_THAT(Run({"ft.search", "i1", "crocodile"}), AreDocIds("d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "whale"}), AreDocIds("d:3"));
}
TEST_F(SearchFamilyTest, SimpleUpdates) {
EXPECT_EQ(Run({"ft.create", "i1", "schema", "title", "text", "visits", "numeric"}), "OK");
Run({"hset", "d:1", "title", "Dragonfly article", "visits", "100"});
Run({"hset", "d:2", "title", "Butterfly observations", "visits", "50"});
Run({"hset", "d:3", "title", "Bumblebee studies", "visits", "30"});
// Check values above were added to the index
EXPECT_THAT(Run({"ft.search", "i1", "article | observations | studies"}),
AreDocIds("d:1", "d:2", "d:3"));
// Update title - text value
{
Run({"hset", "d:2", "title", "Butterfly studies"});
EXPECT_THAT(Run({"ft.search", "i1", "observations"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "studies"}), AreDocIds("d:2", "d:3"));
Run({"hset", "d:1", "title", "Upcoming Dragonfly presentation"});
EXPECT_THAT(Run({"ft.search", "i1", "article"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "upcoming presentation"}), AreDocIds("d:1"));
Run({"hset", "d:3", "title", "Secret bumblebee research"});
EXPECT_THAT(Run({"ft.search", "i1", "studies"}), AreDocIds("d:2"));
EXPECT_THAT(Run({"ft.search", "i1", "secret research"}), AreDocIds("d:3"));
}
// Update visits - numeric value
{
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[50 1000]"}), AreDocIds("d:1", "d:2"));
Run({"hset", "d:3", "visits", "75"});
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[0 49]"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[50 1000]"}), AreDocIds("d:1", "d:2", "d:3"));
Run({"hset", "d:1", "visits", "125"});
Run({"hset", "d:2", "visits", "150"});
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[100 1000]"}), AreDocIds("d:1", "d:2"));
Run({"hset", "d:3", "visits", "175"});
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[0 100]"}), kNoResults);
EXPECT_THAT(Run({"ft.search", "i1", "@visits:[150 1000]"}), AreDocIds("d:2", "d:3"));
}
// Delete documents
{
Run({"del", "d:2", "d:3"});
EXPECT_THAT(Run({"ft.search", "i1", "dragonfly"}), AreDocIds("d:1"));
EXPECT_THAT(Run({"ft.search", "i1", "butterfly | bumblebee"}), kNoResults);
}
}
TEST_F(SearchFamilyTest, Unicode) {
EXPECT_EQ(Run({"ft.create", "i1", "schema", "title", "text", "visits", "numeric"}), "OK");
// Explicitly using screaming uppercase to check utf-8 to lowercase functionality
Run({"hset", "d:1", "title", "Веселая СТРЕКОЗА Иван", "visits", "400"});
Run({"hset", "d:2", "title", "Die fröhliche Libelle Günther", "visits", "300"});
Run({"hset", "d:3", "title", "השפירית המהירה יעקב", "visits", "200"});
Run({"hset", "d:4", "title", "πανίσχυρη ΛΙΒΕΛΛΟΎΛΗ Δίας", "visits", "100"});
// Check we find our dragonfly in all languages
EXPECT_THAT(Run({"ft.search", "i1", "стРекоЗа|liBellE|השפירית|λΙβελλοΎλη"}),
AreDocIds("d:1", "d:2", "d:3", "d:4"));
// Check the result is valid
auto resp = Run({"ft.search", "i1", "λιβελλούλη"});
EXPECT_THAT(resp,
IsMapWithSize("d:4", IsMap("visits", "100", "title", "πανίσχυρη ΛΙΒΕΛΛΟΎΛΗ Δίας")));
}
TEST_F(SearchFamilyTest, UnicodeWords) {
EXPECT_EQ(Run({"ft.create", "i1", "schema", "title", "text"}), "OK");
Run({"hset", "d:1", "title",
"WORD!!! Одно слово? Zwei Wörter. Comma before ,sentence, "
"Τρεις λέξεις: χελώνα-σκύλου-γάτας. !זה עובד",
"visits", "400"});
// Make sure it includes ALL those words
EXPECT_THAT(Run({"ft.search", "i1", "word слово wörter sentence λέξεις γάτας עובד"}),
AreDocIds("d:1"));
}
TEST_F(SearchFamilyTest, BasicSort) {
auto AreRange = [](size_t total, size_t l, size_t r, string_view prefix) {
vector<string> out;
for (size_t i = min(l, r); i < max(l, r); i++)
out.push_back(absl::StrCat(prefix, i));
if (l > r)
reverse(out.begin(), out.end());
return DocIds(total, out);
};
// max_memory_limit = INT_MAX;
Run({"ft.create", "i1", "prefix", "1", "d:", "schema", "ord", "numeric", "sortable"});
for (size_t i = 0; i < 100; i++)
Run({"hset", absl::StrCat("d:", i), "ord", absl::StrCat(i)});
// Sort ranges of 23 elements
for (size_t i = 0; i < 77; i++)
EXPECT_THAT(Run({"ft.search", "i1", "*", "SORTBY", "ord", "LIMIT", to_string(i), "23"}),
AreRange(100, i, i + 23, "d:"));
// Sort ranges of 27 elements in reverse
for (size_t i = 0; i < 73; i++)
EXPECT_THAT(Run({"ft.search", "i1", "*", "SORTBY", "ord", "DESC", "LIMIT", to_string(i), "27"}),
AreRange(100, 100 - i, 100 - i - 27, "d:"));
Run({"ft.create", "i2", "prefix", "1", "d2:", "schema", "name", "text", "sortable"});
absl::InsecureBitGen gen;
vector<string> random_strs;
for (size_t i = 0; i < 10; i++)
random_strs.emplace_back(dfly::GetRandomHex(gen, 7));
sort(random_strs.begin(), random_strs.end());
for (size_t i = 0; i < 10; i++)
Run({"hset", absl::StrCat("d2:", i), "name", random_strs[i]});
for (size_t i = 0; i < 7; i++)
EXPECT_THAT(Run({"ft.search", "i2", "*", "SORTBY", "name", "DESC", "LIMIT", to_string(i), "3"}),
AreRange(10, 10 - i, 10 - i - 3, "d2:"));
}
TEST_F(SearchFamilyTest, FtProfile) {
Run({"ft.create", "i1", "schema", "name", "text"});
auto resp = Run({"ft.profile", "i1", "search", "query", "(a | b) c d"});
ASSERT_ARRAY_OF_TWO_ARRAYS(resp);
const auto& top_level = resp.GetVec();
EXPECT_THAT(top_level[0], IsMapWithSize());
const auto& profile_result = top_level[1].GetVec();
EXPECT_EQ(profile_result.size(), shard_set->size() + 1);
EXPECT_THAT(profile_result[0].GetVec(), ElementsAre("took", _, "hits", _, "serialized", _));
for (size_t sid = 0; sid < shard_set->size(); sid++) {
const auto& shard_resp = profile_result[sid + 1].GetVec();
EXPECT_THAT(shard_resp, ElementsAre("took", _, "tree", _));
const auto& tree = shard_resp[3].GetVec();
EXPECT_THAT(tree[0].GetString(), HasSubstr("Logical{n=3,o=and}"sv));
EXPECT_EQ(tree[1].GetVec().size(), 3);
}
// Test LIMITED throws no errors
resp = Run({"ft.profile", "i1", "search", "limited", "query", "(a | b) c d"});
ASSERT_ARRAY_OF_TWO_ARRAYS(resp);
}
#ifndef SANITIZERS
TEST_F(SearchFamilyTest, FtProfileInvalidQuery) {
Run({"json.set", "j1", ".", R"({"id":"1"})"});
Run({"ft.create", "i1", "on", "json", "schema", "$.id", "as", "id", "tag"});
auto resp = Run({"ft.profile", "i1", "search", "query", "@id:[1 1]"});
ASSERT_ARRAY_OF_TWO_ARRAYS(resp);
EXPECT_THAT(resp.GetVec()[0], IsMapWithSize());
resp = Run({"ft.profile", "i1", "search", "query", "@{invalid13289}"});
EXPECT_THAT(resp, ErrArg("query syntax error"));
}
#endif
TEST_F(SearchFamilyTest, FtProfileErrorReply) {
Run({"ft.create", "i1", "schema", "name", "text"});
;
auto resp = Run({"ft.profile", "i1", "not_search", "query", "(a | b) c d"});
EXPECT_THAT(resp, ErrArg("no `SEARCH` or `AGGREGATE` provided"));
resp = Run({"ft.profile", "i1", "search", "not_query", "(a | b) c d"});
EXPECT_THAT(resp, ErrArg(kSyntaxErr));
resp = Run({"ft.profile", "non_existent_key", "search", "query", "(a | b) c d"});
EXPECT_THAT(resp, ErrArg("non_existent_key: no such index"));
}
TEST_F(SearchFamilyTest, SimpleExpiry) {
EXPECT_EQ(Run({"ft.create", "i1", "schema", "title", "text", "expires-in", "numeric"}), "OK");
Run({"hset", "d:1", "title", "never to expire", "expires-in", "100500"});
Run({"hset", "d:2", "title", "first to expire", "expires-in", "50"});
Run({"pexpire", "d:2", "50"});
Run({"hset", "d:3", "title", "second to expire", "expires-in", "100"});
Run({"pexpire", "d:3", "100"});
EXPECT_THAT(Run({"ft.search", "i1", "*"}), AreDocIds("d:1", "d:2", "d:3"));
AdvanceTime(60);
ThisFiber::SleepFor(5ms); // Give heartbeat time to delete expired doc
EXPECT_THAT(Run({"ft.search", "i1", "*"}), AreDocIds("d:1", "d:3"));
AdvanceTime(60);
Run({"HGETALL", "d:3"}); // Trigger expiry by access
EXPECT_THAT(Run({"ft.search", "i1", "*"}), AreDocIds("d:1"));
Run({"flushall"});
}
TEST_F(SearchFamilyTest, DocsEditing) {
auto resp = Run({"JSON.SET", "k1", ".", R"({"a":"1"})"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "index", "ON", "JSON", "SCHEMA", "$.a", "AS", "a", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "index", "*"});
EXPECT_THAT(resp, IsMapWithSize("k1", IsMap("$", R"({"a":"1"})")));
// Test dump and restore
resp = Run({"DUMP", "k1"});
auto dump = resp.GetBuf();
resp = Run({"DEL", "k1"});
EXPECT_THAT(resp, IntArg(1));
resp = Run({"RESTORE", "k1", "0", ToSV(dump)});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "index", "*"});
EXPECT_THAT(resp, IsMapWithSize("k1", IsMap("$", R"({"a":"1"})")));
// Test renaming a key
EXPECT_EQ(Run({"RENAME", "k1", "new_k1"}), "OK");
resp = Run({"FT.SEARCH", "index", "*"});
EXPECT_THAT(resp, IsMapWithSize("new_k1", IsMap("$", R"({"a":"1"})")));
EXPECT_EQ(Run({"RENAME", "new_k1", "k1"}), "OK");
resp = Run({"FT.SEARCH", "index", "*"});
EXPECT_THAT(resp, IsMapWithSize("k1", IsMap("$", R"({"a":"1"})")));
}
TEST_F(SearchFamilyTest, AggregateGroupBy) {
Run({"hset", "key:1", "word", "item1", "foo", "10", "text", "\"first key\"", "non_indexed_value",
"1"});
Run({"hset", "key:2", "word", "item2", "foo", "20", "text", "\"second key\"", "non_indexed_value",
"2"});
Run({"hset", "key:3", "word", "item1", "foo", "40", "text", "\"third key\"", "non_indexed_value",
"3"});
auto resp = Run(
{"ft.create", "i1", "ON", "HASH", "SCHEMA", "word", "TAG", "foo", "NUMERIC", "text", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run(
{"ft.aggregate", "i1", "*", "GROUPBY", "1", "@word", "REDUCE", "COUNT", "0", "AS", "count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("count", "2", "word", "item1"),
IsMap("word", "item2", "count", "1")));
resp = Run({"ft.aggregate", "i1", "*", "GROUPBY", "1", "@word", "REDUCE", "SUM", "1", "@foo",
"AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("foo_total", "50", "word", "item1"),
IsMap("foo_total", "20", "word", "item2")));
resp = Run({"ft.aggregate", "i1", "*", "GROUPBY", "1", "@word", "REDUCE", "AVG", "1", "@foo",
"AS", "foo_average"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("foo_average", "20", "word", "item2"),
IsMap("foo_average", "25", "word", "item1")));
resp = Run({"ft.aggregate", "i1", "*", "GROUPBY", "2", "@word", "@text", "REDUCE", "SUM", "1",
"@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("foo_total", "10", "word", "item1", "text", "\"first key\""),
IsMap("foo_total", "40", "word", "item1", "text", "\"third key\""),
IsMap("foo_total", "20", "word", "item2", "text", "\"second key\"")));
resp = Run({"ft.aggregate", "i1", "*", "LOAD", "2", "foo", "word", "GROUPBY", "1", "@word",
"REDUCE", "SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("foo_total", "20", "word", "item2"),
IsMap("foo_total", "50", "word", "item1")));
resp = Run({"ft.aggregate", "i1", "*", "LOAD", "2", "foo", "text", "GROUPBY", "2", "@word",
"@text", "REDUCE", "SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("foo_total", "40", "word", "item1", "text", "\"third key\""),
IsMap("foo_total", "20", "word", "item2", "text", "\"second key\""),
IsMap("foo_total", "10", "word", "item1", "text", "\"first key\"")));
}
TEST_F(SearchFamilyTest, JsonAggregateGroupBy) {
Run({"JSON.SET", "product:1", "$", R"({"name": "Product A", "price": 10, "quantity": 2})"});
Run({"JSON.SET", "product:2", "$", R"({"name": "Product B", "price": 20, "quantity": 3})"});
Run({"JSON.SET", "product:3", "$", R"({"name": "Product C", "price": 30, "quantity": 5})"});
auto resp =
Run({"FT.CREATE", "json_index", "ON", "JSON", "SCHEMA", "$.name", "AS", "name", "TEXT",
"$.price", "AS", "price", "NUMERIC", "$.quantity", "AS", "quantity", "NUMERIC"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.AGGREGATE", "json_index", "*", "GROUPBY", "0", "REDUCE", "SUM", "1", "price",
"AS", "total_price"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("total_price", "60")));
resp = Run({"FT.AGGREGATE", "json_index", "*", "GROUPBY", "0", "REDUCE", "AVG", "1", "price",
"AS", "avg_price"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("avg_price", "20")));
}
TEST_F(SearchFamilyTest, JsonAggregateGroupByWithoutAtSign) {
Run({"HSET", "h1", "group", "first", "value", "1"});
Run({"HSET", "h2", "group", "second", "value", "2"});
Run({"HSET", "h3", "group", "first", "value", "3"});
auto resp =
Run({"FT.CREATE", "index", "ON", "HASH", "SCHEMA", "group", "TAG", "value", "NUMERIC"});
EXPECT_EQ(resp, "OK");
// TODO: Throw an error when no '@' is provided in the GROUPBY option
resp = Run({"FT.AGGREGATE", "index", "*", "GROUPBY", "1", "group", "REDUCE", "COUNT", "0", "AS",
"count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("count", "2", "group", "first"),
IsMap("group", "second", "count", "1")));
}
TEST_F(SearchFamilyTest, AggregateGroupByReduceSort) {
for (size_t i = 0; i < 101; i++) { // 51 even, 50 odd
Run({"hset", absl::StrCat("k", i), "even", (i % 2 == 0) ? "true" : "false", "value",
absl::StrCat(i)});
}
Run({"ft.create", "i1", "schema", "even", "tag", "sortable", "value", "numeric", "sortable"});
// clang-format off
auto resp = Run({"ft.aggregate", "i1", "*",
"GROUPBY", "1", "@even",
"REDUCE", "count", "0", "as", "count",
"REDUCE", "count_distinct", "1", "even", "as", "distinct_tags",
"REDUCE", "count_distinct", "1", "value", "as", "distinct_vals",
"REDUCE", "max", "1", "value", "as", "max_val",
"REDUCE", "min", "1", "value", "as", "min_val",
"SORTBY", "1", "count"});
// clang-format on
EXPECT_THAT(resp,
IsUnordArrayWithSize(IsMap("even", "false", "count", "50", "distinct_tags", "1",
"distinct_vals", "50", "max_val", "99", "min_val", "1"),
IsMap("even", "true", "count", "51", "distinct_tags", "1",
"distinct_vals", "51", "max_val", "100", "min_val", "0")));
}
TEST_F(SearchFamilyTest, AggregateLoadGroupBy) {
for (size_t i = 0; i < 101; i++) { // 51 even, 50 odd
Run({"hset", absl::StrCat("k", i), "even", (i % 2 == 0) ? "true" : "false", "value",
absl::StrCat(i)});
}
Run({"ft.create", "i1", "schema", "value", "numeric", "sortable"});
// clang-format off
auto resp = Run({"ft.aggregate", "i1", "*",
"LOAD", "1", "even",
"GROUPBY", "1", "@even"});
// clang-format on
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("even", "false"), IsMap("even", "true")));
}
TEST_F(SearchFamilyTest, AggregateLoad) {
Run({"hset", "key:1", "word", "item1", "foo", "10"});
Run({"hset", "key:2", "word", "item2", "foo", "20"});
Run({"hset", "key:3", "word", "item1", "foo", "30"});
auto resp = Run({"ft.create", "index", "ON", "HASH", "SCHEMA", "word", "TAG", "foo", "NUMERIC"});
EXPECT_EQ(resp, "OK");
// ft.aggregate index "*" LOAD 1 @word LOAD 1 @foo
resp = Run({"ft.aggregate", "index", "*", "LOAD", "1", "@word", "LOAD", "1", "@foo"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("word", "item1", "foo", "30"),
IsMap("word", "item2", "foo", "20"),
IsMap("word", "item1", "foo", "10")));
// ft.aggregate index "*" GROUPBY 1 @word REDUCE SUM 1 @foo AS foo_total LOAD 1 foo_total
resp = Run({"ft.aggregate", "index", "*", "GROUPBY", "1", "@word", "REDUCE", "SUM", "1", "@foo",
"AS", "foo_total", "LOAD", "1", "foo_total"});
EXPECT_THAT(resp, ErrArg("LOAD cannot be applied after projectors or reducers"));
}
TEST_F(SearchFamilyTest, Vector) {
auto resp = Run({"ft.create", "ann", "ON", "HASH", "SCHEMA", "vector", "VECTOR", "HNSW", "8",
"TYPE", "FLOAT32", "DIM", "100", "distance_metric", "cosine", "M", "64"});
EXPECT_EQ(resp, "OK");
}
TEST_F(SearchFamilyTest, EscapedSymbols) {
Run({"ft.create", "i1", "ON", "HASH", "SCHEMA", "color", "tag"});
// TODO ',' is separator, we need to check should next request work or not
// In redis it works for JSON but not for HASH
// Run({"hset", "i1", "color", R"(blue,1\$+)"});
// EXPECT_THAT(Run({"ft.search", "i1", R"(@color:{blue\,1\\\$\+})"}), AreDocIds("i1"));
// EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue.1\"%="});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\.1\\\"\\%\\=}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue<1'^~"});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\<1\\'\\^\\~}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue>1:&/"});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\>1\\:\\&\\/}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue{1;* "});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\{1\\;\\*\\ }"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue}1!("});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\}1\\!\\(}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue[1@)"});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\[1\\@\\)}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
Run({"hset", "i1", "color", "blue]1#-"});
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue\\]1\\#\\-}"}), AreDocIds("i1"));
EXPECT_THAT(Run({"ft.search", "i1", "@color:{blue}"}), kNoResults);
}
TEST_F(SearchFamilyTest, FlushSearchIndices) {
auto resp =
Run({"FT.CREATE", "json", "ON", "JSON", "SCHEMA", "$.nested.value", "AS", "value", "TEXT"});
EXPECT_EQ(resp, "OK");
EXPECT_EQ(Run({"FLUSHALL"}), "OK");
// Test that the index was removed
resp = Run({"FT.CREATE", "json", "ON", "JSON", "SCHEMA", "$.another.nested.value", "AS", "value",
"TEXT"});
EXPECT_EQ(resp, "OK");
EXPECT_EQ(Run({"FLUSHDB"}), "OK");
// Test that the index was removed
resp = Run({"FT.CREATE", "json", "ON", "JSON", "SCHEMA", "$.another.nested.value", "AS", "value",
"TEXT"});
EXPECT_EQ(resp, "OK");
EXPECT_EQ(Run({"select", "1"}), "OK");
EXPECT_EQ(Run({"FLUSHDB"}), "OK");
EXPECT_EQ(Run({"select", "0"}), "OK");
// Test that index was not removed
resp = Run({"FT.CREATE", "json", "ON", "JSON", "SCHEMA", "$.another.nested.value", "AS", "value",
"TEXT"});
EXPECT_THAT(resp, ErrArg("ERR Index already exists"));
}
TEST_F(SearchFamilyTest, SearchWithLoadOptionHard) {
// Test HASH
Run({"HSET", "h1", "a", "1", "b", "one", "first", "h1"});
Run({"HSET", "h2", "a", "2", "b", "two", "second", "h2"});
Run({"HSET", "h3", "a", "1", "b", "one", "third", "h3"});
auto resp = Run({"FT.CREATE", "i1", "ON", "HASH", "SCHEMA", "@a", "TAG"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i1", "*", "LOAD", "1", "@b"});
EXPECT_THAT(resp, IsMapWithSize("h1", IsMap("a", "1", "b", "one", "first", "h1"), "h2",
IsMap("a", "2", "b", "two", "second", "h2"), "h3",
IsMap("a", "1", "b", "one", "third", "h3")));
// Test JSON
Run({"JSON.SET", "j1", ".", R"({"a":"1","b":"one","first":"j1"})"});
Run({"JSON.SET", "j2", ".", R"({"a":"2","b":"two","second":"j2"})"});
Run({"JSON.SET", "j3", ".", R"({"a":"1","b":"one","third":"j3"})"});
resp = Run({"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.a", "AS", "a", "TAG"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "$.b", "AS", "b"});
EXPECT_THAT(
resp, IsMapWithSize("j1", IsMap("b", "\"one\"", "$", R"({"a":"1","b":"one","first":"j1"})"),
"j2", IsMap("b", "\"two\"", "$", R"({"a":"2","b":"two","second":"j2"})"),
"j3", IsMap("b", "\"one\"", "$", R"({"a":"1","b":"one","third":"j3"})")));
}
// todo: ASAN fails heres on arm
#ifndef SANITIZERS
TEST_F(SearchFamilyTest, AggregateWithLoadOptionHard) {
// Test HASH
Run({"HSET", "h1", "word", "item1", "foo", "10", "text", "first key"});
Run({"HSET", "h2", "word", "item2", "foo", "20", "text", "second key"});
auto resp = Run(
{"FT.CREATE", "i1", "ON", "HASH", "SCHEMA", "word", "TAG", "foo", "NUMERIC", "text", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.AGGREGATE", "i1", "*", "LOAD", "2", "foo", "text", "GROUPBY", "2", "@word",
"@text", "REDUCE", "SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp,
IsUnordArrayWithSize(IsMap("foo_total", "20", "word", "item2", "text", "second key"),
IsMap("foo_total", "10", "word", "item1", "text", "first key")));
resp = Run({"FT.AGGREGATE", "i1", "*", "LOAD", "1", "@word", "GROUPBY", "1", "@word", "REDUCE",
"SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("foo_total", "20", "word", "item2"),
IsMap("foo_total", "10", "word", "item1")));
// Test JSON
Run({"JSON.SET", "j1", ".", R"({"word":"item1","foo":10,"text":"first key"})"});
Run({"JSON.SET", "j2", ".", R"({"word":"item2","foo":20,"text":"second key"})"});
resp = Run({"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.word", "AS", "word", "TAG", "$.foo",
"AS", "foo", "NUMERIC", "$.text", "AS", "text", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.AGGREGATE", "i2", "*", "LOAD", "2", "foo", "text", "GROUPBY", "2", "@word",
"@text", "REDUCE", "SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("foo_total", "20", "word", "\"item2\"", "text", "\"second key\""),
IsMap("foo_total", "10", "word", "\"item1\"", "text", "\"first key\"")));
resp = Run({"FT.AGGREGATE", "i2", "*", "LOAD", "1", "@word", "GROUPBY", "1", "@word", "REDUCE",
"SUM", "1", "@foo", "AS", "foo_total"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("foo_total", "20", "word", "\"item2\""),
IsMap("foo_total", "10", "word", "\"item1\"")));
}
#endif
TEST_F(SearchFamilyTest, WrongFieldTypeJson) {
// Test simple
Run({"JSON.SET", "j1", ".", R"({"value":"one"})"});
Run({"JSON.SET", "j2", ".", R"({"value":1})"});
EXPECT_EQ(Run({"FT.CREATE", "i1", "ON", "JSON", "SCHEMA", "$.value", "AS", "value", "NUMERIC",
"SORTABLE"}),
"OK");
auto resp = Run({"FT.SEARCH", "i1", "*"});
EXPECT_THAT(resp, AreDocIds("j2"));
resp = Run({"FT.AGGREGATE", "i1", "*", "LOAD", "1", "$.value"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("$.value", "1")));
// Test with two fields. One is loading
Run({"JSON.SET", "j3", ".", R"({"value":"two","another_value":1})"});
Run({"JSON.SET", "j4", ".", R"({"value":2,"another_value":2})"});
EXPECT_EQ(Run({"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.value", "AS", "value", "NUMERIC"}),
"OK");
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "$.another_value"});
EXPECT_THAT(
resp, IsMapWithSize("j2", IsMap("$", R"({"value":1})"), "j4",
IsMap("$", R"({"another_value":2,"value":2})", "$.another_value", "2")));
resp = Run({"FT.AGGREGATE", "i2", "*", "LOAD", "2", "$.value", "$.another_value", "GROUPBY", "2",
"$.value", "$.another_value", "REDUCE", "COUNT", "0", "AS", "count"});
EXPECT_THAT(resp,
IsUnordArrayWithSize(
IsMap("$.value", "1", "$.another_value", ArgType(RespExpr::NIL), "count", "1"),
IsMap("$.value", "2", "$.another_value", "2", "count", "1")));
// Test multiple field values
Run({"JSON.SET", "j5", ".", R"({"arr":[{"id":1},{"id":"two"}]})"});
Run({"JSON.SET", "j6", ".", R"({"arr":[{"id":1},{"id":2}]})"});
Run({"JSON.SET", "j7", ".", R"({"arr":[]})"});
resp = Run({"FT.CREATE", "i3", "ON", "JSON", "SCHEMA", "$.arr[*].id", "AS", "id", "NUMERIC"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i3", "*"});
EXPECT_THAT(resp, AreDocIds("j1", "j2", "j3", "j4", "j6", "j7")); // Only j5 fails
resp = Run({"FT.CREATE", "i4", "ON", "JSON", "SCHEMA", "$.arr[*].id", "AS", "id", "NUMERIC",
"SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i4", "*"});
EXPECT_THAT(resp, AreDocIds("j1", "j2", "j3", "j4", "j6", "j7")); // Only j5 fails
}
TEST_F(SearchFamilyTest, WrongFieldTypeHash) {
// Test simple
Run({"HSET", "h1", "value", "one"});
Run({"HSET", "h2", "value", "1"});
EXPECT_EQ(Run({"FT.CREATE", "i1", "ON", "HASH", "SCHEMA", "value", "NUMERIC", "SORTABLE"}), "OK");
auto resp = Run({"FT.SEARCH", "i1", "*"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("value", "1")));
resp = Run({"FT.AGGREGATE", "i1", "*", "LOAD", "1", "@value"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("value", "1")));
// Test with two fields. One is loading
Run({"HSET", "h3", "value", "two", "another_value", "1"});
Run({"HSET", "h4", "value", "2", "another_value", "2"});
EXPECT_EQ(Run({"FT.CREATE", "i2", "ON", "HASH", "SCHEMA", "value", "NUMERIC"}), "OK");
resp = Run({"FT.SEARCH", "i2", "*", "LOAD", "1", "@another_value"});
EXPECT_THAT(resp, IsMapWithSize("h2", IsMap("value", "1"), "h4",
IsMap("value", "2", "another_value", "2")));
resp = Run({"FT.AGGREGATE", "i2", "*", "LOAD", "2", "@value", "@another_value", "GROUPBY", "2",
"@value", "@another_value", "REDUCE", "COUNT", "0", "AS", "count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("value", "1", "another_value", ArgType(RespExpr::NIL), "count", "1"),
IsMap("value", "2", "another_value", "2", "count", "1")));
}
TEST_F(SearchFamilyTest, WrongFieldTypeHardJson) {
Run({"JSON.SET", "j1", ".", R"({"data":1,"name":"doc_with_int"})"});
Run({"JSON.SET", "j2", ".", R"({"data":"1","name":"doc_with_int_as_string"})"});
Run({"JSON.SET", "j3", ".", R"({"data":"string","name":"doc_with_string"})"});
Run({"JSON.SET", "j4", ".",
R"({"data":["first", "second", "third"],"name":"doc_with_strings"})"});
Run({"JSON.SET", "j5", ".", R"({"name":"no_data"})"});
Run({"JSON.SET", "j6", ".", R"({"data":[5,4,3],"name":"doc_with_vector"})"});
Run({"JSON.SET", "j7", ".", R"({"data":"[5,4,3]","name":"doc_with_vector_as_string"})"});
Run({"JSON.SET", "j8", ".", R"({"data":null,"name":"doc_with_null"})"});
Run({"JSON.SET", "j9", ".", R"({"data":[null, null, null],"name":"doc_with_nulls"})"});
Run({"JSON.SET", "j10", ".", R"({"data":true,"name":"doc_with_boolean"})"});
Run({"JSON.SET", "j11", ".", R"({"data":[true, false, true],"name":"doc_with_booleans"})"});
auto resp = Run({"FT.CREATE", "i1", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "NUMERIC"});
EXPECT_EQ(resp, "OK");
resp = Run(
{"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "NUMERIC", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i3", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "TAG"});
EXPECT_EQ(resp, "OK");
resp =
Run({"FT.CREATE", "i4", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "TAG", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i5", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "TEXT"});
EXPECT_EQ(resp, "OK");
resp =
Run({"FT.CREATE", "i6", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "TEXT", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i7", "ON", "JSON", "SCHEMA", "$.data", "AS", "data", "VECTOR", "FLAT",
"6", "TYPE", "FLOAT32", "DIM", "3", "DISTANCE_METRIC", "L2"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i1", "*"});
EXPECT_THAT(resp, AreDocIds("j1", "j5", "j6", "j8", "j9"));
resp = Run({"FT.SEARCH", "i2", "*"});
EXPECT_THAT(resp, AreDocIds("j1", "j5", "j6", "j8", "j9"));
resp = Run({"FT.SEARCH", "i3", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j3", "j4", "j5", "j7", "j8", "j9", "j10", "j11"));
resp = Run({"FT.SEARCH", "i4", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j3", "j4", "j5", "j7", "j8", "j9", "j10", "j11"));
resp = Run({"FT.SEARCH", "i5", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j3", "j4", "j5", "j7", "j8", "j9"));
resp = Run({"FT.SEARCH", "i6", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j3", "j4", "j5", "j7", "j8", "j9"));
resp = Run({"FT.SEARCH", "i7", "*"});
EXPECT_THAT(resp, AreDocIds("j5", "j6", "j8"));
}
TEST_F(SearchFamilyTest, WrongFieldTypeHardHash) {
Run({"HSET", "j1", "data", "1", "name", "doc_with_int"});
Run({"HSET", "j2", "data", "1", "name", "doc_with_int_as_string"});
Run({"HSET", "j3", "data", "string", "name", "doc_with_string"});
Run({"HSET", "j4", "name", "no_data"});
Run({"HSET", "j5", "data", "5,4,3", "name", "doc_with_fake_vector"});
Run({"HSET", "j6", "data", "[5,4,3]", "name", "doc_with_fake_vector_as_string"});
// Vector [1, 2, 3]
std::string vector = std::string("\x3f\x80\x00\x00\x40\x00\x00\x00\x40\x40\x00\x00", 12);
Run({"HSET", "j7", "data", vector, "name", "doc_with_vector [1, 2, 3]"});
auto resp = Run({"FT.CREATE", "i1", "ON", "HASH", "SCHEMA", "data", "NUMERIC"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i2", "ON", "HASH", "SCHEMA", "data", "NUMERIC", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i3", "ON", "HASH", "SCHEMA", "data", "TAG"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i4", "ON", "HASH", "SCHEMA", "data", "TAG", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i5", "ON", "HASH", "SCHEMA", "data", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i6", "ON", "HASH", "SCHEMA", "data", "TEXT", "SORTABLE"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.CREATE", "i7", "ON", "HASH", "SCHEMA", "data", "VECTOR", "FLAT", "6", "TYPE",
"FLOAT32", "DIM", "3", "DISTANCE_METRIC", "L2"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "i1", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j1", "j4"));
resp = Run({"FT.SEARCH", "i2", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j1", "j4"));
resp = Run({"FT.SEARCH", "i3", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j7", "j3", "j6", "j1", "j4", "j5"));
resp = Run({"FT.SEARCH", "i4", "*"});
EXPECT_THAT(resp, AreDocIds("j2", "j7", "j3", "j6", "j1", "j4", "j5"));
resp = Run({"FT.SEARCH", "i5", "*"});
EXPECT_THAT(resp, AreDocIds("j4", "j2", "j7", "j3", "j6", "j1", "j5"));
resp = Run({"FT.SEARCH", "i6", "*"});
EXPECT_THAT(resp, AreDocIds("j4", "j2", "j7", "j3", "j6", "j1", "j5"));
resp = Run({"FT.SEARCH", "i7", "*"});
EXPECT_THAT(resp, AreDocIds("j4", "j7"));
}
TEST_F(SearchFamilyTest, WrongVectorFieldType) {
Run({"JSON.SET", "j1", ".",
R"({"vector_field": [0.1, 0.2, 0.3], "name": "doc_with_correct_dim"})"});
Run({"JSON.SET", "j2", ".", R"({"vector_field": [0.1, 0.2], "name": "doc_with_small_dim"})"});
Run({"JSON.SET", "j3", ".",
R"({"vector_field": [0.1, 0.2, 0.3, 0.4], "name": "doc_with_large_dim"})"});
Run({"JSON.SET", "j4", ".", R"({"vector_field": [1, 2, 3], "name": "doc_with_int_values"})"});
Run({"JSON.SET", "j5", ".",
R"({"vector_field":"not_vector", "name":"doc_with_incorrect_field_type"})"});
Run({"JSON.SET", "j6", ".", R"({"name":"doc_with_no_field"})"});
Run({"JSON.SET", "j7", ".",
R"({"vector_field": [999999999999999999999999999999999999999, -999999999999999999999999999999999999999, 500000000000000000000000000000000000000], "name": "doc_with_out_of_range_values"})"});
Run({"JSON.SET", "j8", ".", R"({"vector_field":null, "name": "doc_with_null"})"});
Run({"JSON.SET", "j9", ".", R"({"vector_field":[null, null, null], "name": "doc_with_nulls"})"});
Run({"JSON.SET", "j10", ".", R"({"vector_field":true, "name": "doc_with_boolean"})"});
Run({"JSON.SET", "j11", ".",
R"({"vector_field":[true, false, true], "name": "doc_with_booleans"})"});
Run({"JSON.SET", "j12", ".", R"({"vector_field":1, "name": "doc_with_int"})"});
auto resp =
Run({"FT.CREATE", "index", "ON", "JSON", "SCHEMA", "$.vector_field", "AS", "vector_field",
"VECTOR", "FLAT", "6", "TYPE", "FLOAT32", "DIM", "3", "DISTANCE_METRIC", "L2"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.SEARCH", "index", "*"});
EXPECT_THAT(resp, AreDocIds("j6", "j7", "j1", "j4", "j8"));
}
#ifndef SANITIZERS
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"})")));
}
#endif
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")));
}
// Test that FT.AGGREGATE prints only needed fields
TEST_F(SearchFamilyTest, AggregateResultFields) {
Run({"JSON.SET", "j1", ".", R"({"a":"1","b":"2","c":"3"})"});
Run({"JSON.SET", "j2", ".", R"({"a":"4","b":"5","c":"6"})"});
Run({"JSON.SET", "j3", ".", R"({"a":"7","b":"8","c":"9"})"});
auto resp = Run({"FT.CREATE", "i1", "ON", "JSON", "SCHEMA", "$.a", "AS", "a", "TEXT", "SORTABLE",
"$.b", "AS", "b", "TEXT", "$.c", "AS", "c", "TEXT"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.AGGREGATE", "i1", "*"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap(), IsMap(), IsMap()));
resp = Run({"FT.AGGREGATE", "i1", "*", "SORTBY", "1", "a"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("a", "1"), IsMap("a", "4"), IsMap("a", "7")));
resp = Run({"FT.AGGREGATE", "i1", "*", "LOAD", "1", "@b", "SORTBY", "1", "a"});
EXPECT_THAT(resp,
IsUnordArrayWithSize(IsMap("b", "\"2\"", "a", "1"), IsMap("b", "\"5\"", "a", "4"),
IsMap("b", "\"8\"", "a", "7")));
resp = Run({"FT.AGGREGATE", "i1", "*", "SORTBY", "1", "a", "GROUPBY", "2", "@b", "@a", "REDUCE",
"COUNT", "0", "AS", "count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("b", "\"8\"", "a", "7", "count", "1"),
IsMap("b", "\"2\"", "a", "1", "count", "1"),
IsMap("b", "\"5\"", "a", "4", "count", "1")));
Run({"JSON.SET", "j4", ".", R"({"id":1, "number":4})"});
Run({"JSON.SET", "j5", ".", R"({"id":2})"});
resp = Run({"FT.CREATE", "i2", "ON", "JSON", "SCHEMA", "$.id", "AS", "id", "NUMERIC", "$.number",
"AS", "number", "NUMERIC"});
EXPECT_EQ(resp, "OK");
resp = Run({"FT.AGGREGATE", "i2", "*", "LOAD", "2", "@id", "@number"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("id", "1", "number", "4"), IsMap("id", "2"), IsMap(),
IsMap(), IsMap()));
}
TEST_F(SearchFamilyTest, AggregateSortByJson) {
Run({"JSON.SET", "j1", "$", R"({"name": "first", "number": 1200, "group": "first"})"});
Run({"JSON.SET", "j2", "$", R"({"name": "second", "number": 800, "group": "first"})"});
Run({"JSON.SET", "j3", "$", R"({"name": "third", "number": 300, "group": "first"})"});
Run({"JSON.SET", "j4", "$", R"({"name": "fourth", "number": 400, "group": "second"})"});
Run({"JSON.SET", "j5", "$", R"({"name": "fifth", "number": 900, "group": "second"})"});
Run({"JSON.SET", "j6", "$", R"({"name": "sixth", "number": 300, "group": "first"})"});
Run({"JSON.SET", "j7", "$", R"({"name": "seventh", "number": 400, "group": "second"})"});
Run({"JSON.SET", "j8", "$", R"({"name": "eighth", "group": "first"})"});
Run({"JSON.SET", "j9", "$", R"({"name": "ninth", "group": "second"})"});
Run({"FT.CREATE", "index", "ON", "JSON", "SCHEMA", "$.name", "AS", "name", "TEXT", "$.number",
"AS", "number", "NUMERIC", "$.group", "AS", "group", "TAG"});
// Test sorting by name (DESC) and number (ASC)
auto resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "4", "@name", "DESC", "@number", "ASC"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("name", "\"third\"", "number", "300"),
IsMap("name", "\"sixth\"", "number", "300"),
IsMap("name", "\"seventh\"", "number", "400"),
IsMap("name", "\"second\"", "number", "800"), IsMap("name", "\"ninth\""),
IsMap("name", "\"fourth\"", "number", "400"),
IsMap("name", "\"first\"", "number", "1200"),
IsMap("name", "\"fifth\"", "number", "900"), IsMap("name", "\"eighth\"")));
// Test sorting by name (ASC) and number (DESC)
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "4", "@name", "ASC", "@number", "DESC"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("name", "\"eighth\""), IsMap("name", "\"fifth\"", "number", "900"),
IsMap("name", "\"first\"", "number", "1200"),
IsMap("name", "\"fourth\"", "number", "400"), IsMap("name", "\"ninth\""),
IsMap("name", "\"second\"", "number", "800"),
IsMap("name", "\"seventh\"", "number", "400"),
IsMap("name", "\"sixth\"", "number", "300"),
IsMap("name", "\"third\"", "number", "300")));
// Test sorting by group (ASC), number (DESC), and name
resp = Run(
{"FT.AGGREGATE", "index", "*", "SORTBY", "5", "@group", "ASC", "@number", "DESC", "@name"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("group", "\"first\"", "number", "1200", "name", "\"first\""),
IsMap("group", "\"first\"", "number", "800", "name", "\"second\""),
IsMap("group", "\"first\"", "number", "300", "name", "\"sixth\""),
IsMap("group", "\"first\"", "number", "300", "name", "\"third\""),
IsMap("group", "\"first\"", "name", "\"eighth\""),
IsMap("group", "\"second\"", "number", "900", "name", "\"fifth\""),
IsMap("group", "\"second\"", "number", "400", "name", "\"fourth\""),
IsMap("group", "\"second\"", "number", "400", "name", "\"seventh\""),
IsMap("group", "\"second\"", "name", "\"ninth\"")));
// Test sorting by number (ASC), group (DESC), and name
resp = Run(
{"FT.AGGREGATE", "index", "*", "SORTBY", "5", "@number", "ASC", "@group", "DESC", "@name"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("number", "300", "group", "\"first\"", "name", "\"sixth\""),
IsMap("number", "300", "group", "\"first\"", "name", "\"third\""),
IsMap("number", "400", "group", "\"second\"", "name", "\"fourth\""),
IsMap("number", "400", "group", "\"second\"", "name", "\"seventh\""),
IsMap("number", "800", "group", "\"first\"", "name", "\"second\""),
IsMap("number", "900", "group", "\"second\"", "name", "\"fifth\""),
IsMap("number", "1200", "group", "\"first\"", "name", "\"first\""),
IsMap("group", "\"second\"", "name", "\"ninth\""),
IsMap("group", "\"first\"", "name", "\"eighth\"")));
// Test sorting with MAX 3
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@number", "MAX", "3"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("number", "300"), IsMap("number", "300"),
IsMap("number", "400")));
// Test sorting with MAX 3
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "2", "@number", "DESC", "MAX", "3"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("number", "1200"), IsMap("number", "900"),
IsMap("number", "800")));
// Test sorting by number (ASC) with MAX 999
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@number", "MAX", "999"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("number", "300"), IsMap("number", "300"),
IsMap("number", "400"), IsMap("number", "400"),
IsMap("number", "800"), IsMap("number", "900"),
IsMap("number", "1200"), IsMap(), IsMap()));
// Test sorting by name and number (DESC)
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "3", "@name", "@number", "DESC"});
EXPECT_THAT(resp, IsUnordArrayWithSize(
IsMap("name", "\"eighth\""), IsMap("name", "\"fifth\"", "number", "900"),
IsMap("name", "\"first\"", "number", "1200"),
IsMap("name", "\"fourth\"", "number", "400"), IsMap("name", "\"ninth\""),
IsMap("name", "\"second\"", "number", "800"),
IsMap("name", "\"seventh\"", "number", "400"),
IsMap("name", "\"sixth\"", "number", "300"),
IsMap("name", "\"third\"", "number", "300")));
// Test SORTBY with MAX, GROUPBY, and REDUCE COUNT
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@name", "MAX", "3", "GROUPBY", "1",
"@number", "REDUCE", "COUNT", "0", "AS", "count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("number", "900", "count", "1"),
IsMap("number", ArgType(RespExpr::NIL), "count", "1"),
IsMap("number", "1200", "count", "1")));
// Test SORTBY with MAX, GROUPBY (0 fields), and REDUCE COUNT
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@name", "MAX", "3", "GROUPBY", "0",
"REDUCE", "COUNT", "0", "AS", "count"});
EXPECT_THAT(resp, IsUnordArrayWithSize(IsMap("count", "3")));
}
TEST_F(SearchFamilyTest, AggregateSortByParsingErrors) {
Run({"JSON.SET", "j1", "$", R"({"name": "first", "number": 1200, "group": "first"})"});
Run({"JSON.SET", "j2", "$", R"({"name": "second", "number": 800, "group": "first"})"});
Run({"JSON.SET", "j3", "$", R"({"name": "third", "number": 300, "group": "first"})"});
Run({"JSON.SET", "j4", "$", R"({"name": "fourth", "number": 400, "group": "second"})"});
Run({"JSON.SET", "j5", "$", R"({"name": "fifth", "number": 900, "group": "second"})"});
Run({"JSON.SET", "j6", "$", R"({"name": "sixth", "number": 300, "group": "first"})"});
Run({"JSON.SET", "j7", "$", R"({"name": "seventh", "number": 400, "group": "second"})"});
Run({"JSON.SET", "j8", "$", R"({"name": "eighth", "group": "first"})"});
Run({"JSON.SET", "j9", "$", R"({"name": "ninth", "group": "second"})"});
Run({"FT.CREATE", "index", "ON", "JSON", "SCHEMA", "$.name", "AS", "name", "TEXT", "$.number",
"AS", "number", "NUMERIC", "$.group", "AS", "group", "TAG"});
// Test SORTBY with invalid argument count
auto resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "999", "@name", "@number", "DESC"});
EXPECT_THAT(resp, ErrArg("bad arguments for SORTBY: specified invalid number of strings"));
// Test SORTBY with negative argument count
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "-3", "@name", "@number", "DESC"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test MAX with invalid value
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@name", "MAX", "-10"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test MAX without a value
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@name", "MAX"});
EXPECT_THAT(resp, ErrArg(kSyntaxErr));
// Test SORTBY with a non-existing field
/* Temporary unsupported
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "1", "@nonexistingfield"});
EXPECT_THAT(resp, ErrArg("Property `nonexistingfield` not loaded nor in schema")); */
// Test SORTBY with an invalid value
resp = Run({"FT.AGGREGATE", "index", "*", "SORTBY", "notvalue", "@name"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
}
TEST_F(SearchFamilyTest, InvalidSearchOptions) {
Run({"JSON.SET", "j1", ".", R"({"field1":"first","field2":"second"})"});
Run({"FT.CREATE", "idx", "ON", "JSON", "SCHEMA", "$.field1", "AS", "field1", "TEXT", "$.field2",
"AS", "field2", "TEXT"});
/* Test with an empty query and LOAD. TODO: Add separate test for query syntax
auto resp = Run({"FT.SEARCH", "idx", "", "LOAD", "1", "@field1"});
EXPECT_THAT(resp, IsMapWithSize()); */
// Test with LIMIT missing arguments
auto resp = Run({"FT.SEARCH", "idx", "*", "LIMIT", "0"});
EXPECT_THAT(resp, ErrArg(kSyntaxErr));
// Test with LIMIT exceeding the maximum allowed value
resp = Run({"FT.SEARCH", "idx", "*", "LIMIT", "0", "100000000000000000000"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with LIMIT and negative arguments
resp = Run({"FT.SEARCH", "idx", "*", "LIMIT", "-1", "10"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with LIMIT and invalid argument types
resp = Run({"FT.SEARCH", "idx", "*", "LIMIT", "start", "count"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with invalid LOAD arguments
resp = Run({"FT.SEARCH", "idx", "*", "LOAD", "@field1", "@field2"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with duplicate fields in LOAD
resp = Run({"FT.SEARCH", "idx", "*", "LOAD", "4", "@field1", "@field1", "@field2", "@field2"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("field1", "\"first\"", "field2", "\"second\"", "$",
R"({"field1":"first","field2":"second"})")));
// Test with LOAD exceeding maximum allowed count
resp = Run({"FT.SEARCH", "idx", "*", "LOAD", "100000000000000000000", "@field1", "@field2"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with invalid RETURN syntax (missing count)
resp = Run({"FT.SEARCH", "idx", "*", "RETURN", "@field1", "@field2"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with RETURN having duplicate fields
resp = Run({"FT.SEARCH", "idx", "*", "RETURN", "4", "field1", "field1", "field2", "field2"});
EXPECT_THAT(resp, IsMapWithSize("j1", IsMap("field1", "\"first\"", "field2", "\"second\"")));
// Test with RETURN exceeding maximum allowed count
resp = Run({"FT.SEARCH", "idx", "*", "RETURN", "100000000000000000000", "@field1", "@field2"});
EXPECT_THAT(resp, ErrArg(kInvalidIntErr));
// Test with NOCONTENT and LOAD
resp = Run({"FT.SEARCH", "idx", "*", "NOCONTENT", "LOAD", "2", "@field1", "@field2"});
EXPECT_THAT(resp, IsArray(IntArg(1), "j1"));
// Test with NOCONTENT and RETURN
resp = Run({"FT.SEARCH", "idx", "*", "NOCONTENT", "RETURN", "2", "@field1", "@field2"});
EXPECT_THAT(resp, IsArray(IntArg(1), "j1"));
}
} // namespace dfly