blob: 49c024e4722647295f5aa191d53ffa3d5a95364d [file] [log] [blame]
// Copyright (C) 2019 Google LLC
//
// Licensed 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 <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <utility>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "icing/document-builder.h"
#include "icing/file/filesystem.h"
#include "icing/file/mock-filesystem.h"
#include "icing/icing-search-engine.h"
#include "icing/jni/jni-cache.h"
#include "icing/join/join-processor.h"
#include "icing/portable/equals-proto.h"
#include "icing/portable/platform.h"
#include "icing/proto/debug.pb.h"
#include "icing/proto/document.pb.h"
#include "icing/proto/document_wrapper.pb.h"
#include "icing/proto/initialize.pb.h"
#include "icing/proto/optimize.pb.h"
#include "icing/proto/persist.pb.h"
#include "icing/proto/reset.pb.h"
#include "icing/proto/schema.pb.h"
#include "icing/proto/scoring.pb.h"
#include "icing/proto/search.pb.h"
#include "icing/proto/status.pb.h"
#include "icing/proto/storage.pb.h"
#include "icing/proto/term.pb.h"
#include "icing/proto/usage.pb.h"
#include "icing/query/query-features.h"
#include "icing/schema-builder.h"
#include "icing/schema/section.h"
#include "icing/testing/common-matchers.h"
#include "icing/testing/fake-clock.h"
#include "icing/testing/icu-data-file-helper.h"
#include "icing/testing/jni-test-helpers.h"
#include "icing/testing/test-data.h"
#include "icing/testing/tmp-directory.h"
namespace icing {
namespace lib {
namespace {
using ::icing::lib::portable_equals_proto::EqualsProto;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::Return;
// For mocking purpose, we allow tests to provide a custom Filesystem.
class TestIcingSearchEngine : public IcingSearchEngine {
public:
TestIcingSearchEngine(const IcingSearchEngineOptions& options,
std::unique_ptr<const Filesystem> filesystem,
std::unique_ptr<const IcingFilesystem> icing_filesystem,
std::unique_ptr<Clock> clock,
std::unique_ptr<JniCache> jni_cache)
: IcingSearchEngine(options, std::move(filesystem),
std::move(icing_filesystem), std::move(clock),
std::move(jni_cache)) {}
};
std::string GetTestBaseDir() { return GetTestTempDir() + "/icing"; }
// This test is meant to cover all tests relating to
// IcingSearchEngine::GetSchema and IcingSearchEngine::SetSchema.
class IcingSearchEngineSchemaTest : public testing::Test {
protected:
void SetUp() override {
if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
// If we've specified using the reverse-JNI method for segmentation (i.e.
// not ICU), then we won't have the ICU data file included to set up.
// Technically, we could choose to use reverse-JNI for segmentation AND
// include an ICU data file, but that seems unlikely and our current BUILD
// setup doesn't do this.
// File generated via icu_data_file rule in //icing/BUILD.
std::string icu_data_file_path =
GetTestFilePath("icing/icu.dat");
ICING_ASSERT_OK(
icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
}
filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
}
void TearDown() override {
filesystem_.DeleteDirectoryRecursively(GetTestBaseDir().c_str());
}
const Filesystem* filesystem() const { return &filesystem_; }
private:
Filesystem filesystem_;
};
// Non-zero value so we don't override it to be the current time
constexpr int64_t kDefaultCreationTimestampMs = 1575492852000;
std::string GetSchemaDir() { return GetTestBaseDir() + "/schema_dir"; }
IcingSearchEngineOptions GetDefaultIcingOptions() {
IcingSearchEngineOptions icing_options;
icing_options.set_base_dir(GetTestBaseDir());
icing_options.set_document_store_namespace_id_fingerprint(true);
icing_options.set_use_new_qualified_id_join_index(true);
return icing_options;
}
DocumentProto CreateMessageDocument(std::string name_space, std::string uri) {
return DocumentBuilder()
.SetKey(std::move(name_space), std::move(uri))
.SetSchema("Message")
.AddStringProperty("body", "message body")
.AddInt64Property("indexableInteger", 123)
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
}
SchemaTypeConfigProto CreateMessageSchemaTypeConfig() {
return SchemaTypeConfigBuilder()
.SetType("Message")
.AddProperty(PropertyConfigBuilder()
.SetName("body")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("indexableInteger")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_REQUIRED))
.Build();
}
SchemaProto CreateMessageSchema() {
return SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
}
ScoringSpecProto GetDefaultScoringSpec() {
ScoringSpecProto scoring_spec;
scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE);
return scoring_spec;
}
// TODO(b/272145329): create SearchSpecBuilder, JoinSpecBuilder,
// SearchResultProtoBuilder and ResultProtoBuilder for unit tests and build all
// instances by them.
TEST_F(IcingSearchEngineSchemaTest,
CircularReferenceCreateSectionManagerReturnsInvalidArgument) {
// Create a type config with a circular reference.
SchemaProto schema;
auto* type = schema.add_types();
type->set_schema_type("Message");
auto* body = type->add_properties();
body->set_property_name("recipient");
body->set_schema_type("Person");
body->set_data_type(PropertyConfigProto::DataType::DOCUMENT);
body->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
body->mutable_document_indexing_config()->set_index_nested_properties(true);
type = schema.add_types();
type->set_schema_type("Person");
body = type->add_properties();
body->set_property_name("recipient");
body->set_schema_type("Message");
body->set_data_type(PropertyConfigProto::DataType::DOCUMENT);
body->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
body->mutable_document_indexing_config()->set_index_nested_properties(true);
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.SetSchema(schema).status(),
ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
}
TEST_F(IcingSearchEngineSchemaTest, FailToReadSchema) {
IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
{
// Successfully initialize and set a schema
IcingSearchEngine icing(icing_options, GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
}
auto mock_filesystem = std::make_unique<MockFilesystem>();
// This fails FileBackedProto::Read() when we try to check the schema we
// had previously set
ON_CALL(*mock_filesystem,
OpenForRead(Eq(icing_options.base_dir() + "/schema_dir/schema.pb")))
.WillByDefault(Return(-1));
TestIcingSearchEngine test_icing(icing_options, std::move(mock_filesystem),
std::make_unique<IcingFilesystem>(),
std::make_unique<FakeClock>(),
GetTestJniCache());
InitializeResultProto initialize_result_proto = test_icing.Initialize();
EXPECT_THAT(initialize_result_proto.status(),
ProtoStatusIs(StatusProto::INTERNAL));
EXPECT_THAT(initialize_result_proto.status().message(),
HasSubstr("Unable to open file for read"));
}
TEST_F(IcingSearchEngineSchemaTest, FailToWriteSchema) {
IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
auto mock_filesystem = std::make_unique<MockFilesystem>();
// This fails FileBackedProto::Write()
ON_CALL(*mock_filesystem, OpenForWrite(HasSubstr("schema.pb")))
.WillByDefault(Return(-1));
TestIcingSearchEngine icing(icing_options, std::move(mock_filesystem),
std::make_unique<IcingFilesystem>(),
std::make_unique<FakeClock>(), GetTestJniCache());
EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
SetSchemaResultProto set_schema_result_proto =
icing.SetSchema(CreateMessageSchema());
EXPECT_THAT(set_schema_result_proto.status(),
ProtoStatusIs(StatusProto::INTERNAL));
EXPECT_THAT(set_schema_result_proto.status().message(),
HasSubstr("Unable to open file for write"));
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaIncompatibleFails) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with properties { "title", "body"}
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
// 2. Add an email document
DocumentProto doc = DocumentBuilder()
.SetKey("emails", "email#1")
.SetSchema("Email")
.AddStringProperty("title", "Hello world.")
.AddStringProperty("body", "Goodnight Moon.")
.Build();
EXPECT_THAT(icing.Put(std::move(doc)).status(), ProtoIsOk());
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 3. Set a schema that deletes email. This should fail.
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Message");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(
icing.SetSchema(schema, /*ignore_errors_and_delete_documents=*/false)
.status(),
ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
// 4. Try to delete by email type. This should succeed because email wasn't
// deleted in step 3.
EXPECT_THAT(icing.DeleteBySchemaType("Email").status(), ProtoIsOk());
}
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaIncompatibleForceOverrideSucceeds) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with properties { "title", "body"}
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
// 2. Add an email document
DocumentProto doc = DocumentBuilder()
.SetKey("emails", "email#1")
.SetSchema("Email")
.AddStringProperty("title", "Hello world.")
.AddStringProperty("body", "Goodnight Moon.")
.Build();
EXPECT_THAT(icing.Put(std::move(doc)).status(), ProtoIsOk());
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 3. Set a schema that deletes email with force override. This should
// succeed and delete the email type.
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Message");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema, true).status(), ProtoIsOk());
// 4. Try to delete by email type. This should fail because email was
// already deleted.
EXPECT_THAT(icing.DeleteBySchemaType("Email").status(),
ProtoStatusIs(StatusProto::NOT_FOUND));
}
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaUnsetVersionIsZero) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with version 1
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(0));
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaCompatibleVersionUpdateSucceeds) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with version 1
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(1);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
SetSchemaResultProto set_schema_result = icing.SetSchema(schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 2. Create schema that adds a new optional property and updates version.
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(2);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
// 3. SetSchema should succeed and the version number should be updated.
SetSchemaResultProto set_schema_result = icing.SetSchema(schema, true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_fully_compatible_changed_schema_types()
->Add("Email");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(2));
}
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaIncompatibleVersionUpdateFails) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with version 1
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(1);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 2. Create schema that makes an incompatible change (OPTIONAL -> REQUIRED)
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(2);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
// 3. SetSchema should fail and the version number should NOT be updated.
EXPECT_THAT(icing.SetSchema(schema).status(),
ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
}
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaIncompatibleVersionUpdateForceOverrideSucceeds) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with version 1
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(1);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 2. Create schema that makes an incompatible change (OPTIONAL -> REQUIRED)
// with force override to true.
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(2);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
// 3. SetSchema should succeed and the version number should be updated.
EXPECT_THAT(icing.SetSchema(schema, true).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(2));
}
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaNoChangeVersionUpdateSucceeds) {
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 1. Create a schema with an Email type with version 1
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(1);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
}
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// 2. Create schema that only changes the version.
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_version(2);
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
// 3. SetSchema should succeed and the version number should be updated.
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(2));
}
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaDuplicateTypesReturnsAlreadyExists) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with types { "Email", "Message" and "Email" }
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
type = schema.add_types();
type->set_schema_type("Message");
property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
*schema.add_types() = schema.types(0);
EXPECT_THAT(icing.SetSchema(schema).status(),
ProtoStatusIs(StatusProto::ALREADY_EXISTS));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaDuplicatePropertiesReturnsAlreadyExists) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with an Email type with properties { "title", "body" and
// "title" }
SchemaProto schema;
SchemaTypeConfigProto* type = schema.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
property = type->add_properties();
property->set_property_name("body");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema).status(),
ProtoStatusIs(StatusProto::ALREADY_EXISTS));
}
TEST_F(IcingSearchEngineSchemaTest, SetSchema) {
auto fake_clock = std::make_unique<FakeClock>();
fake_clock->SetTimerElapsedMilliseconds(1000);
TestIcingSearchEngine icing(GetDefaultIcingOptions(),
std::make_unique<Filesystem>(),
std::make_unique<IcingFilesystem>(),
std::move(fake_clock), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
auto message_document = CreateMessageDocument("namespace", "uri");
auto schema_with_message = CreateMessageSchema();
SchemaProto schema_with_email;
SchemaTypeConfigProto* type = schema_with_email.add_types();
type->set_schema_type("Email");
PropertyConfigProto* property = type->add_properties();
property->set_property_name("title");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
SchemaProto schema_with_email_and_message = schema_with_email;
*schema_with_email_and_message.add_types() = CreateMessageSchemaTypeConfig();
// Create an arbitrary invalid schema
SchemaProto invalid_schema;
SchemaTypeConfigProto* empty_type = invalid_schema.add_types();
empty_type->set_schema_type("");
// Make sure we can't set invalid schemas
SetSchemaResultProto set_schema_result = icing.SetSchema(invalid_schema);
EXPECT_THAT(set_schema_result.status(),
ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
// Can add an document of a set schema
set_schema_result = icing.SetSchema(schema_with_message);
EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
EXPECT_THAT(icing.Put(message_document).status(), ProtoIsOk());
// Schema with Email doesn't have Message, so would result incompatible
// data
set_schema_result = icing.SetSchema(schema_with_email);
EXPECT_THAT(set_schema_result.status(),
ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
// Can expand the set of schema types and add an document of a new
// schema type
set_schema_result = icing.SetSchema(schema_with_email_and_message);
EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
EXPECT_THAT(icing.Put(message_document).status(), ProtoIsOk());
// Can't add an document whose schema isn't set
auto photo_document = DocumentBuilder()
.SetKey("namespace", "uri")
.SetSchema("Photo")
.AddStringProperty("creator", "icing")
.Build();
PutResultProto put_result_proto = icing.Put(photo_document);
EXPECT_THAT(put_result_proto.status(), ProtoStatusIs(StatusProto::NOT_FOUND));
EXPECT_THAT(put_result_proto.status().message(),
HasSubstr("'Photo' not found"));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaNewIndexedStringPropertyTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with 2 properties:
// - 'a': string type, unindexed. No section id assigned.
// - 'b': int64 type, indexed. Section id = 0.
SchemaProto schema_one =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Schema")
.AddProperty(PropertyConfigBuilder()
.SetName("a")
.SetDataTypeString(TERM_MATCH_UNKNOWN,
TOKENIZER_NONE)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("b")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_REQUIRED)))
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Schema");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto document =
DocumentBuilder()
.SetKey("namespace", "uri")
.SetSchema("Schema")
.AddStringProperty("a", "message body")
.AddInt64Property("b", 123)
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
// Only 'b' will be indexed.
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
// Verify term search: won't get anything.
SearchSpecProto search_spec1;
search_spec1.set_query("a:message");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Verify numeric (integer) search: will get document.
SearchSpecProto search_spec2;
search_spec2.set_query("b == 123");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Change the schema to:
// - 'a': string type, indexed. Section id = 0.
// - 'b': int64 type, indexed. Section id = 1.
SchemaProto schema_two = schema_one;
schema_two.mutable_types(0)
->mutable_properties(0)
->mutable_string_indexing_config()
->set_term_match_type(TERM_MATCH_PREFIX);
schema_two.mutable_types(0)
->mutable_properties(0)
->mutable_string_indexing_config()
->set_tokenizer_type(TOKENIZER_PLAIN);
// Index restoration should be triggered here because new schema requires more
// properties to be indexed. Also new section ids will be reassigned and index
// restoration should use new section ids to rebuild.
set_schema_result = icing.SetSchema(schema_two);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Schema");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search: will get document now.
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search: will still get document.
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaNewIndexedIntegerPropertyTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with 2 properties:
// - 'a': int64 type, unindexed. No section id assigned.
// - 'b': string type, indexed. Section id = 0.
SchemaProto schema_one =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Schema")
.AddProperty(PropertyConfigBuilder()
.SetName("a")
.SetDataTypeInt64(NUMERIC_MATCH_UNKNOWN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("b")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Schema");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto document =
DocumentBuilder()
.SetKey("namespace", "uri")
.SetSchema("Schema")
.AddInt64Property("a", 123)
.AddStringProperty("b", "message body")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
// Only 'b' will be indexed.
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
// Verify term search: will get document.
SearchSpecProto search_spec1;
search_spec1.set_query("b:message");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search: won't get anything.
SearchSpecProto search_spec2;
search_spec2.set_query("a == 123");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Change the schema to:
// - 'a': int64 type, indexed. Section id = 0.
// - 'b': string type, indexed. Section id = 1.
SchemaProto schema_two = schema_one;
schema_two.mutable_types(0)
->mutable_properties(0)
->mutable_integer_indexing_config()
->set_numeric_match_type(NUMERIC_MATCH_RANGE);
// Index restoration should be triggered here because new schema requires more
// properties to be indexed. Also new section ids will be reassigned and index
// restoration should use new section ids to rebuild.
set_schema_result = icing.SetSchema(schema_two);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Schema");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search: will still get document.
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search: will get document now.
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(
IcingSearchEngineSchemaTest,
SetSchemaNewIndexedDocumentPropertyTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with a nested document type:
//
// Section id assignment for 'Person':
// - "age": integer type, indexed. Section id = 0
// - "name": string type, indexed. Section id = 1.
// - "worksFor.name": string type, (nested) indexed. Section id = 2.
//
// Joinable property id assignment for 'Person':
// - "worksFor.listRef": string type, Qualified Id type joinable. Joinable
// property id = 0.
SchemaProto schema_one =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("List").AddProperty(
PropertyConfigBuilder()
.SetName("title")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Person")
.AddProperty(PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("age")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("worksFor")
.SetDataTypeDocument(
"Organization",
/*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Organization")
.AddProperty(PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("listRef")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_REQUIRED)))
.Build();
ASSERT_THAT(icing.SetSchema(schema_one).status(), ProtoIsOk());
DocumentProto list_document = DocumentBuilder()
.SetKey("namespace", "list/1")
.SetSchema("List")
.SetCreationTimestampMs(1000)
.AddStringProperty("title", "title")
.Build();
DocumentProto person_document =
DocumentBuilder()
.SetKey("namespace", "person/2")
.SetSchema("Person")
.SetCreationTimestampMs(1000)
.AddStringProperty("name", "John")
.AddInt64Property("age", 20)
.AddDocumentProperty(
"worksFor", DocumentBuilder()
.SetKey("namespace", "org/1")
.SetSchema("Organization")
.AddStringProperty("name", "Google")
.AddStringProperty("listRef", "namespace#list/1")
.Build())
.Build();
EXPECT_THAT(icing.Put(list_document).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(person_document).status(), ProtoIsOk());
ResultSpecProto result_spec = ResultSpecProto::default_instance();
result_spec.set_max_joined_children_per_parent_to_return(
std::numeric_limits<int32_t>::max());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
person_document;
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
// Verify term search
SearchSpecProto search_spec1;
search_spec1.set_query("worksFor.name:Google");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search
SearchSpecProto search_spec2;
search_spec2.set_query("age == 20");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results =
icing.Search(search_spec2, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify join search: join a query for `title:title` (which will get
// list_document) with a child query for `name:John` (which will get
// person_document) based on the child's `worksFor.listRef` field.
SearchSpecProto search_spec_with_join;
search_spec_with_join.set_query("title:title");
search_spec_with_join.set_term_match_type(TermMatchType::EXACT_ONLY);
JoinSpecProto* join_spec = search_spec_with_join.mutable_join_spec();
join_spec->set_parent_property_expression(
std::string(JoinProcessor::kQualifiedIdExpr));
join_spec->set_child_property_expression("worksFor.listRef");
join_spec->set_aggregation_scoring_strategy(
JoinSpecProto::AggregationScoringStrategy::COUNT);
JoinSpecProto::NestedSpecProto* nested_spec =
join_spec->mutable_nested_spec();
SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
nested_search_spec->set_term_match_type(TermMatchType::EXACT_ONLY);
nested_search_spec->set_query("name:John");
*nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
*nested_spec->mutable_result_spec() = result_spec;
SearchResultProto expected_join_search_result_proto;
expected_join_search_result_proto.mutable_status()->set_code(StatusProto::OK);
SearchResultProto::ResultProto* result_proto =
expected_join_search_result_proto.mutable_results()->Add();
*result_proto->mutable_document() = list_document;
*result_proto->mutable_joined_results()->Add()->mutable_document() =
person_document;
actual_results =
icing.Search(search_spec_with_join, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_join_search_result_proto));
// Change the schema to add another nested document property to 'Person'
//
// New section id assignment for 'Person':
// - "age": integer type, indexed. Section id = 0
// - "almaMater.name", string type, indexed. Section id = 1
// - "name": string type, indexed. Section id = 2
// - "worksFor.name": string type, (nested) indexed. Section id = 3
//
// New joinable property id assignment for 'Person':
// - "almaMater.listRef": string type, Qualified Id type joinable. Joinable
// property id = 0.
// - "worksFor.listRef": string type, Qualified Id type joinable. Joinable
// property id = 1.
SchemaProto schema_two =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("List").AddProperty(
PropertyConfigBuilder()
.SetName("title")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Person")
.AddProperty(PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("age")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("worksFor")
.SetDataTypeDocument(
"Organization",
/*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("almaMater")
.SetDataTypeDocument(
"Organization",
/*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Organization")
.AddProperty(PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("listRef")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_REQUIRED)))
.Build();
// This schema change is compatible since the added 'almaMater' property has
// CARDINALITY_OPTIONAL.
//
// Index restoration should be triggered here because new schema requires more
// properties to be indexed. Also new section ids will be reassigned and index
// restoration should use new section ids to rebuild.
SetSchemaResultProto set_schema_result = icing.SetSchema(schema_two);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Person");
expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
->Add("Person");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search:
// Searching for "worksFor.name:Google" should still match document
actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// In new_schema the 'name' property is now indexed at section id 2. If
// searching for "name:Google" matched the document, this means that index
// rebuild was not triggered and Icing is still searching the old index, where
// 'worksFor.name' was indexed at section id 2.
search_spec1.set_query("name:Google");
actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Verify numeric (integer) search: should still match document
actual_results =
icing.Search(search_spec2, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify join search: should still able to join by `worksFor.listRef`
actual_results =
icing.Search(search_spec_with_join, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_join_search_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaChangeNestedPropertiesTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
SchemaTypeConfigProto person_proto =
SchemaTypeConfigBuilder()
.SetType("Person")
.AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("age")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL))
.Build();
// Create a schema with nested properties:
// - "sender.age": int64 type, (nested) indexed. Section id = 0.
// - "sender.name": string type, (nested) indexed. Section id = 1.
// - "subject": string type, indexed. Section id = 2.
// - "timestamp": int64 type, indexed. Section id = 3.
SchemaProto nested_schema =
SchemaBuilder()
.AddType(person_proto)
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument(
"Person",
/*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto document =
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddDocumentProperty("sender",
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Person")
.AddStringProperty("name", "Bill Lundbergh")
.AddInt64Property("age", 20)
.Build())
.AddInt64Property("timestamp", 1234)
.Build();
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
// Verify term search
// document should match a query for 'Bill' in 'sender.name', but not in
// 'subject'
SearchSpecProto search_spec1;
search_spec1.set_query("sender.name:Bill");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
search_spec1.set_query("subject:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Verify numeric (integer) search
// document should match a query for 20 in 'sender.age', but not in
// 'timestamp'
SearchSpecProto search_spec2;
search_spec2.set_query("sender.age == 20");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
search_spec2.set_query("timestamp == 20");
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Now update the schema with index_nested_properties=false. This should
// reassign section ids, lead to an index rebuild and ensure that nothing
// match a query for "Bill" or 20.
// - "sender.age": int64 type, (nested) unindexed. No section id assigned.
// - "sender.name": string type, (nested) unindexed. No section id assigned.
// - "subject": string type, indexed. Section id = 0.
// - "timestamp": int64 type, indexed. Section id = 1.
SchemaProto no_nested_schema =
SchemaBuilder()
.AddType(person_proto)
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument(
"Person",
/*index_nested_properties=*/false)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
set_schema_result = icing.SetSchema(no_nested_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Email");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search
// document shouldn't match a query for 'Bill' in either 'sender.name' or
// 'subject'
search_spec1.set_query("sender.name:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
search_spec1.set_query("subject:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Verify numeric (integer) search
// document shouldn't match a query for 20 in either 'sender.age' or
// 'timestamp'
search_spec2.set_query("sender.age == 20");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
search_spec2.set_query("timestamp == 20");
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
}
TEST_F(
IcingSearchEngineSchemaTest,
SetSchemaChangeNestedPropertiesListTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
SchemaTypeConfigProto person_proto =
SchemaTypeConfigBuilder()
.SetType("Person")
.AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("lastName")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("address")
.SetDataTypeString(TERM_MATCH_UNKNOWN, TOKENIZER_NONE)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("age")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("birthday")
.SetDataTypeInt64(NUMERIC_MATCH_UNKNOWN)
.SetCardinality(CARDINALITY_OPTIONAL))
.Build();
// Create a schema with nested properties:
// - "sender.address": string type, (nested) non-indexable. Section id = 0.
// - "sender.age": int64 type, (nested) indexed. Section id = 1.
// - "sender.birthday": int64 type, (nested) non-indexable. Section id = 2.
// - "sender.lastName": int64 type, (nested) indexed. Section id = 3.
// - "sender.name": string type, (nested) indexed. Section id = 4.
// - "subject": string type, indexed. Section id = 5.
// - "timestamp": int64 type, indexed. Section id = 6.
// - "sender.foo": unknown type, (nested) non-indexable. Section id = 7.
//
// "sender.address" and "sender.birthday" are assigned a section id because
// they are listed in the indexable_nested_properties_list for 'Email.sender'.
// They are assigned a sectionId but are not indexed since their indexing
// configs are non-indexable.
//
// "sender.foo" is also assigned a section id, but is also not undefined by
// the schema definition. Trying to index a document with this nested property
// should fail.
SchemaProto nested_schema =
SchemaBuilder()
.AddType(person_proto)
.AddType(
SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(
PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument(
"Person", /*indexable_nested_properties_list=*/
{"age", "lastName", "address", "name", "birthday",
"foo"})
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto document =
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddDocumentProperty(
"sender",
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Person")
.AddStringProperty("name", "Bill")
.AddStringProperty("lastName", "Lundbergh")
.AddStringProperty("address", "1600 Amphitheatre Pkwy")
.AddInt64Property("age", 20)
.AddInt64Property("birthday", 20)
.Build())
.AddInt64Property("timestamp", 1234)
.Build();
// Indexing this doc should fail, since the 'sender.foo' property is not found
DocumentProto invalid_document =
DocumentBuilder()
.SetKey("namespace2", "uri1")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddDocumentProperty(
"sender",
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Person")
.AddStringProperty("name", "Bill")
.AddStringProperty("lastName", "Lundbergh")
.AddStringProperty("address", "1600 Amphitheatre Pkwy")
.AddInt64Property("age", 20)
.AddInt64Property("birthday", 20)
.AddBytesProperty("foo", "bar bytes")
.Build())
.AddInt64Property("timestamp", 1234)
.Build();
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(invalid_document).status(),
ProtoStatusIs(StatusProto::NOT_FOUND));
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
// Verify term search
// document should match a query for 'Bill' in 'sender.name', but not in
// 'sender.lastName'
SearchSpecProto search_spec1;
search_spec1.set_query("sender.name:Bill");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
search_spec1.set_query("sender.lastName:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// document should match a query for 'Lundber' in 'sender.lastName', but not
// in 'sender.name'.
SearchSpecProto search_spec2;
search_spec2.set_query("sender.lastName:Lundber");
search_spec2.set_term_match_type(TermMatchType::PREFIX);
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
search_spec2.set_query("sender.name:Lundber");
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// document should not match a query for 'Amphitheatre' because the
// 'sender.address' field is not indexed.
search_spec2.set_query("Amphitheatre");
search_spec2.set_term_match_type(TermMatchType::PREFIX);
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Verify numeric (integer) search
// document should match a query for 20 in 'sender.age', but not in
// 'timestamp' or 'sender.birthday'
SearchSpecProto search_spec3;
search_spec3.set_query("sender.age == 20");
search_spec3.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec3.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec3, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
search_spec3.set_query("timestamp == 20");
actual_results = icing.Search(search_spec3, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
search_spec3.set_query("birthday == 20");
actual_results = icing.Search(search_spec3, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
// Now update the schema and don't index "sender.name", "sender.birthday" and
// "sender.foo".
// This should reassign section ids, lead to an index rebuild and ensure that
// nothing match a query for "Bill".
//
// Section id assignment:
// - "sender.address": string type, (nested) non-indexable. Section id = 0.
// - "sender.age": int64 type, (nested) indexed. Section id = 1.
// - "sender.birthday": int64 type, (nested) unindexed. No section id.
// - "sender.lastName": int64 type, (nested) indexed. Section id = 2.
// - "sender.name": string type, (nested) unindexed. No section id.
// - "subject": string type, indexed. Section id = 3.
// - "timestamp": int64 type, indexed. Section id = 4.
// - "sender.foo": unknown type, invalid. No section id.
SchemaProto nested_schema_with_less_props =
SchemaBuilder()
.AddType(person_proto)
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(
PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument(
"Person", /*indexable_nested_properties=*/
{"age", "lastName", "address"})
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
set_schema_result = icing.SetSchema(nested_schema_with_less_props);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Email");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search
// document shouldn't match a query for 'Bill' in either 'sender.name' or
// 'subject'
search_spec1.set_query("sender.name:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
search_spec1.set_query("subject:Bill");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaNewJoinablePropertyTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create "Message" schema with 3 properties:
// - "subject": string type, non-joinable. No joinable property id assigned.
// It is indexed and used for searching only.
// - "receiverQualifiedId": string type, non-joinable. No joinable property id
// assigned.
// - "senderQualifiedId": string type, Qualified Id type joinable. Joinable
// property id = 0.
SchemaProto schema_one =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Message")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("receiverQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_NONE)
.SetCardinality(CARDINALITY_REQUIRED))
.AddProperty(PropertyConfigBuilder()
.SetName("senderQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_REQUIRED)))
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_new_schema_types()->Add("Message");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto person1 =
DocumentBuilder()
.SetKey("namespace", "person1")
.SetSchema("Person")
.AddStringProperty("name", "person one")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
DocumentProto person2 =
DocumentBuilder()
.SetKey("namespace", "person2")
.SetSchema("Person")
.AddStringProperty("name", "person two")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
DocumentProto message =
DocumentBuilder()
.SetKey("namespace", "message1")
.SetSchema("Message")
.AddStringProperty("subject", "message")
.AddStringProperty("receiverQualifiedId", "namespace#person1")
.AddStringProperty("senderQualifiedId", "namespace#person2")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
EXPECT_THAT(icing.Put(person1).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(person2).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(message).status(), ProtoIsOk());
ResultSpecProto result_spec = ResultSpecProto::default_instance();
result_spec.set_max_joined_children_per_parent_to_return(
std::numeric_limits<int32_t>::max());
// Verify join search: join a query for `name:person` with a child query for
// `subject:message` based on the child's `receiverQualifiedId` field.
// Since "receiverQualifiedId" is not JOINABLE_VALUE_TYPE_QUALIFIED_ID,
// joining on that property should only return the "left-side" (`name:person`)
// of the join.
SearchSpecProto search_spec_join_by_receiver;
search_spec_join_by_receiver.set_query("name:person");
search_spec_join_by_receiver.set_term_match_type(TermMatchType::EXACT_ONLY);
JoinSpecProto* join_spec = search_spec_join_by_receiver.mutable_join_spec();
join_spec->set_parent_property_expression(
std::string(JoinProcessor::kQualifiedIdExpr));
join_spec->set_child_property_expression("receiverQualifiedId");
join_spec->set_aggregation_scoring_strategy(
JoinSpecProto::AggregationScoringStrategy::COUNT);
JoinSpecProto::NestedSpecProto* nested_spec =
join_spec->mutable_nested_spec();
SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
nested_search_spec->set_term_match_type(TermMatchType::EXACT_ONLY);
nested_search_spec->set_query("subject:message");
*nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
*nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
SearchResultProto expected_empty_child_search_result_proto;
expected_empty_child_search_result_proto.mutable_status()->set_code(
StatusProto::OK);
*expected_empty_child_search_result_proto.mutable_results()
->Add()
->mutable_document() = person2;
*expected_empty_child_search_result_proto.mutable_results()
->Add()
->mutable_document() = person1;
SearchResultProto actual_results = icing.Search(
search_spec_join_by_receiver, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_empty_child_search_result_proto));
// Verify join search: join a query for `name:person` with a child query for
// `subject:message` based on the child's `senderQualifiedId` field.
// Since "senderQualifiedId" is JOINABLE_VALUE_TYPE_QUALIFIED_ID, joining on
// that property should return both "left-side" (`name:person`) and
// "right-side" (`subject:message`) of the join.
SearchSpecProto search_spec_join_by_sender = search_spec_join_by_receiver;
join_spec = search_spec_join_by_sender.mutable_join_spec();
join_spec->set_child_property_expression("senderQualifiedId");
SearchResultProto expected_join_by_sender_search_result_proto;
expected_join_by_sender_search_result_proto.mutable_status()->set_code(
StatusProto::OK);
SearchResultProto::ResultProto* result_proto =
expected_join_by_sender_search_result_proto.mutable_results()->Add();
*result_proto->mutable_document() = person2;
*result_proto->mutable_joined_results()->Add()->mutable_document() = message;
*expected_join_by_sender_search_result_proto.mutable_results()
->Add()
->mutable_document() = person1;
actual_results = icing.Search(search_spec_join_by_sender,
GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_join_by_sender_search_result_proto));
// Change "Message" schema to:
// - "subject": string type, non-joinable. No joinable property id assigned.
// - "receiverQualifiedId": string type, Qualified Id joinable. Joinable
// property id = 0.
// - "senderQualifiedId": string type, Qualified Id joinable. Joinable
// property id = 1.
SchemaProto schema_two = schema_one;
schema_two.mutable_types(1)
->mutable_properties(1)
->mutable_joinable_config()
->set_value_type(JOINABLE_VALUE_TYPE_QUALIFIED_ID);
// Index restoration should be triggered here because new schema requires more
// joinable properties. Also new joinable property ids will be reassigned and
// index restoration should use new joinable property ids to rebuild.
set_schema_result = icing.SetSchema(schema_two);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
->Add("Message");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify join search: join a query for `name:person` with a child query for
// `subject:message` based on the child's `receiverQualifiedId` field.
// Since we've changed "receiverQualifiedId" to be
// JOINABLE_VALUE_TYPE_QUALIFIED_ID, joining on that property should return
// should return both "left-side" (`name:person`) and "right-side"
// (`subject:message`) of the join now.
SearchResultProto expected_join_by_receiver_search_result_proto;
expected_join_by_receiver_search_result_proto.mutable_status()->set_code(
StatusProto::OK);
result_proto =
expected_join_by_receiver_search_result_proto.mutable_results()->Add();
*result_proto->mutable_document() = person1;
*result_proto->mutable_joined_results()->Add()->mutable_document() = message;
*expected_join_by_receiver_search_result_proto.mutable_results()
->Add()
->mutable_document() = person2;
actual_results = icing.Search(search_spec_join_by_receiver,
GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(
expected_join_by_receiver_search_result_proto));
// Verify join search: join a query for `name:person` with a child query for
// `subject:message` based on the child's `senderQualifiedId` field. We should
// get the same set of result since `senderQualifiedId` is unchanged.
actual_results = icing.Search(search_spec_join_by_sender,
GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_join_by_sender_search_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaWithValidCycle_circularSchemaDefinitionNotAllowedFails) {
IcingSearchEngineOptions options = GetDefaultIcingOptions();
options.set_allow_circular_schema_definitions(false);
IcingSearchEngine icing(options, GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create schema with circular type definitions: A <-> B
SchemaProto schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("A").AddProperty(
PropertyConfigBuilder()
.SetName("b")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("B", /*index_nested_properties=*/true)))
.AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
PropertyConfigBuilder()
.SetName("a")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("A", /*index_nested_properties=*/false)))
.Build();
EXPECT_THAT(
icing.SetSchema(schema, /*ignore_errors_and_delete_documents=*/false)
.status(),
ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaWithValidCycle_allowCircularSchemaDefinitionsOK) {
IcingSearchEngineOptions options = GetDefaultIcingOptions();
options.set_allow_circular_schema_definitions(true);
IcingSearchEngine icing(options, GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create schema with valid circular type definitions: A <-> B, B->A sets
// index_nested_properties=false
SchemaProto schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("A").AddProperty(
PropertyConfigBuilder()
.SetName("b")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("B", /*index_nested_properties=*/true)))
.AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
PropertyConfigBuilder()
.SetName("a")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("A", /*index_nested_properties=*/false)))
.Build();
EXPECT_THAT(
icing.SetSchema(schema, /*ignore_errors_and_delete_documents=*/false)
.status(),
ProtoStatusIs(StatusProto::OK));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaWithInvalidCycle_allowCircularSchemaDefinitionsFails) {
IcingSearchEngineOptions options = GetDefaultIcingOptions();
options.set_allow_circular_schema_definitions(true);
IcingSearchEngine icing(options, GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create schema with invalid circular type definitions: A <-> B, all edges
// set index_nested_properties=true
SchemaProto schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("A").AddProperty(
PropertyConfigBuilder()
.SetName("b")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("B", /*index_nested_properties=*/true)))
.AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
PropertyConfigBuilder()
.SetName("a")
.SetCardinality(CARDINALITY_OPTIONAL)
.SetDataTypeDocument("A", /*index_nested_properties=*/true)))
.Build();
EXPECT_THAT(
icing.SetSchema(schema, /*ignore_errors_and_delete_documents=*/false)
.status(),
ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
}
TEST_F(
IcingSearchEngineSchemaTest,
ForceSetSchemaIndexedPropertyDeletionTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with 4 properties:
// - "body": string type, indexed. Section id = 0.
// - "subject": string type, indexed. Section id = 1.
// - "timestamp1": int64 type, indexed. Section id = 2.
// - "timestamp2": int64 type, indexed. Section id = 3.
SchemaProto email_with_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("body")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp1")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp2")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result =
icing.SetSchema(email_with_body_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Create a document with only subject and timestamp2 property.
DocumentProto document =
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddInt64Property("timestamp2", 1234)
.Build();
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
// Verify term search
// We should be able to retrieve the document by searching for 'tps' in
// 'subject'.
SearchSpecProto search_spec1;
search_spec1.set_query("subject:tps");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search
// We should be able to retrieve the document by searching for 1234 in
// 'timestamp2'.
SearchSpecProto search_spec2;
search_spec2.set_query("timestamp2 == 1234");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Now update the schema to remove the 'body' and 'timestamp1' field. This is
// backwards incompatible, but document should be preserved because it doesn't
// contain a 'body' or 'timestamp1' field.
// - "subject": string type, indexed. Section id = 0.
// - "timestamp2": int64 type, indexed. Section id = 1.
//
// If the index is not correctly rebuilt, then the hits of 'subject' and
// 'timestamp2' in the index will still have old section ids of 1, 3 and
// therefore they won't be found.
SchemaProto email_no_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp2")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
set_schema_result = icing.SetSchema(
email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search
// We should be able to retrieve the document by searching for 'tps' in
// 'subject'.
search_spec1.set_query("subject:tps");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search
// We should be able to retrieve the document by searching for 1234 in
// 'timestamp'.
search_spec2.set_query("timestamp2 == 1234");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
ForceSetSchemaJoinablePropertyDeletionTriggersIndexRestoration) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create "Email" schema with 2 joinable properties:
// - "receiverQualifiedId": qualified id joinable. Joinable property id = 0.
// - "senderQualifiedId": qualified id joinable. Joinable property id = 1.
SchemaProto email_with_receiver_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("receiverQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("senderQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result =
icing.SetSchema(email_with_receiver_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto person = DocumentBuilder()
.SetKey("namespace", "person")
.SetSchema("Person")
.SetCreationTimestampMs(1000)
.AddStringProperty("name", "person")
.Build();
// Create an email document with only "senderQualifiedId" joinable property.
DocumentProto email =
DocumentBuilder()
.SetKey("namespace", "email")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddStringProperty("senderQualifiedId", "namespace#person")
.Build();
EXPECT_THAT(icing.Put(person).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(email).status(), ProtoIsOk());
// Verify join search: join a query for `name:person` with a child query for
// `subject:tps` based on the child's `senderQualifiedId` field. We should be
// able to join person and email documents by this property.
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
SearchResultProto::ResultProto* result_proto =
expected_search_result_proto.mutable_results()->Add();
*result_proto->mutable_document() = person;
*result_proto->mutable_joined_results()->Add()->mutable_document() = email;
SearchSpecProto search_spec;
search_spec.set_query("name:person");
search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
JoinSpecProto* join_spec = search_spec.mutable_join_spec();
join_spec->set_parent_property_expression(
std::string(JoinProcessor::kQualifiedIdExpr));
join_spec->set_child_property_expression("senderQualifiedId");
join_spec->set_aggregation_scoring_strategy(
JoinSpecProto::AggregationScoringStrategy::COUNT);
JoinSpecProto::NestedSpecProto* nested_spec =
join_spec->mutable_nested_spec();
SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
nested_search_spec->set_term_match_type(TermMatchType::EXACT_ONLY);
nested_search_spec->set_query("subject:tps");
*nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
*nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
ResultSpecProto result_spec = ResultSpecProto::default_instance();
result_spec.set_max_joined_children_per_parent_to_return(
std::numeric_limits<int32_t>::max());
SearchResultProto actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Now update the schema to remove "receiverQualifiedId" fields. This is
// backwards incompatible, but document should be preserved because it doesn't
// contain "receiverQualifiedId" field. Also since it is join incompatible, we
// have to rebuild join index.
// - "senderQualifiedId": qualified id joinable. Joinable property id = 0.
//
// If the index is not correctly rebuilt, then the joinable data of
// "senderQualifiedId" in the join index will still have old joinable property
// id of 1 and therefore won't take effect for join search query.
SchemaProto email_without_receiver_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("senderQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
// Although we've just deleted an existing property "receiverQualifiedId" from
// schema "Email", some email documents will still be preserved because they
// don't have "receiverQualifiedId" property.
set_schema_result =
icing.SetSchema(email_without_receiver_schema,
/*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify join search: join a query for `name:person` with a child query for
// `subject:tps` based on the child's `senderQualifiedId` field. We should
// still be able to join person and email documents by this property.
actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(
IcingSearchEngineSchemaTest,
ForceSetSchemaIndexedPropertyDeletionAndAdditionTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create a schema with 3 properties:
// - "body": string type, indexed. Section id = 0.
// - "subject": string type, indexed. Section id = 1.
// - "timestamp": int64 type, indexed. Section id = 2.
SchemaProto email_with_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("body")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result =
icing.SetSchema(email_with_body_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Create a document with only subject and timestamp property.
DocumentProto document =
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddInt64Property("timestamp", 1234)
.Build();
EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
document;
// Verify term search
// We should be able to retrieve the document by searching for 'tps' in
// 'subject'.
SearchSpecProto search_spec1;
search_spec1.set_query("subject:tps");
search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
SearchResultProto actual_results =
icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search
// We should be able to retrieve the document by searching for 1234 in
// 'timestamp'.
SearchSpecProto search_spec2;
search_spec2.set_query("timestamp == 1234");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Now update the schema to remove the 'body' field. This is backwards
// incompatible, but document should be preserved because it doesn't contain a
// 'body' field.
// - "subject": string type, indexed. Section id = 0.
// - "timestamp": int64 type, indexed. Section id = 1.
// - "to": string type, indexed. Section id = 2.
//
// If the index is not correctly rebuilt, then the hits of 'subject' and
// 'timestamp' in the index will still have old section ids of 1, 2 and
// therefore they won't be found.
SchemaProto email_no_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("to")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("timestamp")
.SetDataTypeInt64(NUMERIC_MATCH_RANGE)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
set_schema_result = icing.SetSchema(
email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify term search
// We should be able to retrieve the document by searching for 'tps' in
// 'subject'.
search_spec1.set_query("subject:tps");
actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Verify numeric (integer) search
// We should be able to retrieve the document by searching for 1234 in
// 'timestamp'.
search_spec2.set_query("timestamp == 1234");
search_spec2.set_search_type(
SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY);
search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(
IcingSearchEngineSchemaTest,
ForceSetSchemaJoinablePropertyDeletionAndAdditionTriggersIndexRestorationAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
// Create "Email" schema with 2 joinable properties:
// - "receiverQualifiedId": qualified id joinable. Joinable property id = 0.
// - "senderQualifiedId": qualified id joinable. Joinable property id = 1.
SchemaProto email_with_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("receiverQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("senderQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
SetSchemaResultProto set_schema_result =
icing.SetSchema(email_with_body_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
DocumentProto person = DocumentBuilder()
.SetKey("namespace", "person")
.SetSchema("Person")
.SetCreationTimestampMs(1000)
.AddStringProperty("name", "person")
.Build();
// Create an email document with only subject and timestamp property.
DocumentProto email =
DocumentBuilder()
.SetKey("namespace", "email")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddStringProperty("senderQualifiedId", "namespace#person")
.Build();
EXPECT_THAT(icing.Put(person).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(email).status(), ProtoIsOk());
// Verify join search: join a query for `name:person` with a child query for
// `subject:tps` based on the child's `senderQualifiedId` field. We should be
// able to join person and email documents by this property.
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
SearchResultProto::ResultProto* result_proto =
expected_search_result_proto.mutable_results()->Add();
*result_proto->mutable_document() = person;
*result_proto->mutable_joined_results()->Add()->mutable_document() = email;
SearchSpecProto search_spec;
search_spec.set_query("name:person");
search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
JoinSpecProto* join_spec = search_spec.mutable_join_spec();
join_spec->set_parent_property_expression(
std::string(JoinProcessor::kQualifiedIdExpr));
join_spec->set_child_property_expression("senderQualifiedId");
join_spec->set_aggregation_scoring_strategy(
JoinSpecProto::AggregationScoringStrategy::COUNT);
JoinSpecProto::NestedSpecProto* nested_spec =
join_spec->mutable_nested_spec();
SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
nested_search_spec->set_term_match_type(TermMatchType::EXACT_ONLY);
nested_search_spec->set_query("subject:tps");
*nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
*nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
ResultSpecProto result_spec = ResultSpecProto::default_instance();
result_spec.set_max_joined_children_per_parent_to_return(
std::numeric_limits<int32_t>::max());
SearchResultProto actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
// Now update the schema to remove the "receiverQualified" field and add
// "zQualifiedId". This is backwards incompatible, but document should
// be preserved because it doesn't contain a "receiverQualified" field and
// "zQualifiedId" is optional.
// - "senderQualifiedId": qualified id joinable. Joinable property id = 0.
// - "zQualifiedId": qualified id joinable. Joinable property id = 1.
//
// If the index is not correctly rebuilt, then the joinable data of
// "senderQualifiedId" in the join index will still have old joinable property
// id of 1 and therefore won't take effect for join search query.
SchemaProto email_no_body_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_REQUIRED)))
.AddType(SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("zQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("senderQualifiedId")
.SetDataTypeJoinableString(
JOINABLE_VALUE_TYPE_QUALIFIED_ID)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
set_schema_result = icing.SetSchema(
email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
->Add("Email");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Verify join search: join a query for `name:person` with a child query for
// `subject:tps` based on the child's `senderQualifiedId` field. We should
// still be able to join person and email documents by this property.
actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
ForceSetSchemaIncompatibleNestedDocsAreDeleted) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
SchemaTypeConfigProto email_schema_type =
SchemaTypeConfigBuilder()
.SetType("Email")
.AddProperty(
PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument("Person",
/*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.Build();
SchemaProto nested_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
.SetType("Person")
.AddProperty(PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("company")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL)))
.AddType(email_schema_type)
.Build();
SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result;
expected_set_schema_result.mutable_new_schema_types()->Add("Email");
expected_set_schema_result.mutable_new_schema_types()->Add("Person");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Create two documents - a person document and an email document - both docs
// should be deleted when we remove the 'company' field from the person type.
DocumentProto person_document =
DocumentBuilder()
.SetKey("namespace1", "uri1")
.SetSchema("Person")
.SetCreationTimestampMs(1000)
.AddStringProperty("name", "Bill Lundbergh")
.AddStringProperty("company", "Initech Corp.")
.Build();
EXPECT_THAT(icing.Put(person_document).status(), ProtoIsOk());
DocumentProto email_document =
DocumentBuilder()
.SetKey("namespace1", "uri2")
.SetSchema("Email")
.SetCreationTimestampMs(1000)
.AddStringProperty("subject",
"Did you get the memo about TPS reports?")
.AddDocumentProperty("sender", person_document)
.Build();
EXPECT_THAT(icing.Put(email_document).status(), ProtoIsOk());
// We should be able to retrieve both documents.
GetResultProto get_result =
icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance());
EXPECT_THAT(get_result.status(), ProtoIsOk());
EXPECT_THAT(get_result.document(), EqualsProto(person_document));
get_result =
icing.Get("namespace1", "uri2", GetResultSpecProto::default_instance());
EXPECT_THAT(get_result.status(), ProtoIsOk());
EXPECT_THAT(get_result.document(), EqualsProto(email_document));
// Now update the schema to remove the 'company' field. This is backwards
// incompatible, *both* documents should be deleted because both fail
// validation (they each contain a 'Person' that has a non-existent property).
nested_schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
PropertyConfigBuilder()
.SetName("name")
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL)))
.AddType(email_schema_type)
.Build();
set_schema_result = icing.SetSchema(
nested_schema, /*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result = SetSchemaResultProto();
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Person");
expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Email");
expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
->Add("Person");
expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
// Both documents should be deleted now.
get_result =
icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance());
EXPECT_THAT(get_result.status(), ProtoStatusIs(StatusProto::NOT_FOUND));
get_result =
icing.Get("namespace1", "uri2", GetResultSpecProto::default_instance());
EXPECT_THAT(get_result.status(), ProtoStatusIs(StatusProto::NOT_FOUND));
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaRevalidatesDocumentsAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
SchemaProto schema_with_optional_subject;
auto type = schema_with_optional_subject.add_types();
type->set_schema_type("email");
// Add a OPTIONAL property
auto property = type->add_properties();
property->set_property_name("subject");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
EXPECT_THAT(icing.SetSchema(schema_with_optional_subject).status(),
ProtoIsOk());
DocumentProto email_document_without_subject =
DocumentBuilder()
.SetKey("namespace", "without_subject")
.SetSchema("email")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
DocumentProto email_document_with_subject =
DocumentBuilder()
.SetKey("namespace", "with_subject")
.SetSchema("email")
.AddStringProperty("subject", "foo")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
EXPECT_THAT(icing.Put(email_document_without_subject).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(email_document_with_subject).status(), ProtoIsOk());
SchemaProto schema_with_required_subject;
type = schema_with_required_subject.add_types();
type->set_schema_type("email");
// Add a REQUIRED property
property = type->add_properties();
property->set_property_name("subject");
property->set_data_type(PropertyConfigProto::DataType::STRING);
property->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED);
// Can't set the schema since it's incompatible
SetSchemaResultProto set_schema_result =
icing.SetSchema(schema_with_required_subject);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_set_schema_result_proto;
expected_set_schema_result_proto.mutable_status()->set_code(
StatusProto::FAILED_PRECONDITION);
expected_set_schema_result_proto.mutable_status()->set_message(
"Schema is incompatible.");
expected_set_schema_result_proto.add_incompatible_schema_types("email");
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result_proto));
// Force set it
set_schema_result =
icing.SetSchema(schema_with_required_subject,
/*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_set_schema_result_proto.mutable_status()->set_code(StatusProto::OK);
expected_set_schema_result_proto.mutable_status()->clear_message();
EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result_proto));
GetResultProto expected_get_result_proto;
expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_get_result_proto.mutable_document() = email_document_with_subject;
EXPECT_THAT(icing.Get("namespace", "with_subject",
GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
// The document without a subject got deleted because it failed validation
// against the new schema
expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
expected_get_result_proto.mutable_status()->set_message(
"Document (namespace, without_subject) not found.");
expected_get_result_proto.clear_document();
EXPECT_THAT(icing.Get("namespace", "without_subject",
GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaDeletesDocumentsAndReturnsOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
SchemaProto schema;
auto type = schema.add_types();
type->set_schema_type("email");
type = schema.add_types();
type->set_schema_type("message");
EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
DocumentProto email_document =
DocumentBuilder()
.SetKey("namespace", "email_uri")
.SetSchema("email")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
DocumentProto message_document =
DocumentBuilder()
.SetKey("namespace", "message_uri")
.SetSchema("message")
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
EXPECT_THAT(icing.Put(email_document).status(), ProtoIsOk());
EXPECT_THAT(icing.Put(message_document).status(), ProtoIsOk());
// Clear the schema and only add the "email" type, essentially deleting the
// "message" type
SchemaProto new_schema;
type = new_schema.add_types();
type->set_schema_type("email");
// Can't set the schema since it's incompatible
SetSchemaResultProto set_schema_result = icing.SetSchema(new_schema);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
SetSchemaResultProto expected_result;
expected_result.mutable_status()->set_code(StatusProto::FAILED_PRECONDITION);
expected_result.mutable_status()->set_message("Schema is incompatible.");
expected_result.add_deleted_schema_types("message");
EXPECT_THAT(set_schema_result, EqualsProto(expected_result));
// Force set it
set_schema_result =
icing.SetSchema(new_schema,
/*ignore_errors_and_delete_documents=*/true);
// Ignore latency numbers. They're covered elsewhere.
set_schema_result.clear_latency_ms();
expected_result.mutable_status()->set_code(StatusProto::OK);
expected_result.mutable_status()->clear_message();
EXPECT_THAT(set_schema_result, EqualsProto(expected_result));
// "email" document is still there
GetResultProto expected_get_result_proto;
expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_get_result_proto.mutable_document() = email_document;
EXPECT_THAT(icing.Get("namespace", "email_uri",
GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
// "message" document got deleted
expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
expected_get_result_proto.mutable_status()->set_message(
"Document (namespace, message_uri) not found.");
expected_get_result_proto.clear_document();
EXPECT_THAT(icing.Get("namespace", "message_uri",
GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest, GetSchemaNotFound) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.GetSchema().status(),
ProtoStatusIs(StatusProto::NOT_FOUND));
}
TEST_F(IcingSearchEngineSchemaTest, GetSchemaOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
GetSchemaResultProto expected_get_schema_result_proto;
expected_get_schema_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_get_schema_result_proto.mutable_schema() = CreateMessageSchema();
EXPECT_THAT(icing.GetSchema(), EqualsProto(expected_get_schema_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest, GetSchemaTypeFailedPrecondition) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
GetSchemaTypeResultProto get_schema_type_result_proto =
icing.GetSchemaType("nonexistent_schema");
EXPECT_THAT(get_schema_type_result_proto.status(),
ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
EXPECT_THAT(get_schema_type_result_proto.status().message(),
HasSubstr("Schema not set"));
}
TEST_F(IcingSearchEngineSchemaTest, GetSchemaTypeOk) {
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
GetSchemaTypeResultProto expected_get_schema_type_result_proto;
expected_get_schema_type_result_proto.mutable_status()->set_code(
StatusProto::OK);
*expected_get_schema_type_result_proto.mutable_schema_type_config() =
CreateMessageSchema().types(0);
EXPECT_THAT(icing.GetSchemaType(CreateMessageSchema().types(0).schema_type()),
EqualsProto(expected_get_schema_type_result_proto));
}
TEST_F(IcingSearchEngineSchemaTest,
SetSchemaCanNotDetectPreviousSchemaWasLostWithoutDocuments) {
SchemaProto schema;
auto type = schema.add_types();
type->set_schema_type("Message");
auto body = type->add_properties();
body->set_property_name("body");
body->set_data_type(PropertyConfigProto::DataType::STRING);
body->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL);
// Make an incompatible schema, a previously OPTIONAL field is REQUIRED
SchemaProto incompatible_schema = schema;
incompatible_schema.mutable_types(0)->mutable_properties(0)->set_cardinality(
PropertyConfigProto::Cardinality::REQUIRED);
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
} // This should shut down IcingSearchEngine and persist anything it needs to
ASSERT_TRUE(filesystem()->DeleteDirectoryRecursively(GetSchemaDir().c_str()));
// Since we don't have any documents yet, we can't detect this edge-case. But
// it should be fine since there aren't any documents to be invalidated.
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.SetSchema(incompatible_schema).status(), ProtoIsOk());
}
TEST_F(IcingSearchEngineSchemaTest, SetSchemaCanDetectPreviousSchemaWasLost) {
SchemaTypeConfigProto message_schema_type_config =
CreateMessageSchemaTypeConfig();
message_schema_type_config.mutable_properties(0)->set_cardinality(
CARDINALITY_OPTIONAL);
SchemaProto schema;
*schema.add_types() = message_schema_type_config;
// Make an incompatible schema, a previously OPTIONAL field is REQUIRED
SchemaProto incompatible_schema = schema;
incompatible_schema.mutable_types(0)->mutable_properties(0)->set_cardinality(
PropertyConfigProto::Cardinality::REQUIRED);
SearchSpecProto search_spec;
search_spec.set_query("message");
search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
{
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
DocumentProto document = CreateMessageDocument("namespace", "uri");
ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
// Can retrieve by namespace/uri
GetResultProto expected_get_result_proto;
expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_get_result_proto.mutable_document() = document;
ASSERT_THAT(
icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
// Can search for it
SearchResultProto expected_search_result_proto;
expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
*expected_search_result_proto.mutable_results()->Add()->mutable_document() =
CreateMessageDocument("namespace", "uri");
SearchResultProto search_result_proto =
icing.Search(search_spec, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
expected_search_result_proto));
} // This should shut down IcingSearchEngine and persist anything it needs to
ASSERT_TRUE(filesystem()->DeleteDirectoryRecursively(GetSchemaDir().c_str()));
// Setting the new, different schema will remove incompatible documents
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
EXPECT_THAT(icing.SetSchema(incompatible_schema).status(), ProtoIsOk());
// Can't retrieve by namespace/uri
GetResultProto expected_get_result_proto;
expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
expected_get_result_proto.mutable_status()->set_message(
"Document (namespace, uri) not found.");
EXPECT_THAT(
icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
EqualsProto(expected_get_result_proto));
// Can't search for it
SearchResultProto empty_result;
empty_result.mutable_status()->set_code(StatusProto::OK);
SearchResultProto search_result_proto =
icing.Search(search_spec, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(search_result_proto,
EqualsSearchResultIgnoreStatsAndScores(empty_result));
}
TEST_F(IcingSearchEngineSchemaTest, IcingShouldWorkFor64Sections) {
// Create a schema with 64 sections
SchemaProto schema =
SchemaBuilder()
.AddType(SchemaTypeConfigBuilder()
// Person has 4 sections.
.SetType("Person")
.AddProperty(PropertyConfigBuilder()
.SetName("firstName")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("lastName")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("emailAddress")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("phoneNumber")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL)))
.AddType(SchemaTypeConfigBuilder()
// Email has 16 sections.
.SetType("Email")
.AddProperty(PropertyConfigBuilder()
.SetName("body")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("subject")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("date")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(PropertyConfigBuilder()
.SetName("time")
.SetDataTypeString(TERM_MATCH_PREFIX,
TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("sender")
.SetDataTypeDocument(
"Person", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("receiver")
.SetDataTypeDocument(
"Person", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("cc")
.SetDataTypeDocument(
"Person", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_REPEATED)))
.AddType(SchemaTypeConfigBuilder()
// EmailCollection has 64 sections.
.SetType("EmailCollection")
.AddProperty(
PropertyConfigBuilder()
.SetName("email1")
.SetDataTypeDocument(
"Email", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("email2")
.SetDataTypeDocument(
"Email", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("email3")
.SetDataTypeDocument(
"Email", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL))
.AddProperty(
PropertyConfigBuilder()
.SetName("email4")
.SetDataTypeDocument(
"Email", /*index_nested_properties=*/true)
.SetCardinality(CARDINALITY_OPTIONAL)))
.Build();
DocumentProto person1 =
DocumentBuilder()
.SetKey("namespace", "person1")
.SetSchema("Person")
.AddStringProperty("firstName", "first1")
.AddStringProperty("lastName", "last1")
.AddStringProperty("emailAddress", "email1@gmail.com")
.AddStringProperty("phoneNumber", "000-000-001")
.Build();
DocumentProto person2 =
DocumentBuilder()
.SetKey("namespace", "person2")
.SetSchema("Person")
.AddStringProperty("firstName", "first2")
.AddStringProperty("lastName", "last2")
.AddStringProperty("emailAddress", "email2@gmail.com")
.AddStringProperty("phoneNumber", "000-000-002")
.Build();
DocumentProto person3 =
DocumentBuilder()
.SetKey("namespace", "person3")
.SetSchema("Person")
.AddStringProperty("firstName", "first3")
.AddStringProperty("lastName", "last3")
.AddStringProperty("emailAddress", "email3@gmail.com")
.AddStringProperty("phoneNumber", "000-000-003")
.Build();
DocumentProto email1 = DocumentBuilder()
.SetKey("namespace", "email1")
.SetSchema("Email")
.AddStringProperty("body", "test body")
.AddStringProperty("subject", "test subject")
.AddStringProperty("date", "2022-08-01")
.AddStringProperty("time", "1:00 PM")
.AddDocumentProperty("sender", person1)
.AddDocumentProperty("receiver", person2)
.AddDocumentProperty("cc", person3)
.Build();
DocumentProto email2 = DocumentBuilder()
.SetKey("namespace", "email2")
.SetSchema("Email")
.AddStringProperty("body", "test body")
.AddStringProperty("subject", "test subject")
.AddStringProperty("date", "2022-08-02")
.AddStringProperty("time", "2:00 PM")
.AddDocumentProperty("sender", person2)
.AddDocumentProperty("receiver", person1)
.AddDocumentProperty("cc", person3)
.Build();
DocumentProto email3 = DocumentBuilder()
.SetKey("namespace", "email3")
.SetSchema("Email")
.AddStringProperty("body", "test body")
.AddStringProperty("subject", "test subject")
.AddStringProperty("date", "2022-08-03")
.AddStringProperty("time", "3:00 PM")
.AddDocumentProperty("sender", person3)
.AddDocumentProperty("receiver", person1)
.AddDocumentProperty("cc", person2)
.Build();
DocumentProto email4 = DocumentBuilder()
.SetKey("namespace", "email4")
.SetSchema("Email")
.AddStringProperty("body", "test body")
.AddStringProperty("subject", "test subject")
.AddStringProperty("date", "2022-08-04")
.AddStringProperty("time", "4:00 PM")
.AddDocumentProperty("sender", person3)
.AddDocumentProperty("receiver", person2)
.AddDocumentProperty("cc", person1)
.Build();
DocumentProto email_collection =
DocumentBuilder()
.SetKey("namespace", "email_collection")
.SetSchema("EmailCollection")
.AddDocumentProperty("email1", email1)
.AddDocumentProperty("email2", email2)
.AddDocumentProperty("email3", email3)
.AddDocumentProperty("email4", email4)
.SetCreationTimestampMs(kDefaultCreationTimestampMs)
.Build();
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
ASSERT_THAT(icing.Put(email_collection).status(), ProtoIsOk());
SearchSpecProto search_spec;
search_spec.set_term_match_type(TermMatchType::PREFIX);
const std::vector<std::string> query_terms = {
"first1", "last2", "email3@gmail.com", "000-000-001",
"body", "subject", "2022-08-02", "3\\:00"};
SearchResultProto expected_document;
expected_document.mutable_status()->set_code(StatusProto::OK);
*expected_document.mutable_results()->Add()->mutable_document() =
email_collection;
for (const std::string& query_term : query_terms) {
search_spec.set_query(query_term);
SearchResultProto actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(expected_document));
}
search_spec.set_query("foo");
SearchResultProto expected_no_documents;
expected_no_documents.mutable_status()->set_code(StatusProto::OK);
SearchResultProto actual_results =
icing.Search(search_spec, GetDefaultScoringSpec(),
ResultSpecProto::default_instance());
EXPECT_THAT(actual_results,
EqualsSearchResultIgnoreStatsAndScores(expected_no_documents));
}
TEST_F(IcingSearchEngineSchemaTest, IcingShouldReturnErrorForExtraSections) {
// Create a schema with more sections than allowed.
SchemaTypeConfigBuilder schema_type_config_builder =
SchemaTypeConfigBuilder().SetType("type");
for (int i = 0; i <= kMaxSectionId + 1; ++i) {
schema_type_config_builder.AddProperty(
PropertyConfigBuilder()
.SetName("prop" + std::to_string(i))
.SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
.SetCardinality(CARDINALITY_OPTIONAL));
}
SchemaProto schema =
SchemaBuilder().AddType(schema_type_config_builder).Build();
IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
ASSERT_THAT(icing.SetSchema(schema).status().message(),
HasSubstr("Too many properties to be indexed"));
}
} // namespace
} // namespace lib
} // namespace icing