Skip to content

feat(index): per-query index cache hit/miss stats - #7862

Merged
Xuanwo merged 1 commit into
lance-format:mainfrom
yanghua:feat/index-cache-per-query-stats
Jul 27, 2026
Merged

feat(index): per-query index cache hit/miss stats#7862
Xuanwo merged 1 commit into
lance-format:mainfrom
yanghua:feat/index-cache-per-query-stats

Conversation

@yanghua

@yanghua yanghua commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What

Adds per-query index cache hit/miss statistics alongside the existing iops, parts_loaded, index_comparisons metrics, so users can tell whether a scan warmed from the in-memory index cache or paid the storage cost.

New fields surfaced in every EXPLAIN ANALYZE output and in the ExecutionSummaryCounts / Python ScanStatistics / Java ScanStats structures:

  • index_cache_hits — page-level lookups served from cache
  • index_cache_misses — page-level lookups that had to load from storage
  • Convenience helper: ExecutionSummaryCounts::index_cache_hit_ratio()

Why

There is a global hit/miss counter on LanceCache, but it mixes all queries in a session. Users diagnosing cache warm-up, tuning cache size, or writing benchmarks need per-query attribution. This piggy-backs on the existing MetricsCollector wire.

How

  • Extend MetricsCollector with record_index_cache_hit/miss (default no-op) and mirror on LocalMetricsCollector + dump_into.
  • Add matching Count fields to IndexMetrics so the two metrics show up automatically in every ExecutionPlan node that already uses it.
  • Report hit/miss at the page-level cache boundaries via a shared AtomicBool sentinel around get_or_insert_with_key:
    • BTreeIndex::lookup_page
    • IVFIndex::load_partition (both write_cache and read-only branches)
    • InvertedPartition::posting_list (grouped + per-token fallback)
  • Aggregate into ExecutionSummaryCounts and propagate through Python ScanStatistics and Java ScanStats (Java constructor grows two extra long parameters; the JNI is updated).

Follow-up

HNSW graph pages, quantizer codebooks, and inverted read_positions are intentionally left for a follow-up (their fetch sites don't have a MetricsCollector reachable today or don't uniformly go through get_or_insert_with_key), so this PR stays reviewable.

Tests

  • lance-index-core: local_metrics_collector_forwards_cache_counts, no_op_metrics_collector_ignores_cache_counts
  • lance-index: new test_page_cache_hit_miss_counts in btree.rs that runs a cold scan then a warm scan and asserts the expected 0/1 and 1/0 hit/miss counts
  • Existing lance-core cache tests and lance-datafusion lib tests pass unchanged

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI enhancement New feature or request and removed A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Index cache hit and miss tracking now flows from cache lookups through index metrics and execution summaries into Python and Java scan statistics. Cache APIs return hit indicators, index implementations record them, and language bindings expose the resulting counters.

Changes

Index cache metrics

