blob: 8706b6d2a0b1ce7946dbfb397fe411f62225683d [file] [log] [blame]
// 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-state-v2.h"
#include <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "gtest/gtest.h"
#include "icing/absl_ports/mutex.h"
#include "icing/file/filesystem.h"
#include "icing/proto/document.pb.h"
#include "icing/proto/schema.pb.h"
#include "icing/proto/search.pb.h"
#include "icing/schema/schema-store.h"
#include "icing/scoring/priority-queue-scored-document-hits-ranker.h"
#include "icing/scoring/scored-document-hit.h"
#include "icing/store/document-store.h"
#include "icing/testing/common-matchers.h"
#include "icing/testing/tmp-directory.h"
#include "icing/util/clock.h"
namespace icing {
namespace lib {
namespace {
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
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;
}
class ResultStateV2Test : public ::testing::Test {
protected:
void SetUp() override {
schema_store_base_dir_ = GetTestTempDir() + "/schema_store";
filesystem_.CreateDirectoryRecursively(schema_store_base_dir_.c_str());
ICING_ASSERT_OK_AND_ASSIGN(
schema_store_,
SchemaStore::Create(&filesystem_, schema_store_base_dir_, &clock_));
SchemaProto schema;
schema.add_types()->set_schema_type("Document");
ICING_ASSERT_OK(schema_store_->SetSchema(std::move(schema)));
doc_store_base_dir_ = GetTestTempDir() + "/document_store";
filesystem_.CreateDirectoryRecursively(doc_store_base_dir_.c_str());
ICING_ASSERT_OK_AND_ASSIGN(
DocumentStore::CreateResult result,
DocumentStore::Create(&filesystem_, doc_store_base_dir_, &clock_,
schema_store_.get()));
document_store_ = std::move(result.document_store);
num_total_hits_ = 0;
}
void TearDown() override {
filesystem_.DeleteDirectoryRecursively(doc_store_base_dir_.c_str());
filesystem_.DeleteDirectoryRecursively(schema_store_base_dir_.c_str());
}
ScoredDocumentHit AddScoredDocument(DocumentId document_id) {
DocumentProto document;
document.set_namespace_("namespace");
document.set_uri(std::to_string(document_id));
document.set_schema("Document");
document_store_->Put(std::move(document));
return ScoredDocumentHit(document_id, kSectionIdMaskNone, /*score=*/1);
}
DocumentStore& document_store() { return *document_store_; }
std::atomic<int>& num_total_hits() { return num_total_hits_; }
const std::atomic<int>& num_total_hits() const { return num_total_hits_; }
private:
Filesystem filesystem_;
std::string doc_store_base_dir_;
std::string schema_store_base_dir_;
Clock clock_;
std::unique_ptr<DocumentStore> document_store_;
std::unique_ptr<SchemaStore> schema_store_;
std::atomic<int> num_total_hits_;
};
TEST_F(ResultStateV2Test, ShouldInitializeValuesAccordingToSpecs) {
ResultSpecProto result_spec =
CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE);
result_spec.set_num_total_bytes_per_page_threshold(4096);
// Adjustment info is not important in this test.
ResultStateV2 result_state(
std::make_unique<
PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
std::vector<ScoredDocumentHit>(), /*is_descending=*/true),
/*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
result_spec, document_store());
absl_ports::shared_lock l(&result_state.mutex);
EXPECT_THAT(result_state.num_returned, Eq(0));
EXPECT_THAT(result_state.num_per_page(), Eq(result_spec.num_per_page()));
EXPECT_THAT(result_state.num_total_bytes_per_page_threshold(),
Eq(result_spec.num_total_bytes_per_page_threshold()));
}
TEST_F(ResultStateV2Test, ShouldInitializeValuesAccordingToDefaultSpecs) {
ResultSpecProto default_result_spec = ResultSpecProto::default_instance();
ASSERT_THAT(default_result_spec.num_per_page(), Eq(10));
ASSERT_THAT(default_result_spec.num_total_bytes_per_page_threshold(),
Eq(std::numeric_limits<int32_t>::max()));
// Adjustment info is not important in this test.
ResultStateV2 result_state(
std::make_unique<
PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
std::vector<ScoredDocumentHit>(),
/*is_descending=*/true),
/*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
default_result_spec, document_store());
absl_ports::shared_lock l(&result_state.mutex);
EXPECT_THAT(result_state.num_returned, Eq(0));
EXPECT_THAT(result_state.num_per_page(),
Eq(default_result_spec.num_per_page()));
EXPECT_THAT(result_state.num_total_bytes_per_page_threshold(),
Eq(default_result_spec.num_total_bytes_per_page_threshold()));
}
TEST_F(ResultStateV2Test,
ShouldConstructNamespaceGroupIdMapAndGroupResultLimitsAccordingToSpecs) {
// Create 3 docs under namespace1, namespace2, namespace3.
DocumentProto document1;
document1.set_namespace_("namespace1");
document1.set_uri("uri/1");
document1.set_schema("Document");
ICING_ASSERT_OK(document_store().Put(std::move(document1)));
DocumentProto document2;
document2.set_namespace_("namespace2");
document2.set_uri("uri/2");
document2.set_schema("Document");
ICING_ASSERT_OK(document_store().Put(std::move(document2)));
DocumentProto document3;
document3.set_namespace_("namespace3");
document3.set_uri("uri/3");
document3.set_schema("Document");
ICING_ASSERT_OK(document_store().Put(std::move(document3)));
// Create a ResultSpec that limits "namespace1" to 3 results and limits
// "namespace2"+"namespace3" to a total of 2 results. Also add
// "nonexistentNamespace1" and "nonexistentNamespace2" to test the behavior.
ResultSpecProto::ResultGroupingType result_grouping_type =
ResultSpecProto::NAMESPACE;
ResultSpecProto result_spec =
CreateResultSpec(/*num_per_page=*/5, result_grouping_type);
ResultSpecProto::ResultGrouping* result_grouping =
result_spec.add_result_groupings();
ResultSpecProto::ResultGrouping::Entry* entry =
result_grouping->add_entry_groupings();
result_grouping->set_max_results(3);
entry->set_namespace_("namespace1");
result_grouping = result_spec.add_result_groupings();
result_grouping->set_max_results(5);
entry = result_grouping->add_entry_groupings();
entry->set_namespace_("nonexistentNamespace2");
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");
entry = result_grouping->add_entry_groupings();
entry->set_namespace_("nonexistentNamespace1");
// Get entry ids.
ICING_ASSERT_OK_AND_ASSIGN(
int32_t entry_id1, document_store().GetResultGroupingEntryId(
result_grouping_type, "namespace1", "Document"));
ICING_ASSERT_OK_AND_ASSIGN(
int32_t entry_id2, document_store().GetResultGroupingEntryId(
result_grouping_type, "namespace2", "Document"));
ICING_ASSERT_OK_AND_ASSIGN(
int32_t entry_id3, document_store().GetResultGroupingEntryId(
result_grouping_type, "namespace3", "Document"));
// Adjustment info is not important in this test.
ResultStateV2 result_state(
std::make_unique<
PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
std::vector<ScoredDocumentHit>(),
/*is_descending=*/true),
/*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
result_spec, document_store());
absl_ports::shared_lock l(&result_state.mutex);
// "namespace1" should be in group 0, and "namespace2"+"namespace3" should be
// in group 2.
// "nonexistentNamespace1" and "nonexistentNamespace2" shouldn't exist.
EXPECT_THAT(result_state.entry_id_group_id_map(),
UnorderedElementsAre(Pair(entry_id1, 0), Pair(entry_id2, 2),
Pair(entry_id3, 2)));
// group_result_limits should contain 3 (at index 0 for group 0), 5 (at index
// 1 for group 1), 2 (at index 2 for group 2), even though there is no valid
// namespace in group 1.
EXPECT_THAT(result_state.group_result_limits, ElementsAre(3, 5, 2));
}
TEST_F(ResultStateV2Test, ShouldUpdateNumTotalHits) {
std::vector<ScoredDocumentHit> scored_document_hits = {
AddScoredDocument(/*document_id=*/1),
AddScoredDocument(/*document_id=*/0),
AddScoredDocument(/*document_id=*/2),
AddScoredDocument(/*document_id=*/4),
AddScoredDocument(/*document_id=*/3)};
// Adjustment info is not important in this test.
// Creates a ResultState with 5 ScoredDocumentHits.
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=*/5, ResultSpecProto::NAMESPACE),
document_store());
absl_ports::unique_lock l(&result_state.mutex);
EXPECT_THAT(num_total_hits(), Eq(0));
result_state.RegisterNumTotalHits(&num_total_hits());
EXPECT_THAT(num_total_hits(), Eq(5));
result_state.IncrementNumTotalHits(500);
EXPECT_THAT(num_total_hits(), Eq(505));
}
TEST_F(ResultStateV2Test, ShouldUpdateNumTotalHitsWhenDestructed) {
std::vector<ScoredDocumentHit> scored_document_hits1 = {
AddScoredDocument(/*document_id=*/1),
AddScoredDocument(/*document_id=*/0),
AddScoredDocument(/*document_id=*/2),
AddScoredDocument(/*document_id=*/4),
AddScoredDocument(/*document_id=*/3)};
std::vector<ScoredDocumentHit> scored_document_hits2 = {
AddScoredDocument(/*document_id=*/6),
AddScoredDocument(/*document_id=*/5)};
num_total_hits() = 2;
{
// Adjustment info is not important in this test.
// Creates a ResultState with 5 ScoredDocumentHits.
ResultStateV2 result_state1(
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=*/5, ResultSpecProto::NAMESPACE),
document_store());
absl_ports::unique_lock l(&result_state1.mutex);
result_state1.RegisterNumTotalHits(&num_total_hits());
ASSERT_THAT(num_total_hits(), Eq(7));
{
// Adjustment info is not important in this test.
// Creates another ResultState with 2 ScoredDocumentHits.
ResultStateV2 result_state2(
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=*/5, ResultSpecProto::NAMESPACE),
document_store());
absl_ports::unique_lock l(&result_state2.mutex);
result_state2.RegisterNumTotalHits(&num_total_hits());
ASSERT_THAT(num_total_hits(), Eq(9));
}
EXPECT_THAT(num_total_hits(), Eq(7));
}
EXPECT_THAT(num_total_hits(), Eq(2));
}
TEST_F(ResultStateV2Test, ShouldNotUpdateNumTotalHitsWhenNotRegistered) {
std::vector<ScoredDocumentHit> scored_document_hits = {
AddScoredDocument(/*document_id=*/1),
AddScoredDocument(/*document_id=*/0),
AddScoredDocument(/*document_id=*/2),
AddScoredDocument(/*document_id=*/4),
AddScoredDocument(/*document_id=*/3)};
// Creates a ResultState with 5 ScoredDocumentHits.
{
// Adjustment info is not important in this test.
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=*/5, ResultSpecProto::NAMESPACE),
document_store());
{
absl_ports::unique_lock l(&result_state.mutex);
EXPECT_THAT(num_total_hits(), Eq(0));
result_state.IncrementNumTotalHits(500);
EXPECT_THAT(num_total_hits(), Eq(0));
}
}
EXPECT_THAT(num_total_hits(), Eq(0));
}
TEST_F(ResultStateV2Test, ShouldDecrementOriginalNumTotalHitsWhenReregister) {
std::atomic<int> another_num_total_hits = 11;
std::vector<ScoredDocumentHit> scored_document_hits = {
AddScoredDocument(/*document_id=*/1),
AddScoredDocument(/*document_id=*/0),
AddScoredDocument(/*document_id=*/2),
AddScoredDocument(/*document_id=*/4),
AddScoredDocument(/*document_id=*/3)};
// Adjustment info is not important in this test.
// Creates a ResultState with 5 ScoredDocumentHits.
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=*/5, ResultSpecProto::NAMESPACE),
document_store());
absl_ports::unique_lock l(&result_state.mutex);
num_total_hits() = 7;
result_state.RegisterNumTotalHits(&num_total_hits());
EXPECT_THAT(num_total_hits(), Eq(12));
result_state.RegisterNumTotalHits(&another_num_total_hits);
// The original num_total_hits should be decremented after re-registration.
EXPECT_THAT(num_total_hits(), Eq(7));
// another_num_total_hits should be incremented after re-registration.
EXPECT_THAT(another_num_total_hits, Eq(16));
result_state.IncrementNumTotalHits(500);
// The original num_total_hits should be unchanged.
EXPECT_THAT(num_total_hits(), Eq(7));
// Increment should be done on another_num_total_hits.
EXPECT_THAT(another_num_total_hits, Eq(516));
}
} // namespace
} // namespace lib
} // namespace icing