This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 7974b035493 branch-4.2: [improvement](be) Keep hot SNII term lookups 
across queries #69879 (#69959)
7974b035493 is described below

commit 7974b0354932f2c460f69acf6891427343466975
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Nov 3 10:09:25 2026 +0800

    branch-4.2: [improvement](be) Keep hot SNII term lookups across queries 
#69879 (#69959)
    
    Cherry-picked from #69879
    
    Co-authored-by: Jack <[email protected]>
---
 .../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]

Reply via email to