| // 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 "icing/icing-search-engine.h" |
| |
| #include <cstdint> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "icing/jni/jni-cache.h" |
| #include "icing/text_classifier/lib3/utils/base/status.h" |
| #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/legacy/index/icing-mock-filesystem.h" |
| #include "icing/portable/endian.h" |
| #include "icing/portable/equals-proto.h" |
| #include "icing/portable/platform.h" |
| #include "icing/proto/document.pb.h" |
| #include "icing/proto/initialize.pb.h" |
| #include "icing/proto/optimize.pb.h" |
| #include "icing/proto/persist.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/schema-builder.h" |
| #include "icing/schema/schema-store.h" |
| #include "icing/schema/section.h" |
| #include "icing/store/document-log-creator.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/random-string.h" |
| #include "icing/testing/snippet-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::_; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Ge; |
| using ::testing::Gt; |
| using ::testing::HasSubstr; |
| using ::testing::IsEmpty; |
| using ::testing::Le; |
| using ::testing::Lt; |
| using ::testing::Matcher; |
| using ::testing::Ne; |
| using ::testing::Return; |
| using ::testing::SizeIs; |
| using ::testing::StrEq; |
| using ::testing::UnorderedElementsAre; |
| |
| constexpr std::string_view kIpsumText = |
| "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla convallis " |
| "scelerisque orci quis hendrerit. Sed augue turpis, sodales eu gravida " |
| "nec, scelerisque nec leo. Maecenas accumsan interdum commodo. Aliquam " |
| "mattis sapien est, sit amet interdum risus dapibus sed. Maecenas leo " |
| "erat, fringilla in nisl a, venenatis gravida metus. Phasellus venenatis, " |
| "orci in aliquet mattis, lectus sapien volutpat arcu, sed hendrerit ligula " |
| "arcu nec mauris. Integer dolor mi, rhoncus eget gravida et, pulvinar et " |
| "nunc. Aliquam ac sollicitudin nisi. Vivamus sit amet urna vestibulum, " |
| "tincidunt eros sed, efficitur nisl. Fusce non neque accumsan, sagittis " |
| "nisi eget, sagittis turpis. Ut pulvinar nibh eu purus feugiat faucibus. " |
| "Donec tellus nulla, tincidunt vel lacus id, bibendum fermentum turpis. " |
| "Nullam ultrices sed nibh vitae aliquet. Ut risus neque, consectetur " |
| "vehicula posuere vitae, convallis eu lorem. Donec semper augue eu nibh " |
| "placerat semper."; |
| |
| constexpr PropertyConfigProto::Cardinality::Code CARDINALITY_OPTIONAL = |
| PropertyConfigProto::Cardinality::OPTIONAL; |
| constexpr PropertyConfigProto::Cardinality::Code CARDINALITY_REQUIRED = |
| PropertyConfigProto::Cardinality::REQUIRED; |
| constexpr PropertyConfigProto::Cardinality::Code CARDINALITY_REPEATED = |
| PropertyConfigProto::Cardinality::REPEATED; |
| |
| constexpr StringIndexingConfig::TokenizerType::Code TOKENIZER_PLAIN = |
| StringIndexingConfig::TokenizerType::PLAIN; |
| constexpr StringIndexingConfig::TokenizerType::Code TOKENIZER_NONE = |
| StringIndexingConfig::TokenizerType::NONE; |
| |
| #ifndef ICING_JNI_TEST |
| constexpr TermMatchType::Code MATCH_EXACT = TermMatchType::EXACT_ONLY; |
| #endif // !ICING_JNI_TEST |
| |
| constexpr TermMatchType::Code MATCH_PREFIX = TermMatchType::PREFIX; |
| constexpr TermMatchType::Code MATCH_NONE = TermMatchType::UNKNOWN; |
| |
| PortableFileBackedProtoLog<DocumentWrapper>::Header ReadDocumentLogHeader( |
| Filesystem filesystem, const std::string& file_path) { |
| PortableFileBackedProtoLog<DocumentWrapper>::Header header; |
| filesystem.PRead(file_path.c_str(), &header, |
| sizeof(PortableFileBackedProtoLog<DocumentWrapper>::Header), |
| /*offset=*/0); |
| return header; |
| } |
| |
| void WriteDocumentLogHeader( |
| Filesystem filesystem, const std::string& file_path, |
| PortableFileBackedProtoLog<DocumentWrapper>::Header& header) { |
| filesystem.Write(file_path.c_str(), &header, |
| sizeof(PortableFileBackedProtoLog<DocumentWrapper>::Header)); |
| } |
| |
| // 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"; } |
| |
| class IcingSearchEngineTest : 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_; |
| }; |
| |
| constexpr int kMaxSupportedDocumentSize = (1u << 24) - 1; |
| |
| // Non-zero value so we don't override it to be the current time |
| constexpr int64_t kDefaultCreationTimestampMs = 1575492852000; |
| |
| std::string GetDocumentDir() { return GetTestBaseDir() + "/document_dir"; } |
| |
| std::string GetIndexDir() { return GetTestBaseDir() + "/index_dir"; } |
| |
| std::string GetSchemaDir() { return GetTestBaseDir() + "/schema_dir"; } |
| |
| std::string GetHeaderFilename() { |
| return GetTestBaseDir() + "/icing_search_engine_header"; |
| } |
| |
| IcingSearchEngineOptions GetDefaultIcingOptions() { |
| IcingSearchEngineOptions icing_options; |
| icing_options.set_base_dir(GetTestBaseDir()); |
| 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") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| } |
| |
| DocumentProto CreateEmailDocument(const std::string& name_space, |
| const std::string& uri, int score, |
| const std::string& subject_content, |
| const std::string& body_content) { |
| return DocumentBuilder() |
| .SetKey(name_space, uri) |
| .SetSchema("Email") |
| .SetScore(score) |
| .AddStringProperty("subject", subject_content) |
| .AddStringProperty("body", body_content) |
| .Build(); |
| } |
| |
| SchemaProto CreateMessageSchema() { |
| return SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .Build(); |
| } |
| |
| SchemaProto CreateEmailSchema() { |
| return SchemaBuilder() |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .Build(); |
| } |
| |
| SchemaProto CreatePersonAndEmailSchema() { |
| return SchemaBuilder() |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("sender") |
| .SetDataTypeDocument( |
| "Person", /*index_nested_properties=*/true) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| } |
| |
| ScoringSpecProto GetDefaultScoringSpec() { |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| return scoring_spec; |
| } |
| |
| UsageReport CreateUsageReport(std::string name_space, std::string uri, |
| int64 timestamp_ms, |
| UsageReport::UsageType usage_type) { |
| UsageReport usage_report; |
| usage_report.set_document_namespace(name_space); |
| usage_report.set_document_uri(uri); |
| usage_report.set_usage_timestamp_ms(timestamp_ms); |
| usage_report.set_usage_type(usage_type); |
| return usage_report; |
| } |
| |
| std::vector<std::string> GetUrisFromSearchResults( |
| SearchResultProto& search_result_proto) { |
| std::vector<std::string> result_uris; |
| result_uris.reserve(search_result_proto.results_size()); |
| for (int i = 0; i < search_result_proto.results_size(); i++) { |
| result_uris.push_back( |
| search_result_proto.mutable_results(i)->document().uri()); |
| } |
| return result_uris; |
| } |
| |
| TEST_F(IcingSearchEngineTest, SimpleInitialization) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(DocumentProto(document)).status(), ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializingAgainSavesNonPersistedData) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| 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)); |
| |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, MaxIndexMergeSizeReturnsInvalidArgument) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(std::numeric_limits<int32_t>::max()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, NegativeMergeSizeReturnsInvalidArgument) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(-1); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ZeroMergeSizeReturnsInvalidArgument) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(0); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GoodIndexMergeSizeReturnsOk) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| // One is fine, if a bit weird. It just means that the lite index will be |
| // smaller and will request a merge any time content is added to it. |
| options.set_index_merge_size(1); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, NegativeMaxTokenLenReturnsInvalidArgument) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_max_token_length(-1); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ZeroMaxTokenLenReturnsInvalidArgument) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_max_token_length(0); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, MaxTokenLenReturnsOkAndTruncatesTokens) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| // A length of 1 is allowed - even though it would be strange to want |
| // this. |
| options.set_max_token_length(1); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // "message" should have been truncated to "m" |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| // The indexed tokens were truncated to length of 1, so "m" will match |
| search_spec.set_query("m"); |
| |
| 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 actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // The query token is also truncated to length of 1, so "me"->"m" matches "m" |
| search_spec.set_query("me"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // The query token is still truncated to length of 1, so "massage"->"m" |
| // matches "m" |
| search_spec.set_query("massage"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| MaxIntMaxTokenLenReturnsOkTooLargeTokenReturnsResourceExhausted) { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| // Set token length to max. This is allowed (it just means never to |
| // truncate tokens). However, this does mean that tokens that exceed the |
| // size of the lexicon will cause indexing to fail. |
| options.set_max_token_length(std::numeric_limits<int32_t>::max()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add a document that just barely fits under the max document limit. |
| // This will still fail to index because we won't actually have enough |
| // room in the lexicon to fit this content. |
| std::string enormous_string(kMaxSupportedDocumentSize - 256, 'p'); |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri") |
| .SetSchema("Message") |
| .AddStringProperty("body", std::move(enormous_string)) |
| .Build(); |
| EXPECT_THAT(icing.Put(document).status(), |
| ProtoStatusIs(StatusProto::OUT_OF_SPACE)); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("p"); |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, FailToCreateDocStore) { |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| // This fails DocumentStore::Create() |
| ON_CALL(*mock_filesystem, CreateDirectoryRecursively(_)) |
| .WillByDefault(Return(false)); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), |
| ProtoStatusIs(StatusProto::INTERNAL)); |
| EXPECT_THAT(initialize_result_proto.status().message(), |
| HasSubstr("Could not create directory")); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitMarkerFilePreviousFailuresAtThreshold) { |
| Filesystem filesystem; |
| DocumentProto email1 = |
| CreateEmailDocument("namespace", "uri1", 100, "subject1", "body1"); |
| email1.set_creation_timestamp_ms(10000); |
| DocumentProto email2 = |
| CreateEmailDocument("namespace", "uri2", 50, "subject2", "body2"); |
| email2.set_creation_timestamp_ms(10000); |
| |
| { |
| // Create an index with a few documents. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoIsOk()); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(0)); |
| ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk()); |
| } |
| |
| // Write an init marker file with 5 previously failed attempts. |
| std::string marker_filepath = GetTestBaseDir() + "/init_marker"; |
| |
| { |
| ScopedFd marker_file_fd(filesystem.OpenForWrite(marker_filepath.c_str())); |
| int network_init_attempts = GHostToNetworkL(5); |
| // Write the updated number of attempts before we get started. |
| ASSERT_TRUE(filesystem.PWrite(marker_file_fd.get(), 0, |
| &network_init_attempts, |
| sizeof(network_init_attempts))); |
| ASSERT_TRUE(filesystem.DataSync(marker_file_fd.get())); |
| } |
| |
| { |
| // Create the index again and verify that initialization succeeds and no |
| // data is thrown out. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoIsOk()); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(5)); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(email1)); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(email2)); |
| } |
| |
| // The successful init should have thrown out the marker file. |
| ASSERT_FALSE(filesystem.FileExists(marker_filepath.c_str())); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitMarkerFilePreviousFailuresBeyondThreshold) { |
| Filesystem filesystem; |
| DocumentProto email1 = |
| CreateEmailDocument("namespace", "uri1", 100, "subject1", "body1"); |
| DocumentProto email2 = |
| CreateEmailDocument("namespace", "uri2", 50, "subject2", "body2"); |
| |
| { |
| // Create an index with a few documents. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoIsOk()); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(0)); |
| ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk()); |
| } |
| |
| // Write an init marker file with 6 previously failed attempts. |
| std::string marker_filepath = GetTestBaseDir() + "/init_marker"; |
| |
| { |
| ScopedFd marker_file_fd(filesystem.OpenForWrite(marker_filepath.c_str())); |
| int network_init_attempts = GHostToNetworkL(6); |
| // Write the updated number of attempts before we get started. |
| ASSERT_TRUE(filesystem.PWrite(marker_file_fd.get(), 0, |
| &network_init_attempts, |
| sizeof(network_init_attempts))); |
| ASSERT_TRUE(filesystem.DataSync(marker_file_fd.get())); |
| } |
| |
| { |
| // Create the index again and verify that initialization succeeds and all |
| // data is thrown out. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), |
| ProtoStatusIs(StatusProto::WARNING_DATA_LOSS)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(6)); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| } |
| |
| // The successful init should have thrown out the marker file. |
| ASSERT_FALSE(filesystem.FileExists(marker_filepath.c_str())); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SuccessiveInitFailuresIncrementsInitMarker) { |
| Filesystem filesystem; |
| DocumentProto email1 = |
| CreateEmailDocument("namespace", "uri1", 100, "subject1", "body1"); |
| DocumentProto email2 = |
| CreateEmailDocument("namespace", "uri2", 50, "subject2", "body2"); |
| |
| { |
| // 1. Create an index with a few documents. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoIsOk()); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(0)); |
| ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk()); |
| } |
| |
| { |
| // 2. Create an index that will encounter an IO failure when trying to |
| // create the document log. |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| std::string document_log_filepath = |
| icing_options.base_dir() + "/document_dir/document_log_v1"; |
| auto get_filesize_lambda = [this, |
| &document_log_filepath](const char* filename) { |
| if (strncmp(document_log_filepath.c_str(), filename, |
| document_log_filepath.length()) == 0) { |
| return Filesystem::kBadFileSize; |
| } |
| return this->filesystem()->GetFileSize(filename); |
| }; |
| ON_CALL(*mock_filesystem, GetFileSize(A<const char*>())) |
| .WillByDefault(get_filesize_lambda); |
| |
| TestIcingSearchEngine icing(icing_options, std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), |
| GetTestJniCache()); |
| |
| // Fail to initialize six times in a row. |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(0)); |
| |
| init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(1)); |
| |
| init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(2)); |
| |
| init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(3)); |
| |
| init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(4)); |
| |
| init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(5)); |
| } |
| |
| { |
| // 3. Create the index again and verify that initialization succeeds and all |
| // data is thrown out. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| ASSERT_THAT(init_result.status(), |
| ProtoStatusIs(StatusProto::WARNING_DATA_LOSS)); |
| ASSERT_THAT(init_result.initialize_stats().num_previous_init_failures(), |
| Eq(6)); |
| |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| } |
| |
| // The successful init should have thrown out the marker file. |
| std::string marker_filepath = GetTestBaseDir() + "/init_marker"; |
| ASSERT_FALSE(filesystem.FileExists(marker_filepath.c_str())); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| 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(IcingSearchEngineTest, PutWithoutSchemaFailedPrecondition) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| PutResultProto put_result_proto = icing.Put(document); |
| EXPECT_THAT(put_result_proto.status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(put_result_proto.status().message(), HasSubstr("Schema not set")); |
| } |
| |
| TEST_F(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, |
| 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, |
| 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(IcingSearchEngineTest, 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; |
| type = schema_with_email_and_message.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); |
| |
| // 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(IcingSearchEngineTest, |
| SetSchemaNewIndexedPropertyTriggersIndexRestorationAndReturnsOk) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SchemaProto schema_with_no_indexed_property = CreateMessageSchema(); |
| schema_with_no_indexed_property.mutable_types(0) |
| ->mutable_properties(0) |
| ->clear_string_indexing_config(); |
| |
| SetSchemaResultProto set_schema_result = |
| icing.SetSchema(schema_with_no_indexed_property); |
| // 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"); |
| EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result)); |
| |
| // Nothing will be index and Search() won't return anything. |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto empty_result; |
| empty_result.mutable_status()->set_code(StatusProto::OK); |
| |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(empty_result)); |
| |
| SchemaProto schema_with_indexed_property = CreateMessageSchema(); |
| // Index restoration should be triggered here because new schema requires more |
| // properties to be indexed. |
| set_schema_result = icing.SetSchema(schema_with_indexed_property); |
| // 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("Message"); |
| EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result)); |
| |
| 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"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SetSchemaChangeNestedPropertiesTriggersIndexRestorationAndReturnsOk) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SchemaTypeConfigProto person_proto = |
| SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .Build(); |
| 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(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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") |
| .Build()) |
| .Build(); |
| |
| // "sender.name" should get assigned property id 0 and subject should get |
| // property id 1. |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // document should match a query for 'Bill' in 'sender.name', but not in |
| // 'subject' |
| SearchSpecProto search_spec; |
| search_spec.set_query("sender.name:Bill"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto result; |
| result.mutable_status()->set_code(StatusProto::OK); |
| *result.mutable_results()->Add()->mutable_document() = document; |
| |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(result)); |
| |
| SearchResultProto empty_result; |
| empty_result.mutable_status()->set_code(StatusProto::OK); |
| search_spec.set_query("subject:Bill"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(empty_result)); |
| |
| // Now update the schema with index_nested_properties=false. This should |
| // reassign property ids, lead to an index rebuild and ensure that nothing |
| // match a query for "Bill". |
| 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(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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)); |
| |
| // document shouldn't match a query for 'Bill' in either 'sender.name' or |
| // 'subject' |
| search_spec.set_query("sender.name:Bill"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(empty_result)); |
| |
| search_spec.set_query("subject:Bill"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(empty_result)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| ForceSetSchemaPropertyDeletionTriggersIndexRestorationAndReturnsOk) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // 'body' should have a property id of 0 and 'subject' should have a property |
| // id of 1. |
| SchemaProto email_with_body_schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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 a subject property. |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(1000) |
| .AddStringProperty("subject", |
| "Did you get the memo about TPS reports?") |
| .Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // We should be able to retrieve the document by searching for 'tps' in |
| // 'subject'. |
| SearchSpecProto search_spec; |
| search_spec.set_query("subject:tps"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto result; |
| result.mutable_status()->set_code(StatusProto::OK); |
| *result.mutable_results()->Add()->mutable_document() = document; |
| |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(result)); |
| |
| // 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. If the index is correctly rebuilt, then 'subject' will now |
| // have a property id of 0. If not, then the hits in the index will still have |
| // have a property id of 1 and therefore it won't be found. |
| SchemaProto email_no_body_schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Email").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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)); |
| |
| // We should be able to retrieve the document by searching for 'tps' in |
| // 'subject'. |
| search_spec.set_query("subject:tps"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(result)); |
| } |
| |
| TEST_F( |
| IcingSearchEngineTest, |
| ForceSetSchemaPropertyDeletionAndAdditionTriggersIndexRestorationAndReturnsOk) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // 'body' should have a property id of 0 and 'subject' should have a property |
| // id of 1. |
| SchemaProto email_with_body_schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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 a subject property. |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(1000) |
| .AddStringProperty("subject", |
| "Did you get the memo about TPS reports?") |
| .Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // We should be able to retrieve the document by searching for 'tps' in |
| // 'subject'. |
| SearchSpecProto search_spec; |
| search_spec.set_query("subject:tps"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto result; |
| result.mutable_status()->set_code(StatusProto::OK); |
| *result.mutable_results()->Add()->mutable_document() = document; |
| |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(result)); |
| |
| // 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. If the index is correctly rebuilt, then 'subject' and 'to' |
| // will now have property ids of 0 and 1 respectively. If not, then the hits |
| // in the index will still have have a property id of 1 and therefore it won't |
| // be found. |
| SchemaProto email_no_body_schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("to") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .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)); |
| |
| // We should be able to retrieve the document by searching for 'tps' in |
| // 'subject'. |
| search_spec.set_query("subject:tps"); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(result)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, 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(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .Build(); |
| SchemaProto nested_schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("company") |
| .SetDataTypeString(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(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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, GetSchemaNotFound) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| EXPECT_THAT(icing.GetSchema().status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, 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(IcingSearchEngineTest, GetDocument) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Simple put and get |
| ASSERT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Put an invalid document |
| PutResultProto put_result_proto = icing.Put(DocumentProto()); |
| EXPECT_THAT(put_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| EXPECT_THAT(put_result_proto.status().message(), |
| HasSubstr("'namespace' is empty")); |
| |
| // Get a non-existing key |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (wrong, uri) not found."); |
| expected_get_result_proto.clear_document(); |
| ASSERT_THAT(icing.Get("wrong", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetDocumentProjectionEmpty) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| GetResultSpecProto result_spec; |
| TypePropertyMask* mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type(document.schema()); |
| mask->add_paths(""); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document; |
| expected_get_result_proto.mutable_document()->clear_properties(); |
| ASSERT_THAT(icing.Get("namespace", "uri", result_spec), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetDocumentWildCardProjectionEmpty) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| GetResultSpecProto result_spec; |
| TypePropertyMask* mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type("*"); |
| mask->add_paths(""); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document; |
| expected_get_result_proto.mutable_document()->clear_properties(); |
| ASSERT_THAT(icing.Get("namespace", "uri", result_spec), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetDocumentProjectionMultipleFieldPaths) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // 1. Add an email document |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| GetResultSpecProto result_spec; |
| TypePropertyMask* mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type("Email"); |
| mask->add_paths("sender.name"); |
| mask->add_paths("subject"); |
| |
| // 2. Verify that the returned result only contains the 'sender.name' |
| // property and the 'subject' property. |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty("sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .Build(); |
| ASSERT_THAT(icing.Get("namespace", "uri1", result_spec), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetDocumentWildcardProjectionMultipleFieldPaths) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // 1. Add an email document |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| GetResultSpecProto result_spec; |
| TypePropertyMask* mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type("*"); |
| mask->add_paths("sender.name"); |
| mask->add_paths("subject"); |
| |
| // 2. Verify that the returned result only contains the 'sender.name' |
| // property and the 'subject' property. |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty("sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .Build(); |
| ASSERT_THAT(icing.Get("namespace", "uri1", result_spec), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| GetDocumentSpecificProjectionOverridesWildcardProjection) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // 1. Add an email document |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // 2. Add type property masks for the wildcard and the specific type of the |
| // document 'Email'. The wildcard should be ignored and only the 'Email' |
| // projection should apply. |
| GetResultSpecProto result_spec; |
| TypePropertyMask* mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type("*"); |
| mask->add_paths("subject"); |
| mask = result_spec.add_type_property_masks(); |
| mask->set_schema_type("Email"); |
| mask->add_paths("body"); |
| |
| // 3. Verify that the returned result only contains the 'body' property. |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Get("namespace", "uri1", result_spec), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchReturnsValidResults) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = CreateMessageDocument("namespace", "uri1"); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = CreateMessageDocument("namespace", "uri2"); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(1); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(1); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(2)); |
| |
| const DocumentProto& document = results.results(0).document(); |
| EXPECT_THAT(document, EqualsProto(document_two)); |
| |
| const SnippetProto& snippet = results.results(0).snippet(); |
| EXPECT_THAT(snippet.entries(), SizeIs(1)); |
| EXPECT_THAT(snippet.entries(0).property_name(), Eq("body")); |
| std::string_view content = |
| GetString(&document, snippet.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, snippet.entries(0)), |
| ElementsAre("message body")); |
| EXPECT_THAT(GetMatches(content, snippet.entries(0)), ElementsAre("message")); |
| |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document_one)); |
| EXPECT_THAT(results.results(1).snippet().entries(), IsEmpty()); |
| |
| search_spec.set_query("foo"); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchReturnsScoresDocumentScore) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = CreateMessageDocument("namespace", "uri1"); |
| document_one.set_score(93); |
| document_one.set_creation_timestamp_ms(10000); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = CreateMessageDocument("namespace", "uri2"); |
| document_two.set_score(15); |
| document_two.set_creation_timestamp_ms(12000); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| // Rank by DOCUMENT_SCORE and ensure that the score field is populated with |
| // document score. |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| SearchResultProto results = icing.Search(search_spec, scoring_spec, |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(2)); |
| |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_one)); |
| EXPECT_THAT(results.results(0).score(), 93); |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document_two)); |
| EXPECT_THAT(results.results(1).score(), 15); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchReturnsScoresCreationTimestamp) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = CreateMessageDocument("namespace", "uri1"); |
| document_one.set_score(93); |
| document_one.set_creation_timestamp_ms(10000); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = CreateMessageDocument("namespace", "uri2"); |
| document_two.set_score(15); |
| document_two.set_creation_timestamp_ms(12000); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| // Rank by CREATION_TS and ensure that the score field is populated with |
| // creation ts. |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::CREATION_TIMESTAMP); |
| |
| SearchResultProto results = icing.Search(search_spec, scoring_spec, |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(2)); |
| |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_two)); |
| EXPECT_THAT(results.results(0).score(), 12000); |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document_one)); |
| EXPECT_THAT(results.results(1).score(), 10000); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchReturnsOneResult) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = CreateMessageDocument("namespace", "uri1"); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = CreateMessageDocument("namespace", "uri2"); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| 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_two; |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| // The token is a random number so we don't verify it. |
| expected_search_result_proto.set_next_page_token( |
| search_result_proto.next_page_token()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchZeroResultLimitReturnsEmptyResults) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query(""); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(0); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchNegativeResultLimitReturnsInvalidArgument) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query(""); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(-5); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code( |
| StatusProto::INVALID_ARGUMENT); |
| expected_search_result_proto.mutable_status()->set_message( |
| "ResultSpecProto.num_per_page cannot be negative."); |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchWithPersistenceReturnsValidResults) { |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| |
| { |
| // Set the schema up beforehand. |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| // Schema will be persisted to disk when icing goes out of scope. |
| } |
| |
| { |
| // Ensure that icing initializes the schema and section_manager |
| // properly from the pre-existing file. |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| // The index and document store will be persisted to disk when icing goes |
| // out of scope. |
| } |
| |
| { |
| // Ensure that the index is brought back up without problems and we |
| // can query for the content that we expect. |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| 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 actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| search_spec.set_query("foo"); |
| |
| SearchResultProto empty_result; |
| empty_result.mutable_status()->set_code(StatusProto::OK); |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(empty_result)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchShouldReturnEmpty) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| // Empty result, no next-page token |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchShouldReturnMultiplePages) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates and inserts 5 documents |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| DocumentProto document4 = CreateMessageDocument("namespace", "uri4"); |
| DocumentProto document5 = CreateMessageDocument("namespace", "uri5"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(2); |
| |
| // Searches and gets the first page, 2 results |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document5; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document4; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken)); |
| uint64_t next_page_token = search_result_proto.next_page_token(); |
| // Since the token is a random number, we don't need to verify |
| expected_search_result_proto.set_next_page_token(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Second page, 2 results |
| expected_search_result_proto.clear_results(); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Third page, 1 result |
| expected_search_result_proto.clear_results(); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| // Because there are no more results, we should not return the next page |
| // token. |
| expected_search_result_proto.clear_next_page_token(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // No more results |
| expected_search_result_proto.clear_results(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchWithNoScoringShouldReturnMultiplePages) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates and inserts 5 documents |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| DocumentProto document4 = CreateMessageDocument("namespace", "uri4"); |
| DocumentProto document5 = CreateMessageDocument("namespace", "uri5"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::NONE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(2); |
| |
| // Searches and gets the first page, 2 results |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document5; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document4; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken)); |
| uint64_t next_page_token = search_result_proto.next_page_token(); |
| // Since the token is a random number, we don't need to verify |
| expected_search_result_proto.set_next_page_token(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Second page, 2 results |
| expected_search_result_proto.clear_results(); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Third page, 1 result |
| expected_search_result_proto.clear_results(); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| // Because there are no more results, we should not return the next page |
| // token. |
| expected_search_result_proto.clear_next_page_token(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // No more results |
| expected_search_result_proto.clear_results(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ShouldReturnMultiplePagesWithSnippets) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates and inserts 5 documents |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| DocumentProto document4 = CreateMessageDocument("namespace", "uri4"); |
| DocumentProto document5 = CreateMessageDocument("namespace", "uri5"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(2); |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(1); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(3); |
| |
| // Searches and gets the first page, 2 results with 2 snippets |
| SearchResultProto search_result = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(2)); |
| ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken)); |
| |
| const DocumentProto& document_result_1 = search_result.results(0).document(); |
| EXPECT_THAT(document_result_1, EqualsProto(document5)); |
| const SnippetProto& snippet_result_1 = search_result.results(0).snippet(); |
| EXPECT_THAT(snippet_result_1.entries(), SizeIs(1)); |
| EXPECT_THAT(snippet_result_1.entries(0).property_name(), Eq("body")); |
| std::string_view content = GetString( |
| &document_result_1, snippet_result_1.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, snippet_result_1.entries(0)), |
| ElementsAre("message body")); |
| EXPECT_THAT(GetMatches(content, snippet_result_1.entries(0)), |
| ElementsAre("message")); |
| |
| const DocumentProto& document_result_2 = search_result.results(1).document(); |
| EXPECT_THAT(document_result_2, EqualsProto(document4)); |
| const SnippetProto& snippet_result_2 = search_result.results(1).snippet(); |
| EXPECT_THAT(snippet_result_2.entries(0).property_name(), Eq("body")); |
| content = GetString(&document_result_2, |
| snippet_result_2.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, snippet_result_2.entries(0)), |
| ElementsAre("message body")); |
| EXPECT_THAT(GetMatches(content, snippet_result_2.entries(0)), |
| ElementsAre("message")); |
| |
| // Second page, 2 result with 1 snippet |
| search_result = icing.GetNextPage(search_result.next_page_token()); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(2)); |
| ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken)); |
| |
| const DocumentProto& document_result_3 = search_result.results(0).document(); |
| EXPECT_THAT(document_result_3, EqualsProto(document3)); |
| const SnippetProto& snippet_result_3 = search_result.results(0).snippet(); |
| EXPECT_THAT(snippet_result_3.entries(0).property_name(), Eq("body")); |
| content = GetString(&document_result_3, |
| snippet_result_3.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, snippet_result_3.entries(0)), |
| ElementsAre("message body")); |
| EXPECT_THAT(GetMatches(content, snippet_result_3.entries(0)), |
| ElementsAre("message")); |
| |
| EXPECT_THAT(search_result.results(1).document(), EqualsProto(document2)); |
| EXPECT_THAT(search_result.results(1).snippet().entries(), IsEmpty()); |
| |
| // Third page, 1 result with 0 snippets |
| search_result = icing.GetNextPage(search_result.next_page_token()); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(1)); |
| ASSERT_THAT(search_result.next_page_token(), Eq(kInvalidNextPageToken)); |
| |
| EXPECT_THAT(search_result.results(0).document(), EqualsProto(document1)); |
| EXPECT_THAT(search_result.results(0).snippet().entries(), IsEmpty()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ShouldInvalidateNextPageToken) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| // Searches and gets the first page, 1 result |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken)); |
| uint64_t next_page_token = search_result_proto.next_page_token(); |
| // Since the token is a random number, we don't need to verify |
| expected_search_result_proto.set_next_page_token(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| // Now document1 is still to be fetched. |
| |
| // Invalidates token |
| icing.InvalidateNextPageToken(next_page_token); |
| |
| // Tries to fetch the second page, no result since it's invalidated |
| expected_search_result_proto.clear_results(); |
| expected_search_result_proto.clear_next_page_token(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| AllPageTokensShouldBeInvalidatedAfterOptimization) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| // Searches and gets the first page, 1 result |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken)); |
| uint64_t next_page_token = search_result_proto.next_page_token(); |
| // Since the token is a random number, we don't need to verify |
| expected_search_result_proto.set_next_page_token(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| // Now document1 is still to be fetched. |
| |
| OptimizeResultProto optimize_result_proto; |
| optimize_result_proto.mutable_status()->set_code(StatusProto::OK); |
| optimize_result_proto.mutable_status()->set_message(""); |
| OptimizeResultProto actual_result = icing.Optimize(); |
| actual_result.clear_optimize_stats(); |
| ASSERT_THAT(actual_result, EqualsProto(optimize_result_proto)); |
| |
| // Tries to fetch the second page, no results since all tokens have been |
| // invalidated during Optimize() |
| expected_search_result_proto.clear_results(); |
| expected_search_result_proto.clear_next_page_token(); |
| search_result_proto = icing.GetNextPage(next_page_token); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OptimizationShouldRemoveDeletedDocs) { |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| |
| 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, uri1) not found."); |
| { |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| |
| // Deletes document1 |
| ASSERT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk()); |
| const std::string document_log_path = |
| icing_options.base_dir() + "/document_dir/" + |
| DocumentLogCreator::GetDocumentLogFilename(); |
| int64_t document_log_size_before = |
| filesystem()->GetFileSize(document_log_path.c_str()); |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| int64_t document_log_size_after = |
| filesystem()->GetFileSize(document_log_path.c_str()); |
| |
| // Validates that document can't be found right after Optimize() |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| // Validates that document is actually removed from document log |
| EXPECT_THAT(document_log_size_after, Lt(document_log_size_before)); |
| } // Destroys IcingSearchEngine to make sure nothing is cached. |
| |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OptimizationShouldDeleteTemporaryDirectory) { |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Create a tmp dir that will be used in Optimize() to swap files, |
| // this validates that any tmp dirs will be deleted before using. |
| const std::string tmp_dir = |
| icing_options.base_dir() + "/document_dir_optimize_tmp"; |
| |
| const std::string tmp_file = tmp_dir + "/file"; |
| ASSERT_TRUE(filesystem()->CreateDirectory(tmp_dir.c_str())); |
| ScopedFd fd(filesystem()->OpenForWrite(tmp_file.c_str())); |
| ASSERT_TRUE(fd.is_valid()); |
| ASSERT_TRUE(filesystem()->Write(fd.get(), "1234", 4)); |
| fd.reset(); |
| |
| EXPECT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| |
| EXPECT_FALSE(filesystem()->DirectoryExists(tmp_dir.c_str())); |
| EXPECT_FALSE(filesystem()->FileExists(tmp_file.c_str())); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetOptimizeInfoHasCorrectStats) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(500) |
| .Build(); |
| |
| { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(1000); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Just initialized, nothing is optimizable yet. |
| GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo(); |
| EXPECT_THAT(optimize_info.status(), ProtoIsOk()); |
| EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0)); |
| EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0)); |
| EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0)); |
| |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| |
| // Only have active documents, nothing is optimizable yet. |
| optimize_info = icing.GetOptimizeInfo(); |
| EXPECT_THAT(optimize_info.status(), ProtoIsOk()); |
| EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0)); |
| EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0)); |
| EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0)); |
| |
| // Deletes document1 |
| ASSERT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk()); |
| |
| optimize_info = icing.GetOptimizeInfo(); |
| EXPECT_THAT(optimize_info.status(), ProtoIsOk()); |
| EXPECT_THAT(optimize_info.optimizable_docs(), Eq(1)); |
| EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Gt(0)); |
| EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0)); |
| int64_t first_estimated_optimizable_bytes = |
| optimize_info.estimated_optimizable_bytes(); |
| |
| // Add a second document, but it'll be expired since the time (1000) is |
| // greater than the document's creation timestamp (100) + the document's ttl |
| // (500) |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| optimize_info = icing.GetOptimizeInfo(); |
| EXPECT_THAT(optimize_info.status(), ProtoIsOk()); |
| EXPECT_THAT(optimize_info.optimizable_docs(), Eq(2)); |
| EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), |
| Gt(first_estimated_optimizable_bytes)); |
| EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0)); |
| |
| // Optimize |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| } |
| |
| { |
| // Recreate with new time |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(5000); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Nothing is optimizable now that everything has been optimized away. |
| GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo(); |
| EXPECT_THAT(optimize_info.status(), ProtoIsOk()); |
| EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0)); |
| EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0)); |
| EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(4000)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetAndPutShouldWorkAfterOptimization) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| |
| // Validates that Get() and Put() are good right after Optimize() |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| } // Destroys IcingSearchEngine to make sure nothing is cached. |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| EXPECT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteShouldWorkAfterOptimization) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| |
| // Validates that Delete() works right after Optimize() |
| EXPECT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk()); |
| |
| 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, uri1) not found."); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } // Destroys IcingSearchEngine to make sure nothing is cached. |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Delete("namespace", "uri2").status(), ProtoIsOk()); |
| |
| 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, uri1) not found."); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace, uri2) not found."); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OptimizationFailureUninitializesIcing) { |
| // Setup filesystem to fail |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| bool just_swapped_files = false; |
| auto create_dir_lambda = [this, &just_swapped_files](const char* dir_name) { |
| if (just_swapped_files) { |
| // We should fail the first call immediately after swapping files. |
| just_swapped_files = false; |
| return false; |
| } |
| return filesystem()->CreateDirectoryRecursively(dir_name); |
| }; |
| ON_CALL(*mock_filesystem, CreateDirectoryRecursively) |
| .WillByDefault(create_dir_lambda); |
| |
| auto swap_lambda = [&just_swapped_files](const char* first_dir, |
| const char* second_dir) { |
| just_swapped_files = true; |
| return false; |
| }; |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| ON_CALL(*mock_filesystem, SwapFiles(HasSubstr("document_dir_optimize_tmp"), |
| HasSubstr("document_dir"))) |
| .WillByDefault(swap_lambda); |
| TestIcingSearchEngine icing(options, std::move(mock_filesystem), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // The mocks should cause an unrecoverable error during Optimize - returning |
| // INTERNAL. |
| ASSERT_THAT(icing.Optimize().status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| |
| // Ordinary operations should fail safely. |
| SchemaProto simple_schema; |
| auto type = simple_schema.add_types(); |
| type->set_schema_type("type0"); |
| auto property = type->add_properties(); |
| property->set_property_name("prop0"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| |
| DocumentProto simple_doc = DocumentBuilder() |
| .SetKey("namespace0", "uri0") |
| .SetSchema("type0") |
| .AddStringProperty("prop0", "foo") |
| .Build(); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| ResultSpecProto result_spec; |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::CREATION_TIMESTAMP); |
| |
| EXPECT_THAT(icing.SetSchema(simple_schema).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.Put(simple_doc).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing |
| .Get(simple_doc.namespace_(), simple_doc.uri(), |
| GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| |
| // Reset should get icing back to a safe (empty) and working state. |
| EXPECT_THAT(icing.Reset().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(simple_schema).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(simple_doc).status(), ProtoIsOk()); |
| EXPECT_THAT(icing |
| .Get(simple_doc.namespace_(), simple_doc.uri(), |
| GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec).status(), |
| ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteBySchemaType) { |
| SchemaProto schema; |
| // Add an email type |
| auto type = schema.add_types(); |
| type->set_schema_type("email"); |
| auto property = type->add_properties(); |
| property->set_property_name("subject"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::EXACT_ONLY); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| // Add an message type |
| 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); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::EXACT_ONLY); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("email") |
| .AddStringProperty("subject", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(7); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete the first type. The first doc should be irretrievable. The |
| // second should still be present. |
| DeleteBySchemaTypeResultProto result_proto = |
| icing.DeleteBySchemaType("message"); |
| EXPECT_THAT(result_proto.status(), ProtoIsOk()); |
| DeleteStatsProto exp_stats; |
| exp_stats.set_delete_type(DeleteStatsProto::DeleteType::SCHEMA_TYPE); |
| exp_stats.set_latency_ms(7); |
| exp_stats.set_num_documents_deleted(1); |
| EXPECT_THAT(result_proto.delete_stats(), EqualsProto(exp_stats)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri1) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Search for "message", only document2 should show up. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("message"); |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteSchemaTypeByQuery) { |
| SchemaProto schema = CreateMessageSchema(); |
| // Add an email type |
| SchemaProto tmp = CreateEmailSchema(); |
| *schema.add_types() = tmp.types(0); |
| |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema(schema.types(0).schema_type()) |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema(schema.types(1).schema_type()) |
| .AddStringProperty("subject", "subject subject2") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete the first type. The first doc should be irretrievable. The |
| // second should still be present. |
| SearchSpecProto search_spec; |
| search_spec.add_schema_type_filters(schema.types(0).schema_type()); |
| EXPECT_THAT(icing.DeleteByQuery(search_spec).status(), ProtoIsOk()); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri1) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| search_spec = SearchSpecProto::default_instance(); |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteByNamespace) { |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace3", "uri3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(7); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document3; |
| EXPECT_THAT( |
| icing.Get("namespace3", "uri3", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete namespace1. Document1 and document2 should be irretrievable. |
| // Document3 should still be present. |
| DeleteByNamespaceResultProto result_proto = |
| icing.DeleteByNamespace("namespace1"); |
| EXPECT_THAT(result_proto.status(), ProtoIsOk()); |
| DeleteStatsProto exp_stats; |
| exp_stats.set_delete_type(DeleteStatsProto::DeleteType::NAMESPACE); |
| exp_stats.set_latency_ms(7); |
| exp_stats.set_num_documents_deleted(2); |
| EXPECT_THAT(result_proto.delete_stats(), EqualsProto(exp_stats)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri1) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri2) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document3; |
| EXPECT_THAT( |
| icing.Get("namespace3", "uri3", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Search for "message", only document3 should show up. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("message"); |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteNamespaceByQuery) { |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete the first namespace. The first doc should be irretrievable. The |
| // second should still be present. |
| SearchSpecProto search_spec; |
| search_spec.add_namespace_filters("namespace1"); |
| EXPECT_THAT(icing.DeleteByQuery(search_spec).status(), ProtoIsOk()); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri1) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| search_spec = SearchSpecProto::default_instance(); |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteByQuery) { |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(7); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete all docs containing 'body1'. The first doc should be irretrievable. |
| // The second should still be present. |
| SearchSpecProto search_spec; |
| search_spec.set_query("body1"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| DeleteByQueryResultProto result_proto = icing.DeleteByQuery(search_spec); |
| EXPECT_THAT(result_proto.status(), ProtoIsOk()); |
| DeleteByQueryStatsProto exp_stats; |
| exp_stats.set_latency_ms(7); |
| exp_stats.set_num_documents_deleted(1); |
| exp_stats.set_query_length(search_spec.query().length()); |
| exp_stats.set_num_terms(1); |
| exp_stats.set_num_namespaces_filtered(0); |
| exp_stats.set_num_schema_types_filtered(0); |
| exp_stats.set_parse_query_latency_ms(7); |
| exp_stats.set_document_removal_latency_ms(7); |
| EXPECT_THAT(result_proto.delete_by_query_stats(), EqualsProto(exp_stats)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND); |
| expected_get_result_proto.mutable_status()->set_message( |
| "Document (namespace1, uri1) not found."); |
| expected_get_result_proto.clear_document(); |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| search_spec = SearchSpecProto::default_instance(); |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteByQueryReturnInfo) { |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(7); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document3; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri3", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete all docs to test the information is correctly grouped. |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| DeleteByQueryResultProto result_proto = |
| icing.DeleteByQuery(search_spec, true); |
| EXPECT_THAT(result_proto.status(), ProtoIsOk()); |
| DeleteByQueryStatsProto exp_stats; |
| exp_stats.set_latency_ms(7); |
| exp_stats.set_num_documents_deleted(3); |
| exp_stats.set_query_length(search_spec.query().length()); |
| exp_stats.set_num_terms(1); |
| exp_stats.set_num_namespaces_filtered(0); |
| exp_stats.set_num_schema_types_filtered(0); |
| exp_stats.set_parse_query_latency_ms(7); |
| exp_stats.set_document_removal_latency_ms(7); |
| EXPECT_THAT(result_proto.delete_by_query_stats(), EqualsProto(exp_stats)); |
| |
| // Check that DeleteByQuery can return information for deleted documents. |
| DeleteByQueryResultProto::DocumentGroupInfo info1, info2; |
| info1.set_namespace_("namespace1"); |
| info1.set_schema("Message"); |
| info1.add_uris("uri1"); |
| info2.set_namespace_("namespace2"); |
| info2.set_schema("Message"); |
| info2.add_uris("uri3"); |
| info2.add_uris("uri2"); |
| EXPECT_THAT(result_proto.deleted_documents(), |
| UnorderedElementsAre(EqualsProto(info1), EqualsProto(info2))); |
| |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()) |
| .status() |
| .code(), |
| Eq(StatusProto::NOT_FOUND)); |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()) |
| .status() |
| .code(), |
| Eq(StatusProto::NOT_FOUND)); |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri3", GetResultSpecProto::default_instance()) |
| .status() |
| .code(), |
| Eq(StatusProto::NOT_FOUND)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, DeleteByQueryNotFound) { |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Delete all docs containing 'foo', which should be none of them. Both docs |
| // should still be present. |
| SearchSpecProto search_spec; |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| EXPECT_THAT(icing.DeleteByQuery(search_spec).status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace1", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| expected_get_result_proto.mutable_status()->clear_message(); |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace2", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| search_spec = SearchSpecProto::default_instance(); |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SetSchemaShouldWorkAfterOptimization) { |
| // Creates 3 test schemas |
| SchemaProto schema1 = SchemaProto(CreateMessageSchema()); |
| |
| SchemaProto schema2 = SchemaProto(schema1); |
| auto new_property2 = schema2.mutable_types(0)->add_properties(); |
| new_property2->set_property_name("property2"); |
| new_property2->set_data_type(PropertyConfigProto::DataType::STRING); |
| new_property2->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| new_property2->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| new_property2->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| SchemaProto schema3 = SchemaProto(schema2); |
| auto new_property3 = schema3.mutable_types(0)->add_properties(); |
| new_property3->set_property_name("property3"); |
| new_property3->set_data_type(PropertyConfigProto::DataType::STRING); |
| new_property3->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| new_property3->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| new_property3->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| |
| // Validates that SetSchema() works right after Optimize() |
| EXPECT_THAT(icing.SetSchema(schema2).status(), ProtoIsOk()); |
| } // Destroys IcingSearchEngine to make sure nothing is cached. |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(schema3).status(), ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchShouldWorkAfterOptimization) { |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| 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; |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Optimize().status(), ProtoIsOk()); |
| |
| // Validates that Search() works right after Optimize() |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } // Destroys IcingSearchEngine to make sure nothing is cached. |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, IcingShouldWorkFineIfOptimizationIsAborted) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| { |
| // Initializes a normal icing to create files needed |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| } |
| |
| // Creates a mock filesystem in which DeleteDirectoryRecursively() always |
| // fails. This will fail IcingSearchEngine::OptimizeDocumentStore() and makes |
| // it return ABORTED_ERROR. |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| ON_CALL(*mock_filesystem, |
| DeleteDirectoryRecursively(HasSubstr("_optimize_tmp"))) |
| .WillByDefault(Return(false)); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Optimize().status(), ProtoStatusIs(StatusProto::ABORTED)); |
| |
| // Now optimization is aborted, we verify that document-related functions |
| // still work as expected. |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| |
| EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("m"); |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| OptimizationShouldRecoverIfFileDirectoriesAreMissing) { |
| // Creates a mock filesystem in which SwapFiles() always fails and deletes the |
| // directories. This will fail IcingSearchEngine::OptimizeDocumentStore(). |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| ON_CALL(*mock_filesystem, SwapFiles(HasSubstr("document_dir_optimize_tmp"), |
| HasSubstr("document_dir"))) |
| .WillByDefault([this](const char* one, const char* two) { |
| filesystem()->DeleteDirectoryRecursively(one); |
| filesystem()->DeleteDirectoryRecursively(two); |
| return false; |
| }); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| // Optimize() fails due to filesystem error |
| EXPECT_THAT(icing.Optimize().status(), |
| ProtoStatusIs(StatusProto::WARNING_DATA_LOSS)); |
| |
| // Document is not found because original file directory is missing |
| 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)); |
| |
| DocumentProto new_document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "new body") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| EXPECT_THAT(icing.Put(new_document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("m"); |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| // Searching old content returns nothing because original file directory is |
| // missing |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| search_spec.set_query("n"); |
| |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| new_document; |
| |
| // Searching new content returns the new document |
| search_result_proto = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OptimizationShouldRecoverIfDataFilesAreMissing) { |
| // Creates a mock filesystem in which SwapFiles() always fails and empties the |
| // directories. This will fail IcingSearchEngine::OptimizeDocumentStore(). |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| ON_CALL(*mock_filesystem, SwapFiles(HasSubstr("document_dir_optimize_tmp"), |
| HasSubstr("document_dir"))) |
| .WillByDefault([this](const char* one, const char* two) { |
| filesystem()->DeleteDirectoryRecursively(one); |
| filesystem()->CreateDirectoryRecursively(one); |
| filesystem()->DeleteDirectoryRecursively(two); |
| filesystem()->CreateDirectoryRecursively(two); |
| return false; |
| }); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| // Optimize() fails due to filesystem error |
| EXPECT_THAT(icing.Optimize().status(), |
| ProtoStatusIs(StatusProto::WARNING_DATA_LOSS)); |
| |
| // Document is not found because original files are missing |
| 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)); |
| |
| DocumentProto new_document = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "new body") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| EXPECT_THAT(icing.Put(new_document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("m"); |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| // Searching old content returns nothing because original files are missing |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| search_spec.set_query("n"); |
| |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| new_document; |
| |
| // Searching new content returns the new document |
| search_result_proto = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchIncludesDocumentsBeforeTtl) { |
| 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::REQUIRED); |
| body->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| body->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(500) |
| .Build(); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| 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; |
| |
| // Time just has to be less than the document's creation timestamp (100) + the |
| // document's ttl (500) |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(400); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Check that the document is returned as part of search results |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchDoesntIncludeDocumentsPastTtl) { |
| 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::REQUIRED); |
| body->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| body->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(500) |
| .Build(); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| // Time just has to be greater than the document's creation timestamp (100) + |
| // the document's ttl (500) |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(700); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Check that the document is not returned as part of search results |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchWorksAfterSchemaTypesCompatiblyModified) { |
| SchemaProto schema; |
| auto type_config = schema.add_types(); |
| type_config->set_schema_type("message"); |
| |
| auto property = type_config->add_properties(); |
| property->set_property_name("body"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| |
| DocumentProto message_document = |
| DocumentBuilder() |
| .SetKey("namespace", "message_uri") |
| .SetSchema("message") |
| .AddStringProperty("body", "foo") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(message_document).status(), ProtoIsOk()); |
| |
| // Make sure we can search for message document |
| SearchSpecProto search_spec; |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| // The message isn't indexed, so we get nothing |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // With just the schema type filter, we can search for the message |
| search_spec.Clear(); |
| search_spec.add_schema_type_filters("message"); |
| |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| message_document; |
| |
| search_result_proto = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Since SchemaTypeIds are assigned based on order in the SchemaProto, this |
| // will force a change in the DocumentStore's cached SchemaTypeIds |
| schema.clear_types(); |
| type_config = schema.add_types(); |
| type_config->set_schema_type("email"); |
| |
| // Adding a new indexed property will require reindexing |
| type_config = schema.add_types(); |
| type_config->set_schema_type("message"); |
| |
| property = type_config->add_properties(); |
| property->set_property_name("body"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| search_spec.Clear(); |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.add_schema_type_filters("message"); |
| |
| // We can still search for the message document |
| search_result_proto = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RecoverFromMissingHeaderFile) { |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| 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"); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| CreateMessageDocument("namespace", "uri"); |
| |
| { |
| // Basic initialization/setup |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| 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 |
| |
| EXPECT_TRUE(filesystem()->DeleteFile(GetHeaderFilename().c_str())); |
| |
| // We should be able to recover from this and access all our previous data |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Checks that DocumentLog is still ok |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Checks that the index is still ok so we can search over it |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| |
| // Checks that Schema is still since it'll be needed to validate the document |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, UnableToRecoverFromCorruptSchema) { |
| { |
| // Basic initialization/setup |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| CreateMessageDocument("namespace", "uri"); |
| |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } // This should shut down IcingSearchEngine and persist anything it needs to |
| |
| const std::string schema_file = |
| absl_ports::StrCat(GetSchemaDir(), "/schema.pb"); |
| const std::string corrupt_data = "1234"; |
| EXPECT_TRUE(filesystem()->Write(schema_file.c_str(), corrupt_data.data(), |
| corrupt_data.size())); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INTERNAL)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, UnableToRecoverFromCorruptDocumentLog) { |
| { |
| // Basic initialization/setup |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = |
| CreateMessageDocument("namespace", "uri"); |
| |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| } // This should shut down IcingSearchEngine and persist anything it needs to |
| |
| const std::string document_log_file = absl_ports::StrCat( |
| GetDocumentDir(), "/", DocumentLogCreator::GetDocumentLogFilename()); |
| const std::string corrupt_data = "1234"; |
| EXPECT_TRUE(filesystem()->Write(document_log_file.c_str(), |
| corrupt_data.data(), corrupt_data.size())); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::INTERNAL)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RecoverFromInconsistentSchemaStore) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2_with_additional_property = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("additional", "content") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| { |
| // Initializes folder and schema |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SchemaProto schema; |
| auto type = schema.add_types(); |
| type->set_schema_type("Message"); |
| |
| auto property = type->add_properties(); |
| property->set_property_name("body"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| property = type->add_properties(); |
| property->set_property_name("additional"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| |
| EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document2_with_additional_property).status(), |
| ProtoIsOk()); |
| |
| // Won't get us anything because "additional" isn't marked as an indexed |
| // property in the schema |
| SearchSpecProto search_spec; |
| search_spec.set_query("additional:content"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| 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 |
| |
| { |
| // This schema will change the SchemaTypeIds from the previous schema_ |
| // (since SchemaTypeIds are assigned based on order of the types, and this |
| // new schema changes the ordering of previous types) |
| SchemaProto new_schema; |
| auto type = new_schema.add_types(); |
| type->set_schema_type("Email"); |
| |
| type = new_schema.add_types(); |
| type->set_schema_type("Message"); |
| |
| // Adding a new property changes the SectionIds (since SectionIds are |
| // assigned based on alphabetical order of indexed sections, marking |
| // "additional" as an indexed property will push the "body" property to a |
| // different SectionId) |
| auto property = type->add_properties(); |
| property->set_property_name("body"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| property = type->add_properties(); |
| property->set_property_name("additional"); |
| property->set_data_type(PropertyConfigProto::DataType::STRING); |
| property->set_cardinality(PropertyConfigProto::Cardinality::OPTIONAL); |
| property->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| property->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| // Write the marker file |
| std::string marker_filepath = |
| absl_ports::StrCat(options.base_dir(), "/set_schema_marker"); |
| ScopedFd sfd(filesystem()->OpenForWrite(marker_filepath.c_str())); |
| ASSERT_TRUE(sfd.is_valid()); |
| |
| // Write the new schema |
| FakeClock fake_clock; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<SchemaStore> schema_store, |
| SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock)); |
| ICING_EXPECT_OK(schema_store->SetSchema(new_schema)); |
| } // Will persist new schema |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // We can insert a Email document since we kept the new schema |
| DocumentProto email_document = |
| DocumentBuilder() |
| .SetKey("namespace", "email_uri") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| EXPECT_THAT(icing.Put(email_document).status(), ProtoIsOk()); |
| |
| 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)); |
| |
| SearchSpecProto search_spec; |
| |
| // The section restrict will ensure we are using the correct, updated |
| // SectionId in the Index |
| search_spec.set_query("additional:content"); |
| |
| // Schema type filter will ensure we're using the correct, updated |
| // SchemaTypeId in the DocumentStore |
| search_spec.add_schema_type_filters("Message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2_with_additional_property; |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RecoverFromInconsistentDocumentStore) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| |
| { |
| // Initializes folder and schema, index one document |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| } // This should shut down IcingSearchEngine and persist anything it needs to |
| |
| { |
| FakeClock fake_clock; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<SchemaStore> schema_store, |
| SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock)); |
| ICING_EXPECT_OK(schema_store->SetSchema(CreateMessageSchema())); |
| |
| // Puts a second document into DocumentStore but doesn't index it. |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| DocumentStore::Create(filesystem(), GetDocumentDir(), &fake_clock, |
| schema_store.get())); |
| std::unique_ptr<DocumentStore> document_store = |
| std::move(create_result.document_store); |
| |
| ICING_EXPECT_OK(document_store->Put(document2)); |
| } |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| // Index Restoration should be triggered here and document2 should be |
| // indexed. |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document1; |
| |
| // DocumentStore kept the additional document |
| EXPECT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| *expected_get_result_proto.mutable_document() = document2; |
| EXPECT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // We indexed the additional document |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RecoverFromInconsistentIndex) { |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| 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"); |
| |
| { |
| // Initializes folder and schema, index one document |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| 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 |
| |
| // Pretend we lost the entire index |
| EXPECT_TRUE(filesystem()->DeleteDirectoryRecursively( |
| absl_ports::StrCat(GetIndexDir(), "/idx/lite.").c_str())); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Check that our index is ok by searching over the restored index |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RecoverFromCorruptIndex) { |
| SearchSpecProto search_spec; |
| search_spec.set_query("message"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| |
| 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"); |
| |
| { |
| // Initializes folder and schema, index one document |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(), |
| ProtoIsOk()); |
| 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 |
| |
| // Pretend index is corrupted |
| const std::string index_hit_buffer_file = GetIndexDir() + "/idx/lite.hb"; |
| ScopedFd fd(filesystem()->OpenForWrite(index_hit_buffer_file.c_str())); |
| ASSERT_TRUE(fd.is_valid()); |
| ASSERT_TRUE(filesystem()->Write(fd.get(), "1234", 4)); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Check that our index is ok by searching over the restored index |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultShouldBeRankedByDocumentScore) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 documents and ensures the relationship in terms of document |
| // score is: document1 < document2 < document3 |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetScore(3) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| // Intentionally inserts the documents in the order that is different than |
| // their score order |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in descending score order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchShouldAllowNoScoring) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 documents and ensures the relationship of them is: |
| // document1 < document2 < document3 |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(1571111111111) |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(1572222222222) |
| .Build(); |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetScore(3) |
| .SetCreationTimestampMs(1573333333333) |
| .Build(); |
| |
| // Intentionally inserts the documents in the order that is different than |
| // their score order |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| |
| // Results should not be ranked by score but returned in reverse insertion |
| // order. |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::NONE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultShouldBeRankedByCreationTimestamp) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 documents and ensures the relationship in terms of creation |
| // timestamp score is: document1 < document2 < document3 |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(1571111111111) |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(1572222222222) |
| .Build(); |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(1573333333333) |
| .Build(); |
| |
| // Intentionally inserts the documents in the order that is different than |
| // their score order |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in descending timestamp order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::CREATION_TIMESTAMP); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultShouldBeRankedByUsageCount) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 test documents |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| // Intentionally inserts the documents in a different order to eliminate the |
| // possibility that the following results are sorted in the default reverse |
| // insertion order. |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // Report usage for doc3 twice and doc2 once. The order will be doc3 > doc2 > |
| // doc1 when ranked by USAGE_TYPE1_COUNT. |
| UsageReport usage_report_doc3 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/3", /*timestamp_ms=*/0, |
| UsageReport::USAGE_TYPE1); |
| UsageReport usage_report_doc2 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/2", /*timestamp_ms=*/0, |
| UsageReport::USAGE_TYPE1); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc2).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in descending USAGE_TYPE1_COUNT order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::USAGE_TYPE1_COUNT); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchResultShouldHaveDefaultOrderWithoutUsageCounts) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 test documents |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // None of the documents have usage reports. Result should be in the default |
| // reverse insertion order. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::USAGE_TYPE1_COUNT); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultShouldBeRankedByUsageTimestamp) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 test documents |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| // Intentionally inserts the documents in a different order to eliminate the |
| // possibility that the following results are sorted in the default reverse |
| // insertion order. |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // Report usage for doc2 and doc3. The order will be doc3 > doc2 > doc1 when |
| // ranked by USAGE_TYPE1_LAST_USED_TIMESTAMP. |
| UsageReport usage_report_doc2 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/2", /*timestamp_ms=*/1000, |
| UsageReport::USAGE_TYPE1); |
| UsageReport usage_report_doc3 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/3", /*timestamp_ms=*/5000, |
| UsageReport::USAGE_TYPE1); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc3).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in descending USAGE_TYPE1_LAST_USED_TIMESTAMP order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::USAGE_TYPE1_LAST_USED_TIMESTAMP); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, Bm25fRelevanceScoringOneNamespace) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace1". |
| DocumentProto document = CreateEmailDocument( |
| "namespace1", "namespace1/uri0", /*score=*/10, "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri7", /*score=*/4, |
| "starbucks coffee", |
| "habit. birthday rewards. good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("coffee OR food"); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| |
| // Result should be in descending score order |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| // Both doc5 and doc7 have "coffee" in name and text sections. |
| // However, doc5 has more matches in the text section. |
| // Documents with "food" are ranked lower as the term "food" is commonly |
| // present in this corpus, and thus, has a lower IDF. |
| EXPECT_THAT(GetUrisFromSearchResults(search_result_proto), |
| ElementsAre("namespace1/uri5", // 'coffee' 3 times |
| "namespace1/uri7", // 'coffee' 2 times |
| "namespace1/uri1", // 'food' 2 times |
| "namespace1/uri4", // 'food' 2 times |
| "namespace1/uri2", // 'food' 1 time |
| "namespace1/uri6")); // 'food' 1 time |
| } |
| |
| TEST_F(IcingSearchEngineTest, Bm25fRelevanceScoringOneNamespaceNotOperator) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace1". |
| DocumentProto document = CreateEmailDocument( |
| "namespace1", "namespace1/uri0", /*score=*/10, "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri3", /*score=*/23, "speederia pizza", |
| "thin-crust pizza. good and fast. nice coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri7", /*score=*/4, |
| "starbucks coffee", |
| "habit. birthday rewards. good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("coffee -starbucks"); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| |
| // Result should be in descending score order |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT( |
| GetUrisFromSearchResults(search_result_proto), |
| ElementsAre("namespace1/uri5", // 'coffee' 3 times, 'starbucks' 0 times |
| "namespace1/uri3")); // 'coffee' 1 times, 'starbucks' 0 times |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| Bm25fRelevanceScoringOneNamespaceSectionRestrict) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace1". |
| DocumentProto document = CreateEmailDocument( |
| "namespace1", "namespace1/uri0", /*score=*/10, "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = |
| CreateEmailDocument("namespace1", "namespace1/uri5", /*score=*/18, |
| "peets coffee, best coffee", |
| "espresso. decaf. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri7", /*score=*/4, "starbucks", |
| "habit. birthday rewards. good coffee. brewed coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("subject:coffee OR body:food"); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| |
| // Result should be in descending score order |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| // The term frequencies of "coffee" and "food" are calculated respectively |
| // from the subject section and the body section. |
| // Documents with "food" are ranked lower as the term "food" is commonly |
| // present in this corpus, and thus, has a lower IDF. |
| EXPECT_THAT( |
| GetUrisFromSearchResults(search_result_proto), |
| ElementsAre("namespace1/uri5", // 'coffee' 2 times in section subject |
| "namespace1/uri1", // 'food' 2 times in section body |
| "namespace1/uri4", // 'food' 2 times in section body |
| "namespace1/uri2", // 'food' 1 time in section body |
| "namespace1/uri6")); // 'food' 1 time in section body |
| } |
| |
| TEST_F(IcingSearchEngineTest, Bm25fRelevanceScoringTwoNamespaces) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace1". |
| DocumentProto document = CreateEmailDocument( |
| "namespace1", "namespace1/uri0", /*score=*/10, "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri7", /*score=*/4, |
| "starbucks coffee", |
| "habit. birthday rewards. good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace2". |
| document = CreateEmailDocument("namespace2", "namespace2/uri0", /*score=*/10, |
| "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri7", /*score=*/4, |
| "starbucks coffee", "good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("coffee OR food"); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE); |
| ResultSpecProto result_spec_proto; |
| result_spec_proto.set_num_per_page(16); |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec_proto); |
| |
| // Result should be in descending score order |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| // The two corpora have the same documents except for document 7, which in |
| // "namespace2" is much shorter than the average dcoument length, so it is |
| // boosted. |
| EXPECT_THAT(GetUrisFromSearchResults(search_result_proto), |
| ElementsAre("namespace2/uri7", // 'coffee' 2 times, short doc |
| "namespace1/uri5", // 'coffee' 3 times |
| "namespace2/uri5", // 'coffee' 3 times |
| "namespace1/uri7", // 'coffee' 2 times |
| "namespace1/uri1", // 'food' 2 times |
| "namespace2/uri1", // 'food' 2 times |
| "namespace1/uri4", // 'food' 2 times |
| "namespace2/uri4", // 'food' 2 times |
| "namespace1/uri2", // 'food' 1 time |
| "namespace2/uri2", // 'food' 1 time |
| "namespace1/uri6", // 'food' 1 time |
| "namespace2/uri6")); // 'food' 1 time |
| } |
| |
| TEST_F(IcingSearchEngineTest, Bm25fRelevanceScoringWithNamespaceFilter) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace1". |
| DocumentProto document = CreateEmailDocument( |
| "namespace1", "namespace1/uri0", /*score=*/10, "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace1", "namespace1/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace1", "namespace1/uri7", /*score=*/4, |
| "starbucks coffee", |
| "habit. birthday rewards. good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Create and index documents in namespace "namespace2". |
| document = CreateEmailDocument("namespace2", "namespace2/uri0", /*score=*/10, |
| "sushi belmont", |
| "fresh fish. inexpensive. good sushi."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri1", /*score=*/13, "peacock koriander", |
| "indian food. buffet. spicy food. kadai chicken."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri2", /*score=*/4, |
| "panda express", |
| "chinese food. cheap. inexpensive. kung pao."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri3", /*score=*/23, |
| "speederia pizza", |
| "thin-crust pizza. good and fast."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri4", /*score=*/8, |
| "whole foods", |
| "salads. pizza. organic food. expensive."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri5", /*score=*/18, "peets coffee", |
| "espresso. decaf. brewed coffee. whole beans. excellent coffee."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument( |
| "namespace2", "namespace2/uri6", /*score=*/4, "costco", |
| "bulk. cheap whole beans. frozen fish. food samples."); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = CreateEmailDocument("namespace2", "namespace2/uri7", /*score=*/4, |
| "starbucks coffee", "good coffee"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("coffee OR food"); |
| // Now query only corpus 2 |
| search_spec.add_namespace_filters("namespace2"); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| search_result_proto = icing.Search(search_spec, scoring_spec, |
| ResultSpecProto::default_instance()); |
| |
| // Result from namespace "namespace2" should be in descending score order |
| EXPECT_THAT(search_result_proto.status(), ProtoIsOk()); |
| // Both doc5 and doc7 have "coffee" in name and text sections. |
| // Even though doc5 has more matches in the text section, doc7's length is |
| // much shorter than the average corpus's length, so it's being boosted. |
| // Documents with "food" are ranked lower as the term "food" is commonly |
| // present in this corpus, and thus, has a lower IDF. |
| EXPECT_THAT(GetUrisFromSearchResults(search_result_proto), |
| ElementsAre("namespace2/uri7", // 'coffee' 2 times, short doc |
| "namespace2/uri5", // 'coffee' 3 times |
| "namespace2/uri1", // 'food' 2 times |
| "namespace2/uri4", // 'food' 2 times |
| "namespace2/uri2", // 'food' 1 time |
| "namespace2/uri6")); // 'food' 1 time |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchResultShouldHaveDefaultOrderWithoutUsageTimestamp) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 test documents |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // None of the documents have usage reports. Result should be in the default |
| // reverse insertion order. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::USAGE_TYPE1_LAST_USED_TIMESTAMP); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OlderUsageTimestampShouldNotOverrideNewerOnes) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 test documents |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // Report usage for doc1 and doc2. The older timestamp 5000 shouldn't be |
| // overridden by 1000. The order will be doc1 > doc2 when ranked by |
| // USAGE_TYPE1_LAST_USED_TIMESTAMP. |
| UsageReport usage_report_doc1_time1 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/1", /*timestamp_ms=*/1000, |
| UsageReport::USAGE_TYPE1); |
| UsageReport usage_report_doc1_time5 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/1", /*timestamp_ms=*/5000, |
| UsageReport::USAGE_TYPE1); |
| UsageReport usage_report_doc2_time3 = CreateUsageReport( |
| /*name_space=*/"namespace", /*uri=*/"uri/2", /*timestamp_ms=*/3000, |
| UsageReport::USAGE_TYPE1); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc1_time5).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc2_time3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.ReportUsage(usage_report_doc1_time1).status(), ProtoIsOk()); |
| |
| // "m" will match both documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in descending USAGE_TYPE1_LAST_USED_TIMESTAMP order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::USAGE_TYPE1_LAST_USED_TIMESTAMP); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultShouldBeRankedAscendingly) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 documents and ensures the relationship in terms of document |
| // score is: document1 < document2 < document3 |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetScore(3) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| // Intentionally inserts the documents in the order that is different than |
| // their score order |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| |
| // "m" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| // Result should be in ascending score order |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document3; |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| scoring_spec.set_order_by(ScoringSpecProto::Order::ASC); |
| SearchResultProto search_result_proto = icing.Search( |
| search_spec, scoring_spec, ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchResultGroupingDuplicateNamespaceShouldReturnError) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 2 documents and ensures the relationship in terms of document |
| // score is: document1 < document2 |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // "m" will match all 2 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| // Specify "namespace1" twice. This should result in an error. |
| ResultSpecProto result_spec; |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| result_grouping->add_namespaces("namespace1"); |
| result_grouping->add_namespaces("namespace2"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| result_grouping->add_namespaces("namespace1"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchResultGroupingNonPositiveMaxResultsShouldReturnError) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 2 documents and ensures the relationship in terms of document |
| // score is: document1 < document2 |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| // "m" will match all 2 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| // Specify zero results. This should result in an error. |
| ResultSpecProto result_spec; |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| result_grouping->set_max_results(0); |
| result_grouping->add_namespaces("namespace1"); |
| result_grouping->add_namespaces("namespace2"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| |
| // Specify negative results. This should result in an error. |
| result_spec.mutable_result_groupings(0)->set_max_results(-1); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchResultGroupingMultiNamespaceGrouping) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates 3 documents and ensures the relationship in terms of document |
| // score is: document1 < document2 < document3 < document4 < document5 < |
| // document6 |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(1) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message2") |
| .SetScore(2) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri/3") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetScore(3) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document4 = |
| DocumentBuilder() |
| .SetKey("namespace2", "uri/4") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(4) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document5 = |
| DocumentBuilder() |
| .SetKey("namespace3", "uri/5") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message3") |
| .SetScore(5) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| DocumentProto document6 = |
| DocumentBuilder() |
| .SetKey("namespace3", "uri/6") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message1") |
| .SetScore(6) |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document6).status(), ProtoIsOk()); |
| |
| // "m" will match all 6 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| result_grouping->add_namespaces("namespace1"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(2); |
| result_grouping->add_namespaces("namespace2"); |
| result_grouping->add_namespaces("namespace3"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // The last result (document1) in namespace "namespace1" should not be |
| // included. "namespace2" and "namespace3" are grouped together. So only the |
| // two highest scored documents between the two (both of which are in |
| // "namespace3") should be returned. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document6; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document5; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| 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(IcingSearchEngineTest, SetSchemaCanDetectPreviousSchemaWasLost) { |
| 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); |
| body->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| body->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| // 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(IcingSearchEngineTest, ImplicitPersistToDiskFullSavesEverything) { |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } // Destructing calls a PersistToDisk(FULL) |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| |
| // There should be no recovery since everything should be saved properly. |
| InitializeResultProto init_result = icing.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| |
| // Schema is still intact. |
| 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)); |
| |
| // Documents are still intact. |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document; |
| |
| EXPECT_THAT( |
| icing.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Index is still intact. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); // Content in the Message document. |
| |
| 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 actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ExplicitPersistToDiskFullSavesEverything) { |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| |
| // Add schema and documents to our first icing1 instance. |
| IcingSearchEngine icing1(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.Put(document).status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.PersistToDisk(PersistType::FULL).status(), ProtoIsOk()); |
| |
| // Initialize a second icing2 instance which should have it's own memory |
| // space. If data from icing1 isn't being persisted to the files, then icing2 |
| // won't be able to see those changes. |
| IcingSearchEngine icing2(GetDefaultIcingOptions(), GetTestJniCache()); |
| |
| // There should be no recovery since everything should be saved properly. |
| InitializeResultProto init_result = icing2.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| |
| // Schema is still intact. |
| 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(icing2.GetSchema(), |
| EqualsProto(expected_get_schema_result_proto)); |
| |
| // Documents are still intact. |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_result_proto.mutable_document() = document; |
| |
| EXPECT_THAT( |
| icing2.Get("namespace", "uri", GetResultSpecProto::default_instance()), |
| EqualsProto(expected_get_result_proto)); |
| |
| // Index is still intact. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); // Content in the Message document. |
| |
| 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 actual_results = |
| icing2.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, NoPersistToDiskLosesAllDocumentsAndIndex) { |
| IcingSearchEngine icing1(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| EXPECT_THAT(icing1.Put(document).status(), ProtoIsOk()); |
| EXPECT_THAT( |
| icing1.Get("namespace", "uri", GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(document)); |
| |
| // It's intentional that no PersistToDisk call is made before initializing a |
| // second instance of icing. |
| |
| IcingSearchEngine icing2(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing2.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::PARTIAL_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| |
| // The document shouldn't be found because we forgot to call |
| // PersistToDisk(LITE)! |
| EXPECT_THAT( |
| icing2.Get("namespace", "uri", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| |
| // Searching also shouldn't get us anything because the index wasn't |
| // recovered. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); // Content in the Message document. |
| |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| |
| SearchResultProto actual_results = |
| icing2.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, PersistToDiskLiteSavesGroundTruth) { |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| |
| IcingSearchEngine icing1(GetDefaultIcingOptions(), GetTestJniCache()); |
| EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.Put(document).status(), ProtoIsOk()); |
| EXPECT_THAT(icing1.PersistToDisk(PersistType::LITE).status(), ProtoIsOk()); |
| EXPECT_THAT( |
| icing1.Get("namespace", "uri", GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(document)); |
| |
| IcingSearchEngine icing2(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing2.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| |
| // A checksum mismatch gets reported as an IO error. The document store and |
| // index didn't have their derived files included in the checksum previously, |
| // so reinitializing will trigger a checksum mismatch. |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::IO_ERROR)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::IO_ERROR)); |
| |
| // Schema is still intact. |
| 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(icing2.GetSchema(), |
| EqualsProto(expected_get_schema_result_proto)); |
| |
| // The document should be found because we called PersistToDisk(LITE)! |
| EXPECT_THAT( |
| icing2.Get("namespace", "uri", GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(document)); |
| |
| // Recovered index is still intact. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); // Content in the Message document. |
| |
| 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 actual_results = |
| icing2.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ResetOk) { |
| SchemaProto message_schema = CreateMessageSchema(); |
| SchemaProto empty_schema = SchemaProto(message_schema); |
| empty_schema.clear_types(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(message_schema).status(), ProtoIsOk()); |
| |
| int64_t empty_state_size = |
| filesystem()->GetFileDiskUsage(GetTestBaseDir().c_str()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Check that things have been added |
| EXPECT_THAT(filesystem()->GetDiskUsage(GetTestBaseDir().c_str()), |
| Gt(empty_state_size)); |
| |
| EXPECT_THAT(icing.Reset().status(), ProtoIsOk()); |
| |
| // Check that we're back to an empty state |
| EXPECT_EQ(filesystem()->GetFileDiskUsage(GetTestBaseDir().c_str()), |
| empty_state_size); |
| |
| // Sanity check that we can still call other APIs. If things aren't cleared, |
| // then this should raise an error since the empty schema is incompatible with |
| // the old message_schema. |
| EXPECT_THAT(icing.SetSchema(empty_schema).status(), ProtoIsOk()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, ResetDeleteFailureCausesInternalError) { |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| |
| // This fails IcingSearchEngine::Reset() with status code INTERNAL and leaves |
| // the IcingSearchEngine instance in an uninitialized state. |
| ON_CALL(*mock_filesystem, |
| DeleteDirectoryRecursively(StrEq(GetTestBaseDir().c_str()))) |
| .WillByDefault(Return(false)); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::make_unique<FakeClock>(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document = CreateMessageDocument("namespace", "uri"); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Reset().status(), ProtoStatusIs(StatusProto::INTERNAL)); |
| |
| GetResultProto expected_get_result_proto; |
| expected_get_result_proto.mutable_status()->set_code( |
| StatusProto::FAILED_PRECONDITION); |
| *expected_get_result_proto.mutable_document() = document; |
| EXPECT_THAT(icing |
| .Get(document.namespace_(), document.uri(), |
| GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SnippetNormalization) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "MDI zurich Team Meeting") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "mdi Zürich Team Meeting") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("mdi Zürich"); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(2); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(2); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(2)); |
| const DocumentProto& result_document_1 = results.results(0).document(); |
| const SnippetProto& result_snippet_1 = results.results(0).snippet(); |
| EXPECT_THAT(result_document_1, EqualsProto(document_two)); |
| EXPECT_THAT(result_snippet_1.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_1.entries(0).property_name(), Eq("body")); |
| std::string_view content = GetString( |
| &result_document_1, result_snippet_1.entries(0).property_name()); |
| EXPECT_THAT( |
| GetWindows(content, result_snippet_1.entries(0)), |
| ElementsAre("mdi Zürich Team Meeting", "mdi Zürich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet_1.entries(0)), |
| ElementsAre("mdi", "Zürich")); |
| |
| const DocumentProto& result_document_2 = results.results(1).document(); |
| const SnippetProto& result_snippet_2 = results.results(1).snippet(); |
| EXPECT_THAT(result_document_2, EqualsProto(document_one)); |
| EXPECT_THAT(result_snippet_2.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_2.entries(0).property_name(), Eq("body")); |
| content = GetString(&result_document_2, |
| result_snippet_2.entries(0).property_name()); |
| EXPECT_THAT( |
| GetWindows(content, result_snippet_2.entries(0)), |
| ElementsAre("MDI zurich Team Meeting", "MDI zurich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet_2.entries(0)), |
| ElementsAre("MDI", "zurich")); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SnippetNormalizationPrefix) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "MDI zurich Team Meeting") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "mdi Zürich Team Meeting") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("md Zür"); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(2); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(2); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(2)); |
| const DocumentProto& result_document_1 = results.results(0).document(); |
| const SnippetProto& result_snippet_1 = results.results(0).snippet(); |
| EXPECT_THAT(result_document_1, EqualsProto(document_two)); |
| EXPECT_THAT(result_snippet_1.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_1.entries(0).property_name(), Eq("body")); |
| std::string_view content = GetString( |
| &result_document_1, result_snippet_1.entries(0).property_name()); |
| EXPECT_THAT( |
| GetWindows(content, result_snippet_1.entries(0)), |
| ElementsAre("mdi Zürich Team Meeting", "mdi Zürich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet_1.entries(0)), |
| ElementsAre("mdi", "Zürich")); |
| |
| const DocumentProto& result_document_2 = results.results(1).document(); |
| const SnippetProto& result_snippet_2 = results.results(1).snippet(); |
| EXPECT_THAT(result_document_2, EqualsProto(document_one)); |
| EXPECT_THAT(result_snippet_2.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_2.entries(0).property_name(), Eq("body")); |
| content = GetString(&result_document_2, |
| result_snippet_2.entries(0).property_name()); |
| EXPECT_THAT( |
| GetWindows(content, result_snippet_2.entries(0)), |
| ElementsAre("MDI zurich Team Meeting", "MDI zurich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet_2.entries(0)), |
| ElementsAre("MDI", "zurich")); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SnippetSectionRestrict) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "MDI zurich Team Meeting") |
| .AddStringProperty("body", "MDI zurich Team Meeting") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("body:Zür"); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(10); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(10); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| |
| const DocumentProto& result_document = results.results(0).document(); |
| const SnippetProto& result_snippet = results.results(0).snippet(); |
| EXPECT_THAT(result_document, EqualsProto(document_one)); |
| EXPECT_THAT(result_snippet.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet.entries(0).property_name(), Eq("body")); |
| std::string_view content = |
| GetString(&result_document, result_snippet.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, result_snippet.entries(0)), |
| ElementsAre("MDI zurich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet.entries(0)), |
| ElementsAre("zurich")); |
| } |
| |
| TEST_F(IcingSearchEngineTest, UninitializedInstanceFailsSafely) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| |
| SchemaProto email_schema = CreateMessageSchema(); |
| EXPECT_THAT(icing.SetSchema(email_schema).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.GetSchema().status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.GetSchemaType(email_schema.types(0).schema_type()).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| |
| DocumentProto doc = CreateMessageDocument("namespace", "uri"); |
| EXPECT_THAT(icing.Put(doc).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing |
| .Get(doc.namespace_(), doc.uri(), |
| GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.Delete(doc.namespace_(), doc.uri()).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.DeleteByNamespace(doc.namespace_()).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.DeleteBySchemaType(email_schema.types(0).schema_type()) |
| .status() |
| .code(), |
| Eq(StatusProto::FAILED_PRECONDITION)); |
| |
| SearchSpecProto search_spec = SearchSpecProto::default_instance(); |
| ScoringSpecProto scoring_spec = ScoringSpecProto::default_instance(); |
| ResultSpecProto result_spec = ResultSpecProto::default_instance(); |
| EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| constexpr int kSomePageToken = 12; |
| EXPECT_THAT(icing.GetNextPage(kSomePageToken).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| icing.InvalidateNextPageToken(kSomePageToken); // Verify this doesn't crash. |
| |
| EXPECT_THAT(icing.PersistToDisk(PersistType::FULL).status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| EXPECT_THAT(icing.Optimize().status(), |
| ProtoStatusIs(StatusProto::FAILED_PRECONDITION)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, GetAllNamespaces) { |
| DocumentProto namespace1 = DocumentBuilder() |
| .SetKey("namespace1", "uri") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(1000) |
| .Build(); |
| DocumentProto namespace2_uri1 = DocumentBuilder() |
| .SetKey("namespace2", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(1000) |
| .Build(); |
| DocumentProto namespace2_uri2 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(1000) |
| .Build(); |
| |
| DocumentProto namespace3 = DocumentBuilder() |
| .SetKey("namespace3", "uri") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(500) |
| .Build(); |
| { |
| // Some arbitrary time that's less than all the document's creation time + |
| // ttl |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(500); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // No namespaces exist yet |
| GetAllNamespacesResultProto result = icing.GetAllNamespaces(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.namespaces(), IsEmpty()); |
| |
| ASSERT_THAT(icing.Put(namespace1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(namespace2_uri1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(namespace2_uri2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(namespace3).status(), ProtoIsOk()); |
| |
| // All namespaces should exist now |
| result = icing.GetAllNamespaces(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.namespaces(), |
| UnorderedElementsAre("namespace1", "namespace2", "namespace3")); |
| |
| // After deleting namespace2_uri1 document, we still have namespace2_uri2 in |
| // "namespace2" so it should still show up |
| ASSERT_THAT(icing.Delete("namespace2", "uri1").status(), ProtoIsOk()); |
| |
| result = icing.GetAllNamespaces(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.namespaces(), |
| UnorderedElementsAre("namespace1", "namespace2", "namespace3")); |
| |
| // After deleting namespace2_uri2 document, we no longer have any documents |
| // in "namespace2" |
| ASSERT_THAT(icing.Delete("namespace2", "uri2").status(), ProtoIsOk()); |
| |
| result = icing.GetAllNamespaces(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.namespaces(), |
| UnorderedElementsAre("namespace1", "namespace3")); |
| } |
| |
| // We reinitialize here so we can feed in a fake clock this time |
| { |
| // Time needs to be past namespace3's creation time (100) + ttl (500) for it |
| // to count as "expired" |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(1000); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Only valid document left is the one in "namespace1" |
| GetAllNamespacesResultProto result = icing.GetAllNamespaces(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.namespaces(), UnorderedElementsAre("namespace1")); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, Hyphens) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SchemaProto schema; |
| SchemaTypeConfigProto* type = schema.add_types(); |
| type->set_schema_type("MyType"); |
| PropertyConfigProto* prop = type->add_properties(); |
| prop->set_property_name("foo"); |
| prop->set_data_type(PropertyConfigProto::DataType::STRING); |
| prop->set_cardinality(PropertyConfigProto::Cardinality::REQUIRED); |
| prop->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::EXACT_ONLY); |
| prop->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("MyType") |
| .AddStringProperty("foo", "foo bar-baz bat") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("MyType") |
| .AddStringProperty("foo", "bar for baz bat-man") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("foo:bar-baz"); |
| |
| ResultSpecProto result_spec; |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(2)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_two)); |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document_one)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, RestoreIndex) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| // 1. Create an index with a LiteIndex that will only allow one document |
| // before needing a merge. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add two documents. These should get merged into the main index. |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = DocumentBuilder(document).SetUri("fake_type/1").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| // Add one document. This one should get remain in the lite index. |
| document = DocumentBuilder(document).SetUri("fake_type/2").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Delete the index file to trigger RestoreIndexIfNeeded. |
| std::string idx_subdir = GetIndexDir() + "/idx"; |
| filesystem()->DeleteDirectoryRecursively(idx_subdir.c_str()); |
| |
| // 3. Create the index again. This should trigger index restoration. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("consectetur"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.next_page_token(), Eq(0)); |
| // All documents should be retrievable. |
| ASSERT_THAT(results.results(), SizeIs(3)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("fake_type/2")); |
| EXPECT_THAT(results.results(1).document().uri(), Eq("fake_type/1")); |
| EXPECT_THAT(results.results(2).document().uri(), Eq("fake_type/0")); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, RestoreIndexLoseLiteIndex) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| // 1. Create an index with a LiteIndex that will only allow one document |
| // before needing a merge. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add two documents. These should get merged into the main index. |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = DocumentBuilder(document).SetUri("fake_type/1").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| // Add one document. This one should get remain in the lite index. |
| document = DocumentBuilder(document).SetUri("fake_type/2").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Delete the last document from the document log |
| { |
| const std::string document_log_file = absl_ports::StrCat( |
| GetDocumentDir(), "/", DocumentLogCreator::GetDocumentLogFilename()); |
| filesystem()->DeleteFile(document_log_file.c_str()); |
| ICING_ASSERT_OK_AND_ASSIGN( |
| auto create_result, |
| PortableFileBackedProtoLog<DocumentWrapper>::Create( |
| filesystem(), document_log_file.c_str(), |
| PortableFileBackedProtoLog<DocumentWrapper>::Options( |
| /*compress_in=*/true))); |
| std::unique_ptr<PortableFileBackedProtoLog<DocumentWrapper>> document_log = |
| std::move(create_result.proto_log); |
| |
| document = DocumentBuilder(document).SetUri("fake_type/0").Build(); |
| DocumentWrapper wrapper; |
| *wrapper.mutable_document() = document; |
| ASSERT_THAT(document_log->WriteProto(wrapper), IsOk()); |
| |
| document = DocumentBuilder(document).SetUri("fake_type/1").Build(); |
| *wrapper.mutable_document() = document; |
| ASSERT_THAT(document_log->WriteProto(wrapper), IsOk()); |
| } |
| |
| // 3. Create the index again. This should throw out the lite index and trigger |
| // index restoration which will only restore the two documents in the main |
| // index. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("consectetur"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.next_page_token(), Eq(0)); |
| // Only the documents that were in the main index should be retrievable. |
| ASSERT_THAT(results.results(), SizeIs(2)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("fake_type/1")); |
| EXPECT_THAT(results.results(1).document().uri(), Eq("fake_type/0")); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, RestoreIndexLoseIndex) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| // 1. Create an index with a LiteIndex that will only allow one document |
| // before needing a merge. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add two documents. These should get merged into the main index. |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = DocumentBuilder(document).SetUri("fake_type/1").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| // Add one document. This one should get remain in the lite index. |
| document = DocumentBuilder(document).SetUri("fake_type/2").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Delete the last two documents from the document log. |
| { |
| const std::string document_log_file = absl_ports::StrCat( |
| GetDocumentDir(), "/", DocumentLogCreator::GetDocumentLogFilename()); |
| filesystem()->DeleteFile(document_log_file.c_str()); |
| ICING_ASSERT_OK_AND_ASSIGN( |
| auto create_result, |
| PortableFileBackedProtoLog<DocumentWrapper>::Create( |
| filesystem(), document_log_file.c_str(), |
| PortableFileBackedProtoLog<DocumentWrapper>::Options( |
| /*compress_in=*/true))); |
| std::unique_ptr<PortableFileBackedProtoLog<DocumentWrapper>> document_log = |
| std::move(create_result.proto_log); |
| |
| document = DocumentBuilder(document).SetUri("fake_type/0").Build(); |
| DocumentWrapper wrapper; |
| *wrapper.mutable_document() = document; |
| ASSERT_THAT(document_log->WriteProto(wrapper), IsOk()); |
| } |
| |
| // 3. Create the index again. This should throw out the lite and main index |
| // and trigger index restoration. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("consectetur"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.next_page_token(), Eq(0)); |
| // Only the first document should be retrievable. |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("fake_type/0")); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| DocumentWithNoIndexedContentDoesntCauseRestoreIndex) { |
| // 1. Create an index with a single document in it that has no indexed |
| // content. |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Set a schema for a single type that has no indexed properties. |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("unindexedField") |
| .SetDataTypeString(MATCH_NONE, TOKENIZER_NONE) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| // Add a document that contains no indexed content. |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("unindexedField", |
| "Don't you dare search over this!") |
| .Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Create the index again. This should NOT trigger a recovery of any kind. |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| DocumentWithNoValidIndexedContentDoesntCauseRestoreIndex) { |
| // 1. Create an index with a single document in it that has no valid indexed |
| // tokens in its content. |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Set a schema for a single type that has no indexed properties. |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add a document that contains no valid indexed content - just punctuation. |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "?...!") |
| .Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Create the index again. This should NOT trigger a recovery of any kind. |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, IndexingDocMergeFailureResets) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| // 1. Create an index with a LiteIndex that will only allow one document |
| // before needing a merge. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Add two documents. These should get merged into the main index. |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| document = DocumentBuilder(document).SetUri("fake_type/1").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| // Add one document. This one should get remain in the lite index. |
| document = DocumentBuilder(document).SetUri("fake_type/2").Build(); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // 2. Delete the index file to trigger RestoreIndexIfNeeded. |
| std::string idx_subdir = GetIndexDir() + "/idx"; |
| filesystem()->DeleteDirectoryRecursively(idx_subdir.c_str()); |
| |
| // 3. Setup a mock filesystem to fail to grow the main index once. |
| bool has_failed_already = false; |
| auto open_write_lambda = [this, &has_failed_already](const char* filename) { |
| std::string main_lexicon_suffix = "/main-lexicon.prop.2"; |
| std::string filename_string(filename); |
| if (!has_failed_already && |
| filename_string.length() >= main_lexicon_suffix.length() && |
| filename_string.substr( |
| filename_string.length() - main_lexicon_suffix.length(), |
| main_lexicon_suffix.length()) == main_lexicon_suffix) { |
| has_failed_already = true; |
| return -1; |
| } |
| return this->filesystem()->OpenForWrite(filename); |
| }; |
| auto mock_icing_filesystem = std::make_unique<IcingMockFilesystem>(); |
| ON_CALL(*mock_icing_filesystem, OpenForWrite) |
| .WillByDefault(open_write_lambda); |
| |
| // 4. Create the index again. This should trigger index restoration. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_index_merge_size(document.ByteSizeLong()); |
| TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(), |
| std::move(mock_icing_filesystem), |
| std::make_unique<FakeClock>(), |
| GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), |
| ProtoStatusIs(StatusProto::WARNING_DATA_LOSS)); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("consectetur"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.next_page_token(), Eq(0)); |
| // Only the last document that was added should still be retrievable. |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("fake_type/2")); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogFunctionLatency) { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats().latency_ms(), Eq(10)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogNumberOfDocuments) { |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("icing", "fake_type/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("icing", "fake_type/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| { |
| // Initialize and put a document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_documents(), |
| Eq(0)); |
| |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| } |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_documents(), |
| Eq(1)); |
| |
| // Put another document. |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| } |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_documents(), |
| Eq(2)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| InitializeShouldNotLogRecoveryCauseForFirstTimeInitialize) { |
| // Even though the fake timer will return 10, all the latency numbers related |
| // to recovery / restoration should be 0 during the first-time initialization. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogRecoveryCausePartialDataLoss) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| { |
| // Initialize and put a document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| { |
| // Append a non-checksummed document. This will mess up the checksum of the |
| // proto log, forcing it to rewind and later return a DATA_LOSS error. |
| const std::string serialized_document = document.SerializeAsString(); |
| const std::string document_log_file = absl_ports::StrCat( |
| GetDocumentDir(), "/", DocumentLogCreator::GetDocumentLogFilename()); |
| |
| int64_t file_size = filesystem()->GetFileSize(document_log_file.c_str()); |
| filesystem()->PWrite(document_log_file.c_str(), file_size, |
| serialized_document.data(), |
| serialized_document.size()); |
| } |
| |
| { |
| // Document store will rewind to previous checkpoint. The cause should be |
| // DATA_LOSS and the data status should be PARTIAL_LOSS. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::DATA_LOSS)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::PARTIAL_LOSS)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| InitializeShouldLogRecoveryCauseCompleteDataLoss) { |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("icing", "fake_type/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| const std::string document_log_file = absl_ports::StrCat( |
| GetDocumentDir(), "/", DocumentLogCreator::GetDocumentLogFilename()); |
| int64_t corruptible_offset; |
| |
| { |
| // Initialize and put a document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // There's some space at the beginning of the file (e.g. header, kmagic, |
| // etc) that is necessary to initialize the FileBackedProtoLog. We can't |
| // corrupt that region, so we need to figure out the offset at which |
| // documents will be written to - which is the file size after |
| // initialization. |
| corruptible_offset = filesystem()->GetFileSize(document_log_file.c_str()); |
| |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| } |
| |
| { |
| // "Corrupt" the content written in the log. Make the corrupt document |
| // smaller than our original one so we don't accidentally write past our |
| // file. |
| DocumentProto document = |
| DocumentBuilder().SetKey("invalid_namespace", "invalid_uri").Build(); |
| std::string serialized_document = document.SerializeAsString(); |
| ASSERT_TRUE(filesystem()->PWrite( |
| document_log_file.c_str(), corruptible_offset, |
| serialized_document.data(), serialized_document.size())); |
| |
| PortableFileBackedProtoLog<DocumentWrapper>::Header header = |
| ReadDocumentLogHeader(*filesystem(), document_log_file); |
| |
| // Set dirty bit to true to reflect that something changed in the log. |
| header.SetDirtyFlag(true); |
| header.SetHeaderChecksum(header.CalculateHeaderChecksum()); |
| |
| WriteDocumentLogHeader(*filesystem(), document_log_file, header); |
| } |
| |
| { |
| // Document store will completely rewind. The cause should be DATA_LOSS and |
| // the data status should be COMPLETE_LOSS. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::DATA_LOSS)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::COMPLETE_LOSS)); |
| // The complete rewind of ground truth causes us to clear the index, but |
| // that's not considered a restoration. |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| InitializeShouldLogRecoveryCauseInconsistentWithGroundTruth) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| { |
| // Initialize and put a document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| { |
| // Delete the index file to trigger RestoreIndexIfNeeded. |
| std::string idx_subdir = GetIndexDir() + "/idx"; |
| filesystem()->DeleteDirectoryRecursively(idx_subdir.c_str()); |
| } |
| |
| { |
| // Index is empty but ground truth is not. Index should be restored due to |
| // the inconsistency. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::INCONSISTENT_WITH_GROUND_TRUTH)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| InitializeShouldLogRecoveryCauseSchemaChangesOutofSync) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| { |
| // Initialize and put one document. |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| { |
| // Simulate a schema change where power is lost after the schema is written. |
| SchemaProto new_schema = |
| SchemaBuilder() |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("Message") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| // Write the marker file |
| std::string marker_filepath = |
| absl_ports::StrCat(options.base_dir(), "/set_schema_marker"); |
| ScopedFd sfd(filesystem()->OpenForWrite(marker_filepath.c_str())); |
| ASSERT_TRUE(sfd.is_valid()); |
| |
| // Write the new schema |
| FakeClock fake_clock; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<SchemaStore> schema_store, |
| SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock)); |
| ICING_EXPECT_OK(schema_store->SetSchema(new_schema)); |
| } |
| |
| { |
| // Both document store and index should be recovered from checksum mismatch. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::SCHEMA_CHANGES_OUT_OF_SYNC)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::SCHEMA_CHANGES_OUT_OF_SYNC)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| |
| { |
| // No recovery should be needed. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogRecoveryCauseIndexIOError) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| { |
| // Initialize and put one document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // lambda to fail OpenForWrite on lite index hit buffer once. |
| bool has_failed_already = false; |
| auto open_write_lambda = [this, &has_failed_already](const char* filename) { |
| std::string lite_index_buffer_file_path = |
| absl_ports::StrCat(GetIndexDir(), "/idx/lite.hb"); |
| std::string filename_string(filename); |
| if (!has_failed_already && filename_string == lite_index_buffer_file_path) { |
| has_failed_already = true; |
| return -1; |
| } |
| return this->filesystem()->OpenForWrite(filename); |
| }; |
| |
| auto mock_icing_filesystem = std::make_unique<IcingMockFilesystem>(); |
| // This fails Index::Create() once. |
| ON_CALL(*mock_icing_filesystem, OpenForWrite) |
| .WillByDefault(open_write_lambda); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::move(mock_icing_filesystem), |
| std::move(fake_clock), GetTestJniCache()); |
| |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::IO_ERROR)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogRecoveryCauseDocStoreIOError) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| { |
| // Initialize and put one document. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| } |
| |
| // lambda to fail Read on document store header once. |
| bool has_failed_already = false; |
| auto read_lambda = [this, &has_failed_already](const char* filename, |
| void* buf, size_t buf_size) { |
| std::string document_store_header_file_path = |
| absl_ports::StrCat(GetDocumentDir(), "/document_store_header"); |
| std::string filename_string(filename); |
| if (!has_failed_already && |
| filename_string == document_store_header_file_path) { |
| has_failed_already = true; |
| return false; |
| } |
| return this->filesystem()->Read(filename, buf, buf_size); |
| }; |
| |
| auto mock_filesystem = std::make_unique<MockFilesystem>(); |
| // This fails DocumentStore::InitializeDerivedFiles() once. |
| ON_CALL(*mock_filesystem, Read(A<const char*>(), _, _)) |
| .WillByDefault(read_lambda); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::move(mock_filesystem), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::IO_ERROR)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(0)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| InitializeShouldLogRecoveryCauseSchemaStoreIOError) { |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| } |
| |
| { |
| // Delete the schema store header file to trigger an I/O error. |
| std::string schema_store_header_file_path = |
| GetSchemaDir() + "/schema_store_header"; |
| filesystem()->DeleteFile(schema_store_header_file_path.c_str()); |
| } |
| |
| { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::IO_ERROR)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .schema_store_recovery_latency_ms(), |
| Eq(10)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .document_store_recovery_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT( |
| initialize_result_proto.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(initialize_result_proto.initialize_stats() |
| .index_restoration_latency_ms(), |
| Eq(0)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, InitializeShouldLogNumberOfSchemaTypes) { |
| { |
| // Initialize an empty storage. |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| // There should be 0 schema types. |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_schema_types(), |
| Eq(0)); |
| |
| // Set a schema with one type config. |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| } |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| // There should be 1 schema type. |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_schema_types(), |
| Eq(1)); |
| |
| // Create and set a schema with two type configs: Email and Message. |
| SchemaProto schema = CreateEmailSchema(); |
| |
| 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::REQUIRED); |
| body->mutable_string_indexing_config()->set_term_match_type( |
| TermMatchType::PREFIX); |
| body->mutable_string_indexing_config()->set_tokenizer_type( |
| StringIndexingConfig::TokenizerType::PLAIN); |
| |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| } |
| |
| { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| InitializeResultProto initialize_result_proto = icing.Initialize(); |
| EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(initialize_result_proto.initialize_stats().num_schema_types(), |
| Eq(2)); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, PutDocumentShouldLogFunctionLatency) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| PutResultProto put_result_proto = icing.Put(document); |
| EXPECT_THAT(put_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(put_result_proto.put_document_stats().latency_ms(), Eq(10)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, PutDocumentShouldLogDocumentStoreStats) { |
| DocumentProto document = |
| DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| PutResultProto put_result_proto = icing.Put(document); |
| EXPECT_THAT(put_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(put_result_proto.put_document_stats().document_store_latency_ms(), |
| Eq(10)); |
| size_t document_size = put_result_proto.put_document_stats().document_size(); |
| EXPECT_THAT(document_size, Ge(document.ByteSizeLong())); |
| EXPECT_THAT(document_size, Le(document.ByteSizeLong() + |
| sizeof(DocumentProto::InternalFields))); |
| } |
| |
| TEST_F(IcingSearchEngineTest, PutDocumentShouldLogIndexingStats) { |
| DocumentProto document = DocumentBuilder() |
| .SetKey("icing", "fake_type/0") |
| .SetSchema("Message") |
| .AddStringProperty("body", "message body") |
| .Build(); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| PutResultProto put_result_proto = icing.Put(document); |
| EXPECT_THAT(put_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(put_result_proto.put_document_stats().index_latency_ms(), Eq(10)); |
| // No merge should happen. |
| EXPECT_THAT(put_result_proto.put_document_stats().index_merge_latency_ms(), |
| Eq(0)); |
| // The input document has 2 tokens. |
| EXPECT_THAT(put_result_proto.put_document_stats() |
| .tokenization_stats() |
| .num_tokens_indexed(), |
| Eq(2)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, PutDocumentShouldLogIndexMergeLatency) { |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("icing", "fake_type/1") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("icing", "fake_type/2") |
| .SetSchema("Message") |
| .AddStringProperty("body", kIpsumText) |
| .Build(); |
| |
| // Create an icing instance with index_merge_size = document1's size. |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| icing_options.set_index_merge_size(document1.ByteSizeLong()); |
| |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(10); |
| TestIcingSearchEngine icing(icing_options, std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| |
| // Putting document2 should trigger an index merge. |
| PutResultProto put_result_proto = icing.Put(document2); |
| EXPECT_THAT(put_result_proto.status(), ProtoIsOk()); |
| EXPECT_THAT(put_result_proto.put_document_stats().index_merge_latency_ms(), |
| Eq(10)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchWithProjectionEmptyFieldPath) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // 1. Add two email documents |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Tom Hanks") |
| .AddStringProperty("emailAddress", "ny152@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Goodnight Moon!") |
| .AddStringProperty("body", |
| "Count all the sheep and tell them 'Hello'.") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| // 2. Issue a query that will match those documents and use an empty field |
| // mask to request NO properties. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("hello"); |
| |
| ResultSpecProto result_spec; |
| // Retrieve only one result at a time to make sure that projection works when |
| // retrieving all pages. |
| result_spec.set_num_per_page(1); |
| TypePropertyMask* email_field_mask = result_spec.add_type_property_masks(); |
| email_field_mask->set_schema_type("Email"); |
| email_field_mask->add_paths(""); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(1)); |
| |
| // 3. Verify that the returned results contain no properties. |
| DocumentProto projected_document_two = DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .Build(); |
| EXPECT_THAT(results.results(0).document(), |
| EqualsProto(projected_document_two)); |
| |
| results = icing.GetNextPage(results.next_page_token()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(1)); |
| DocumentProto projected_document_one = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .Build(); |
| EXPECT_THAT(results.results(0).document(), |
| EqualsProto(projected_document_one)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchWithProjectionMultipleFieldPaths) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // 1. Add two email documents |
| DocumentProto document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .AddStringProperty( |
| "body", "Oh what a beautiful morning! Oh what a beautiful day!") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty( |
| "sender", DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Tom Hanks") |
| .AddStringProperty("emailAddress", "ny152@aol.com") |
| .Build()) |
| .AddStringProperty("subject", "Goodnight Moon!") |
| .AddStringProperty("body", |
| "Count all the sheep and tell them 'Hello'.") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| // 2. Issue a query that will match those documents and request only |
| // 'sender.name' and 'subject' properties. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("hello"); |
| |
| ResultSpecProto result_spec; |
| // Retrieve only one result at a time to make sure that projection works when |
| // retrieving all pages. |
| result_spec.set_num_per_page(1); |
| TypePropertyMask* email_field_mask = result_spec.add_type_property_masks(); |
| email_field_mask->set_schema_type("Email"); |
| email_field_mask->add_paths("sender.name"); |
| email_field_mask->add_paths("subject"); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(1)); |
| |
| // 3. Verify that the returned results only contain the 'sender.name' |
| // property. |
| DocumentProto projected_document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty("sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Tom Hanks") |
| .Build()) |
| .AddStringProperty("subject", "Goodnight Moon!") |
| .Build(); |
| EXPECT_THAT(results.results(0).document(), |
| EqualsProto(projected_document_two)); |
| |
| results = icing.GetNextPage(results.next_page_token()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(1)); |
| DocumentProto projected_document_one = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddDocumentProperty("sender", |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .Build()) |
| .AddStringProperty("subject", "Hello World!") |
| .Build(); |
| EXPECT_THAT(results.results(0).document(), |
| EqualsProto(projected_document_one)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, QueryStatsProtoTest) { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(5); |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates and inserts 5 documents |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| DocumentProto document4 = CreateMessageDocument("namespace", "uri4"); |
| DocumentProto document5 = CreateMessageDocument("namespace", "uri5"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.add_namespace_filters("namespace"); |
| search_spec.add_schema_type_filters(document1.schema()); |
| search_spec.set_query("message"); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(2); |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(64); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(1); |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(3); |
| |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::CREATION_TIMESTAMP); |
| |
| // Searches and gets the first page, 2 results with 2 snippets |
| SearchResultProto search_result = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(2)); |
| ASSERT_THAT(search_result.next_page_token(), Ne(kInvalidNextPageToken)); |
| |
| // Check the stats |
| QueryStatsProto exp_stats; |
| exp_stats.set_query_length(7); |
| exp_stats.set_num_terms(1); |
| exp_stats.set_num_namespaces_filtered(1); |
| exp_stats.set_num_schema_types_filtered(1); |
| exp_stats.set_ranking_strategy( |
| ScoringSpecProto::RankingStrategy::CREATION_TIMESTAMP); |
| exp_stats.set_is_first_page(true); |
| exp_stats.set_requested_page_size(2); |
| exp_stats.set_num_results_returned_current_page(2); |
| exp_stats.set_num_documents_scored(5); |
| exp_stats.set_num_results_with_snippets(2); |
| exp_stats.set_latency_ms(5); |
| exp_stats.set_parse_query_latency_ms(5); |
| exp_stats.set_scoring_latency_ms(5); |
| exp_stats.set_ranking_latency_ms(5); |
| exp_stats.set_document_retrieval_latency_ms(5); |
| EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats)); |
| |
| // Second page, 2 result with 1 snippet |
| search_result = icing.GetNextPage(search_result.next_page_token()); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(2)); |
| ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken)); |
| |
| exp_stats = QueryStatsProto(); |
| exp_stats.set_is_first_page(false); |
| exp_stats.set_requested_page_size(2); |
| exp_stats.set_num_results_returned_current_page(2); |
| exp_stats.set_num_results_with_snippets(1); |
| exp_stats.set_latency_ms(5); |
| exp_stats.set_document_retrieval_latency_ms(5); |
| EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats)); |
| |
| // Third page, 1 result with 0 snippets |
| search_result = icing.GetNextPage(search_result.next_page_token()); |
| ASSERT_THAT(search_result.status(), ProtoIsOk()); |
| ASSERT_THAT(search_result.results(), SizeIs(1)); |
| ASSERT_THAT(search_result.next_page_token(), Eq(kInvalidNextPageToken)); |
| |
| exp_stats = QueryStatsProto(); |
| exp_stats.set_is_first_page(false); |
| exp_stats.set_requested_page_size(2); |
| exp_stats.set_num_results_returned_current_page(1); |
| exp_stats.set_num_results_with_snippets(0); |
| exp_stats.set_latency_ms(5); |
| exp_stats.set_document_retrieval_latency_ms(5); |
| EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, OptimizeStatsProtoTest) { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(5); |
| fake_clock->SetSystemTimeMilliseconds(10000); |
| auto icing = std::make_unique<TestIcingSearchEngine>( |
| GetDefaultIcingOptions(), std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), std::move(fake_clock), |
| GetTestJniCache()); |
| ASSERT_THAT(icing->Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing->SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Create three documents. |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| document2.set_creation_timestamp_ms(9000); |
| document2.set_ttl_ms(500); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| ASSERT_THAT(icing->Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing->Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing->Put(document3).status(), ProtoIsOk()); |
| |
| // Delete the first document. |
| ASSERT_THAT(icing->Delete(document1.namespace_(), document1.uri()).status(), |
| ProtoIsOk()); |
| ASSERT_THAT(icing->PersistToDisk(PersistType::FULL).status(), ProtoIsOk()); |
| |
| OptimizeStatsProto expected; |
| expected.set_latency_ms(5); |
| expected.set_document_store_optimize_latency_ms(5); |
| expected.set_index_restoration_latency_ms(5); |
| expected.set_num_original_documents(3); |
| expected.set_num_deleted_documents(1); |
| expected.set_num_expired_documents(1); |
| |
| // Run Optimize |
| OptimizeResultProto result = icing->Optimize(); |
| // Depending on how many blocks the documents end up spread across, it's |
| // possible that Optimize can remove documents without shrinking storage. The |
| // first Optimize call will also write the OptimizeStatusProto for the first |
| // time which will take up 1 block. So make sure that before_size is no less |
| // than after_size - 1 block. |
| uint32_t page_size = getpagesize(); |
| EXPECT_THAT(result.optimize_stats().storage_size_before(), |
| Ge(result.optimize_stats().storage_size_after() - page_size)); |
| result.mutable_optimize_stats()->clear_storage_size_before(); |
| result.mutable_optimize_stats()->clear_storage_size_after(); |
| EXPECT_THAT(result.optimize_stats(), EqualsProto(expected)); |
| |
| fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetTimerElapsedMilliseconds(5); |
| fake_clock->SetSystemTimeMilliseconds(20000); |
| icing = std::make_unique<TestIcingSearchEngine>( |
| GetDefaultIcingOptions(), std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), std::move(fake_clock), |
| GetTestJniCache()); |
| ASSERT_THAT(icing->Initialize().status(), ProtoIsOk()); |
| |
| expected = OptimizeStatsProto(); |
| expected.set_latency_ms(5); |
| expected.set_document_store_optimize_latency_ms(5); |
| expected.set_index_restoration_latency_ms(5); |
| expected.set_num_original_documents(1); |
| expected.set_num_deleted_documents(0); |
| expected.set_num_expired_documents(0); |
| expected.set_time_since_last_optimize_ms(10000); |
| |
| // Run Optimize |
| result = icing->Optimize(); |
| EXPECT_THAT(result.optimize_stats().storage_size_before(), |
| Eq(result.optimize_stats().storage_size_after())); |
| result.mutable_optimize_stats()->clear_storage_size_before(); |
| result.mutable_optimize_stats()->clear_storage_size_after(); |
| EXPECT_THAT(result.optimize_stats(), EqualsProto(expected)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, StorageInfoTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Create three documents. |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| // Ensure that total_storage_size is set. All the other stats are covered by |
| // the classes that generate them. |
| StorageInfoResultProto result = icing.GetStorageInfo(); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.storage_info().total_storage_size(), Ge(0)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SnippetErrorTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Generic").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REPEATED))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetScore(10) |
| .SetSchema("Generic") |
| .AddStringProperty("subject", "I like cats", "I like dogs", |
| "I like birds", "I like fish") |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetScore(20) |
| .SetSchema("Generic") |
| .AddStringProperty("subject", "I like red", "I like green", |
| "I like blue", "I like yellow") |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri3") |
| .SetScore(5) |
| .SetSchema("Generic") |
| .AddStringProperty("subject", "I like cupcakes", "I like donuts", |
| "I like eclairs", "I like froyo") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.add_schema_type_filters("Generic"); |
| search_spec.set_term_match_type(TermMatchType::EXACT_ONLY); |
| search_spec.set_query("like"); |
| ScoringSpecProto scoring_spec; |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(2); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(3); |
| result_spec.mutable_snippet_spec()->set_max_window_utf32_length(4); |
| SearchResultProto search_results = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| ASSERT_THAT(search_results.results(), SizeIs(3)); |
| const SearchResultProto::ResultProto* result = &search_results.results(0); |
| EXPECT_THAT(result->document().uri(), Eq("uri2")); |
| ASSERT_THAT(result->snippet().entries(), SizeIs(3)); |
| const SnippetProto::EntryProto* entry = &result->snippet().entries(0); |
| EXPECT_THAT(entry->property_name(), "subject[0]"); |
| std::string_view content = GetString(&result->document(), "subject[0]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| entry = &result->snippet().entries(1); |
| EXPECT_THAT(entry->property_name(), "subject[1]"); |
| content = GetString(&result->document(), "subject[1]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| entry = &result->snippet().entries(2); |
| EXPECT_THAT(entry->property_name(), "subject[2]"); |
| content = GetString(&result->document(), "subject[2]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| result = &search_results.results(1); |
| EXPECT_THAT(result->document().uri(), Eq("uri1")); |
| ASSERT_THAT(result->snippet().entries(), SizeIs(3)); |
| entry = &result->snippet().entries(0); |
| EXPECT_THAT(entry->property_name(), "subject[0]"); |
| content = GetString(&result->document(), "subject[0]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| entry = &result->snippet().entries(1); |
| ASSERT_THAT(entry->property_name(), "subject[1]"); |
| content = GetString(&result->document(), "subject[1]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| entry = &result->snippet().entries(2); |
| ASSERT_THAT(entry->property_name(), "subject[2]"); |
| content = GetString(&result->document(), "subject[2]"); |
| EXPECT_THAT(GetMatches(content, *entry), ElementsAre("like")); |
| |
| result = &search_results.results(2); |
| ASSERT_THAT(result->document().uri(), Eq("uri3")); |
| ASSERT_THAT(result->snippet().entries(), IsEmpty()); |
| } |
| |
| TEST_F(IcingSearchEngineTest, CJKSnippetTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // String: "我每天走路去上班。" |
| // ^ ^ ^ ^^ |
| // UTF8 idx: 0 3 9 15 18 |
| // UTF16 idx: 0 1 3 5 6 |
| // Breaks into segments: "我", "每天", "走路", "去", "上班" |
| constexpr std::string_view kChinese = "我每天走路去上班。"; |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", kChinese) |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| // Search and request snippet matching but no windowing. |
| SearchSpecProto search_spec; |
| search_spec.set_query("走"); |
| search_spec.set_term_match_type(MATCH_PREFIX); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_num_to_snippet( |
| std::numeric_limits<int>::max()); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property( |
| std::numeric_limits<int>::max()); |
| |
| // Search and make sure that we got a single successful result |
| SearchResultProto search_results = icing.Search( |
| search_spec, ScoringSpecProto::default_instance(), result_spec); |
| ASSERT_THAT(search_results.status(), ProtoIsOk()); |
| ASSERT_THAT(search_results.results(), SizeIs(1)); |
| const SearchResultProto::ResultProto* result = &search_results.results(0); |
| EXPECT_THAT(result->document().uri(), Eq("uri1")); |
| |
| // Ensure that one and only one property was matched and it was "body" |
| ASSERT_THAT(result->snippet().entries(), SizeIs(1)); |
| const SnippetProto::EntryProto* entry = &result->snippet().entries(0); |
| EXPECT_THAT(entry->property_name(), Eq("body")); |
| |
| // Get the content for "subject" and see what the match is. |
| std::string_view content = GetString(&result->document(), "body"); |
| ASSERT_THAT(content, Eq(kChinese)); |
| |
| // Ensure that there is one and only one match within "subject" |
| ASSERT_THAT(entry->snippet_matches(), SizeIs(1)); |
| const SnippetMatchProto& match_proto = entry->snippet_matches(0); |
| |
| EXPECT_THAT(match_proto.exact_match_byte_position(), Eq(9)); |
| EXPECT_THAT(match_proto.exact_match_byte_length(), Eq(6)); |
| std::string_view match = |
| content.substr(match_proto.exact_match_byte_position(), |
| match_proto.exact_match_byte_length()); |
| ASSERT_THAT(match, Eq("走路")); |
| |
| // Ensure that the utf-16 values are also as expected |
| EXPECT_THAT(match_proto.exact_match_utf16_position(), Eq(3)); |
| EXPECT_THAT(match_proto.exact_match_utf16_length(), Eq(2)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, InvalidToEmptyQueryTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // String: "Luca Brasi sleeps with the 🐟🐟🐟." |
| // ^ ^ ^ ^ ^ ^ ^ ^ ^ |
| // UTF8 idx: 0 5 11 18 23 27 3135 39 |
| // UTF16 idx: 0 5 11 18 23 27 2931 33 |
| // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "🐟", "🐟" |
| // and "🐟". |
| constexpr std::string_view kSicilianMessage = |
| "Luca Brasi sleeps with the 🐟🐟🐟."; |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", kSicilianMessage) |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "Some other content.") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| // Search and request snippet matching but no windowing. |
| SearchSpecProto search_spec; |
| search_spec.set_query("?"); |
| search_spec.set_term_match_type(MATCH_PREFIX); |
| ScoringSpecProto scoring_spec; |
| ResultSpecProto result_spec; |
| |
| // Search and make sure that we got a single successful result |
| SearchResultProto search_results = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| |
| search_spec.set_query("。"); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| |
| search_spec.set_query("-"); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| |
| search_spec.set_query(":"); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| |
| search_spec.set_query("OR"); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| |
| search_spec.set_query(" "); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_results.status(), ProtoIsOk()); |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, EmojiSnippetTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // String: "Luca Brasi sleeps with the 🐟🐟🐟." |
| // ^ ^ ^ ^ ^ ^ ^ ^ ^ |
| // UTF8 idx: 0 5 11 18 23 27 3135 39 |
| // UTF16 idx: 0 5 11 18 23 27 2931 33 |
| // Breaks into segments: "Luca", "Brasi", "sleeps", "with", "the", "🐟", "🐟" |
| // and "🐟". |
| constexpr std::string_view kSicilianMessage = |
| "Luca Brasi sleeps with the 🐟🐟🐟."; |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", kSicilianMessage) |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Message") |
| .AddStringProperty("body", "Some other content.") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| // Search and request snippet matching but no windowing. |
| SearchSpecProto search_spec; |
| search_spec.set_query("🐟"); |
| search_spec.set_term_match_type(MATCH_PREFIX); |
| |
| ResultSpecProto result_spec; |
| result_spec.mutable_snippet_spec()->set_num_to_snippet(1); |
| result_spec.mutable_snippet_spec()->set_num_matches_per_property(1); |
| |
| // Search and make sure that we got a single successful result |
| SearchResultProto search_results = icing.Search( |
| search_spec, ScoringSpecProto::default_instance(), result_spec); |
| ASSERT_THAT(search_results.status(), ProtoIsOk()); |
| ASSERT_THAT(search_results.results(), SizeIs(1)); |
| const SearchResultProto::ResultProto* result = &search_results.results(0); |
| EXPECT_THAT(result->document().uri(), Eq("uri1")); |
| |
| // Ensure that one and only one property was matched and it was "body" |
| ASSERT_THAT(result->snippet().entries(), SizeIs(1)); |
| const SnippetProto::EntryProto* entry = &result->snippet().entries(0); |
| EXPECT_THAT(entry->property_name(), Eq("body")); |
| |
| // Get the content for "subject" and see what the match is. |
| std::string_view content = GetString(&result->document(), "body"); |
| ASSERT_THAT(content, Eq(kSicilianMessage)); |
| |
| // Ensure that there is one and only one match within "subject" |
| ASSERT_THAT(entry->snippet_matches(), SizeIs(1)); |
| const SnippetMatchProto& match_proto = entry->snippet_matches(0); |
| |
| EXPECT_THAT(match_proto.exact_match_byte_position(), Eq(27)); |
| EXPECT_THAT(match_proto.exact_match_byte_length(), Eq(4)); |
| std::string_view match = |
| content.substr(match_proto.exact_match_byte_position(), |
| match_proto.exact_match_byte_length()); |
| ASSERT_THAT(match, Eq("🐟")); |
| |
| // Ensure that the utf-16 values are also as expected |
| EXPECT_THAT(match_proto.exact_match_utf16_position(), Eq(27)); |
| EXPECT_THAT(match_proto.exact_match_utf16_length(), Eq(2)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, PutDocumentIndexFailureDeletion) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Testing has shown that adding ~600,000 terms generated this way will |
| // fill up the hit buffer. |
| std::vector<std::string> terms = GenerateUniqueTerms(600000); |
| std::string content = absl_ports::StrJoin(terms, " "); |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", "foo " + content) |
| .Build(); |
| // We failed to add the document to the index fully. This means that we should |
| // reject the document from Icing entirely. |
| ASSERT_THAT(icing.Put(document).status(), |
| ProtoStatusIs(StatusProto::OUT_OF_SPACE)); |
| |
| // Make sure that the document isn't searchable. |
| SearchSpecProto search_spec; |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(MATCH_PREFIX); |
| |
| SearchResultProto search_results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(search_results.status(), ProtoIsOk()); |
| ASSERT_THAT(search_results.results(), IsEmpty()); |
| |
| // Make sure that the document isn't retrievable. |
| GetResultProto get_result = |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()); |
| ASSERT_THAT(get_result.status(), ProtoStatusIs(StatusProto::NOT_FOUND)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchSuggestionsTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // Creates and inserts 6 documents, and index 6 termSix, 5 termFive, 4 |
| // termFour, 3 termThree, 2 termTwo and one termOne. |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty( |
| "subject", "termOne termTwo termThree termFour termFive termSix") |
| .Build(); |
| DocumentProto document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", |
| "termTwo termThree termFour termFive termSix") |
| .Build(); |
| DocumentProto document3 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri3") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termThree termFour termFive termSix") |
| .Build(); |
| DocumentProto document4 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri4") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termFour termFive termSix") |
| .Build(); |
| DocumentProto document5 = |
| DocumentBuilder() |
| .SetKey("namespace", "uri5") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termFive termSix") |
| .Build(); |
| DocumentProto document6 = DocumentBuilder() |
| .SetKey("namespace", "uri6") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termSix") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document6).status(), ProtoIsOk()); |
| |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("t"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| // Query all suggestions, and they will be ranked. |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions().at(0).query(), "termsix"); |
| ASSERT_THAT(response.suggestions().at(1).query(), "termfive"); |
| ASSERT_THAT(response.suggestions().at(2).query(), "termfour"); |
| ASSERT_THAT(response.suggestions().at(3).query(), "termthree"); |
| ASSERT_THAT(response.suggestions().at(4).query(), "termtwo"); |
| ASSERT_THAT(response.suggestions().at(5).query(), "termone"); |
| |
| // Query first three suggestions, and they will be ranked. |
| suggestion_spec.set_num_to_return(3); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions().at(0).query(), "termsix"); |
| ASSERT_THAT(response.suggestions().at(1).query(), "termfive"); |
| ASSERT_THAT(response.suggestions().at(2).query(), "termfour"); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchSuggestionsTest_ShouldReturnInOneNamespace) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "foo fool") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SuggestionResponse::Suggestion suggestionFoo; |
| suggestionFoo.set_query("foo"); |
| SuggestionResponse::Suggestion suggestionFool; |
| suggestionFool.set_query("fool"); |
| |
| // namespace1 has 2 results. |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("f"); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFoo), |
| EqualsProto(suggestionFool))); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchSuggestionsTest_ShouldReturnInMultipleNamespace) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "fo") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "foo") |
| .Build(); |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace3", "uri3") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| SuggestionResponse::Suggestion suggestionFoo; |
| suggestionFoo.set_query("foo"); |
| SuggestionResponse::Suggestion suggestionFool; |
| suggestionFool.set_query("fool"); |
| |
| // namespace2 and namespace3 has 2 results. |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("f"); |
| suggestion_spec.add_namespace_filters("namespace2"); |
| suggestion_spec.add_namespace_filters("namespace3"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFoo), |
| EqualsProto(suggestionFool))); |
| } |
| |
| TEST_F(IcingSearchEngineTest, |
| SearchSuggestionsTest_OtherNamespaceDontContributeToHitCount) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| // Index 4 documents, |
| // namespace1 has 2 hit2 for term one |
| // namespace2 has 2 hit2 for term two and 1 hit for term one. |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termone") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace1", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termone") |
| .Build(); |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termone termtwo") |
| .Build(); |
| DocumentProto document4 = DocumentBuilder() |
| .SetKey("namespace2", "uri3") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "termtwo") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk()); |
| |
| SuggestionResponse::Suggestion suggestionTermOne; |
| suggestionTermOne.set_query("termone"); |
| SuggestionResponse::Suggestion suggestionTermTwo; |
| suggestionTermTwo.set_query("termtwo"); |
| |
| // only search suggestion for namespace2. The correctly order should be |
| // {"termtwo", "termone"}. If we're not filtering out namespace1 when |
| // calculating our score, then it will be {"termone", "termtwo"}. |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("t"); |
| suggestion_spec.add_namespace_filters("namespace2"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| ElementsAre(EqualsProto(suggestionTermTwo), |
| EqualsProto(suggestionTermOne))); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchSuggestionsTest_DeletionTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SuggestionResponse::Suggestion suggestionFool; |
| suggestionFool.set_query("fool"); |
| |
| // namespace1 has this suggestion |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("f"); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| |
| // namespace2 has this suggestion |
| suggestion_spec.clear_namespace_filters(); |
| suggestion_spec.add_namespace_filters("namespace2"); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| |
| // delete document from namespace 1 |
| EXPECT_THAT(icing.Delete("namespace1", "uri1").status(), ProtoIsOk()); |
| |
| // Now namespace1 will return empty |
| suggestion_spec.clear_namespace_filters(); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), IsEmpty()); |
| |
| // namespace2 still has this suggestion, so we can prove the reason of |
| // namespace 1 cannot find it is we filter it out, not it doesn't exist. |
| suggestion_spec.add_namespace_filters("namespace2"); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchSuggestionsTest_ExpiredTest) { |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(500) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace2", "uri2") |
| .SetSchema("Email") |
| .SetCreationTimestampMs(100) |
| .SetTtlMs(1000) |
| .AddStringProperty("subject", "fool") |
| .Build(); |
| { |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(400); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| EXPECT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(), |
| ProtoIsOk()); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| |
| SuggestionResponse::Suggestion suggestionFool; |
| suggestionFool.set_query("fool"); |
| |
| // namespace1 has this suggestion |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("f"); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| |
| // namespace2 has this suggestion |
| suggestion_spec.clear_namespace_filters(); |
| suggestion_spec.add_namespace_filters("namespace2"); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| } |
| // We reinitialize here so we can feed in a fake clock this time |
| { |
| // Time needs to be past document1 creation time (100) + ttl (500) for it |
| // to count as "expired". document2 is not expired since its ttl is 1000. |
| auto fake_clock = std::make_unique<FakeClock>(); |
| fake_clock->SetSystemTimeMilliseconds(800); |
| |
| TestIcingSearchEngine icing(GetDefaultIcingOptions(), |
| std::make_unique<Filesystem>(), |
| std::make_unique<IcingFilesystem>(), |
| std::move(fake_clock), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("f"); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| // Now namespace1 will return empty |
| suggestion_spec.clear_namespace_filters(); |
| suggestion_spec.add_namespace_filters("namespace1"); |
| SuggestionResponse response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), IsEmpty()); |
| |
| // namespace2 still has this suggestion |
| SuggestionResponse::Suggestion suggestionFool; |
| suggestionFool.set_query("fool"); |
| |
| suggestion_spec.add_namespace_filters("namespace2"); |
| response = icing.SearchSuggestions(suggestion_spec); |
| ASSERT_THAT(response.status(), ProtoIsOk()); |
| ASSERT_THAT(response.suggestions(), |
| UnorderedElementsAre(EqualsProto(suggestionFool))); |
| } |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchSuggestionsTest_emptyPrefix) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix(""); |
| suggestion_spec.set_num_to_return(10); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| ASSERT_THAT(icing.SearchSuggestions(suggestion_spec).status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_F(IcingSearchEngineTest, SearchSuggestionsTest_NonPositiveNumToReturn) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| SuggestionSpecProto suggestion_spec; |
| suggestion_spec.set_prefix("prefix"); |
| suggestion_spec.set_num_to_return(0); |
| suggestion_spec.mutable_scoring_spec()->set_scoring_match_type( |
| TermMatchType::PREFIX); |
| |
| ASSERT_THAT(icing.SearchSuggestions(suggestion_spec).status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| #ifndef ICING_JNI_TEST |
| // We skip this test case when we're running in a jni_test since the data files |
| // will be stored in the android-instrumented storage location, rather than the |
| // normal cc_library runfiles directory. To get that storage location, it's |
| // recommended to use the TestStorage APIs which handles different API |
| // levels/absolute vs relative/etc differences. Since that's only accessible on |
| // the java-side, and I haven't figured out a way to pass that directory path to |
| // this native side yet, we're just going to disable this. The functionality is |
| // already well-tested across 4 different emulated OS's so we're not losing much |
| // test coverage here. |
| TEST_F(IcingSearchEngineTest, MigrateToPortableFileBackedProtoLog) { |
| // Copy the testdata files into our IcingSearchEngine directory |
| std::string dir_without_portable_log; |
| if (IsAndroidX86()) { |
| dir_without_portable_log = GetTestFilePath( |
| "icing/testdata/not_portable_log/" |
| "icing_search_engine_android_x86"); |
| } else if (IsAndroidArm()) { |
| dir_without_portable_log = GetTestFilePath( |
| "icing/testdata/not_portable_log/" |
| "icing_search_engine_android_arm"); |
| } else if (IsIosPlatform()) { |
| dir_without_portable_log = GetTestFilePath( |
| "icing/testdata/not_portable_log/" |
| "icing_search_engine_ios"); |
| } else { |
| dir_without_portable_log = GetTestFilePath( |
| "icing/testdata/not_portable_log/" |
| "icing_search_engine_linux"); |
| } |
| |
| // Create dst directory that we'll initialize the IcingSearchEngine over. |
| std::string base_dir = GetTestBaseDir() + "_migrate"; |
| ASSERT_THAT(filesystem()->DeleteDirectoryRecursively(base_dir.c_str()), true); |
| ASSERT_THAT(filesystem()->CreateDirectoryRecursively(base_dir.c_str()), true); |
| |
| ASSERT_TRUE(filesystem()->CopyDirectory(dir_without_portable_log.c_str(), |
| base_dir.c_str(), |
| /*recursive=*/true)); |
| |
| IcingSearchEngineOptions icing_options; |
| icing_options.set_base_dir(base_dir); |
| |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| InitializeResultProto init_result = icing.Initialize(); |
| EXPECT_THAT(init_result.status(), ProtoIsOk()); |
| EXPECT_THAT(init_result.initialize_stats().document_store_data_status(), |
| Eq(InitializeStatsProto::NO_DATA_LOSS)); |
| EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(), |
| Eq(InitializeStatsProto::LEGACY_DOCUMENT_LOG_FORMAT)); |
| EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(), |
| Eq(InitializeStatsProto::NONE)); |
| |
| // Set up schema, this is the one used to validate documents in the testdata |
| // files. Do not change unless you're also updating the testdata files. |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("email") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(MATCH_EXACT, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(MATCH_EXACT, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| // Make sure our schema is still the same as we expect. If not, there's |
| // definitely no way we're getting the documents back that we expect. |
| GetSchemaResultProto expected_get_schema_result_proto; |
| expected_get_schema_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_get_schema_result_proto.mutable_schema() = schema; |
| ASSERT_THAT(icing.GetSchema(), EqualsProto(expected_get_schema_result_proto)); |
| |
| // These are the documents that are stored in the testdata files. Do not |
| // change unless you're also updating the testdata files. |
| DocumentProto document1 = DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("email") |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "foo") |
| .AddStringProperty("body", "bar") |
| .Build(); |
| |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace1", "uri2") |
| .SetSchema("email") |
| .SetCreationTimestampMs(20) |
| .SetScore(321) |
| .AddStringProperty("body", "baz bat") |
| .Build(); |
| |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace2", "uri1") |
| .SetSchema("email") |
| .SetCreationTimestampMs(30) |
| .SetScore(123) |
| .AddStringProperty("subject", "phoo") |
| .Build(); |
| |
| // Document 1 and 3 were put normally, and document 2 was deleted in our |
| // testdata files. |
| EXPECT_THAT(icing |
| .Get(document1.namespace_(), document1.uri(), |
| GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(document1)); |
| EXPECT_THAT(icing |
| .Get(document2.namespace_(), document2.uri(), |
| GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoStatusIs(StatusProto::NOT_FOUND)); |
| EXPECT_THAT(icing |
| .Get(document3.namespace_(), document3.uri(), |
| GetResultSpecProto::default_instance()) |
| .document(), |
| EqualsProto(document3)); |
| |
| // Searching for "foo" should get us document1. |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("foo"); |
| |
| SearchResultProto expected_document1; |
| expected_document1.mutable_status()->set_code(StatusProto::OK); |
| *expected_document1.mutable_results()->Add()->mutable_document() = document1; |
| |
| SearchResultProto actual_results = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(expected_document1)); |
| |
| // Searching for "baz" would've gotten us document2, except it got deleted. |
| // Make sure that it's cleared from our index too. |
| search_spec.set_query("baz"); |
| |
| SearchResultProto expected_no_documents; |
| expected_no_documents.mutable_status()->set_code(StatusProto::OK); |
| |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(expected_no_documents)); |
| |
| // Searching for "phoo" should get us document3. |
| search_spec.set_query("phoo"); |
| |
| SearchResultProto expected_document3; |
| expected_document3.mutable_status()->set_code(StatusProto::OK); |
| *expected_document3.mutable_results()->Add()->mutable_document() = document3; |
| |
| actual_results = icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(actual_results, |
| EqualsSearchResultIgnoreStatsAndScores(expected_document3)); |
| } |
| #endif // !ICING_JNI_TEST |
| |
| } // namespace |
| } // namespace lib |
| } // namespace icing |