| // Copyright (C) 2019 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include <cstdint> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "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/icing-search-engine.h" |
| #include "icing/jni/jni-cache.h" |
| #include "icing/join/join-processor.h" |
| #include "icing/portable/endian.h" |
| #include "icing/portable/equals-proto.h" |
| #include "icing/portable/platform.h" |
| #include "icing/proto/debug.pb.h" |
| #include "icing/proto/document.pb.h" |
| #include "icing/proto/document_wrapper.pb.h" |
| #include "icing/proto/initialize.pb.h" |
| #include "icing/proto/logging.pb.h" |
| #include "icing/proto/optimize.pb.h" |
| #include "icing/proto/persist.pb.h" |
| #include "icing/proto/reset.pb.h" |
| #include "icing/proto/schema.pb.h" |
| #include "icing/proto/scoring.pb.h" |
| #include "icing/proto/search.pb.h" |
| #include "icing/proto/status.pb.h" |
| #include "icing/proto/storage.pb.h" |
| #include "icing/proto/term.pb.h" |
| #include "icing/proto/usage.pb.h" |
| #include "icing/query/query-features.h" |
| #include "icing/schema-builder.h" |
| #include "icing/testing/common-matchers.h" |
| #include "icing/testing/fake-clock.h" |
| #include "icing/testing/icu-data-file-helper.h" |
| #include "icing/testing/jni-test-helpers.h" |
| #include "icing/testing/test-data.h" |
| #include "icing/testing/tmp-directory.h" |
| #include "icing/util/snippet-helpers.h" |
| |
| namespace icing { |
| namespace lib { |
| |
| namespace { |
| |
| using ::icing::lib::portable_equals_proto::EqualsProto; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Gt; |
| using ::testing::IsEmpty; |
| using ::testing::Ne; |
| using ::testing::SizeIs; |
| |
| // For mocking purpose, we allow tests to provide a custom Filesystem. |
| class TestIcingSearchEngine : public IcingSearchEngine { |
| public: |
| TestIcingSearchEngine(const IcingSearchEngineOptions& options, |
| std::unique_ptr<const Filesystem> filesystem, |
| std::unique_ptr<const IcingFilesystem> icing_filesystem, |
| std::unique_ptr<Clock> clock, |
| std::unique_ptr<JniCache> jni_cache) |
| : IcingSearchEngine(options, std::move(filesystem), |
| std::move(icing_filesystem), std::move(clock), |
| std::move(jni_cache)) {} |
| }; |
| |
| std::string GetTestBaseDir() { return GetTestTempDir() + "/icing"; } |
| |
| // This test is meant to cover all tests relating to IcingSearchEngine::Search |
| // and IcingSearchEngine::GetNextPage. |
| class IcingSearchEngineSearchTest |
| : public ::testing::TestWithParam<SearchSpecProto::SearchType::Code> { |
| protected: |
| void SetUp() override { |
| if (!IsCfStringTokenization() && !IsReverseJniTokenization()) { |
| // If we've specified using the reverse-JNI method for segmentation (i.e. |
| // not ICU), then we won't have the ICU data file included to set up. |
| // Technically, we could choose to use reverse-JNI for segmentation AND |
| // include an ICU data file, but that seems unlikely and our current BUILD |
| // setup doesn't do this. |
| // File generated via icu_data_file rule in //icing/BUILD. |
| std::string icu_data_file_path = |
| GetTestFilePath("icing/icu.dat"); |
| ICING_ASSERT_OK( |
| icu_data_file_helper::SetUpICUDataFile(icu_data_file_path)); |
| } |
| filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str()); |
| } |
| |
| void TearDown() override { |
| filesystem_.DeleteDirectoryRecursively(GetTestBaseDir().c_str()); |
| } |
| |
| const Filesystem* filesystem() const { return &filesystem_; } |
| |
| private: |
| Filesystem filesystem_; |
| }; |
| |
| // Non-zero value so we don't override it to be the current time |
| constexpr int64_t kDefaultCreationTimestampMs = 1575492852000; |
| |
| 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(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .Build(); |
| } |
| |
| SchemaProto CreateEmailSchema() { |
| return SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .Build(); |
| } |
| |
| SchemaProto CreatePersonAndEmailSchema() { |
| return SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("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_t 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, SearchReturnsOneResult) { |
| 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()); |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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()); |
| |
| EXPECT_THAT(search_result_proto.query_stats().latency_ms(), Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().parse_query_latency_ms(), |
| Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().scoring_latency_ms(), Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().ranking_latency_ms(), Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().document_retrieval_latency_ms(), |
| Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().lock_acquisition_latency_ms(), |
| Eq(1000)); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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(""); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| 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(""); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| SearchNonPositivePageTotalBytesLimitReturnsInvalidArgument) { |
| 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(""); |
| search_spec.set_search_type(GetParam()); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_num_total_bytes_per_page_threshold(-1); |
| |
| SearchResultProto actual_results1 = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(actual_results1.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| |
| result_spec.set_num_total_bytes_per_page_threshold(0); |
| SearchResultProto actual_results2 = |
| icing.Search(search_spec, GetDefaultScoringSpec(), result_spec); |
| EXPECT_THAT(actual_results2.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, SearchShouldReturnEmpty) { |
| 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()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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.status(), ProtoIsOk()); |
| |
| EXPECT_THAT(search_result_proto.query_stats().latency_ms(), Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().parse_query_latency_ms(), |
| Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().scoring_latency_ms(), Eq(1000)); |
| EXPECT_THAT(search_result_proto.query_stats().ranking_latency_ms(), Eq(0)); |
| EXPECT_THAT(search_result_proto.query_stats().document_retrieval_latency_ms(), |
| Eq(0)); |
| EXPECT_THAT(search_result_proto.query_stats().lock_acquisition_latency_ms(), |
| Eq(1000)); |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| SearchWithUnknownEnabledFeatureShouldReturnError) { |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| search_spec.add_enabled_features("BAD_FEATURE"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, GetDefaultScoringSpec(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| 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); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| 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_P(IcingSearchEngineSearchTest, Bm25fRelevanceScoringOneNamespaceAdvanced) { |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_advanced_scoring_expression("this.relevanceScore() * 2 + 1"); |
| scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION); |
| 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| // 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| // 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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; |
| result_spec.set_result_group_type(ResultSpecProto::NAMESPACE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace2"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace1"); |
| result_grouping = result_spec.add_result_groupings(); |
| entry = result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingDuplicateSchemaShouldReturnError) { |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| // Specify "Message" twice. This should result in an error. |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_schema("Message"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_schema("nonexistentMessage"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_schema("Message"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingDuplicateNamespaceAndSchemaSchemaShouldReturnError) { |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| // Specify "namespace1xMessage" twice. This should result in an error. |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::NAMESPACE_AND_SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| entry->set_schema("Message"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace2"); |
| entry->set_schema("Message"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace1"); |
| entry->set_schema("Message"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace1"); |
| entry->set_schema("Message"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(search_result_proto.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(0); |
| entry->set_namespace_("namespace1"); |
| entry->set_schema("Message"); |
| result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace2"); |
| entry->set_schema("Message"); |
| |
| 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_P(IcingSearchEngineSearchTest, |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::NAMESPACE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(2); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace2"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("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_P(IcingSearchEngineSearchTest, SearchResultGroupingMultiSchemaGrouping) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_REQUIRED))) |
| .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("sender") |
| .SetDataTypeDocument( |
| "Person", |
| /*index_nested_properties=*/true) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document1 = |
| DocumentBuilder() |
| .SetKey("namespace1", "uri1") |
| .SetSchema("Email") |
| .SetScore(1) |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("subject", "foo") |
| .AddDocumentProperty("sender", DocumentBuilder() |
| .SetKey("namespace", "uri1-sender") |
| .SetSchema("Person") |
| .AddStringProperty("name", "foo") |
| .Build()) |
| .Build(); |
| DocumentProto document2 = DocumentBuilder() |
| .SetKey("namespace1", "uri2") |
| .SetSchema("Message") |
| .SetScore(2) |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("body", "fo") |
| .Build(); |
| DocumentProto document3 = DocumentBuilder() |
| .SetKey("namespace2", "uri3") |
| .SetSchema("Message") |
| .SetScore(3) |
| .SetCreationTimestampMs(10) |
| .AddStringProperty("body", "fo") |
| .Build(); |
| |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| |
| // "f" will match all 3 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("f"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_schema("Message"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("Email"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // Each of the highest scored documents of schema type "Message" (document3) |
| // and "Email" (document1) 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() = |
| document3; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document1; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingMultiNamespaceAndSchemaGrouping) { |
| 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::NAMESPACE_AND_SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| entry->set_schema("Message"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace2"); |
| entry->set_schema("Message"); |
| result_grouping = result_spec.add_result_groupings(); |
| result_grouping->set_max_results(1); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("namespace3"); |
| entry->set_schema("Message"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // The three highest scored documents that fit the criteria of |
| // "namespace1xMessage" (document2), "namespace2xMessage" (document4), |
| // and "namespace3xMessage" (document6) 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() = |
| document4; |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document2; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingNonexistentNamespaceShouldBeIgnored) { |
| 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("namespace1", "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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::NAMESPACE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace1"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_namespace_("nonexistentNamespace"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // Only the top ranked document in "namespace" (document2), should be |
| // returned. The presence of "nonexistentNamespace" in the same result |
| // grouping should have no effect. |
| 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; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingNonexistentSchemaShouldBeIgnored) { |
| 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("namespace1", "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"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_schema("Message"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_schema("nonexistentMessage"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // Only the top ranked document in "Message" (document2), should be |
| // returned. The presence of "nonexistentMessage" in the same result |
| // grouping should have no effect. |
| 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; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| SearchResultGroupingNonexistentNamespaceAndSchemaShouldBeIgnored) { |
| 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("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", "message4") |
| .SetScore(4) |
| .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()); |
| |
| // "m" will match all 2 documents |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("m"); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE); |
| |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(ResultSpecProto::SCHEMA_TYPE); |
| ResultSpecProto::ResultGrouping* result_grouping = |
| result_spec.add_result_groupings(); |
| ResultSpecProto::ResultGrouping::Entry* entry = |
| result_grouping->add_entry_groupings(); |
| result_grouping->set_max_results(1); |
| entry->set_namespace_("namespace2"); |
| entry->set_schema("Message"); |
| entry = result_grouping->add_entry_groupings(); |
| entry->set_schema("namespace1"); |
| entry->set_schema("nonexistentMessage"); |
| |
| SearchResultProto search_result_proto = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| |
| // Only the top ranked document in "namespace2xMessage" (document4), should be |
| // returned. The presence of "namespace1xnonexistentMessage" in the same |
| // result grouping should have no effect. If either the namespace or the |
| // schema type is nonexistent, the entire entry will be ignored. |
| SearchResultProto expected_search_result_proto; |
| expected_search_result_proto.mutable_status()->set_code(StatusProto::OK); |
| *expected_search_result_proto.mutable_results()->Add()->mutable_document() = |
| document4; |
| |
| EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores( |
| expected_search_result_proto)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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()); |
| |
| DocumentProto document_two = |
| DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "MDI zurich trip") |
| .AddStringProperty("body", "Let's travel to zurich") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| auto search_spec = std::make_unique<SearchSpecProto>(); |
| search_spec->set_term_match_type(TermMatchType::PREFIX); |
| search_spec->set_query("body:Zür"); |
| search_spec->set_search_type(GetParam()); |
| |
| auto result_spec = std::make_unique<ResultSpecProto>(); |
| result_spec->set_num_per_page(1); |
| 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); |
| |
| auto scoring_spec = std::make_unique<ScoringSpecProto>(); |
| *scoring_spec = GetDefaultScoringSpec(); |
| |
| SearchResultProto results = |
| icing.Search(*search_spec, *scoring_spec, *result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| |
| const DocumentProto& result_document_two = results.results(0).document(); |
| const SnippetProto& result_snippet_two = results.results(0).snippet(); |
| EXPECT_THAT(result_document_two, EqualsProto(document_two)); |
| EXPECT_THAT(result_snippet_two.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_two.entries(0).property_name(), Eq("body")); |
| std::string_view content = GetString( |
| &result_document_two, result_snippet_two.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, result_snippet_two.entries(0)), |
| ElementsAre("Let's travel to zurich")); |
| EXPECT_THAT(GetMatches(content, result_snippet_two.entries(0)), |
| ElementsAre("zurich")); |
| |
| search_spec.reset(); |
| scoring_spec.reset(); |
| result_spec.reset(); |
| |
| results = icing.GetNextPage(results.next_page_token()); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| |
| const DocumentProto& result_document_one = results.results(0).document(); |
| const SnippetProto& result_snippet_one = results.results(0).snippet(); |
| EXPECT_THAT(result_document_one, EqualsProto(document_one)); |
| EXPECT_THAT(result_snippet_one.entries(), SizeIs(1)); |
| EXPECT_THAT(result_snippet_one.entries(0).property_name(), Eq("body")); |
| content = GetString(&result_document_one, |
| result_snippet_one.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, result_snippet_one.entries(0)), |
| ElementsAre("MDI zurich Team Meeting")); |
| EXPECT_THAT(GetMatches(content, result_snippet_one.entries(0)), |
| ElementsAre("zurich")); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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. |
| // Create all of search_spec, result_spec and scoring_spec as objects with |
| // scope that will end before the call to GetNextPage to ensure that the |
| // implementation isn't relying on references to any of them. |
| auto search_spec = std::make_unique<SearchSpecProto>(); |
| search_spec->set_term_match_type(TermMatchType::PREFIX); |
| search_spec->set_query("hello"); |
| search_spec->set_search_type(GetParam()); |
| |
| auto result_spec = std::make_unique<ResultSpecProto>(); |
| // 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"); |
| |
| auto scoring_spec = std::make_unique<ScoringSpecProto>(); |
| *scoring_spec = GetDefaultScoringSpec(); |
| SearchResultProto results = |
| icing.Search(*search_spec, *scoring_spec, *result_spec); |
| EXPECT_THAT(results.status(), ProtoIsOk()); |
| EXPECT_THAT(results.results(), SizeIs(1)); |
| |
| // 3. Verify that the first returned result only contains 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)); |
| |
| // 4. Now, delete all of the specs used in the search. GetNextPage should have |
| // no problem because it shouldn't be keeping any references to them. |
| search_spec.reset(); |
| result_spec.reset(); |
| scoring_spec.reset(); |
| |
| // 5. Verify that the second returned result only contains the 'sender.name' |
| // property. |
| 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_P(IcingSearchEngineSearchTest, 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"); |
| search_spec.set_search_type(GetParam()); |
| |
| 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); |
| exp_stats.set_lock_acquisition_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); |
| exp_stats.set_lock_acquisition_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); |
| exp_stats.set_lock_acquisition_latency_ms(5); |
| EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, SnippetErrorTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Generic").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_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"); |
| search_spec.set_search_type(GetParam()); |
| 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_P(IcingSearchEngineSearchTest, 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(TERM_MATCH_PREFIX); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, 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(TERM_MATCH_PREFIX); |
| search_spec.set_search_type(GetParam()); |
| 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()); |
| if (GetParam() == |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY) { |
| // This is the actual correct behavior. |
| EXPECT_THAT(search_results.results(), IsEmpty()); |
| } else { |
| 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()); |
| if (GetParam() == |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY) { |
| // This is the actual correct behavior. |
| EXPECT_THAT(search_results.results(), IsEmpty()); |
| } else { |
| EXPECT_THAT(search_results.results(), SizeIs(2)); |
| } |
| |
| search_spec.set_query("-"); |
| search_results = icing.Search(search_spec, scoring_spec, result_spec); |
| if (GetParam() == |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY) { |
| // This is the actual correct behavior. |
| EXPECT_THAT(search_results.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } else { |
| 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); |
| if (GetParam() == |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY) { |
| // This is the actual correct behavior. |
| EXPECT_THAT(search_results.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } else { |
| 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); |
| if (GetParam() == |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY) { |
| EXPECT_THAT(search_results.status(), |
| ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } else { |
| 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_P(IcingSearchEngineSearchTest, 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(TERM_MATCH_PREFIX); |
| search_spec.set_search_type(GetParam()); |
| |
| 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_P(IcingSearchEngineSearchTest, JoinByQualifiedId) { |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("firstName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("lastName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("personQualifiedId") |
| .SetDataTypeJoinableString( |
| JOINABLE_VALUE_TYPE_QUALIFIED_ID) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| DocumentProto person1 = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person1") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first1") |
| .AddStringProperty("lastName", "last1") |
| .AddStringProperty("emailAddress", "email1@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(1) |
| .Build(); |
| DocumentProto person2 = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person2") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first2") |
| .AddStringProperty("lastName", "last2") |
| .AddStringProperty("emailAddress", "email2@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(2) |
| .Build(); |
| DocumentProto person3 = |
| DocumentBuilder() |
| .SetKey(R"(pkg$db/name#space\\)", "person3") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first3") |
| .AddStringProperty("lastName", "last3") |
| .AddStringProperty("emailAddress", "email3@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(3) |
| .Build(); |
| |
| DocumentProto email1 = |
| DocumentBuilder() |
| .SetKey("namespace", "email1") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 1") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(3) |
| .Build(); |
| DocumentProto email2 = |
| DocumentBuilder() |
| .SetKey("namespace", "email2") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 2") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(2) |
| .Build(); |
| DocumentProto email3 = |
| DocumentBuilder() |
| .SetKey("namespace", "email3") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 3") |
| .AddStringProperty("personQualifiedId", |
| R"(pkg$db/name\#space\\\\#person3)") // escaped |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(1) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email3).status(), ProtoIsOk()); |
| |
| // Parent SearchSpec |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("firstName:first"); |
| search_spec.set_search_type(GetParam()); |
| |
| // JoinSpec |
| JoinSpecProto* join_spec = search_spec.mutable_join_spec(); |
| join_spec->set_max_joined_child_count(100); |
| join_spec->set_parent_property_expression( |
| std::string(JoinProcessor::kQualifiedIdExpr)); |
| join_spec->set_child_property_expression("personQualifiedId"); |
| join_spec->set_aggregation_scoring_strategy( |
| JoinSpecProto::AggregationScoringStrategy::MAX); |
| JoinSpecProto::NestedSpecProto* nested_spec = |
| join_spec->mutable_nested_spec(); |
| SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec(); |
| nested_search_spec->set_term_match_type(TermMatchType::PREFIX); |
| nested_search_spec->set_query("subject:test"); |
| nested_search_spec->set_search_type(GetParam()); |
| *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec(); |
| *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance(); |
| |
| // Parent ScoringSpec |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| |
| // Parent ResultSpec |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| // Since we: |
| // - Use MAX for aggregation scoring strategy. |
| // - (Default) use DOCUMENT_SCORE to score child documents. |
| // - (Default) use DESC as the ranking order. |
| // |
| // person1 + email1 should have the highest aggregated score (3) and be |
| // returned first. person2 + email2 (aggregated score = 2) should be the |
| // second, and person3 + email3 (aggregated score = 1) should be the last. |
| SearchResultProto expected_result1; |
| expected_result1.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto::ResultProto* result_proto1 = |
| expected_result1.mutable_results()->Add(); |
| *result_proto1->mutable_document() = person1; |
| *result_proto1->mutable_joined_results()->Add()->mutable_document() = email1; |
| |
| SearchResultProto expected_result2; |
| expected_result2.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto::ResultProto* result_google::protobuf = |
| expected_result2.mutable_results()->Add(); |
| *result_google::protobuf->mutable_document() = person2; |
| *result_google::protobuf->mutable_joined_results()->Add()->mutable_document() = email2; |
| |
| SearchResultProto expected_result3; |
| expected_result3.mutable_status()->set_code(StatusProto::OK); |
| SearchResultProto::ResultProto* result_proto3 = |
| expected_result3.mutable_results()->Add(); |
| *result_proto3->mutable_document() = person3; |
| *result_proto3->mutable_joined_results()->Add()->mutable_document() = email3; |
| |
| SearchResultProto result1 = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| uint64_t next_page_token = result1.next_page_token(); |
| EXPECT_THAT(next_page_token, Ne(kInvalidNextPageToken)); |
| expected_result1.set_next_page_token(next_page_token); |
| EXPECT_THAT(result1, |
| EqualsSearchResultIgnoreStatsAndScores(expected_result1)); |
| |
| SearchResultProto result2 = icing.GetNextPage(next_page_token); |
| next_page_token = result2.next_page_token(); |
| EXPECT_THAT(next_page_token, Ne(kInvalidNextPageToken)); |
| expected_result2.set_next_page_token(next_page_token); |
| EXPECT_THAT(result2, |
| EqualsSearchResultIgnoreStatsAndScores(expected_result2)); |
| |
| SearchResultProto result3 = icing.GetNextPage(next_page_token); |
| next_page_token = result3.next_page_token(); |
| EXPECT_THAT(next_page_token, Eq(kInvalidNextPageToken)); |
| EXPECT_THAT(result3, |
| EqualsSearchResultIgnoreStatsAndScores(expected_result3)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, JoinSnippet) { |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("firstName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("lastName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("personQualifiedId") |
| .SetDataTypeJoinableString( |
| JOINABLE_VALUE_TYPE_QUALIFIED_ID) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| DocumentProto person = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first") |
| .AddStringProperty("lastName", "last") |
| .AddStringProperty("emailAddress", "email@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(1) |
| .Build(); |
| |
| DocumentProto email = |
| DocumentBuilder() |
| .SetKey("namespace", "email") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(3) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email).status(), ProtoIsOk()); |
| |
| // Parent SearchSpec |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("firstName:first"); |
| search_spec.set_search_type(GetParam()); |
| |
| // JoinSpec |
| JoinSpecProto* join_spec = search_spec.mutable_join_spec(); |
| join_spec->set_max_joined_child_count(100); |
| join_spec->set_parent_property_expression( |
| std::string(JoinProcessor::kQualifiedIdExpr)); |
| join_spec->set_child_property_expression("personQualifiedId"); |
| join_spec->set_aggregation_scoring_strategy( |
| JoinSpecProto::AggregationScoringStrategy::MAX); |
| JoinSpecProto::NestedSpecProto* nested_spec = |
| join_spec->mutable_nested_spec(); |
| SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec(); |
| nested_search_spec->set_term_match_type(TermMatchType::PREFIX); |
| nested_search_spec->set_query("subject:test"); |
| nested_search_spec->set_search_type(GetParam()); |
| // Child ResultSpec (with snippet) |
| ResultSpecProto* nested_result_spec = nested_spec->mutable_result_spec(); |
| nested_result_spec->mutable_snippet_spec()->set_max_window_utf32_length(64); |
| nested_result_spec->mutable_snippet_spec()->set_num_matches_per_property(1); |
| nested_result_spec->mutable_snippet_spec()->set_num_to_snippet(1); |
| *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec(); |
| |
| // Parent ScoringSpec |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| |
| // Parent ResultSpec (without snippet) |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| SearchResultProto result = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.next_page_token(), Eq(kInvalidNextPageToken)); |
| |
| ASSERT_THAT(result.results(), SizeIs(1)); |
| // Check parent doc (person). |
| const DocumentProto& result_parent_document = result.results(0).document(); |
| EXPECT_THAT(result_parent_document, EqualsProto(person)); |
| EXPECT_THAT(result.results(0).snippet().entries(), IsEmpty()); |
| |
| // Check child doc (email). |
| ASSERT_THAT(result.results(0).joined_results(), SizeIs(1)); |
| const DocumentProto& result_child_document = |
| result.results(0).joined_results(0).document(); |
| const SnippetProto& result_child_snippet = |
| result.results(0).joined_results(0).snippet(); |
| EXPECT_THAT(result_child_document, EqualsProto(email)); |
| ASSERT_THAT(result_child_snippet.entries(), SizeIs(1)); |
| EXPECT_THAT(result_child_snippet.entries(0).property_name(), Eq("subject")); |
| std::string_view content = GetString( |
| &result_child_document, result_child_snippet.entries(0).property_name()); |
| EXPECT_THAT(GetWindows(content, result_child_snippet.entries(0)), |
| ElementsAre("test subject")); |
| EXPECT_THAT(GetMatches(content, result_child_snippet.entries(0)), |
| ElementsAre("test")); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, JoinProjection) { |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("firstName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("lastName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("personQualifiedId") |
| .SetDataTypeJoinableString( |
| JOINABLE_VALUE_TYPE_QUALIFIED_ID) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| DocumentProto person = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first") |
| .AddStringProperty("lastName", "last") |
| .AddStringProperty("emailAddress", "email@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(1) |
| .Build(); |
| |
| DocumentProto email = |
| DocumentBuilder() |
| .SetKey("namespace", "email") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(3) |
| .Build(); |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email).status(), ProtoIsOk()); |
| |
| // Parent SearchSpec |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("firstName:first"); |
| search_spec.set_search_type(GetParam()); |
| |
| // JoinSpec |
| JoinSpecProto* join_spec = search_spec.mutable_join_spec(); |
| join_spec->set_max_joined_child_count(100); |
| join_spec->set_parent_property_expression( |
| std::string(JoinProcessor::kQualifiedIdExpr)); |
| join_spec->set_child_property_expression("personQualifiedId"); |
| join_spec->set_aggregation_scoring_strategy( |
| JoinSpecProto::AggregationScoringStrategy::MAX); |
| JoinSpecProto::NestedSpecProto* nested_spec = |
| join_spec->mutable_nested_spec(); |
| SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec(); |
| nested_search_spec->set_term_match_type(TermMatchType::PREFIX); |
| nested_search_spec->set_query("subject:test"); |
| nested_search_spec->set_search_type(GetParam()); |
| // Child ResultSpec (with projection) |
| ResultSpecProto* nested_result_spec = nested_spec->mutable_result_spec(); |
| TypePropertyMask* type_property_mask = |
| nested_result_spec->add_type_property_masks(); |
| type_property_mask->set_schema_type("Email"); |
| type_property_mask->add_paths("subject"); |
| *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec(); |
| |
| // Parent ScoringSpec |
| ScoringSpecProto scoring_spec = GetDefaultScoringSpec(); |
| |
| // Parent ResultSpec (with projection) |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| type_property_mask = result_spec.add_type_property_masks(); |
| type_property_mask->set_schema_type("Person"); |
| type_property_mask->add_paths("emailAddress"); |
| |
| SearchResultProto result = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(result.status(), ProtoIsOk()); |
| EXPECT_THAT(result.next_page_token(), Eq(kInvalidNextPageToken)); |
| |
| ASSERT_THAT(result.results(), SizeIs(1)); |
| // Check parent doc (person): should contain only the "emailAddress" property. |
| DocumentProto projected_person_document = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person") |
| .SetSchema("Person") |
| .AddStringProperty("emailAddress", "email@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(1) |
| .Build(); |
| EXPECT_THAT(result.results().at(0).document(), |
| EqualsProto(projected_person_document)); |
| |
| // Check child doc (email): should contain only the "subject" property. |
| ASSERT_THAT(result.results(0).joined_results(), SizeIs(1)); |
| DocumentProto projected_email_document = |
| DocumentBuilder() |
| .SetKey("namespace", "email") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(3) |
| .Build(); |
| EXPECT_THAT(result.results(0).joined_results(0).document(), |
| EqualsProto(projected_email_document)); |
| } |
| |
| TEST_F(IcingSearchEngineSearchTest, JoinWithAdvancedScoring) { |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Person") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("firstName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("lastName") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("emailAddress") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("subject") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("personQualifiedId") |
| .SetDataTypeJoinableString( |
| JOINABLE_VALUE_TYPE_QUALIFIED_ID) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| const int32_t person1_doc_score = 10; |
| const int32_t person2_doc_score = 25; |
| const int32_t person3_doc_score = 123; |
| const int32_t email1_doc_score = 10; |
| const int32_t email2_doc_score = 15; |
| const int32_t email3_doc_score = 40; |
| |
| // person1 has children email1 and email2. |
| DocumentProto person1 = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person1") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first1") |
| .AddStringProperty("lastName", "last1") |
| .AddStringProperty("emailAddress", "email1@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(person1_doc_score) |
| .Build(); |
| // person2 has a single child email3 |
| DocumentProto person2 = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person2") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first2") |
| .AddStringProperty("lastName", "last2") |
| .AddStringProperty("emailAddress", "email2@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(person2_doc_score) |
| .Build(); |
| // person3 has no child. |
| DocumentProto person3 = |
| DocumentBuilder() |
| .SetKey("pkg$db/namespace", "person3") |
| .SetSchema("Person") |
| .AddStringProperty("firstName", "first3") |
| .AddStringProperty("lastName", "last3") |
| .AddStringProperty("emailAddress", "email3@gmail.com") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(person3_doc_score) |
| .Build(); |
| |
| DocumentProto email1 = |
| DocumentBuilder() |
| .SetKey("namespace", "email1") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 1") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(email1_doc_score) |
| .Build(); |
| DocumentProto email2 = |
| DocumentBuilder() |
| .SetKey("namespace", "email2") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 2") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person1") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(email2_doc_score) |
| .Build(); |
| DocumentProto email3 = |
| DocumentBuilder() |
| .SetKey("namespace", "email3") |
| .SetSchema("Email") |
| .AddStringProperty("subject", "test subject 3") |
| .AddStringProperty("personQualifiedId", "pkg$db/namespace#person2") |
| .SetCreationTimestampMs(kDefaultCreationTimestampMs) |
| .SetScore(email3_doc_score) |
| .Build(); |
| |
| // Set children scoring expression and their expected value. |
| ScoringSpecProto child_scoring_spec = GetDefaultScoringSpec(); |
| child_scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION); |
| child_scoring_spec.set_advanced_scoring_expression( |
| "this.documentScore() * 2 + 1"); |
| const int32_t exp_email1_score = email1_doc_score * 2 + 1; |
| const int32_t exp_email2_score = email2_doc_score * 2 + 1; |
| const int32_t exp_email3_score = email3_doc_score * 2 + 1; |
| |
| // Set parent scoring expression and their expected value. |
| ScoringSpecProto parent_scoring_spec = GetDefaultScoringSpec(); |
| parent_scoring_spec.set_rank_by( |
| ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION); |
| parent_scoring_spec.set_advanced_scoring_expression( |
| "this.documentScore() * sum(this.childrenScores())"); |
| const int32_t exp_person1_score = |
| person1_doc_score * (exp_email1_score + exp_email2_score); |
| const int32_t exp_person2_score = person2_doc_score * exp_email3_score; |
| const int32_t exp_person3_score = person3_doc_score * 0; |
| |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(person3).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(email3).status(), ProtoIsOk()); |
| |
| // Parent SearchSpec |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("firstName:first"); |
| |
| // JoinSpec |
| JoinSpecProto* join_spec = search_spec.mutable_join_spec(); |
| join_spec->set_max_joined_child_count(100); |
| join_spec->set_parent_property_expression( |
| std::string(JoinProcessor::kQualifiedIdExpr)); |
| join_spec->set_child_property_expression("personQualifiedId"); |
| JoinSpecProto::NestedSpecProto* nested_spec = |
| join_spec->mutable_nested_spec(); |
| SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec(); |
| nested_search_spec->set_term_match_type(TermMatchType::PREFIX); |
| nested_search_spec->set_query("subject:test"); |
| *nested_spec->mutable_scoring_spec() = child_scoring_spec; |
| *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance(); |
| |
| // Parent ResultSpec |
| ResultSpecProto result_spec; |
| result_spec.set_num_per_page(1); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, parent_scoring_spec, result_spec); |
| uint64_t next_page_token = results.next_page_token(); |
| EXPECT_THAT(next_page_token, Ne(kInvalidNextPageToken)); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("person2")); |
| // exp_person2_score = 2025 |
| EXPECT_THAT(results.results(0).score(), Eq(exp_person2_score)); |
| |
| results = icing.GetNextPage(next_page_token); |
| next_page_token = results.next_page_token(); |
| EXPECT_THAT(next_page_token, Ne(kInvalidNextPageToken)); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("person1")); |
| // exp_person1_score = 520 |
| EXPECT_THAT(results.results(0).score(), Eq(exp_person1_score)); |
| |
| results = icing.GetNextPage(next_page_token); |
| next_page_token = results.next_page_token(); |
| EXPECT_THAT(next_page_token, Eq(kInvalidNextPageToken)); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document().uri(), Eq("person3")); |
| // exp_person3_score = 0 |
| EXPECT_THAT(results.results(0).score(), Eq(exp_person3_score)); |
| } |
| |
| TEST_F(IcingSearchEngineSearchTest, NumericFilterAdvancedQuerySucceeds) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Create the schema and document store |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("transaction") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("price") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("cost") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = DocumentBuilder() |
| .SetKey("namespace", "1") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 10) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = DocumentBuilder() |
| .SetKey("namespace", "2") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 25) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| DocumentProto document_three = DocumentBuilder() |
| .SetKey("namespace", "3") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("cost", 2) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("price < 20"); |
| search_spec.set_search_type( |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY); |
| search_spec.add_enabled_features(std::string(kNumericSearchFeature)); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_one)); |
| |
| search_spec.set_query("price == 25"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_two)); |
| |
| search_spec.set_query("cost > 2"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.results(), IsEmpty()); |
| |
| search_spec.set_query("cost >= 2"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_three)); |
| |
| search_spec.set_query("price <= 25"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| 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(IcingSearchEngineSearchTest, |
| NumericFilterAdvancedQueryWithPersistenceSucceeds) { |
| IcingSearchEngineOptions icing_options = GetDefaultIcingOptions(); |
| |
| { |
| // Create the schema and document store |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType( |
| SchemaTypeConfigBuilder() |
| .SetType("transaction") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("price") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("cost") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| |
| IcingSearchEngine icing(icing_options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| // Schema will be persisted to disk when icing goes out of scope. |
| } |
| |
| DocumentProto document_one = DocumentBuilder() |
| .SetKey("namespace", "1") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 10) |
| .Build(); |
| DocumentProto document_two = DocumentBuilder() |
| .SetKey("namespace", "2") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 25) |
| .AddInt64Property("cost", 2) |
| .Build(); |
| { |
| // 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()); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document_two).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_query("price < 20"); |
| search_spec.set_search_type( |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY); |
| search_spec.add_enabled_features(std::string(kNumericSearchFeature)); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_one)); |
| |
| search_spec.set_query("price == 25"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_two)); |
| |
| search_spec.set_query("cost > 2"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.results(), IsEmpty()); |
| |
| search_spec.set_query("cost >= 2"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(1)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document_two)); |
| |
| search_spec.set_query("price <= 25"); |
| results = icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| 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(IcingSearchEngineSearchTest, NumericFilterOldQueryFails) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| // Create the schema and document store |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("transaction") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("price") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("cost") |
| .SetDataTypeInt64(NUMERIC_MATCH_RANGE) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document_one = DocumentBuilder() |
| .SetKey("namespace", "1") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 10) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk()); |
| |
| DocumentProto document_two = DocumentBuilder() |
| .SetKey("namespace", "2") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("price", 25) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| DocumentProto document_three = DocumentBuilder() |
| .SetKey("namespace", "3") |
| .SetSchema("transaction") |
| .SetCreationTimestampMs(1) |
| .AddInt64Property("cost", 2) |
| .Build(); |
| ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("price < 20"); |
| search_spec.set_search_type(SearchSpecProto::SearchType::ICING_RAW_QUERY); |
| search_spec.add_enabled_features(std::string(kNumericSearchFeature)); |
| |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| EXPECT_THAT(results.status(), ProtoStatusIs(StatusProto::INVALID_ARGUMENT)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, BarisNormalizationTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty( |
| PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .Build(); |
| ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk()); |
| |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri") |
| .SetSchema("Person") |
| .SetCreationTimestampMs(1) |
| .AddStringProperty("name", "Barış") |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| DocumentProto document_two = DocumentBuilder() |
| .SetKey("namespace", "uri2") |
| .SetSchema("Person") |
| .SetCreationTimestampMs(1) |
| .AddStringProperty("name", "ıbar") |
| .Build(); |
| ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TERM_MATCH_PREFIX); |
| search_spec.set_search_type(GetParam()); |
| |
| ScoringSpecProto scoring_spec; |
| ResultSpecProto result_spec; |
| |
| SearchResultProto exp_results; |
| exp_results.mutable_status()->set_code(StatusProto::OK); |
| *exp_results.add_results()->mutable_document() = document; |
| |
| search_spec.set_query("barış"); |
| SearchResultProto results = |
| icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(results, EqualsSearchResultIgnoreStatsAndScores(exp_results)); |
| |
| search_spec.set_query("barıs"); |
| results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(results, EqualsSearchResultIgnoreStatsAndScores(exp_results)); |
| |
| search_spec.set_query("baris"); |
| results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(results, EqualsSearchResultIgnoreStatsAndScores(exp_results)); |
| |
| SearchResultProto exp_results2; |
| exp_results2.mutable_status()->set_code(StatusProto::OK); |
| *exp_results2.add_results()->mutable_document() = document_two; |
| search_spec.set_query("ı"); |
| results = icing.Search(search_spec, scoring_spec, result_spec); |
| EXPECT_THAT(results, EqualsSearchResultIgnoreStatsAndScores(exp_results2)); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, LatinSnippetTest) { |
| IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| constexpr std::string_view kLatin = "test ḞÖÖḸĬŞĤ test"; |
| DocumentProto document = DocumentBuilder() |
| .SetKey("namespace", "uri1") |
| .SetSchema("Message") |
| .AddStringProperty("body", kLatin) |
| .Build(); |
| ASSERT_THAT(icing.Put(document).status(), ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_query("foo"); |
| search_spec.set_term_match_type(TERM_MATCH_PREFIX); |
| search_spec.set_search_type(GetParam()); |
| |
| 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")); |
| |
| // Ensure that there is one and only one match within "body" |
| ASSERT_THAT(entry->snippet_matches(), SizeIs(1)); |
| |
| // Check that the match is "ḞÖÖḸĬŞĤ". |
| const SnippetMatchProto& match_proto = entry->snippet_matches(0); |
| std::string_view match = |
| kLatin.substr(match_proto.exact_match_byte_position(), |
| match_proto.submatch_byte_length()); |
| ASSERT_THAT(match, Eq("ḞÖÖ")); |
| } |
| |
| TEST_P(IcingSearchEngineSearchTest, |
| DocumentStoreNamespaceIdFingerprintCompatible) { |
| DocumentProto document1 = CreateMessageDocument("namespace", "uri1"); |
| DocumentProto document2 = CreateMessageDocument("namespace", "uri2"); |
| DocumentProto document3 = CreateMessageDocument("namespace", "uri3"); |
| |
| // Initialize with some documents with document_store_namespace_id_fingerprint |
| // being false. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_document_store_namespace_id_fingerprint(false); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk()); |
| |
| // Creates and inserts 3 documents |
| ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk()); |
| ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk()); |
| } |
| |
| // Reinitializate with document_store_namespace_id_fingerprint being true, |
| // and test that we are still able to read/query docs. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_document_store_namespace_id_fingerprint(true); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| ASSERT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| ASSERT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| ASSERT_THAT( |
| icing.Get("namespace", "uri3", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| search_spec.set_search_type(GetParam()); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(3)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document3)); |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document2)); |
| EXPECT_THAT(results.results(2).document(), EqualsProto(document1)); |
| } |
| |
| // Reinitializate with document_store_namespace_id_fingerprint being false, |
| // and test that we are still able to read/query docs. |
| { |
| IcingSearchEngineOptions options = GetDefaultIcingOptions(); |
| options.set_document_store_namespace_id_fingerprint(false); |
| IcingSearchEngine icing(options, GetTestJniCache()); |
| ASSERT_THAT(icing.Initialize().status(), ProtoIsOk()); |
| |
| ASSERT_THAT( |
| icing.Get("namespace", "uri1", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| ASSERT_THAT( |
| icing.Get("namespace", "uri2", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| ASSERT_THAT( |
| icing.Get("namespace", "uri3", GetResultSpecProto::default_instance()) |
| .status(), |
| ProtoIsOk()); |
| |
| SearchSpecProto search_spec; |
| search_spec.set_term_match_type(TermMatchType::PREFIX); |
| search_spec.set_query("message"); |
| search_spec.set_search_type(GetParam()); |
| SearchResultProto results = |
| icing.Search(search_spec, ScoringSpecProto::default_instance(), |
| ResultSpecProto::default_instance()); |
| ASSERT_THAT(results.results(), SizeIs(3)); |
| EXPECT_THAT(results.results(0).document(), EqualsProto(document3)); |
| EXPECT_THAT(results.results(1).document(), EqualsProto(document2)); |
| EXPECT_THAT(results.results(2).document(), EqualsProto(document1)); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| IcingSearchEngineSearchTest, IcingSearchEngineSearchTest, |
| testing::Values( |
| SearchSpecProto::SearchType::ICING_RAW_QUERY, |
| SearchSpecProto::SearchType::EXPERIMENTAL_ICING_ADVANCED_QUERY)); |
| |
| } // namespace |
| } // namespace lib |
| } // namespace icing |