Layer / File(s) Summary
Hit-aware cache contract
rust/lance-core/src/cache/mod.rs
Cache lookup helpers return cache-hit indicators while retaining value-only wrappers; cold and warm behavior is tested.
Cache metric collection
rust/lance-index-core/src/metrics.rs, rust/lance-index/src/scalar/*, rust/lance/src/index/vector/ivf/v2.rs, rust/lance/src/io/exec/utils.rs, rust/lance-datafusion/src/utils.rs
Collectors, metric counters, and index lookup paths record cache hits and misses.
Execution metric aggregation
rust/lance-datafusion/src/exec.rs
ExecutionSummaryCounts exposes cache counters and hit ratio, and plan summaries report hit and miss totals.
Language statistics exposure
python/src/scanner.rs, python/python/lance/lance/__init__.pyi, java/src/main/java/org/lance/ipc/ScanStats.java, java/lance-jni/src/blocking_scanner.rs, java/src/test/java/org/lance/ScannerTest.java, python/python/tests/test_scalar_index.py
Python and Java scan statistics store and expose cache counters, with JNI propagation and language-level validation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IndexLookup
  participant LanceCache
  participant MetricsCollector
  participant ExecutionSummaryCounts
  participant ScanStatistics
  participant ScanStats
  IndexLookup->>LanceCache: request cached index page
  LanceCache-->>IndexLookup: value and was_cached flag
  IndexLookup->>MetricsCollector: record hit or miss
  MetricsCollector->>ExecutionSummaryCounts: aggregate cache counters
  ExecutionSummaryCounts->>ScanStatistics: populate Python scan statistics
  ExecutionSummaryCounts->>ScanStats: construct Java scan statistics
Loading

Suggested reviewers: jackye1995

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: per-query index cache hit/miss stats.
Description check ✅ Passed The description clearly matches the changeset and explains the new metrics, wiring, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI labels Jul 21, 2026
@yanghua
yanghua force-pushed the feat/index-cache-per-query-stats branch from dad9e47 to 3f83250 Compare July 21, 2026 09:37
@yanghua
yanghua marked this pull request as ready for review July 21, 2026 13:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
java/src/main/java/org/lance/ipc/ScanStats.java (1)

38-89: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Document the added Java binding API.

Add Javadoc for the primary constructor and getters describing per-page semantics and that the deprecated constructor defaults both counters to 0. As per coding guidelines, “Copy Rust documentation about defaults, constraints, and invariants into Javadoc for binding classes.”

Also applies to: 122-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/main/java/org/lance/ipc/ScanStats.java` around lines 38 - 89, Add
Javadoc to the primary ScanStats constructor and its getters documenting
per-page semantics, defaults, constraints, and invariants from the corresponding
Rust documentation, including the meaning of each index-cache counter. Update
the deprecated ScanStats constructor documentation to state that indexCacheHits
and indexCacheMisses default to 0, and ensure the getter documentation covers
these defaults and semantics.

Source: Coding guidelines

🟡 Other comments (1)
rust/lance-index-core/src/metrics.rs-63-67 (1)

63-67: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Describe misses as cache non-hits, not successful storage loads.

The lookup sites record a miss for every non-hit, including loader errors. Update the wording to say the lookup was not served from cache and loading was attempted.

  • rust/lance-index-core/src/metrics.rs#L63-L67: define a miss as a cache non-hit / loader invocation.
  • rust/lance-datafusion/src/exec.rs#L513-L515: use the same non-hit semantics.
  • python/src/scanner.rs#L71-L72: mirror the Rust wording.
  • python/python/lance/lance/__init__.pyi#L891-L892: mirror the Rust wording.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-index-core/src/metrics.rs` around lines 63 - 67, Update the
documentation for record_index_cache_misses and its corresponding miss
definitions to describe cache non-hits or loader invocations, not successful
storage loads. Apply the same wording at
rust/lance-index-core/src/metrics.rs#L63-L67,
rust/lance-datafusion/src/exec.rs#L513-L515, python/src/scanner.rs#L71-L72, and
python/python/lance/lance/__init__.pyi#L891-L892; clarify that a miss occurs
when the lookup is not served from cache and loading is attempted, including
loader errors.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-core/src/cache/mod.rs`:
- Line 420: Update the cache lookup return paths around the downcast operations
to handle type mismatches without panicking. Replace the unwrap-based downcasts
with fallible error propagation that includes the cache key and expected value
type, applying the same contextual handling to both affected locations while
preserving the existing cached-status return.
- Around line 382-389: Add compiling async documentation examples and
cross-links for all listed public APIs: in rust/lance-core/src/cache/mod.rs
lines 382-389, demonstrate cold and warm (value, was_cached) results and link
CacheKey and LanceCache; in rust/lance-core/src/cache/mod.rs lines 535-538, add
the equivalent WeakLanceCache example including unavailable-cache behavior; in
rust/lance-datafusion/src/utils.rs lines 247-248, document the metric-key
contract and link collector/accessor usage; in
rust/lance-index-core/src/metrics.rs lines 52-72, link trait callbacks to
LocalMetricsCollector and provide usage guidance; in
rust/lance-datafusion/src/exec.rs lines 502-533, show absent metrics and
hit-ratio calculation; and in python/src/scanner.rs lines 69-72, document the
exposed Rust binding fields with a synchronized usage example.

---

Outside diff comments:
In `@java/src/main/java/org/lance/ipc/ScanStats.java`:
- Around line 38-89: Add Javadoc to the primary ScanStats constructor and its
getters documenting per-page semantics, defaults, constraints, and invariants
from the corresponding Rust documentation, including the meaning of each
index-cache counter. Update the deprecated ScanStats constructor documentation
to state that indexCacheHits and indexCacheMisses default to 0, and ensure the
getter documentation covers these defaults and semantics.

---

Other comments:
In `@rust/lance-index-core/src/metrics.rs`:
- Around line 63-67: Update the documentation for record_index_cache_misses and
its corresponding miss definitions to describe cache non-hits or loader
invocations, not successful storage loads. Apply the same wording at
rust/lance-index-core/src/metrics.rs#L63-L67,
rust/lance-datafusion/src/exec.rs#L513-L515, python/src/scanner.rs#L71-L72, and
python/python/lance/lance/__init__.pyi#L891-L892; clarify that a miss occurs
when the lookup is not served from cache and loading is attempted, including
loader errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: cffc8a4a-0bb1-475e-9ec6-5c72e94bf3a1

📥 Commits

Reviewing files that changed from the base of the PR and between aea6ded and 3f83250.

📒 Files selected for processing (12)
  • java/lance-jni/src/blocking_scanner.rs
  • java/src/main/java/org/lance/ipc/ScanStats.java
  • python/python/lance/lance/__init__.pyi
  • python/src/scanner.rs
  • rust/lance-core/src/cache/mod.rs
  • rust/lance-datafusion/src/exec.rs
  • rust/lance-datafusion/src/utils.rs
  • rust/lance-index-core/src/metrics.rs
  • rust/lance-index/src/scalar/btree.rs
  • rust/lance-index/src/scalar/inverted/index.rs
  • rust/lance/src/index/vector/ivf/v2.rs
  • rust/lance/src/io/exec/utils.rs

Comment on lines +382 to +389
/// Same as [`get_or_insert_with_key`](Self::get_or_insert_with_key), but
/// also returns whether the entry was served from cache (`true` = hit,
/// `false` = miss / loader ran).
///
/// Prefer this over rolling a caller-side `Arc<AtomicBool>` when the
/// caller needs to record per-query cache hit/miss counters — the backend
/// already tracks this bit internally and this method just exposes it.
pub async fn get_or_insert_with_key_hit<K, F, Fut>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add examples and cross-links for the new public Rust APIs.

  • rust/lance-core/src/cache/mod.rs#L382-L389: add a compiling async example showing cold and warm (value, was_cached) results and links to CacheKey and LanceCache.
  • rust/lance-core/src/cache/mod.rs#L535-L538: add an equivalent WeakLanceCache example, including its unavailable-cache behavior.
  • rust/lance-datafusion/src/utils.rs#L247-L248: document the metric-key contract and link to its collector/accessor usage.
  • rust/lance-index-core/src/metrics.rs#L52-L72: link the trait callbacks to LocalMetricsCollector and include usage guidance.
  • rust/lance-datafusion/src/exec.rs#L502-L533: add an example covering absent metrics and hit-ratio calculation.
  • python/src/scanner.rs#L69-L72: document the exposed Rust binding fields with a synchronized usage example.
📍 Affects 5 files
  • rust/lance-core/src/cache/mod.rs#L382-L389 (this comment)
  • rust/lance-core/src/cache/mod.rs#L535-L538
  • rust/lance-datafusion/src/utils.rs#L247-L248
  • rust/lance-index-core/src/metrics.rs#L52-L72
  • rust/lance-datafusion/src/exec.rs#L502-L533
  • python/src/scanner.rs#L69-L72
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/mod.rs` around lines 382 - 389, Add compiling async
documentation examples and cross-links for all listed public APIs: in
rust/lance-core/src/cache/mod.rs lines 382-389, demonstrate cold and warm
(value, was_cached) results and link CacheKey and LanceCache; in
rust/lance-core/src/cache/mod.rs lines 535-538, add the equivalent
WeakLanceCache example including unavailable-cache behavior; in
rust/lance-datafusion/src/utils.rs lines 247-248, document the metric-key
contract and link collector/accessor usage; in
rust/lance-index-core/src/metrics.rs lines 52-72, link trait callbacks to
LocalMetricsCollector and provide usage guidance; in
rust/lance-datafusion/src/exec.rs lines 502-533, show absent metrics and
hit-ratio calculation; and in python/src/scanner.rs lines 69-72, document the
exposed Rust binding fields with a synchronized usage example.

Source: Coding guidelines

}

Ok(entry.downcast::<K::ValueType>().unwrap())
Ok((entry.downcast::<K::ValueType>().unwrap(), was_cached))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return a contextual error for cache-entry type mismatches.

A malformed or mismatched backend entry makes these downcast calls panic. Convert the failure into an error containing the cache key and expected type instead.

Also applies to: 563-563

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/mod.rs` at line 420, Update the cache lookup return
paths around the downcast operations to handle type mismatches without
panicking. Replace the unwrap-based downcasts with fallible error propagation
that includes the cache key and expected value type, applying the same
contextual handling to both affected locations while preserving the existing
cached-status return.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
rust/lance-core/src/cache/mod.rs (2)

382-396: 📐 Maintainability & Code Quality | 🟠 Major

Complete the required public Rust API documentation.

These new public APIs still need compiling examples and cross-links that remain synchronized with their actual signatures and semantics.

  • rust/lance-core/src/cache/mod.rs#L382-L396: show cold, warm, and coalesced-load behavior.
  • rust/lance-core/src/cache/mod.rs#L543-L546: show the unavailable-cache fallback.
  • rust/lance-index-core/src/metrics.rs#L52-L73: document callback semantics and link to LocalMetricsCollector.
  • rust/lance-index-core/src/metrics.rs#L133-L141: show getter usage with recording and forwarding.
  • rust/lance-datafusion/src/exec.rs#L502-L535: show absent metrics and hit-ratio behavior.

As per coding guidelines, public Rust APIs must document semantic behavior with synchronized examples and links to relevant structs and methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/mod.rs` around lines 382 - 396, The public API
documentation is incomplete and lacks synchronized, compiling examples. In
rust/lance-core/src/cache/mod.rs:382-396, document get_or_insert_with_key’s
cold, warm, and coalesced-load behavior; in
rust/lance-core/src/cache/mod.rs:543-546, show the unavailable-cache fallback.
In rust/lance-index-core/src/metrics.rs:52-73, document callback semantics and
link to LocalMetricsCollector; in rust/lance-index-core/src/metrics.rs:133-141,
demonstrate getter usage with recording and forwarding; and in
rust/lance-datafusion/src/exec.rs:502-535, document absent metrics and hit-ratio
behavior, keeping examples aligned with each API’s actual signature.

Source: Coding guidelines


428-428: 🩺 Stability & Availability | 🟠 Major

Replace unchecked cache-entry downcasts with contextual errors.

Both new completion paths can panic when the backend returns an entry of the wrong concrete type. Propagate a fallible error containing the cache key and expected type instead.

As per coding guidelines, library code must not use .unwrap() for fallible operations, and errors must include full context.

Also applies to: 572-572

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-core/src/cache/mod.rs` at line 428, Replace the unchecked
downcasts in both new completion paths with fallible error handling. Propagate
an error when the cache entry cannot downcast to K::ValueType, including the
cache key and expected concrete type in the error context. Preserve the existing
successful return of the typed entry and was_cached flag.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/lance-index-core/src/metrics.rs`:
- Around line 116-121: Update LocalMetricsCollector so index_cache_hits and
index_cache_misses do not make the public struct’s construction or destructuring
inaccessible outside the crate. Preserve the existing public three-field layout,
or provide an equivalent compatibility layer that allows downstream callers to
keep using it while retaining access to cumulative cache metrics.

---

Duplicate comments:
In `@rust/lance-core/src/cache/mod.rs`:
- Around line 382-396: The public API documentation is incomplete and lacks
synchronized, compiling examples. In rust/lance-core/src/cache/mod.rs:382-396,
document get_or_insert_with_key’s cold, warm, and coalesced-load behavior; in
rust/lance-core/src/cache/mod.rs:543-546, show the unavailable-cache fallback.
In rust/lance-index-core/src/metrics.rs:52-73, document callback semantics and
link to LocalMetricsCollector; in rust/lance-index-core/src/metrics.rs:133-141,
demonstrate getter usage with recording and forwarding; and in
rust/lance-datafusion/src/exec.rs:502-535, document absent metrics and hit-ratio
behavior, keeping examples aligned with each API’s actual signature.
- Line 428: Replace the unchecked downcasts in both new completion paths with
fallible error handling. Propagate an error when the cache entry cannot downcast
to K::ValueType, including the cache key and expected concrete type in the error
context. Preserve the existing successful return of the typed entry and
was_cached flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 54e4e8d9-edbe-424e-b169-9a865179bd92

📥 Commits

Reviewing files that changed from the base of the PR and between 3f83250 and dbaa5a5.

📒 Files selected for processing (3)
  • rust/lance-core/src/cache/mod.rs
  • rust/lance-datafusion/src/exec.rs
  • rust/lance-index-core/src/metrics.rs

Comment thread rust/lance-index-core/src/metrics.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/lance-index/src/scalar/btree.rs (1)

1644-1647: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not count failed page loads as cache misses.

The _ arm records a miss for every Err, even when the cache lookup or read_page failed and no page was loaded. This inflates index_cache_misses for failed searches; record misses only for Ok((_, false)) and add a regression test for the failed-load path.

Proposed fix
         match &result {
             Ok((_, true)) => metrics.record_index_cache_hit(),
-            _ => metrics.record_index_cache_miss(),
+            Ok((_, false)) => metrics.record_index_cache_miss(),
+            Err(_) => {}
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/lance-index/src/scalar/btree.rs` around lines 1644 - 1647, Update the
result handling near the index cache metrics so only Ok((_, false)) records an
index cache miss, while Ok((_, true)) records a hit and Err does not update miss
metrics. Add a regression test covering a failed cache lookup or read_page path
and verify it does not increment index_cache_misses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rust/lance-index/src/scalar/btree.rs`:
- Around line 1644-1647: Update the result handling near the index cache metrics
so only Ok((_, false)) records an index cache miss, while Ok((_, true)) records
a hit and Err does not update miss metrics. Add a regression test covering a
failed cache lookup or read_page path and verify it does not increment
index_cache_misses.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: d6e7a8d8-1e1b-4de5-9006-4f7202dd430e

📥 Commits

Reviewing files that changed from the base of the PR and between dbaa5a5 and 34812ee.

📒 Files selected for processing (1)
  • rust/lance-index/src/scalar/btree.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
java/src/test/java/org/lance/ScannerTest.java-215-218 (1)

215-218: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the documented zero defaults exactly.

These >= 0 checks would pass even if JNI returned incorrect positive counters. Since ScanStats defaults both fields to 0L when no cache metrics exist, assert the exact values.

Proposed fix
-          assertTrue(stats.getIndexCacheHits() >= 0);
-          assertTrue(stats.getIndexCacheMisses() >= 0);
+          assertEquals(0L, stats.getIndexCacheHits());
+          assertEquals(0L, stats.getIndexCacheMisses());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@java/src/test/java/org/lance/ScannerTest.java` around lines 215 - 218, Update
the assertions for getIndexCacheHits() and getIndexCacheMisses() in ScannerTest
to require the documented default value of exactly 0L, replacing the
non-negative checks while preserving the existing JNI marshalling coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Other comments:
In `@java/src/test/java/org/lance/ScannerTest.java`:
- Around line 215-218: Update the assertions for getIndexCacheHits() and
getIndexCacheMisses() in ScannerTest to require the documented default value of
exactly 0L, replacing the non-negative checks while preserving the existing JNI
marshalling coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: QUIET

Plan: Pro Plus

Run ID: 232bbd4a-fddd-464c-9de2-63eb61a31512

📥 Commits

Reviewing files that changed from the base of the PR and between acb8156 and e94f445.

📒 Files selected for processing (3)
  • java/src/main/java/org/lance/ipc/ScanStats.java
  • java/src/test/java/org/lance/ScannerTest.java
  • python/python/tests/test_scalar_index.py

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two issues that need to be addressed before the new per-query cache statistics are reliable.

  1. The inverted-index callbacks are dropped on the main FTS scan path. PostingListReader::posting_list records the new callbacks, but search_segments supplies FtsIndexMetrics, whose MetricsCollector implementation does not forward record_index_cache_hits / record_index_cache_misses to its inner IndexMetrics; the default implementations are no-ops. As a result, indexed FTS posting-cache activity is reported as 0/0, despite the advertised Inverted coverage. Forwarding these methods and adding a cold/warm FTS scan assertion could keep this path covered.

  2. The public metric contract currently describes resident in-memory hits, while get_or_insert_with_key_hit reports true for coalesced followers of a cold load as well. Those followers are therefore counted as hits even though the same lookup group had to load from storage. The accessors also say 0 means the scan did not touch any index cache, but active cache boundaries such as Bitmap, NGram, RTree, legacy IVF, and phrase positions are not instrumented. Could we first choose whether the metric means “loader skipped” or “resident at lookup arrival”, then either cover the full advertised page-cache scope or narrow the cross-language documentation and 0/0 semantics?

@yanghua

yanghua commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @Xuanwo, both points landed as real gaps.

  1. FTS scan path dropped the new cache stats — fixed.

2a. served from cache vs loader skipped semantic — chose "loader skipped", documented everywhere.

2b. Coverage vs the "0 means no cache touched" claim — instrumented the four biggest gaps.

Broadened the instrumented boundary set from BTree / IVF (v2) / inverted posting-list to also cover:

  • bitmap (BitmapKey)
  • ngram (NGramPostingListKey)
  • rtree (RTreeCacheKey::Page and ::Nulls)
  • inverted per-token metadata (PostingMetadataKey) — threaded via an optional MetricsCollector on posting_metadata_for_token, so the metrics-lessposting_len_for_token caller stays a no-op observer

Phrase positions (PositionKey), legacy v1 IVF partitions, HNSW graph pages, and quantizer codebooks remain uninstrumented and are now explicitly called out in the user-facing docstrings on all three languages, so 0 no longer implies "the scan did not touch any cache". I'd like to tackle those in a follow-up because a few of them don't currently have a MetricsCollector in scope at the fetch site and would require signature changes I'd rather isolate.

Let me know if you'd prefer any of the follow-ups (CacheOutcome three-state, remaining boundaries, wrapper-forwarding automation) folded back into this PR.

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrapper forwarding and documented scope are improved, but two query paths still report counts that diverge from the selected "loader executed / loader skipped" contract.

return Ok(metadata.lengths[token_id as usize] as usize);
}
let (_, length) = self.posting_metadata_for_token(token_id).await?;
let (_, length) = self.posting_metadata_for_token(token_id, None).await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default FTS execution still drops this metadata cache lookup from the per-query counters. MatchQueryExec::execute creates FtsIndexMetrics before build_global_bm25_scorer, but that scorer calls bm25_stats_for_terms -> posting_len_for_token, which reaches this line with None. On a cold v2 query this loads and populates PostingMetadataKey without recording a miss; on the warm query the corresponding hit is also unrecorded. The new FTS test only checks that some posting-list count is non-zero, so it cannot detect this per-term/per-partition undercount. Threading the exec collector through the scorer/stat path, with an exact cold/warm metadata assertion, could keep the documented metadata boundary query-local.


// Record that we're loading a partition from disk
if let Some(metrics) = metrics {
metrics.record_index_cache_miss();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This records a cache miss even when no bitmap page exists and no loader runs. For an equality/IN value absent from index_map, execution reaches this line and then returns the empty result without storage I/O or caching it; repeated queries therefore keep adding false misses under the newly documented “loader ran” semantics. Checking index_map before recording the miss, with an absent-value regression assertion, could prevent a no-I/O scan from reporting a cache miss.

yanghua added a commit to yanghua/lance that referenced this pull request Jul 26, 2026
…ircuit

Address review from Xuanwo on lance-format#7862 and the two same-shape gaps found
while auditing per-query index cache metrics.

- inverted: extend `posting_len_for_token`, `df_for_term`,
  `bm25_scorer_for_final_tokens`, `bm25_stats_for_terms`,
  `bm25_base_scorer`, and `build_global_bm25_scorer` to accept an
  `Option<&dyn MetricsCollector>` so the exec-side `FtsIndexMetrics` is
  threaded into every per-token metadata cache lookup. `MatchQueryExec`,
  `FlatMatchFilterExec`, and `FlatMatchQueryExec` now pass their
  collector to `build_global_bm25_scorer`, and the partition-local
  `bm25_search` fallback threads `Some(metrics.as_ref())` into the
  scorer path. Fixes the "default FTS execution drops metadata cache
  lookups" report on `posting_len_for_token(..., None)` and its multi-
  segment corollary in `bm25_stats_for_terms`.

- bitmap: move the `index_map.contains_key` gate above the
  `record_index_cache_miss` in `load_bitmap`, so a value that is not
  in the index short-circuits before touching the counters. Range
  and IsIn queries now thread `Some(metrics)` into `load_bitmap` so
  bitmap page reads on those code paths finally show up in the
  per-query cache stats.

- inverted phrase positions: upgrade `read_positions` to
  `get_or_insert_with_key_hit` and record hit/miss under the caller's
  metrics context. The phrase-query fall-through inside `posting_list`
  now reports the `PositionKey` cache boundary in per-query stats
  instead of being silently uninstrumented.

- docs: refresh `ExecutionSummaryCounts::index_cache_hits`, the
  `ScanStatistics` Python stub, and `ScanStats` Javadoc to list the
  updated instrumented-boundary set (bitmap Range/IsIn, phrase
  positions), call out that IVF streaming and legacy v1 IVF paths
  report as a miss on every call, and note that a cold grouped
  posting-list lookup can record up to two misses for a single term.

Tests:
- Python: tighten `test_btree_index_cache_hit_miss_stats` to exact
  cold 1/0 and warm 0/1 counts; add `test_bitmap_index_cache_hit_miss_stats`
  covering IN cold/warm plus a `color = 'purple'` absent-value regression
  that asserts hits and misses stay at zero; add
  `test_phrase_query_cache_hit_miss_stats` that exercises the new
  `PositionKey` boundary through cold then warm phrase scans.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
yanghua added a commit to yanghua/lance that referenced this pull request Jul 26, 2026
…ircuit

Address review from Xuanwo on lance-format#7862 and the two same-shape gaps found
while auditing per-query index cache metrics.

- inverted: extend `posting_len_for_token`, `df_for_term`,
  `bm25_scorer_for_final_tokens`, `bm25_stats_for_terms`,
  `bm25_base_scorer`, and `build_global_bm25_scorer` to accept an
  `Option<&dyn MetricsCollector>` so the exec-side `FtsIndexMetrics` is
  threaded into every per-token metadata cache lookup. `MatchQueryExec`,
  `FlatMatchFilterExec`, and `FlatMatchQueryExec` now pass their
  collector to `build_global_bm25_scorer`, and the partition-local
  `bm25_search` fallback threads `Some(metrics.as_ref())` into the
  scorer path. Fixes the "default FTS execution drops metadata cache
  lookups" report on `posting_len_for_token(..., None)` and its multi-
  segment corollary in `bm25_stats_for_terms`.

- bitmap: move the `index_map.contains_key` gate above the
  `record_index_cache_miss` in `load_bitmap`, so a value that is not
  in the index short-circuits before touching the counters. Range
  and IsIn queries now thread `Some(metrics)` into `load_bitmap` so
  bitmap page reads on those code paths finally show up in the
  per-query cache stats.

- inverted phrase positions: upgrade `read_positions` to
  `get_or_insert_with_key_hit` and record hit/miss under the caller's
  metrics context. The phrase-query fall-through inside `posting_list`
  now reports the `PositionKey` cache boundary in per-query stats
  instead of being silently uninstrumented.

- docs: refresh `ExecutionSummaryCounts::index_cache_hits`, the
  `ScanStatistics` Python stub, and `ScanStats` Javadoc to list the
  updated instrumented-boundary set (bitmap Range/IsIn, phrase
  positions), call out that IVF streaming and legacy v1 IVF paths
  report as a miss on every call, and note that a cold grouped
  posting-list lookup can record up to two misses for a single term.

Tests:
- Python: tighten `test_btree_index_cache_hit_miss_stats` to exact
  cold 1/0 and warm 0/1 counts; add `test_bitmap_index_cache_hit_miss_stats`
  covering IN cold/warm plus a `color = 'purple'` absent-value regression
  that asserts hits and misses stay at zero; add
  `test_phrase_query_cache_hit_miss_stats` that exercises the new
  `PositionKey` boundary through cold then warm phrase scans.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
@yanghua
yanghua force-pushed the feat/index-cache-per-query-stats branch 4 times, most recently from 1c8478b to cedb403 Compare July 26, 2026 13:28
Surface per-page index cache hit and miss counts alongside the existing
`iops` / `parts_loaded` / `index_comparisons` metrics, so users can tell
whether a scan warmed from the in-memory index cache or paid the storage
cost.

Wiring:

- Extend `MetricsCollector` with `record_index_cache_hit/miss` (default
  no-op) and mirror on `LocalMetricsCollector`, `IndexMetrics`, and
  `FtsIndexMetrics` (wrapper forwarding).
- Add matching `Count` fields to `IndexMetrics` so the two metrics show
  up automatically in every `ExecutionPlan` node that already uses it.
- Expose a hit-aware cache API `LanceCache::get_or_insert_with_key_hit`
  returning `(Arc<V>, was_cached)` and its `WeakLanceCache` counterpart,
  so per-query callers no longer have to wrap loaders in an extra
  `Arc<AtomicBool>`.
- Aggregate into `ExecutionSummaryCounts` with an
  `index_cache_hit_ratio()` helper, propagate through Python
  `ScanStatistics` (pyi stubs updated) and Java `ScanStats` (10-arg
  constructor, 8-arg overload kept as `@Deprecated`).

Instrumented boundaries in this release: BTree page, IVF partition (v2,
write-cache scan path), inverted posting list (grouped and per-token)
and its per-token metadata (`PostingMetadataKey`), inverted phrase
positions (`PositionKey`), bitmap (Equals / Range / IsIn), ngram, and
rtree page / null slot.

Correctness fixes discovered during instrumentation:

- Thread `Option<&dyn MetricsCollector>` through the whole BM25 stat
  path (`posting_len_for_token`, `df_for_term`,
  `bm25_scorer_for_final_tokens`, `bm25_stats_for_terms`,
  `bm25_base_scorer`, `build_global_bm25_scorer`) and forward it from
  `MatchQueryExec`, `FlatMatchFilterExec`, `FlatMatchQueryExec`, and
  the partition-local `bm25_search` fallback so per-token metadata
  cache lookups show up in per-query stats on every FTS path.
- Bitmap: gate `load_bitmap` on `index_map.contains_key` before
  recording a miss, so a value not in the index short-circuits without
  polluting the counters, and thread `Some(metrics)` through the
  Range/IsIn code paths so their bitmap page reads finally count.
- Inverted phrase positions: upgrade `read_positions` to
  `get_or_insert_with_key_hit` and record hit/miss under the caller's
  metrics context.
- All cache-boundary sites record hit/miss even when the loader errors,
  matching the "loader ran" semantics documented on the accessors.

Docs on `ExecutionSummaryCounts::index_cache_hits`, the
`ScanStatistics` Python stub, and `ScanStats` Javadoc call out the
instrumented boundary set, note that IVF streaming and legacy v1 IVF
paths report as a miss on every call, and warn that a cold grouped
posting-list lookup can record up to two misses for a single term.
HNSW graph pages and quantizer codebooks are still uninstrumented and
are noted as follow-up work.

Tests:

- `lance-index-core`: `local_metrics_collector_forwards_cache_counts`,
  `no_op_metrics_collector_ignores_cache_counts`.
- `lance-index`: `test_page_cache_hit_miss_counts` in `btree.rs` runs
  a cold scan then a warm scan and asserts the expected 0/1 and 1/0
  hit/miss counts; a targeted `get_or_insert_with_key_hit` test pins
  the `(value, was_cached)` contract.
- Python: `test_btree_index_cache_hit_miss_stats` uses exact 1/0 and
  0/1 counts; `test_bitmap_index_cache_hit_miss_stats` covers
  cold/warm IN queries plus a `color = 'purple'` absent-value
  regression that asserts hits/misses stay at zero;
  `test_phrase_query_cache_hit_miss_stats` exercises the
  `PositionKey` boundary through cold then warm phrase scans;
  `test_fts_index_cache_hit_miss_stats` proves the FTS wrapper
  forwarding.
- Java: `ScannerTest.getStats()` reads
  `ScanStats.getIndexCacheHits()` / `getIndexCacheMisses()` to prove
  the 10-arg constructor wired up by the JNI patch does not throw
  and defaults to zero.
@yanghua
yanghua force-pushed the feat/index-cache-per-query-stats branch from cedb403 to 893dc73 Compare July 26, 2026 13:47
@yanghua

yanghua commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

@Xuanwo, I have addressed your concerns. Please take a look.

@Xuanwo Xuanwo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

@Xuanwo
Xuanwo merged commit fd9f98d into lance-format:main Jul 27, 2026
47 of 48 checks passed
sbrunk added a commit to sbrunk/lance that referenced this pull request Jul 29, 2026
…build

lance-format#7862 threads a `MetricsCollector` through `bm25_stats_for_terms`,
`df_for_term`, and `posting_len_for_token` so a query's index cache hits and
misses are attributed to it. The single-column path passes one; the cross-field
path had nowhere to put it and discarded the metrics in three places: the V3
arm of `bm25_row_stats_for_terms` delegated with `None`, and
`row_stats_for_terms` passed `None` to `posting_len_for_token` and a no-op
collector to `posting_list`.

So `EXPLAIN ANALYZE` on a cold `combined_fields` query undercounted
`index_cache_misses` by terms x columns x partitions, and
`index_cache_hit_ratio` was not comparable with the same query expressed as
`match`. Accuracy only: collector methods default to no-ops, so nothing
depended on the threading.

`build_combined_bm25_scorer`, `flat_combined_fields_search_stream`,
`bm25_row_stats_for_terms`, and `row_stats_for_terms` now take the collector
in the position upstream uses, and both the indexed and flat execs pass their
own. None of these signatures exist on main, so no released API changes.

Also correct `DocSet::row_ids_strictly_ascending`'s doc comment, which claimed
frag-reuse partitions carrying tombstoned rows "are correctly rejected here".
The function only checks ascension, and `TOMBSTONE_ROW` is the maximum
address, so a set whose last document is tombstoned still reports `true`. It
is harmless for a reason worth recording: the query-side legacy loader drops
deleted rows rather than tombstoning them, tombstones only appear in
build-side sets, and `combined_fields_search` forces the fallback for legacy
partitions regardless.

Finally remove two guards for the deferred-row_id `DocSet` that lance-format#7863 deleted
with `lazy_docset.rs`. The `has_row_ids()` conjunct could only be false for a
zero-document set, since every non-test constructor fills `row_ids`, and the
`row_ids.is_empty()` branch in `num_distinct_rows` became unreachable once
that conjunct went. Zero-document sets now answer `true` and `0`, agreeing
with the modern representation, which is what current writers emit.
sbrunk added a commit to sbrunk/lance that referenced this pull request Jul 30, 2026
…build

lance-format#7862 threads a `MetricsCollector` through `bm25_stats_for_terms`,
`df_for_term`, and `posting_len_for_token` so a query's index cache hits and
misses are attributed to it. The single-column path passes one; the cross-field
path had nowhere to put it and discarded the metrics in three places: the V3
arm of `bm25_row_stats_for_terms` delegated with `None`, and
`row_stats_for_terms` passed `None` to `posting_len_for_token` and a no-op
collector to `posting_list`.

So `EXPLAIN ANALYZE` on a cold `combined_fields` query undercounted
`index_cache_misses` by terms x columns x partitions, and
`index_cache_hit_ratio` was not comparable with the same query expressed as
`match`. Accuracy only: collector methods default to no-ops, so nothing
depended on the threading.

`build_combined_bm25_scorer`, `flat_combined_fields_search_stream`,
`bm25_row_stats_for_terms`, and `row_stats_for_terms` now take the collector
in the position upstream uses, and both the indexed and flat execs pass their
own. None of these signatures exist on main, so no released API changes.

Also correct `DocSet::row_ids_strictly_ascending`'s doc comment, which claimed
frag-reuse partitions carrying tombstoned rows "are correctly rejected here".
The function only checks ascension, and `TOMBSTONE_ROW` is the maximum
address, so a set whose last document is tombstoned still reports `true`. It
is harmless for a reason worth recording: the query-side legacy loader drops
deleted rows rather than tombstoning them, tombstones only appear in
build-side sets, and `combined_fields_search` forces the fallback for legacy
partitions regardless.

Finally remove two guards for the deferred-row_id `DocSet` that lance-format#7863 deleted
with `lazy_docset.rs`. The `has_row_ids()` conjunct could only be false for a
zero-document set, since every non-test constructor fills `row_ids`, and the
`row_ids.is_empty()` branch in `num_distinct_rows` became unreachable once
that conjunct went. Zero-document sets now answer `true` and `0`, agreeing
with the modern representation, which is what current writers emit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-python Python bindings enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants