| // Copyright (C) 2022 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "icing/result/result-retriever-v2.h" |
| |
| #include <atomic> |
| #include <cstddef> |
| #include <cstdint> |
| #include <memory> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "icing/text_classifier/lib3/utils/base/status.h" |
| #include "icing/text_classifier/lib3/utils/base/statusor.h" |
| #include "gmock/gmock.h" |
| #include "gtest/gtest.h" |
| #include "icing/absl_ports/mutex.h" |
| #include "icing/document-builder.h" |
| #include "icing/feature-flags.h" |
| #include "icing/file/filesystem.h" |
| #include "icing/file/mock-filesystem.h" |
| #include "icing/file/portable-file-backed-proto-log.h" |
| #include "icing/portable/equals-proto.h" |
| #include "icing/portable/platform.h" |
| #include "icing/proto/document.pb.h" |
| #include "icing/proto/document_wrapper.pb.h" |
| #include "icing/proto/schema.pb.h" |
| #include "icing/proto/search.pb.h" |
| #include "icing/result/page-result.h" |
| #include "icing/result/result-state-v2.h" |
| #include "icing/schema-builder.h" |
| #include "icing/schema/schema-store.h" |
| #include "icing/schema/section.h" |
| #include "icing/scoring/priority-queue-scored-document-hits-ranker.h" |
| #include "icing/scoring/scored-document-hit.h" |
| #include "icing/store/document-filter-data.h" |
| #include "icing/store/document-id.h" |
| #include "icing/store/document-store.h" |
| #include "icing/testing/common-matchers.h" |
| #include "icing/testing/fake-clock.h" |
| #include "icing/testing/test-data.h" |
| #include "icing/testing/test-feature-flags.h" |
| #include "icing/testing/tmp-directory.h" |
| #include "icing/tokenization/language-segmenter-factory.h" |
| #include "icing/tokenization/language-segmenter.h" |
| #include "icing/transform/normalizer-factory.h" |
| #include "icing/transform/normalizer.h" |
| #include "icing/util/clock.h" |
| #include "icing/util/icu-data-file-helper.h" |
| #include "unicode/uloc.h" |
| |
| namespace icing { |
| namespace lib { |
| |
| namespace { |
| |
| using ::icing::lib::portable_equals_proto::EqualsProto; |
| using ::testing::DoDefault; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Gt; |
| using ::testing::IsEmpty; |
| using ::testing::Pointee; |
| using ::testing::Return; |
| using ::testing::SizeIs; |
| using EntryIdMap = std::unordered_map<int32_t, int>; |
| |
| // Mock the behavior of GroupResultLimiter::ShouldBeRemoved. |
| class MockGroupResultLimiter : public GroupResultLimiterV2 { |
| public: |
| MockGroupResultLimiter() : GroupResultLimiterV2() { |
| ON_CALL(*this, ShouldBeRemoved).WillByDefault(Return(false)); |
| } |
| |
| MOCK_METHOD(bool, ShouldBeRemoved, |
| (const ScoredDocumentHit&, const EntryIdMap&, |
| const DocumentStore&, std::vector<int>&, |
| ResultSpecProto::ResultGroupingType, int64_t), |
| (const, override)); |
| }; |
| |
| class ResultRetrieverV2Test : public ::testing::Test { |
| protected: |
| ResultRetrieverV2Test() : test_dir_(GetTestTempDir() + "/icing") { |
| filesystem_.CreateDirectoryRecursively(test_dir_.c_str()); |
| } |
| |
| void SetUp() override { |
| feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags()); |
| if (!IsCfStringTokenization() && !IsReverseJniTokenization()) { |
| ICING_ASSERT_OK( |
| // File generated via icu_data_file rule in //icing/BUILD. |
| icu_data_file_helper::SetUpIcuDataFile( |
| GetTestFilePath("icing/icu.dat"))); |
| } |
| language_segmenter_factory::SegmenterOptions options(ULOC_US); |
| ICING_ASSERT_OK_AND_ASSIGN( |
| language_segmenter_, |
| language_segmenter_factory::Create(std::move(options))); |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| schema_store_, SchemaStore::Create(&filesystem_, test_dir_, |
| &fake_clock_, feature_flags_.get())); |
| ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create( |
| /*max_term_byte_size=*/10000)); |
| |
| SchemaProto schema = |
| SchemaBuilder() |
| .AddType(SchemaTypeConfigBuilder() |
| .SetType("Email") |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("name") |
| .SetDataTypeString(TERM_MATCH_PREFIX, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty(PropertyConfigBuilder() |
| .SetName("body") |
| .SetDataTypeString(TERM_MATCH_EXACT, |
| TOKENIZER_PLAIN) |
| .SetCardinality(CARDINALITY_OPTIONAL)) |
| .AddProperty( |
| PropertyConfigBuilder() |
| .SetName("sender") |
| .SetDataTypeDocument( |
| "Person", /*index_nested_properties=*/true) |
| .SetCardinality(CARDINALITY_OPTIONAL))) |
| .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))) |
| .Build(); |
| ASSERT_THAT(schema_store_->SetSchema( |
| schema, /*ignore_errors_and_delete_documents=*/false, |
| /*allow_circular_schema_definitions=*/false), |
| IsOk()); |
| |
| num_total_hits_ = 0; |
| } |
| |
| void TearDown() override { |
| filesystem_.DeleteDirectoryRecursively(test_dir_.c_str()); |
| } |
| |
| SectionId GetSectionId(const std::string& type, const std::string& property) { |
| auto type_id_or = schema_store_->GetSchemaTypeId(type); |
| if (!type_id_or.ok()) { |
| return kInvalidSectionId; |
| } |
| SchemaTypeId type_id = type_id_or.ValueOrDie(); |
| for (SectionId section_id = 0; section_id <= kMaxSectionId; ++section_id) { |
| auto metadata_or = schema_store_->GetSectionMetadata(type_id, section_id); |
| if (!metadata_or.ok()) { |
| break; |
| } |
| const SectionMetadata* metadata = metadata_or.ValueOrDie(); |
| if (metadata->path == property) { |
| return metadata->id; |
| } |
| } |
| return kInvalidSectionId; |
| } |
| |
| std::unique_ptr<FeatureFlags> feature_flags_; |
| const Filesystem filesystem_; |
| const std::string test_dir_; |
| std::unique_ptr<LanguageSegmenter> language_segmenter_; |
| std::unique_ptr<SchemaStore> schema_store_; |
| std::unique_ptr<Normalizer> normalizer_; |
| std::atomic<int> num_total_hits_; |
| FakeClock fake_clock_; |
| }; |
| |
| DocumentProto CreateDocument(int id) { |
| return DocumentBuilder() |
| .SetKey("icing", "Email/" + std::to_string(id)) |
| .SetSchema("Email") |
| .AddStringProperty("name", "subject foo " + std::to_string(id)) |
| .AddStringProperty("body", "body bar " + std::to_string(id)) |
| .SetCreationTimestampMs(1574365086666 + id) |
| .Build(); |
| } |
| |
| SectionIdMask CreateSectionIdMask(const std::vector<SectionId>& section_ids) { |
| SectionIdMask mask = 0; |
| for (SectionId section_id : section_ids) { |
| mask |= (UINT64_C(1) << section_id); |
| } |
| return mask; |
| } |
| |
| ResultSpecProto CreateResultSpec( |
| int num_per_page, ResultSpecProto::ResultGroupingType result_group_type) { |
| ResultSpecProto result_spec; |
| result_spec.set_result_group_type(result_group_type); |
| result_spec.set_num_per_page(num_per_page); |
| return result_spec; |
| } |
| |
| libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore( |
| const Filesystem* filesystem, const std::string& base_dir, |
| const Clock* clock, const SchemaStore* schema_store, |
| const FeatureFlags& feature_flags) { |
| return DocumentStore::Create( |
| filesystem, base_dir, clock, schema_store, &feature_flags, |
| /*force_recovery_and_revalidate_documents=*/false, |
| /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true, |
| PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel, |
| /*initialize_stats=*/nullptr); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, CreationWithNullPointerShouldFail) { |
| EXPECT_THAT( |
| ResultRetrieverV2::Create(/*doc_store=*/nullptr, schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get()), |
| StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION)); |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| EXPECT_THAT( |
| ResultRetrieverV2::Create(doc_store.get(), /*schema_store=*/nullptr, |
| language_segmenter_.get(), normalizer_.get()), |
| StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION)); |
| EXPECT_THAT(ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| /*language_segmenter=*/nullptr, |
| normalizer_.get()), |
| StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION)); |
| EXPECT_THAT(ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), |
| /*normalizer=*/nullptr), |
| StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION)); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldRetrieveSimpleResults) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3, |
| doc_store->Put(CreateDocument(/*id=*/3))); |
| DocumentId document_id3 = put_result3.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4, |
| doc_store->Put(CreateDocument(/*id=*/4))); |
| DocumentId document_id4 = put_result4.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5, |
| doc_store->Put(CreateDocument(/*id=*/5))); |
| DocumentId document_id5 = put_result5.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/19}, |
| {document_id2, hit_section_id_mask, /*score=*/12}, |
| {document_id3, hit_section_id_mask, /*score=*/8}, |
| {document_id4, hit_section_id_mask, /*score=*/3}, |
| {document_id5, hit_section_id_mask, /*score=*/1}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(19); |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = CreateDocument(/*id=*/2); |
| result2.set_score(12); |
| SearchResultProto::ResultProto result3; |
| *result3.mutable_document() = CreateDocument(/*id=*/3); |
| result3.set_score(8); |
| SearchResultProto::ResultProto result4; |
| *result4.mutable_document() = CreateDocument(/*id=*/4); |
| result4.set_score(3); |
| SearchResultProto::ResultProto result5; |
| *result5.mutable_document() = CreateDocument(/*id=*/5); |
| result5.set_score(1); |
| |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| |
| // First page, 2 results |
| auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result1.results, |
| ElementsAre(EqualsProto(result1), EqualsProto(result2))); |
| // num_results_with_snippets is 0 when there is no snippet. |
| EXPECT_THAT(page_result1.num_results_with_snippets, Eq(0)); |
| // Requested page size is same as num_per_page. |
| EXPECT_THAT(page_result1.requested_page_size, Eq(2)); |
| // Has more results. |
| EXPECT_TRUE(has_more_results1); |
| |
| // Second page, 2 results |
| auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result2.results, |
| ElementsAre(EqualsProto(result3), EqualsProto(result4))); |
| // num_results_with_snippets is 0 when there is no snippet. |
| EXPECT_THAT(page_result2.num_results_with_snippets, Eq(0)); |
| // Requested page size is same as num_per_page. |
| EXPECT_THAT(page_result2.requested_page_size, Eq(2)); |
| // Has more results. |
| EXPECT_TRUE(has_more_results2); |
| |
| // Third page, 1 result |
| auto [page_result3, has_more_results3] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result3.results, ElementsAre(EqualsProto(result5))); |
| // num_results_with_snippets is 0 when there is no snippet. |
| EXPECT_THAT(page_result3.num_results_with_snippets, Eq(0)); |
| // Requested page size is same as num_per_page. |
| EXPECT_THAT(page_result3.requested_page_size, Eq(2)); |
| // No more results. |
| EXPECT_FALSE(has_more_results3); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldIgnoreNonInternalErrors) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| |
| DocumentId invalid_document_id = -1; |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/12}, |
| {document_id2, hit_section_id_mask, /*score=*/4}, |
| {invalid_document_id, hit_section_id_mask, /*score=*/0}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get(), |
| std::make_unique<MockGroupResultLimiter>())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(12); |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = CreateDocument(/*id=*/2); |
| result2.set_score(4); |
| |
| ResultStateV2 result_state1( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/3, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| PageResult page_result1 = |
| result_retriever |
| ->RetrieveNextPage(result_state1, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| EXPECT_THAT(page_result1.results, |
| ElementsAre(EqualsProto(result1), EqualsProto(result2))); |
| |
| DocumentId non_existing_document_id = 4; |
| scored_document_hits = { |
| {non_existing_document_id, hit_section_id_mask, /*score=*/15}, |
| {document_id1, hit_section_id_mask, /*score=*/12}, |
| {document_id2, hit_section_id_mask, /*score=*/4}}; |
| ResultStateV2 result_state2( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/3, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| PageResult page_result2 = |
| result_retriever |
| ->RetrieveNextPage(result_state2, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| EXPECT_THAT(page_result2.results, |
| ElementsAre(EqualsProto(result1), EqualsProto(result2))); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, |
| ShouldLimitNumChildDocumentsByMaxJoinedChildPerParent) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| // 1. Add 2 Person document |
| DocumentProto person_document1 = |
| DocumentBuilder() |
| .SetKey("namespace", "Person/1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Person") |
| .AddStringProperty("name", "Joe Fox") |
| .AddStringProperty("emailAddress", "ny152@aol.com") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(person_document1)); |
| DocumentId person_document_id1 = put_result1.new_document_id; |
| |
| DocumentProto person_document2 = |
| DocumentBuilder() |
| .SetKey("namespace", "Person/2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Person") |
| .AddStringProperty("name", "Meg Ryan") |
| .AddStringProperty("emailAddress", "shopgirl@aol.com") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(person_document2)); |
| DocumentId person_document_id2 = put_result2.new_document_id; |
| |
| // 2. Add 4 Email documents |
| DocumentProto email_document1 = DocumentBuilder() |
| .SetKey("namespace", "Email/1") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddStringProperty("name", "Test 1") |
| .AddStringProperty("body", "Test 1") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(put_result1, doc_store->Put(email_document1)); |
| DocumentId email_document_id1 = put_result1.new_document_id; |
| |
| DocumentProto email_document2 = DocumentBuilder() |
| .SetKey("namespace", "Email/2") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddStringProperty("name", "Test 2") |
| .AddStringProperty("body", "Test 2") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(put_result2, doc_store->Put(email_document2)); |
| DocumentId email_document_id2 = put_result2.new_document_id; |
| |
| DocumentProto email_document3 = DocumentBuilder() |
| .SetKey("namespace", "Email/3") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddStringProperty("name", "Test 3") |
| .AddStringProperty("body", "Test 3") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3, |
| doc_store->Put(email_document3)); |
| DocumentId email_document_id3 = put_result3.new_document_id; |
| |
| DocumentProto email_document4 = DocumentBuilder() |
| .SetKey("namespace", "Email/4") |
| .SetCreationTimestampMs(1000) |
| .SetSchema("Email") |
| .AddStringProperty("name", "Test 4") |
| .AddStringProperty("body", "Test 4") |
| .Build(); |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4, |
| doc_store->Put(email_document4)); |
| DocumentId email_document_id4 = put_result4.new_document_id; |
| |
| // 3. Setup the joined scored results. |
| std::vector<SectionId> person_hit_section_ids = { |
| GetSectionId("Person", "name")}; |
| std::vector<SectionId> email_hit_section_ids = { |
| GetSectionId("Email", "name"), GetSectionId("Email", "body")}; |
| SectionIdMask person_hit_section_id_mask = |
| CreateSectionIdMask(person_hit_section_ids); |
| SectionIdMask email_hit_section_id_mask = |
| CreateSectionIdMask(email_hit_section_ids); |
| |
| ScoredDocumentHit person1_scored_doc_hit( |
| person_document_id1, person_hit_section_id_mask, /*score=*/1); |
| ScoredDocumentHit person2_scored_doc_hit( |
| person_document_id2, person_hit_section_id_mask, /*score=*/2); |
| ScoredDocumentHit email1_scored_doc_hit( |
| email_document_id1, email_hit_section_id_mask, /*score=*/3); |
| ScoredDocumentHit email2_scored_doc_hit( |
| email_document_id2, email_hit_section_id_mask, /*score=*/4); |
| ScoredDocumentHit email3_scored_doc_hit( |
| email_document_id3, email_hit_section_id_mask, /*score=*/5); |
| ScoredDocumentHit email4_scored_doc_hit( |
| email_document_id4, email_hit_section_id_mask, /*score=*/6); |
| // Create JoinedScoredDocumentHits mapping: |
| // - Person1 to Email1 |
| // - Person2 to Email2, Email3, Email4 |
| std::vector<JoinedScoredDocumentHit> joined_scored_document_hits = { |
| JoinedScoredDocumentHit( |
| /*final_score=*/1, |
| /*parent_scored_document_hit=*/person1_scored_doc_hit, |
| /*child_scored_document_hits=*/{email1_scored_doc_hit}), |
| JoinedScoredDocumentHit( |
| /*final_score=*/3, |
| /*parent_scored_document_hit=*/person2_scored_doc_hit, |
| /*child_scored_document_hits=*/ |
| {email4_scored_doc_hit, email3_scored_doc_hit, |
| email2_scored_doc_hit})}; |
| |
| // 4. Retrieve result with max_joined_children_per_parent_to_return = 2. |
| ResultSpecProto result_spec = |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE); |
| result_spec.set_max_joined_children_per_parent_to_return(2); |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<JoinedScoredDocumentHit>>( |
| std::move(joined_scored_document_hits), /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| result_spec, *doc_store); |
| |
| // Result1: person2 with child docs = [email4, email3] |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = person_document2; |
| result1.set_score(3); |
| SearchResultProto::ResultProto* child1 = result1.add_joined_results(); |
| *child1->mutable_document() = email_document4; |
| child1->set_score(6); |
| SearchResultProto::ResultProto* child2 = result1.add_joined_results(); |
| *child2->mutable_document() = email_document3; |
| child2->set_score(5); |
| |
| // Result2: person1 with child docs = [email1] |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = person_document1; |
| result2.set_score(1); |
| SearchResultProto::ResultProto* child3 = result2.add_joined_results(); |
| *child3->mutable_document() = email_document1; |
| child3->set_score(3); |
| |
| auto [page_result, has_more_results] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result.results, |
| ElementsAre(EqualsProto(result1), EqualsProto(result2))); |
| // No more results. |
| EXPECT_FALSE(has_more_results); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldIgnoreInternalErrors) { |
| MockFilesystem mock_filesystem; |
| EXPECT_CALL(mock_filesystem, |
| PRead(A<int>(), A<void*>(), A<size_t>(), A<off_t>())) |
| .WillOnce(Return(false)) |
| .WillRepeatedly(DoDefault()); |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&mock_filesystem, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/0}, |
| {document_id2, hit_section_id_mask, /*score=*/0}}; |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get(), |
| std::make_unique<MockGroupResultLimiter>())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(0); |
| |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| PageResult page_result = |
| result_retriever |
| ->RetrieveNextPage(result_state, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| // We mocked mock_filesystem to return an internal error when retrieving doc2, |
| // so doc2 should be skipped and doc1 should still be returned. |
| EXPECT_THAT(page_result.results, ElementsAre(EqualsProto(result1))); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldUpdateResultState) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3, |
| doc_store->Put(CreateDocument(/*id=*/3))); |
| DocumentId document_id3 = put_result3.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4, |
| doc_store->Put(CreateDocument(/*id=*/4))); |
| DocumentId document_id4 = put_result4.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5, |
| doc_store->Put(CreateDocument(/*id=*/5))); |
| DocumentId document_id5 = put_result5.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/0}, |
| {document_id2, hit_section_id_mask, /*score=*/0}, |
| {document_id3, hit_section_id_mask, /*score=*/0}, |
| {document_id4, hit_section_id_mask, /*score=*/0}, |
| {document_id5, hit_section_id_mask, /*score=*/0}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| |
| // First page, 2 results |
| PageResult page_result1 = |
| result_retriever |
| ->RetrieveNextPage(result_state, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result1.results, SizeIs(2)); |
| { |
| absl_ports::shared_lock l(&result_state.mutex); |
| |
| // num_returned = size of first page |
| EXPECT_THAT(result_state.num_returned, Eq(2)); |
| // Should remove the 2 returned docs from scored_document_hits and only |
| // contain the remaining 3. |
| EXPECT_THAT(result_state.scored_document_hits_ranker, Pointee(SizeIs(3))); |
| } |
| |
| // Second page, 2 results |
| PageResult page_result2 = |
| result_retriever |
| ->RetrieveNextPage(result_state, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result2.results, SizeIs(2)); |
| { |
| absl_ports::shared_lock l(&result_state.mutex); |
| |
| // num_returned = size of first and second pages |
| EXPECT_THAT(result_state.num_returned, Eq(4)); |
| // Should remove the 2 returned docs from scored_document_hits and only |
| // contain the remaining 1. |
| EXPECT_THAT(result_state.scored_document_hits_ranker, Pointee(SizeIs(1))); |
| } |
| |
| // Third page, 1 result |
| PageResult page_result3 = |
| result_retriever |
| ->RetrieveNextPage(result_state, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result3.results, SizeIs(1)); |
| { |
| absl_ports::shared_lock l(&result_state.mutex); |
| |
| // num_returned = size of first, second and third pages |
| EXPECT_THAT(result_state.num_returned, Eq(5)); |
| // Should remove the 1 returned doc from scored_document_hits and become |
| // empty. |
| EXPECT_THAT(result_state.scored_document_hits_ranker, Pointee(IsEmpty())); |
| } |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldUpdateNumTotalHits) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| std::vector<ScoredDocumentHit> scored_document_hits1 = { |
| {document_id1, hit_section_id_mask, /*score=*/0}, |
| {document_id2, hit_section_id_mask, /*score=*/0}}; |
| std::shared_ptr<ResultStateV2> result_state1 = |
| std::make_shared<ResultStateV2>( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits1), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| { |
| absl_ports::unique_lock l(&result_state1->mutex); |
| |
| result_state1->RegisterNumTotalHits(&num_total_hits_); |
| ASSERT_THAT(num_total_hits_, Eq(2)); |
| } |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3, |
| doc_store->Put(CreateDocument(/*id=*/3))); |
| DocumentId document_id3 = put_result3.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4, |
| doc_store->Put(CreateDocument(/*id=*/4))); |
| DocumentId document_id4 = put_result4.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5, |
| doc_store->Put(CreateDocument(/*id=*/5))); |
| DocumentId document_id5 = put_result5.new_document_id; |
| std::vector<ScoredDocumentHit> scored_document_hits2 = { |
| {document_id3, hit_section_id_mask, /*score=*/0}, |
| {document_id4, hit_section_id_mask, /*score=*/0}, |
| {document_id5, hit_section_id_mask, /*score=*/0}}; |
| std::shared_ptr<ResultStateV2> result_state2 = |
| std::make_shared<ResultStateV2>( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits2), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE), |
| *doc_store); |
| { |
| absl_ports::unique_lock l(&result_state2->mutex); |
| |
| result_state2->RegisterNumTotalHits(&num_total_hits_); |
| ASSERT_THAT(num_total_hits_, Eq(5)); |
| } |
| |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| // Should get 1 doc in the first page of result_state1, and num_total_hits |
| // should be decremented by 1. |
| PageResult page_result1 = |
| result_retriever |
| ->RetrieveNextPage(*result_state1, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result1.results, SizeIs(1)); |
| EXPECT_THAT(num_total_hits_, Eq(4)); |
| |
| // Should get 2 docs in the first page of result_state2, and num_total_hits |
| // should be decremented by 2. |
| PageResult page_result2 = |
| result_retriever |
| ->RetrieveNextPage(*result_state2, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result2.results, SizeIs(2)); |
| EXPECT_THAT(num_total_hits_, Eq(2)); |
| |
| // Should get 1 doc in the second page of result_state2 (although num_per_page |
| // is 2, there is only 1 doc left), and num_total_hits should be decremented |
| // by 1. |
| PageResult page_result3 = |
| result_retriever |
| ->RetrieveNextPage(*result_state2, |
| fake_clock_.GetSystemTimeMilliseconds()) |
| .first; |
| ASSERT_THAT(page_result3.results, SizeIs(1)); |
| EXPECT_THAT(num_total_hits_, Eq(1)); |
| |
| // Destruct result_state1. There is 1 doc left, so num_total_hits should be |
| // decremented by 1 when destructing it. |
| result_state1.reset(); |
| EXPECT_THAT(num_total_hits_, Eq(0)); |
| |
| // Destruct result_state2. There is 0 doc left, so num_total_hits should be |
| // unchanged when destructing it. |
| result_state1.reset(); |
| EXPECT_THAT(num_total_hits_, Eq(0)); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, ShouldLimitNumTotalBytesPerPage) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/5}, |
| {document_id2, hit_section_id_mask, /*score=*/0}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(5); |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = CreateDocument(/*id=*/2); |
| result2.set_score(0); |
| |
| ResultSpecProto result_spec = |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE); |
| result_spec.set_num_total_bytes_per_page_threshold(result1.ByteSizeLong()); |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| result_spec, *doc_store); |
| |
| // First page. Only result1 should be returned, since its byte size meets |
| // num_total_bytes_per_page_threshold and ResultRetriever should terminate |
| // early even though # of results is still below num_per_page. |
| auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1))); |
| // Has more results. |
| EXPECT_TRUE(has_more_results1); |
| |
| // Second page, result2. |
| auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2))); |
| // No more results. |
| EXPECT_FALSE(has_more_results2); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, |
| ShouldReturnSingleLargeResultAboveNumTotalBytesPerPageThreshold) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/5}, |
| {document_id2, hit_section_id_mask, /*score=*/0}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(5); |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = CreateDocument(/*id=*/2); |
| result2.set_score(0); |
| |
| int threshold = 1; |
| ASSERT_THAT(result1.ByteSizeLong(), Gt(threshold)); |
| |
| ResultSpecProto result_spec = |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE); |
| result_spec.set_num_total_bytes_per_page_threshold(threshold); |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| result_spec, *doc_store); |
| |
| // First page. Should return single result1 even though its byte size exceeds |
| // num_total_bytes_per_page_threshold. |
| auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1))); |
| // Has more results. |
| EXPECT_TRUE(has_more_results1); |
| |
| // Second page, result2. |
| auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2))); |
| // No more results. |
| EXPECT_FALSE(has_more_results2); |
| } |
| |
| TEST_F(ResultRetrieverV2Test, |
| ShouldRetrieveNextResultWhenBelowNumTotalBytesPerPageThreshold) { |
| ICING_ASSERT_OK_AND_ASSIGN( |
| DocumentStore::CreateResult create_result, |
| CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_, |
| schema_store_.get(), *feature_flags_)); |
| std::unique_ptr<DocumentStore> doc_store = |
| std::move(create_result.document_store); |
| |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1, |
| doc_store->Put(CreateDocument(/*id=*/1))); |
| DocumentId document_id1 = put_result1.new_document_id; |
| ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2, |
| doc_store->Put(CreateDocument(/*id=*/2))); |
| DocumentId document_id2 = put_result2.new_document_id; |
| |
| std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"), |
| GetSectionId("Email", "body")}; |
| SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids); |
| std::vector<ScoredDocumentHit> scored_document_hits = { |
| {document_id1, hit_section_id_mask, /*score=*/5}, |
| {document_id2, hit_section_id_mask, /*score=*/0}}; |
| ICING_ASSERT_OK_AND_ASSIGN( |
| std::unique_ptr<ResultRetrieverV2> result_retriever, |
| ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(), |
| language_segmenter_.get(), normalizer_.get())); |
| |
| SearchResultProto::ResultProto result1; |
| *result1.mutable_document() = CreateDocument(/*id=*/1); |
| result1.set_score(5); |
| SearchResultProto::ResultProto result2; |
| *result2.mutable_document() = CreateDocument(/*id=*/2); |
| result2.set_score(0); |
| |
| int threshold = result1.ByteSizeLong() + 1; |
| ASSERT_THAT(result1.ByteSizeLong() + result2.ByteSizeLong(), Gt(threshold)); |
| |
| ResultSpecProto result_spec = |
| CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE); |
| result_spec.set_num_total_bytes_per_page_threshold(threshold); |
| ResultStateV2 result_state( |
| std::make_unique< |
| PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>( |
| std::move(scored_document_hits), |
| /*is_descending=*/true), |
| /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr, |
| result_spec, *doc_store); |
| |
| // After retrieving result1, total bytes are still below the threshold and # |
| // of results is still below num_per_page, so ResultRetriever should continue |
| // the retrieval process and thus include result2 into this page, even though |
| // finally total bytes of result1 + result2 exceed the threshold. |
| auto [page_result, has_more_results] = result_retriever->RetrieveNextPage( |
| result_state, fake_clock_.GetSystemTimeMilliseconds()); |
| EXPECT_THAT(page_result.results, |
| ElementsAre(EqualsProto(result1), EqualsProto(result2))); |
| // No more results. |
| EXPECT_FALSE(has_more_results); |
| } |
| |
| } // namespace |
| |
| } // namespace lib |
| } // namespace icing |