blob: 80746f8197068ac1fc78465b8fc3fc8eb6bfe1b2 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#include "search/indexer.h"
#include <gtest/gtest.h>
#include <test_base.h>
#include <memory>
#include "search/index_info.h"
#include "search/search_encoding.h"
#include "storage/redis_metadata.h"
#include "types/redis_hash.h"
static auto T(const std::string& v) { return kqir::MakeValue<kqir::StringArray>(util::Split(v, ",")); }
struct IndexerTest : TestBase {
redis::GlobalIndexer indexer;
kqir::IndexMap map;
std::string ns = "index_test";
IndexerTest() : indexer(storage_.get()) {
redis::IndexMetadata hash_field_meta;
hash_field_meta.on_data_type = redis::IndexOnDataType::HASH;
auto hash_info = std::make_unique<kqir::IndexInfo>("hashtest", hash_field_meta, ns);
hash_info->Add(kqir::FieldInfo("x", std::make_unique<redis::TagFieldMetadata>()));
hash_info->Add(kqir::FieldInfo("y", std::make_unique<redis::NumericFieldMetadata>()));
hash_info->prefixes.prefixes.emplace_back("idxtesthash");
map.emplace("hashtest", std::move(hash_info));
auto hash_updater = std::make_unique<redis::IndexUpdater>(map.at("hashtest").get());
redis::IndexMetadata json_field_meta;
json_field_meta.on_data_type = redis::IndexOnDataType::JSON;
auto json_info = std::make_unique<kqir::IndexInfo>("jsontest", json_field_meta, ns);
json_info->Add(kqir::FieldInfo("$.x", std::make_unique<redis::TagFieldMetadata>()));
json_info->Add(kqir::FieldInfo("$.y", std::make_unique<redis::NumericFieldMetadata>()));
auto hnsw_field_meta = std::make_unique<redis::HnswVectorFieldMetadata>();
hnsw_field_meta->vector_type = redis::VectorType::FLOAT64;
hnsw_field_meta->dim = 3;
hnsw_field_meta->distance_metric = redis::DistanceMetric::L2;
json_info->Add(kqir::FieldInfo("$.z", std::move(hnsw_field_meta)));
json_info->prefixes.prefixes.emplace_back("idxtestjson");
map.emplace("jsontest", std::move(json_info));
auto json_updater = std::make_unique<redis::IndexUpdater>(map.at("jsontest").get());
indexer.Add(std::move(hash_updater));
indexer.Add(std::move(json_updater));
}
};
TEST_F(IndexerTest, HashTag) {
redis::Hash db(storage_.get(), ns);
auto cfhandler = storage_->GetCFHandle(ColumnFamilyID::Search);
{
auto s = indexer.Record(*ctx_, "no_exist", ns);
ASSERT_TRUE(s.Is<Status::NoPrefixMatched>());
}
auto key1 = "idxtesthash:k1";
auto idxname = "hashtest";
{
auto s = indexer.Record(*ctx_, key1, ns);
ASSERT_EQ(s.Msg(), Status::ok_msg);
ASSERT_EQ(s->updater->info->name, idxname);
ASSERT_TRUE(s->fields.empty());
uint64_t cnt = 0;
db.Set(*ctx_, key1, "x", "food,kitChen,Beauty", &cnt);
ASSERT_EQ(cnt, 1);
auto s2 = indexer.Update(*ctx_, *s);
ASSERT_TRUE(s2);
auto key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("food", key1);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("kitchen", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("beauty", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
}
{
auto s = indexer.Record(*ctx_, key1, ns);
ASSERT_TRUE(s);
ASSERT_EQ(s->updater->info->name, idxname);
ASSERT_EQ(s->fields.size(), 1);
ASSERT_EQ(s->fields["x"], T("food,kitChen,Beauty"));
uint64_t cnt = 0;
auto s_set = db.Set(*ctx_, key1, "x", "Clothing,FOOD,sport ", &cnt);
ASSERT_EQ(cnt, 0);
ASSERT_TRUE(s_set.ok());
auto s2 = indexer.Update(*ctx_, *s);
ASSERT_TRUE(s2);
auto key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("food", key1);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("clothing", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("sport", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("kitchen", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.IsNotFound());
key = redis::SearchKey(ns, idxname, "x").ConstructTagFieldData("beauty", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.IsNotFound());
}
}
TEST_F(IndexerTest, JsonTag) {
redis::Json db(storage_.get(), ns);
auto cfhandler = storage_->GetCFHandle(ColumnFamilyID::Search);
{
auto s = indexer.Record(*ctx_, "no_exist", ns);
ASSERT_TRUE(s.Is<Status::NoPrefixMatched>());
}
auto key1 = "idxtestjson:k1";
auto idxname = "jsontest";
{
auto s = indexer.Record(*ctx_, key1, ns);
ASSERT_TRUE(s);
ASSERT_EQ(s->updater->info->name, idxname);
ASSERT_TRUE(s->fields.empty());
auto s_set = db.Set(*ctx_, key1, "$", R"({"x": "food,kitChen,Beauty"})");
ASSERT_TRUE(s_set.ok());
auto s2 = indexer.Update(*ctx_, *s);
ASSERT_TRUE(s2);
auto key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("food", key1);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("kitchen", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("beauty", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
}
{
auto s = indexer.Record(*ctx_, key1, ns);
ASSERT_TRUE(s);
ASSERT_EQ(s->updater->info->name, idxname);
ASSERT_EQ(s->fields.size(), 1);
ASSERT_EQ(s->fields["$.x"], T("food,kitChen,Beauty"));
auto s_set = db.Set(*ctx_, key1, "$.x", "\" Clothing, FOOD ,sport\"");
ASSERT_TRUE(s_set.ok());
auto s2 = indexer.Update(*ctx_, *s);
ASSERT_TRUE(s2);
auto key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("food", key1);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("clothing", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("sport", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("kitchen", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.IsNotFound());
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("beauty", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.IsNotFound());
}
}
TEST_F(IndexerTest, JsonTagBuildIndex) {
redis::Json db(storage_.get(), ns);
auto cfhandler = storage_->GetCFHandle(ColumnFamilyID::Search);
auto key1 = "idxtestjson:k2";
auto idxname = "jsontest";
{
auto s_set = db.Set(*ctx_, key1, "$", R"({"x": "food , \nkitChen,Beauty\t "})");
ASSERT_TRUE(s_set.ok());
auto s2 = indexer.updater_list[1]->Build(*ctx_);
ASSERT_EQ(s2.Msg(), Status::ok_msg);
auto key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("food", key1);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("kitchen", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
key = redis::SearchKey(ns, idxname, "$.x").ConstructTagFieldData("beauty", key1);
s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, key, &val);
ASSERT_TRUE(s3.ok());
ASSERT_EQ(val, "");
}
}
TEST_F(IndexerTest, JsonHnswVector) {
redis::Json db(storage_.get(), ns);
auto cfhandler = storage_->GetCFHandle(ColumnFamilyID::Search);
{
auto s = indexer.Record(*ctx_, "no_exist", ns);
ASSERT_TRUE(s.Is<Status::NoPrefixMatched>());
}
auto key3 = "idxtestjson:k3";
auto idxname = "jsontest";
{
auto s = indexer.Record(*ctx_, key3, ns);
ASSERT_TRUE(s);
ASSERT_EQ(s->updater->info->name, idxname);
ASSERT_TRUE(s->fields.empty());
auto s_set = db.Set(*ctx_, key3, "$", R"({"z": [1,2,3]})");
ASSERT_TRUE(s_set.ok());
auto s2 = indexer.Update(*ctx_, *s);
EXPECT_EQ(s2.Msg(), Status::ok_msg);
auto search_key = redis::SearchKey(ns, idxname, "$.z").ConstructHnswNode(0, key3);
std::string val;
auto s3 = storage_->Get(*ctx_, ctx_->DefaultMultiGetOptions(), cfhandler, search_key, &val);
ASSERT_TRUE(s3.ok());
redis::HnswNodeFieldMetadata node_meta;
Slice input(val);
node_meta.Decode(&input);
EXPECT_EQ(node_meta.num_neighbours, 0);
std::vector<double> expected = {1, 2, 3};
EXPECT_EQ(expected, node_meta.vector);
}
}