perf(query): memoize the accepted catalog and compiled queries, skip idle full-text validation - #674
Merged
azimafroozeh merged 3 commits intoSep 6, 2026
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…idle full-text validation
azimafroozeh
force-pushed
the
catalog-memo-compiled-query-cache
branch
from
September 6, 2026 17:43
adb4b3a to
f0c6ac2
Compare
…idle full-text validation
Collaborator
Author
|
The |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Every query rebuilds three things from text that has not changed: the catalog from the schema contract, the plan from the
.gqtext, and a full-text coverage check for scans that read no full-text index. Each is a pure function of inputs the query already reads, so computing it once per input changes no answer. The cost scales with the schema contract and the.gqtext, not the corpus. Kept out of the #665 follow-up so that PR stays about the retry ladder.ReadCaches::accepted_catalog). The catalog is memoized on the exact bytes of_schema.pg,_schema.ir.jsonand__schema_state.json. The bytes are read on every query, so a SchemaApply by any handle misses on the next query, and a drifted byte fails the full validation. Open and init seed it. One entry per handle. Probecatalog_builds.ReadCaches::compiled_queries). The IR of a named query is cached per handle under SHA-256(source) + name; a hit also requires the same catalogArc(the entry holds aWeak, upgraded andptr_eq-checked), so a rebuilt catalog recompiles everything and a dropped one pins nothing. 256-entry LRU of IRs, no source copies, errors never cached. Probequery_compiles.FtsFilterDemand). Each typed filter is inspected forcontains_tokensas it is set on theScanTuning. A scan with no full-text query, no SQL-string filter and nocontains_tokensdemand runs neitherget_expr_filter(which rebuilds Lance's filterable schema for a set filter) nor the certificate check. SQL-string filters still go throughget_expr_filter; acontains_tokenswhose first argument is not a plain column fails closed to every column; staged scan and staged count check only when a filter is set, likecount_rows. Probefts_validations.Measured effect
From a scratch bench outside this PR (22,000 docs, IVF + full-text index, release build,
file://, one machine), so not rule-13 evidence. Each number is one WARM run of one query: the query is repeated, the first runs are discarded, and the median of the next 30 to 45 is shown, same rows on both trees. On this branch a warm run is a cache hit. The cold cost is paid once per query, not once per run: the catalog is memoized at open, and the first run of each query compiles it and caches the plan. Measured first runs on this branch in the 101-type setting: key lookup 1.6 ms, plainnearest1.7, filterednearest2.1,bm252.9,rrf4.9, each about 0.6 to 0.9 ms above its warm run. Onmainevery run is a cold run. The tables differ in how much text the engine handles before the query runs: the schema, and the.gqtext sent with the request (our CLI sends its whole stored-queries file each time).One node type, request text with these 5 queries:
limit 10)mainnearest31 node types, request text with 55 stored queries (about our deployment):
limit 10)mainnearestnearestwith a pushed filtersearch+bm25rrf(nearest, bm25)101 node types, request text with 205 stored queries:
limit 10)mainnearestnearestwith a pushed filtersearch+bm25rrf(nearest, bm25)Small schema, short request: nothing to save. As both grow,
mainpays about 2 ms then 4 ms per run before the query starts; this branch stays near 0.9 ms on the light queries. The heavier shapes add scan time, equal on both trees.Backing issue / RFC
Checklist
warm_read_cost::warm_query_memoizes_catalog_and_compiled_query_until_schema_apply(warm queries after open build nothing and compile once; a second query with a typed filter compiles once more and records zero full-text validations; the same name in a second source is a second compile answering with its own rows; a query naming a type only the new schema declares is refused with the typecheck's unknown-type error, then answered through the applying handle and a second handle, which rebuilds once and recompiles);runtime_cache::compiled_query_cache_hit_requires_the_live_compiled_under_catalog(equal catalog under anotherArcmisses; a dropped catalog pins nothing and never hits);table_store::fts_filter_demand_names_contains_tokens_columns_and_fails_closed(plain column, CAST-wrapped, literal in column position, no arguments, nested under NOT/AND, no call);search::plain_nearest_skips_the_full_text_validation(unfiltered control); the existing uncertified-index tests keep the full-text-query and SQL-string routes red.docs/releases/v0.11.0.mdHighlights;docs/dev/execution.md§Filters and pushdown says when a scan inspects its filter and when it checks coverage.Local verification
cargo fmt --all --checkclean;cargo clippy --workspace --all-targets -- -D warningssilentcargo test --workspace --no-fail-fast: 102 targets, 3056 passed, 24 ignored, 1 failed =blob::tests::external_blob_file_policy_rejects_special_files(binds a Unix socket, denied by the local sandbox;blob.rsuntouched)cargo test -p omnigraph-engine --test search --test warm_read_cost --test lance_surface_guards --test rrf_prefilter_gate --test schema_apply: 42 / 17 / 34 / 12 / 28;--lib runtime_cache7;--lib table_store62;omnigraph-gqt --test gq_logic_tests16python3 scripts/check-docs.py129 files OK;typos docscleanupstream/mainatd520d2bbNotes for reviewers
build_accepted_catalog_with_schema_gate_heldperforms the threeread_textand twoexistscalls on every query, and the memo key is those bytes. A hit skipscompile_schema_source, the two JSON parses, the cross-checks andbuild_catalog_from_ir.warm_query_validates_schema_contract_oncepins the per-query reads.read_schema_contract_text+validate_schema_contract_text, backs open, refresh,accepted_schema, the write path and the query path, so a validation step cannot drift between copies; every message comes from oneinvalid_contract_fileand two constants. Precedence: a missing or incomplete contract reports an unparseable source first; a storage error from the existence probes or a failing IR/state read is reported before a parse error.Arc<QueryIR>and aWeak<Catalog>. An entry from a replaced catalog stays until evicted but pins nothing and cannot hit. A digest collision between live sources is not defended against.build_catalog_from_irandfixup_physical_schemasreading only the IR (reviewed at source);Cataloghas no equality, so no differential test can state it.invalidate_read_cachesleaves both caches alone: content-keyed, nothing to clear on a branch refresh.filter_exprcall while Lance keeps only the last filter: a scan may validate more columns than it reads, never fewer.contains_tokenstoday; the typed arm serves SDK-shaped callers and is pinned by thetable_storeunit test. The warm test's zero validations on a typed-filter read pin the shape that pays.ann-shapesbench scenario.