This is an automated email from the ASF dual-hosted git repository.
airborne12 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new eb16c4d81fb [improvement](be) Keep hot SNII term lookups across
queries (#69879)
eb16c4d81fb is described below
commit eb16c4d81fb38965e84845472f775f54674003b0
Author: Jack <[email protected]>
AuthorDate: Mon Nov 2 09:22:44 2026 +0800
[improvement](be) Keep hot SNII term lookups across queries (#69879)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: A SNII dictionary larger than the resident threshold
(256 KB) is read block by
block on demand. Every exact term lookup reads the term's DICT block (64
KB target), decompresses
it with zstd, checks its CRC and parses its anchors, and the
request-scoped DictBlockCache keeps
nothing across queries. Every query therefore pays this again for every
term in every segment.
On a 99M-row AgentLogs table with 722 segments, counting one frequent
term spent 68 ms of index
time on these lookups against 16 ms for V3, and stack samples of
postings-only and phrase queries
showed 7-14% of SNII time in decompressing dictionary blocks alone.
A SNII reader that the searcher cache keeps now keeps its recent exact
lookups as well: a bounded
LRU of up to 32 terms per reader, each at most 64 bytes long with at
most 256 bytes of inline
postings, which also remembers terms its dictionary turned out not to
hold. `lookup()` and
`lookup_batch()` check it before they read a block, so a batch reads
only the blocks of the terms
it does not hold. The cache's full footprint, about 25 KB, is reserved
in the reader's searcher
cache charge when the reader is inserted, as norms are, so the memory
stays accounted. Only
readers kept by the searcher cache enable it, and only when their
dictionary is read on demand.
The entries live inside the reader, so they go away with it and never
outlive the file they
describe.
Caching the decoded blocks instead was not chosen: an exact lookup
touches one 64 KB block per
segment, so blocks would cost about 46 MB per hot term and column on
that table, while a kept
lookup costs a few hundred bytes. Keeping blocks in the searcher cache
would also count against
its fd-derived entry limit and push readers out. Prefix and pattern
walks over the dictionary are
not covered.
Hot latency on the 99M-row table, cache off/on interleaved in one BE
process:
| Query | Off -> On |
|---|---|
| phrase 'retry budget depleted' (DS2) | 0.60 |
| AND of two terms in two columns (A4) | 0.74-0.79 |
| one term in two columns (A3) | 0.80 |
| count of one frequent term (A2) | 0.84 |
| regexp, its matched terms looked up again (DS9) | 0.89 |
| phrase prefix (DS4) | 0.93 |
| the other 19 queries | 0.92-1.05, within run-to-run noise |
Against V3 on the same data (SNII/V3), DS2 went from 1.29 to 0.81, A4
from 1.24 to 0.95, A3 from
1.21 to 1.01 and A2 from 1.07 to 0.97.
---
.../inverted/snii/reader/logical_index_reader.cpp | 125 ++++++++++-
.../inverted/snii/reader/logical_index_reader.h | 18 +-
.../index/inverted/snii/snii_index_reader.cpp | 3 +
.../index/inverted/snii/snii_query_test.cpp | 238 +++++++++++++++++++++
4 files changed, 372 insertions(+), 12 deletions(-)
diff --git a/be/src/storage/index/inverted/snii/reader/logical_index_reader.cpp
b/be/src/storage/index/inverted/snii/reader/logical_index_reader.cpp
index 186dd7b936d..3e58ac1e864 100644
--- a/be/src/storage/index/inverted/snii/reader/logical_index_reader.cpp
+++ b/be/src/storage/index/inverted/snii/reader/logical_index_reader.cpp
@@ -21,8 +21,12 @@
#include <cstdlib>
#include <future>
#include <limits>
+#include <list>
#include <memory>
#include <mutex>
+#include <string>
+#include <string_view>
+#include <unordered_map>
#include <utility>
#include <vector>
@@ -53,6 +57,15 @@ struct LogicalIndexReader::NormsCacheState {
std::shared_future<Status> in_flight;
};
+// Exact lookup results of an on-demand dictionary, least recently used last.
+struct LogicalIndexReader::TermLookupCache {
+ using Kept = std::pair<std::string, BatchLookupResult>;
+
+ std::mutex mutex;
+ std::list<Kept> order;
+ std::unordered_map<std::string_view, std::list<Kept>::iterator> index;
+};
+
using format::BlockRef;
using format::bsbf_hash;
using format::DictBlockDirectoryReader;
@@ -73,6 +86,17 @@ constexpr uint64_t kMaxDictLookupBatchBytes = 4ULL * 1024 *
1024;
// for the state/data/vector allocations. The framed norms bytes are charged
// separately at their exact validated length.
constexpr size_t kNormsAllocationOverheadCharge = 128;
+// The term lookup cache keeps at most this many terms per reader, each at
most this long and with
+// at most this many bytes of inline postings, so its footprint has a fixed
bound.
+constexpr size_t kTermLookupCacheTerms = 32;
+constexpr size_t kTermLookupMaxTermBytes = 64;
+constexpr size_t kTermLookupMaxInlineBytes = 256;
+// One kept term at those bounds: its list node (links and the key/result
pair), its hash node and
+// bucket, the heap of the key and of the entry's term, the inline postings,
and allocator
+// bookkeeping for each of those allocations.
+constexpr size_t kTermLookupEntryCharge =
+ sizeof(std::pair<std::string, LogicalIndexReader::BatchLookupResult>)
+ 2 * sizeof(void*) +
+ 64 + 2 * (kTermLookupMaxTermBytes + 1) + kTermLookupMaxInlineBytes + 6
* 16;
// L0/L1 tiering threshold (bytes). Defaults to kBsbfResidentMaxBytes; the env
// SNII_BSBF_RESIDENT_MAX overrides it for tuning and for exercising the
@@ -488,6 +512,10 @@ size_t LogicalIndexReader::memory_usage() const {
for (const auto& block : resident_dict_blocks_) {
bytes += sizeof(block) + block.bytes.capacity() +
block.reader.heap_bytes();
}
+ // The term lookup cache fills lazily as well, so its bounded footprint is
reserved in full.
+ if (term_lookups_ != nullptr) {
+ bytes += sizeof(TermLookupCache) + kTermLookupCacheTerms *
kTermLookupEntryCharge;
+ }
// Norms are loaded lazily, but the searcher-cache charge is fixed when
this
// reader is inserted. Reserve their complete framed size and heap state up
// front so later allocation is already accounted for. Saturation prevents
@@ -508,6 +536,12 @@ size_t LogicalIndexReader::memory_usage() const {
return bytes;
}
+void LogicalIndexReader::enable_term_lookup_cache() {
+ if (resident_dict_blocks_.empty() && dbd_.n_blocks() != 0) {
+ term_lookups_ = std::make_shared<TermLookupCache>();
+ }
+}
+
Status LogicalIndexReader::open_norms(format::NormsPodReader* out) const {
if (out == nullptr) {
return Status::Error<ErrorCode::INVALID_ARGUMENT, false>(
@@ -692,6 +726,18 @@ Status LogicalIndexReader::lookup(std::string_view term,
bool* found, DictEntry*
if (!maybe) {
return Status::OK();
}
+ if (term_lookups_ != nullptr) {
+ BatchLookupResult kept;
+ if (find_kept_lookup(term, &kept)) {
+ *found = kept.found;
+ if (kept.found) {
+ *entry = std::move(kept.entry);
+ *frq_base = kept.frq_base;
+ *prx_base = kept.prx_base;
+ }
+ return Status::OK();
+ }
+ }
// Use a resident small-DICT block when present; otherwise read the DICT
// block on demand and parse it with the same validation path used at open.
@@ -702,16 +748,68 @@ Status LogicalIndexReader::lookup(std::string_view term,
bool* found, DictEntry*
bool hit = false;
RETURN_IF_ERROR(br->find_term(term, &hit, entry));
- if (!hit) {
- return Status::OK();
+ if (hit) {
+ *found = true;
+ *frq_base = br->frq_base();
+ *prx_base = br->prx_base();
}
-
- *found = true;
- *frq_base = br->frq_base();
- *prx_base = br->prx_base();
+ keep_lookup(term, hit, *entry, br->frq_base(), br->prx_base());
return Status::OK();
}
+bool LogicalIndexReader::find_kept_lookup(std::string_view term,
BatchLookupResult* out) const {
+ TermLookupCache& cache = *term_lookups_;
+ std::lock_guard lock(cache.mutex);
+ const auto it = cache.index.find(term);
+ if (it == cache.index.end()) {
+ return false;
+ }
+ cache.order.splice(cache.order.begin(), cache.order, it->second);
+ *out = it->second->second;
+ return true;
+}
+
+void LogicalIndexReader::keep_lookup(std::string_view term, bool found, const
DictEntry& entry,
+ uint64_t frq_base, uint64_t prx_base)
const {
+ // Within these bounds the charge memory_usage() reserved covers the cache
at its fullest.
+ if (term_lookups_ == nullptr || term.size() > kTermLookupMaxTermBytes ||
+ (found && (entry.term.size() > kTermLookupMaxTermBytes ||
+ entry.frq_bytes.size() + entry.prx_bytes.size() >
kTermLookupMaxInlineBytes))) {
+ return;
+ }
+ BatchLookupResult result;
+ if (found) {
+ result.found = true;
+ result.entry = entry;
+ result.frq_base = frq_base;
+ result.prx_base = prx_base;
+ }
+ TermLookupCache& cache = *term_lookups_;
+ std::lock_guard lock(cache.mutex);
+ if (cache.index.contains(term)) {
+ return;
+ }
+ cache.order.emplace_front(std::string(term), std::move(result));
+ cache.index.emplace(cache.order.front().first, cache.order.begin());
+ if (cache.order.size() > kTermLookupCacheTerms) {
+ cache.index.erase(cache.order.back().first);
+ cache.order.pop_back();
+ }
+}
+
+void LogicalIndexReader::keep_group_lookups(const BatchLookupState& state,
+ const BatchLookupGroup& group)
const {
+ if (term_lookups_ == nullptr) {
+ return;
+ }
+ for (size_t i = group.begin; i < group.end; ++i) {
+ const size_t term_index = state.candidates_[i].term_index;
+ const BatchLookupResult& result = (*state.results_)[term_index];
+ keep_lookup((*state.terms_)[term_index], result.found, result.entry,
result.frq_base,
+ result.prx_base);
+ }
+}
+
Status LogicalIndexReader::may_contain(std::string_view term, bool*
maybe_present) const {
uint32_t ordinal = 0;
return locate_candidate_dict_block(term, maybe_present, &ordinal);
@@ -740,14 +838,15 @@ Status
LogicalIndexReader::locate_candidate_dict_block(std::string_view term, bo
Status LogicalIndexReader::collect_batch_lookup_groups(
const std::vector<std::string>& terms,
std::vector<BatchLookupCandidate>* candidates,
- std::vector<BatchLookupGroup>* groups) const {
+ std::vector<BatchLookupGroup>* groups, std::vector<BatchLookupResult>*
kept_results) const {
candidates->clear();
candidates->reserve(terms.size());
for (size_t i = 0; i < terms.size(); ++i) {
bool maybe = false;
uint32_t ordinal = 0;
RETURN_IF_ERROR(locate_candidate_dict_block(terms[i], &maybe,
&ordinal));
- if (maybe) {
+ if (maybe &&
+ (kept_results == nullptr || !find_kept_lookup(terms[i],
&(*kept_results)[i]))) {
candidates->push_back({i, ordinal});
}
}
@@ -792,7 +891,8 @@ Status LogicalIndexReader::prepare_lookup_batch(const
std::vector<std::string>&
return lookup(terms.front(), &result.found, &result.entry,
&result.frq_base,
&result.prx_base);
}
- RETURN_IF_ERROR(collect_batch_lookup_groups(terms, &state->candidates_,
&state->groups_));
+ RETURN_IF_ERROR(collect_batch_lookup_groups(terms, &state->candidates_,
&state->groups_,
+ term_lookups_ != nullptr ?
results : nullptr));
if (!resident_dict_blocks_.empty()) {
for (const BatchLookupGroup& group : state->groups_) {
const DictBlockReader* block_reader = nullptr;
@@ -893,6 +993,7 @@ Status
LogicalIndexReader::consume_lookup_wave(BatchLookupState* state,
RETURN_IF_ERROR(resolve_batch_lookup_entries(*state->terms_,
state->candidates_,
state->groups_[block.group_index],
block_reader,
state->results_));
+ keep_group_lookups(*state, state->groups_[block.group_index]);
}
if (!state->pending_.empty()) {
state->next_group_ = state->pending_.back().group_index + 1;
@@ -916,8 +1017,10 @@ Status LogicalIndexReader::lookup_batch(const
std::vector<std::string>& terms,
std::shared_ptr<const DecodedDictBlock> pin;
RETURN_IF_ERROR(dict_block_reader_for_ordinal(group.ordinal,
/*cache=*/nullptr, &pin,
&block_reader));
- return resolve_batch_lookup_entries(terms, state.candidates_, group,
*block_reader,
- results);
+ RETURN_IF_ERROR(resolve_batch_lookup_entries(terms, state.candidates_,
group, *block_reader,
+ results));
+ keep_group_lookups(state, group);
+ return Status::OK();
}
while (!state.done()) {
io::BatchRangeFetcher fetcher(reader_, /*coalesce_gap=*/0);
diff --git a/be/src/storage/index/inverted/snii/reader/logical_index_reader.h
b/be/src/storage/index/inverted/snii/reader/logical_index_reader.h
index bb7a99a6d2a..2e707f9dab8 100644
--- a/be/src/storage/index/inverted/snii/reader/logical_index_reader.h
+++ b/be/src/storage/index/inverted/snii/reader/logical_index_reader.h
@@ -279,8 +279,15 @@ public:
Status null_docids_scan_memory(NullDocidsScanMemory* out) const;
size_t memory_usage() const;
+ // Keeps the results of exact lookups in an on-demand dictionary for the
queries that share
+ // this reader, so a hot term's next lookup skips the read, zstd and CRC
of its DICT block.
+ // The cache is bounded and memory_usage() reserves its whole footprint,
so enable it before
+ // the reader is charged and shared. A resident dictionary does not need
it.
+ void enable_term_lookup_cache();
+
private:
struct NormsCacheState;
+ struct TermLookupCache;
io::FileReader* reader_ = nullptr;
format::IndexTier tier_ = format::IndexTier::kT1;
bool has_positions_ = false;
@@ -320,18 +327,27 @@ private:
const format::DictBlockReader** out)
const;
Status locate_candidate_dict_block(std::string_view term, bool*
maybe_present,
uint32_t* ordinal) const;
+ // Groups the candidate terms by DICT block. With `kept_results`, a term
whose lookup the term
+ // lookup cache kept is resolved into it and joins no group.
Status collect_batch_lookup_groups(const std::vector<std::string>& terms,
std::vector<BatchLookupCandidate>*
candidates,
- std::vector<BatchLookupGroup>* groups)
const;
+ std::vector<BatchLookupGroup>* groups,
+ std::vector<BatchLookupResult>*
kept_results) const;
static Status resolve_batch_lookup_entries(const std::vector<std::string>&
terms,
const
std::vector<BatchLookupCandidate>& candidates,
const BatchLookupGroup& group,
const format::DictBlockReader&
block_reader,
std::vector<BatchLookupResult>*
results);
+ // The term lookup cache: whether `term`'s lookup is kept (copied into
*out), and keeping one.
+ bool find_kept_lookup(std::string_view term, BatchLookupResult* out) const;
+ void keep_lookup(std::string_view term, bool found, const
format::DictEntry& entry,
+ uint64_t frq_base, uint64_t prx_base) const;
+ void keep_group_lookups(const BatchLookupState& state, const
BatchLookupGroup& group) const;
std::vector<ResidentDictBlock> resident_dict_blocks_;
std::shared_ptr<NormsCacheState> norms_cache_;
size_t norms_reserved_charge_ = 0;
+ std::shared_ptr<TermLookupCache> term_lookups_;
};
} // namespace doris::snii::reader
diff --git a/be/src/storage/index/inverted/snii/snii_index_reader.cpp
b/be/src/storage/index/inverted/snii/snii_index_reader.cpp
index 4aa0dd10936..d26e4fe030c 100644
--- a/be/src/storage/index/inverted/snii/snii_index_reader.cpp
+++ b/be/src/storage/index/inverted/snii/snii_index_reader.cpp
@@ -111,6 +111,9 @@ Status SniiIndexReader::_get_logical_reader(
return Status::OK();
}
+ // A reader the searcher cache keeps serves later queries, so it keeps
their hot terms' lookups
+ // too; memory_usage() then covers that cache at its fullest.
+ opened_reader->enable_term_lookup_cache();
const size_t reader_size = std::max<size_t>(opened_reader->memory_usage(),
1);
auto* cache_value = new
SniiLogicalReaderCacheValue(std::move(opened_reader), reader_size,
UnixMillis(),
_index_file_reader);
diff --git a/be/test/storage/index/inverted/snii/snii_query_test.cpp
b/be/test/storage/index/inverted/snii/snii_query_test.cpp
index a69ca4457a6..5510699b79f 100644
--- a/be/test/storage/index/inverted/snii/snii_query_test.cpp
+++ b/be/test/storage/index/inverted/snii/snii_query_test.cpp
@@ -418,6 +418,244 @@ TEST(SniiSegmentReaderTest,
LogicalIndexOpenCachesResidentMetadataAndSmallHeader
EXPECT_EQ(file.read_bytes(), 0);
}
+// Writes `input` and opens it; the caller sets SNII_DICT_RESIDENT_MAX=0 so
its DICT blocks are
+// read on demand.
+void open_on_demand_index(const writer::SniiIndexInput& input, MemoryFile*
file,
+ reader::SniiSegmentReader* segment_reader,
+ reader::LogicalIndexReader* index_reader) {
+ writer::SniiCompoundWriter writer(file);
+ assert_ok(writer.add_logical_index(input));
+ assert_ok(writer.finish());
+ assert_ok(reader::SniiSegmentReader::open(file, segment_reader));
+ assert_ok(segment_reader->open_index(input.index_id, input.index_suffix,
index_reader));
+}
+
+// Looks `term` up and returns how many DICT blocks the lookup decoded.
+uint64_t lookup_decodes(const reader::LogicalIndexReader& index_reader,
std::string_view term,
+ bool* found, format::DictEntry* entry = nullptr) {
+ format::DictEntry scratch;
+ uint64_t frq_base = 0;
+ uint64_t prx_base = 0;
+ ::doris::snii::testing::reset_dict_decode_counter();
+ assert_ok(index_reader.lookup(term, found, entry != nullptr ? entry :
&scratch, &frq_base,
+ &prx_base));
+ return ::doris::snii::testing::dict_decode_counter();
+}
+
+// Serves finished index bytes to concurrent readers; MemoryFile records its
reads unguarded.
+class SharedBytesReader final : public doris::snii::io::FileReader {
+public:
+ explicit SharedBytesReader(const std::vector<uint8_t>& bytes) :
_bytes(bytes) {}
+
+ Status read_at(uint64_t offset, size_t len, std::vector<uint8_t>* out)
override {
+ if (offset > _bytes.size() || len > _bytes.size() - offset) {
+ return Status::Corruption("shared bytes read past eof");
+ }
+ out->assign(_bytes.begin() + static_cast<ptrdiff_t>(offset),
+ _bytes.begin() + static_cast<ptrdiff_t>(offset + len));
+ return Status::OK();
+ }
+
+ uint64_t size() const override { return _bytes.size(); }
+
+private:
+ const std::vector<uint8_t>& _bytes;
+};
+
+// A reader that keeps term lookups answers a repeated lookup, whether it
found the term or not,
+// without reading or decoding the DICT block again, and returns the same
entry.
+TEST(SniiTermLookupCacheTest, RepeatedLookupSkipsTheDictBlock) {
+ ScopedEnv on_demand_dict("SNII_DICT_RESIDENT_MAX", "0");
+ // Without a resident filter an absent term reaches its candidate block.
+ ScopedEnv on_demand_bsbf("SNII_BSBF_RESIDENT_MAX", "0");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader index_reader;
+ open_on_demand_index(make_many_term_input(7, "Body", 4096), &file,
&segment_reader,
+ &index_reader);
+ index_reader.enable_term_lookup_cache();
+
+ for (const std::string term : {"term_1002048", "term_1002048x"}) {
+ const bool present = term == "term_1002048";
+ bool found = !present;
+ format::DictEntry first;
+ file.clear_reads();
+ EXPECT_EQ(lookup_decodes(index_reader, term, &found, &first), 1) <<
term;
+ EXPECT_EQ(found, present) << term;
+ EXPECT_GT(file.read_bytes(), 0) << term;
+
+ bool found_again = !present;
+ format::DictEntry again;
+ file.clear_reads();
+ EXPECT_EQ(lookup_decodes(index_reader, term, &found_again, &again), 0)
<< term;
+ EXPECT_EQ(found_again, present) << term;
+ EXPECT_EQ(file.read_bytes(), 0) << term;
+ if (present) {
+ EXPECT_EQ(again.term, first.term);
+ EXPECT_EQ(again.df, first.df);
+ EXPECT_EQ(again.kind, first.kind);
+ EXPECT_EQ(again.frq_off_delta, first.frq_off_delta);
+ EXPECT_EQ(again.frq_len, first.frq_len);
+ EXPECT_EQ(again.frq_bytes, first.frq_bytes);
+ }
+ }
+}
+
+// A batch resolves its kept terms without reads and reads only the blocks of
the others.
+// NOLINTNEXTLINE(readability-function-cognitive-complexity)
+TEST(SniiTermLookupCacheTest, BatchLookupReadsOnlyTheBlocksOfTermsNotKept) {
+ ScopedEnv on_demand_dict("SNII_DICT_RESIDENT_MAX", "0");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader index_reader;
+ open_on_demand_index(make_many_term_input(7, "Body", 4096), &file,
&segment_reader,
+ &index_reader);
+ index_reader.enable_term_lookup_cache();
+
+ const std::vector<std::string> terms = {"term_1000100", "term_1001200",
"term_1003300"};
+ std::vector<reader::LogicalIndexReader::BatchLookupResult> first;
+ ::doris::snii::testing::reset_dict_decode_counter();
+ assert_ok(index_reader.lookup_batch(terms, &first));
+ EXPECT_EQ(::doris::snii::testing::dict_decode_counter(), terms.size());
+
+ std::vector<reader::LogicalIndexReader::BatchLookupResult> again;
+ ::doris::snii::testing::reset_dict_decode_counter();
+ file.clear_reads();
+ assert_ok(index_reader.lookup_batch(terms, &again));
+ EXPECT_EQ(::doris::snii::testing::dict_decode_counter(), 0);
+ EXPECT_EQ(file.read_bytes(), 0);
+ ASSERT_EQ(again.size(), first.size());
+ for (size_t i = 0; i < terms.size(); ++i) {
+ EXPECT_TRUE(again[i].found) << terms[i];
+ EXPECT_EQ(again[i].entry.term, terms[i]);
+ EXPECT_EQ(again[i].entry.df, first[i].entry.df);
+ EXPECT_EQ(again[i].entry.frq_off_delta, first[i].entry.frq_off_delta);
+ EXPECT_EQ(again[i].frq_base, first[i].frq_base);
+ }
+
+ const std::vector<std::string> mixed = {"term_1000100", "term_1002200",
"term_1003300"};
+ std::vector<reader::LogicalIndexReader::BatchLookupResult> mixed_results;
+ ::doris::snii::testing::reset_dict_decode_counter();
+ assert_ok(index_reader.lookup_batch(mixed, &mixed_results));
+ EXPECT_EQ(::doris::snii::testing::dict_decode_counter(), 1);
+ for (size_t i = 0; i < mixed.size(); ++i) {
+ EXPECT_TRUE(mixed_results[i].found) << mixed[i];
+ EXPECT_EQ(mixed_results[i].entry.term, mixed[i]);
+ }
+}
+
+// The cache keeps at most 32 terms, least recently used out first, keeps no
term longer than
+// 64 bytes, and memory_usage() reserves its whole footprint when it is
enabled.
+TEST(SniiTermLookupCacheTest, KeepsABoundedSetReservedUpFront) {
+ ScopedEnv on_demand_dict("SNII_DICT_RESIDENT_MAX", "0");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader index_reader;
+ writer::SniiIndexInput input = make_many_term_input(7, "Body", 4096);
+ const std::string long_term(65, 'z');
+ input.terms.push_back(make_term(long_term, {{.docid = 4096, .positions =
{0}}}));
+ input.doc_count = 4097;
+ open_on_demand_index(input, &file, &segment_reader, &index_reader);
+
+ const size_t without_cache = index_reader.memory_usage();
+ index_reader.enable_term_lookup_cache();
+ const size_t reserved = index_reader.memory_usage();
+ EXPECT_GT(reserved, without_cache);
+
+ bool found = false;
+ for (uint32_t i = 0; i <= 32; ++i) {
+ EXPECT_EQ(lookup_decodes(index_reader, "term_" +
std::to_string(1000000 + i * 100), &found),
+ 1);
+ EXPECT_TRUE(found);
+ }
+ // The 33rd term pushed out the first; the latest is still kept.
+ EXPECT_EQ(lookup_decodes(index_reader, "term_1003200", &found), 0);
+ EXPECT_EQ(lookup_decodes(index_reader, "term_1000000", &found), 1);
+
+ EXPECT_EQ(lookup_decodes(index_reader, long_term, &found), 1);
+ EXPECT_TRUE(found);
+ EXPECT_EQ(lookup_decodes(index_reader, long_term, &found), 1);
+ EXPECT_EQ(index_reader.memory_usage(), reserved);
+}
+
+// Without the cache every lookup still reads its block; a resident dictionary
never gets one.
+TEST(SniiTermLookupCacheTest, OffByDefaultAndForResidentDictionaries) {
+ bool found = false;
+ {
+ ScopedEnv on_demand_dict("SNII_DICT_RESIDENT_MAX", "0");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader index_reader;
+ open_on_demand_index(make_many_term_input(7, "Body", 4096), &file,
&segment_reader,
+ &index_reader);
+ EXPECT_EQ(lookup_decodes(index_reader, "term_1002048", &found), 1);
+ EXPECT_EQ(lookup_decodes(index_reader, "term_1002048", &found), 1);
+ }
+ ScopedEnv resident_dict("SNII_DICT_RESIDENT_MAX", "1048576");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader index_reader;
+ open_on_demand_index(make_many_term_input(7, "Body", 4096), &file,
&segment_reader,
+ &index_reader);
+ const size_t before = index_reader.memory_usage();
+ index_reader.enable_term_lookup_cache();
+ EXPECT_EQ(index_reader.memory_usage(), before);
+ EXPECT_EQ(lookup_decodes(index_reader, "term_1002048", &found), 0);
+ EXPECT_TRUE(found);
+}
+
+// Queries share one reader: concurrent lookups that keep, find and push out
terms all return the
+// entries a reader without the cache returns.
+TEST(SniiTermLookupCacheTest, ConcurrentLookupsReturnTheUncachedEntries) {
+ ScopedEnv on_demand_dict("SNII_DICT_RESIDENT_MAX", "0");
+ MemoryFile file;
+ reader::SniiSegmentReader segment_reader;
+ reader::LogicalIndexReader plain_reader;
+ open_on_demand_index(make_many_term_input(7, "Body", 4096), &file,
&segment_reader,
+ &plain_reader);
+
+ std::vector<std::string> terms;
+ std::vector<format::DictEntry> expected;
+ for (uint32_t i = 0; i < 64; ++i) {
+ terms.push_back("term_" + std::to_string(1000000 + i * 61));
+ bool found = false;
+ expected.emplace_back();
+ lookup_decodes(plain_reader, terms.back(), &found, &expected.back());
+ ASSERT_TRUE(found);
+ }
+
+ SharedBytesReader shared(file.data());
+ reader::SniiSegmentReader shared_segment;
+ reader::LogicalIndexReader cached_reader;
+ assert_ok(reader::SniiSegmentReader::open(&shared, &shared_segment));
+ assert_ok(shared_segment.open_index(7, "Body", &cached_reader));
+ cached_reader.enable_term_lookup_cache();
+
+ std::atomic<int> mismatches {0};
+ std::vector<std::thread> threads;
+ for (int t = 0; t < 8; ++t) {
+ threads.emplace_back([&, t] {
+ for (int i = 0; i < 2000; ++i) {
+ const size_t k = static_cast<size_t>(t * 7 + i) % terms.size();
+ bool found = false;
+ format::DictEntry entry;
+ uint64_t frq_base = 0;
+ uint64_t prx_base = 0;
+ if (!cached_reader.lookup(terms[k], &found, &entry, &frq_base,
&prx_base).ok() ||
+ !found || entry.term != expected[k].term || entry.df !=
expected[k].df ||
+ entry.frq_off_delta != expected[k].frq_off_delta ||
+ entry.frq_bytes != expected[k].frq_bytes) {
+ ++mismatches;
+ }
+ }
+ });
+ }
+ for (auto& thread : threads) {
+ thread.join();
+ }
+ EXPECT_EQ(mismatches.load(), 0);
+}
+
TEST(SniiPhraseQueryTest, WindowedPhraseQueryKeepsCorrectCandidateOrdinals) {
MemoryFile file;
reader::SniiSegmentReader segment_reader;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]