diff options
Diffstat (limited to 'icing/result/result-retriever-v2_projection-test.cc')
-rw-r--r-- | icing/result/result-retriever-v2_projection-test.cc | 1281 |
1 files changed, 1281 insertions, 0 deletions
diff --git a/icing/result/result-retriever-v2_projection-test.cc b/icing/result/result-retriever-v2_projection-test.cc new file mode 100644 index 0000000..bdd1715 --- /dev/null +++ b/icing/result/result-retriever-v2_projection-test.cc @@ -0,0 +1,1281 @@ +// 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 <memory> +#include <vector> + +#include "gtest/gtest.h" +#include "icing/document-builder.h" +#include "icing/portable/equals-proto.h" +#include "icing/portable/platform.h" +#include "icing/proto/document.pb.h" +#include "icing/proto/schema.pb.h" +#include "icing/proto/search.pb.h" +#include "icing/proto/term.pb.h" +#include "icing/result/page-result.h" +#include "icing/result/projection-tree.h" +#include "icing/result/result-retriever-v2.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-id.h" +#include "icing/testing/common-matchers.h" +#include "icing/testing/fake-clock.h" +#include "icing/testing/icu-data-file-helper.h" +#include "icing/testing/test-data.h" +#include "icing/testing/tmp-directory.h" +#include "icing/tokenization/language-segmenter-factory.h" +#include "icing/transform/normalizer-factory.h" +#include "icing/transform/normalizer.h" +#include "unicode/uloc.h" + +namespace icing { +namespace lib { + +namespace { + +using ::icing::lib::portable_equals_proto::EqualsProto; +using ::testing::SizeIs; + +constexpr PropertyConfigProto::Cardinality::Code CARDINALITY_OPTIONAL = + PropertyConfigProto::Cardinality::OPTIONAL; + +constexpr StringIndexingConfig::TokenizerType::Code TOKENIZER_PLAIN = + StringIndexingConfig::TokenizerType::PLAIN; + +constexpr TermMatchType::Code MATCH_EXACT = TermMatchType::EXACT_ONLY; +constexpr TermMatchType::Code MATCH_PREFIX = TermMatchType::PREFIX; + +class ResultRetrieverV2ProjectionTest : public testing::Test { + protected: + ResultRetrieverV2ProjectionTest() : test_dir_(GetTestTempDir() + "/icing") { + filesystem_.CreateDirectoryRecursively(test_dir_.c_str()); + } + + void SetUp() override { + 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_)); + 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(MATCH_PREFIX, + TOKENIZER_PLAIN) + .SetCardinality(CARDINALITY_OPTIONAL)) + .AddProperty(PropertyConfigBuilder() + .SetName("body") + .SetDataTypeString(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(MATCH_PREFIX, TOKENIZER_PLAIN) + .SetCardinality(CARDINALITY_OPTIONAL)) + .AddProperty( + PropertyConfigBuilder() + .SetName("emailAddress") + .SetDataTypeString(MATCH_PREFIX, TOKENIZER_PLAIN) + .SetCardinality(CARDINALITY_OPTIONAL))) + .Build(); + ASSERT_THAT(schema_store_->SetSchema(schema), IsOk()); + + ICING_ASSERT_OK_AND_ASSIGN( + DocumentStore::CreateResult create_result, + DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_, + schema_store_.get())); + document_store_ = std::move(create_result.document_store); + } + + 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; + } + + 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::unique_ptr<DocumentStore> document_store_; + FakeClock fake_clock_; +}; + +// TODO(sungyc): Refactor helper functions below (builder classes or common test +// utility). + +SectionIdMask CreateSectionIdMask(const std::vector<SectionId>& section_ids) { + SectionIdMask mask = 0; + for (SectionId section_id : section_ids) { + mask |= (1u << section_id); + } + return mask; +} + +SearchSpecProto CreateSearchSpec(TermMatchType::Code match_type) { + SearchSpecProto search_spec; + search_spec.set_term_match_type(match_type); + return search_spec; +} + +ScoringSpecProto CreateScoringSpec(bool is_descending_order) { + ScoringSpecProto scoring_spec; + scoring_spec.set_order_by(is_descending_order ? ScoringSpecProto::Order::DESC + : ScoringSpecProto::Order::ASC); + return scoring_spec; +} + +ResultSpecProto CreateResultSpec(int num_per_page) { + ResultSpecProto result_spec; + result_spec.set_num_per_page(num_per_page); + return result_spec; +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionTopLevelLeadNodeFieldPath) { + // 1. Add two Email documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results only contain the 'name' property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionNestedLeafNodeFieldPath) { + // 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("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + 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("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("sender.name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results only contain the 'sender.name' + // property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + 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()) + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + 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()) + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionIntermediateNodeFieldPath) { + // 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("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + 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("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("sender"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results only contain the 'sender' + // property and all of the subproperties of 'sender'. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_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()) + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_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()) + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionMultipleNestedFieldPaths) { + // 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("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + 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("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("sender.name"); + type_property_mask->add_paths("sender.emailAddress"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results only contain the 'sender.name' and + // 'sender.address' properties. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_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()) + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_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()) + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionEmptyFieldPath) { + // 1. Add two Email documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results contain *no* properties. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionInvalidFieldPath) { + // 1. Add two Email documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("nonExistentProperty"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results contain *no* properties. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionValidAndInvalidFieldPath) { + // 1. Add two Email documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .AddStringProperty("body", + "Count all the sheep and tell them 'Hello'.") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("name"); + type_property_mask->add_paths("nonExistentProperty"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned results only contain the 'name' property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Goodnight Moon!") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionMultipleTypesNoWildcards) { + // 1. Add two documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* type_property_mask = result_spec.add_type_property_masks(); + type_property_mask->set_schema_type("Email"); + type_property_mask->add_paths("name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned Email results only contain the 'name' + // property and the returned Person results have all of their properties. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, ProjectionMultipleTypesWildcard) { + // 1. Add two documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* wildcard_type_property_mask = + result_spec.add_type_property_masks(); + wildcard_type_property_mask->set_schema_type( + std::string(ProjectionTree::kSchemaTypeWildcard)); + wildcard_type_property_mask->add_paths("name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned Email results only contain the 'name' + // property and the returned Person results only contain the 'name' property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, + ProjectionMultipleTypesWildcardWithOneOverride) { + // 1. Add two documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* email_type_property_mask = + result_spec.add_type_property_masks(); + email_type_property_mask->set_schema_type("Email"); + email_type_property_mask->add_paths("body"); + TypePropertyMask* wildcard_type_property_mask = + result_spec.add_type_property_masks(); + wildcard_type_property_mask->set_schema_type( + std::string(ProjectionTree::kSchemaTypeWildcard)); + wildcard_type_property_mask->add_paths("name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned Email results only contain the 'body' + // property and the returned Person results only contain the 'name' property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, + ProjectionSingleTypesWildcardAndOverride) { + // 1. Add two documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .AddDocumentProperty( + "sender", + DocumentBuilder() + .SetKey("namespace", "uri") + .SetSchema("Person") + .AddStringProperty("name", "Mr. Body") + .AddStringProperty("emailAddress", "mr.body123@gmail.com") + .Build()) + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* email_type_property_mask = + result_spec.add_type_property_masks(); + email_type_property_mask->set_schema_type("Email"); + email_type_property_mask->add_paths("sender.name"); + TypePropertyMask* wildcard_type_property_mask = + result_spec.add_type_property_masks(); + wildcard_type_property_mask->set_schema_type( + std::string(ProjectionTree::kSchemaTypeWildcard)); + wildcard_type_property_mask->add_paths("name"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned Email results only contain the 'sender.name' + // property and the returned Person results only contain the 'name' property. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddDocumentProperty("sender", + DocumentBuilder() + .SetKey("namespace", "uri") + .SetSchema("Person") + .AddStringProperty("name", "Mr. Body") + .Build()) + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +TEST_F(ResultRetrieverV2ProjectionTest, + ProjectionSingleTypesWildcardAndOverrideNestedProperty) { + // 1. Add two documents + DocumentProto document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddStringProperty("name", "Hello World!") + .AddStringProperty( + "body", "Oh what a beautiful morning! Oh what a beautiful day!") + .AddDocumentProperty( + "sender", + DocumentBuilder() + .SetKey("namespace", "uri") + .SetSchema("Person") + .AddStringProperty("name", "Mr. Body") + .AddStringProperty("emailAddress", "mr.body123@gmail.com") + .Build()) + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id1, + document_store_->Put(document_one)); + + DocumentProto document_two = + DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .AddStringProperty("name", "Joe Fox") + .AddStringProperty("emailAddress", "ny152@aol.com") + .Build(); + ICING_ASSERT_OK_AND_ASSIGN(DocumentId document_id2, + document_store_->Put(document_two)); + + // 2. Setup the scored results. + 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}}; + + // 3. Create a ResultSpec with type property mask. + ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/2); + TypePropertyMask* email_type_property_mask = + result_spec.add_type_property_masks(); + email_type_property_mask->set_schema_type("Email"); + email_type_property_mask->add_paths("sender.name"); + TypePropertyMask* wildcard_type_property_mask = + result_spec.add_type_property_masks(); + wildcard_type_property_mask->set_schema_type( + std::string(ProjectionTree::kSchemaTypeWildcard)); + wildcard_type_property_mask->add_paths("sender"); + + // 4. Create ResultState with custom ResultSpec. + ResultStateV2 result_state( + std::make_unique<PriorityQueueScoredDocumentHitsRanker>( + std::move(scored_document_hits), /*is_descending=*/false), + /*query_terms=*/SectionRestrictQueryTermsMap{}, + CreateSearchSpec(TermMatchType::EXACT_ONLY), + CreateScoringSpec(/*is_descending_order=*/false), result_spec, + *document_store_); + + ICING_ASSERT_OK_AND_ASSIGN( + std::unique_ptr<ResultRetrieverV2> result_retriever, + ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(), + language_segmenter_.get(), normalizer_.get())); + + // 5. Verify that the returned Email results only contain the 'sender.name' + // property and the returned Person results contain no properties. + PageResult page_result = + result_retriever->RetrieveNextPage(result_state).first; + ASSERT_THAT(page_result.results, SizeIs(2)); + + DocumentProto projected_document_one = + DocumentBuilder() + .SetKey("namespace", "uri1") + .SetCreationTimestampMs(1000) + .SetSchema("Email") + .AddDocumentProperty("sender", + DocumentBuilder() + .SetKey("namespace", "uri") + .SetSchema("Person") + .AddStringProperty("name", "Mr. Body") + .Build()) + .Build(); + EXPECT_THAT(page_result.results.at(0).document(), + EqualsProto(projected_document_one)); + + DocumentProto projected_document_two = DocumentBuilder() + .SetKey("namespace", "uri2") + .SetCreationTimestampMs(1000) + .SetSchema("Person") + .Build(); + EXPECT_THAT(page_result.results.at(1).document(), + EqualsProto(projected_document_two)); +} + +} // namespace + +} // namespace lib +} // namespace icing |