perf(read-path): O(1) stats, SQLite retrieval adapter, bounded source-file fetch (#1657) - #1664
perf(read-path): O(1) stats, SQLite retrieval adapter, bounded source-file fetch (#1657)#1664trek-e wants to merge 3 commits into
Conversation
…-file fetch (MemPalace#1657) Read-path operations were O(N) in palace size. Three cohesive deepenings: 1. Aggregation seam. Add count_by / crosstab / count_matching to BaseCollection (full-scan defaults) with SQL GROUP BY overrides in ChromaCollection over the same embeddings⋈segments⋈collections join repair.sqlite_drawer_count trusts, so grouped counts sum to count(). New palace_stats module turns these into the wing/room/taxonomy shapes; tool_status / list_wings / list_rooms / get_taxonomy and miner.status now aggregate in one query instead of paginating the whole collection into Python dicts. Deletes the _fetch_all_metadata / _metadata_cache full-scan path. Minutes → ms on large palaces. 2. Verbatim-over-SQLite retrieval adapter. Extract the 237-line BM25/SQLite fallback into verbatim_sqlite.SqliteExactRetriever, which owns all chroma.sqlite3 schema knowledge behind a small interface; searcher keeps a thin shim. 3. Bounded source-file access. New source_file_access module: count_drawers via COUNT(*) and a capped fetch_drawers, replacing two unbounded get(where=source_file) calls on the per-query path. Adversarial-review fixes: - Closet-boost hydration (now _enrich_closet_hits) counts first and skips the grep-overwrite for files larger than the fetch cap, keeping the matched text and reporting the accurate COUNT(*) total — a capped unordered subset could otherwise omit the matched chunk. - SQL aggregates COALESCE the typed metadata columns so int/float/bool keys keep parity with the Python scan instead of collapsing into the missing-key bucket. Correctness verified by parity tests asserting SQL aggregates equal the prior Python scan (string, int, crosstab, missing-key) on fixture palaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request optimizes the read-path aggregation by shifting metadata tallying and taxonomy counting from slow Python-side pagination to efficient SQL GROUP BY queries directly on the SQLite database. It introduces dedicated modules for statistics generation (palace_stats), bounded source-file access (source_file_access), and verbatim BM25 retrieval (verbatim_sqlite), which allows removing the old in-memory metadata cache. The review feedback identifies a potential cross-platform issue where constructing SQLite file URIs using raw f-strings can fail on Windows or with paths containing spaces, and suggests using pathlib.Path.as_uri() for robust URI formatting.
…lace#1657 review) Gemini PR review: constructing the file: URI with an f-string (`f"file:{db_path}?mode=ro"`) can fail on Windows (drive letters, backslashes) and on paths containing spaces or `?`/`#`. Build the URI via `Path(db_path).resolve().as_uri() + "?mode=ro"` in `_open_ro` and the `SqliteExactRetriever`, and also catch ValueError. resolve() first because as_uri() requires an absolute path (the file is known to exist at both sites). Adds a guard test opening a palace whose path contains spaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1664) Brings the stalled read-path performance PR (MemPalace#1664, closes MemPalace#1657) up to date with 113 commits of develop and resolves the conflicts so BOTH the PR's deepenings and upstream's hardening survive. Conflicts resolved (backends/chroma.py, mcp_server.py, tests/test_mcp_server.py): - Kept the PR's SQL-aggregation seam (count_by/crosstab/count_matching on BaseCollection + ChromaCollection overrides), palace_stats, source_file_access, and verbatim_sqlite.SqliteExactRetriever. - Kept upstream's corruption-resilience hardening and the new non-chroma backend paths (sqlite_exact / qdrant / pgvector). Reconciliation of the two independent status rewrites: - develop and MemPalace#1664 each added a SQL-aggregate status path. They are COMPLEMENTARY, not redundant: _sqlite_wing_room_counts() reads chroma.sqlite3 by path and never opens the collection, so `status` avoids cold-loading the HNSW index (~60s on large palaces, MemPalace#1681); palace_stats.taxonomy(col) is the general seam for callers already holding a collection. - miner.status uses _sqlite_wing_room_counts() as the primary fast path and palace_stats.taxonomy(col) as the fallback. Restored _sqlite_wing_room_counts (and its defaultdict import) into chroma.py — an earlier resolution had deleted it as "superseded," which broke miner.py:1928 and 11 status tests. Full suite: 2816 passed, 5 skipped. ruff check + format --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Revived this PR — rebased, conflict-free, and green. Ready for another look. 🙏 This had gone stale (113 commits behind
✅ All 8 CI checks green (build, build-gpu, test-linux 3.9/3.11/3.13, test-macos, test-windows, lint). Full local suite: 2816 passed, 5 skipped; ruff clean. The earlier A follow-up is planned as a separate PR (deliberately not added here, to keep this one focused and landable): an O(N) streaming |
|
@trek-e — heads-up: we've filed #2038 for pgvector's Different reason for filing separately here vs. that one: our PR is scope-disjoint from yours rather than a take-over. You're adding the SQLite/chroma fast path plus the There is an underlying naming question that will presumably want to settle at some point: |
|
Thanks for this contribution, and apologies for the slow turnaround.
If you'd rather not pick it back up, no problem at all — just say so and I'll close it out, and thanks either way for taking the time to send it. |
|
Thank you @trek-e — both for this PR and for #1657. The diagnosis (overview reads were O(N) because SQL aggregation only ran when the index was broken) was right, and it is part of why the later read-path work happened at all. We are going to close this as superseded, not rejected. The stats path landed on
What is still left from #1657 (item 1 only): The per-query If you want that follow-up, please open a new PR against current #2296 ( Thank you again. |
Closes #1657.
Read-path operations were O(N) in palace size and slow on large palaces (
mempalace statustook minutes on ~400K drawers). Three cohesive read-path deepenings, plus the two fixes from an adversarial review.1. Metadata aggregation seam
BaseCollection:count_by,crosstab,count_matching(full-scan defaults so every backend works), with SQLGROUP BY/COUNT(*)overrides inChromaCollection.embeddings ⋈ segments ⋈ collectionsjoin thatrepair.sqlite_drawer_countalready trusts as the ground-truth drawer count, so grouped counts sum back tocollection.count().LEFT JOINonembedding_metadataplus a typed-columnCOALESCEkeeps byte-for-byte parity with the prior Python scan (including missing keys and int/float/bool-valued keys).palace_statsmodule turns those primitives into the wing/room/taxonomy shapes.tool_status/tool_list_wings/tool_list_rooms/tool_get_taxonomyandminer.statusnow aggregate in one query._fetch_all_metadata,_get_cached_metadata, the_metadata_cacheglobals and their write-tool invalidations). Minutes → ms.2. Verbatim-over-SQLite retrieval adapter
verbatim_sqlite.SqliteExactRetriever, which owns allchroma.sqlite3schema knowledge behind a smallsearch(...)interface and is testable in isolation.searcherkeeps a thin delegating shim, so existing call sites/tests are unchanged.3. Bounded source-file access
source_file_accessmodule:count_drawersviaCOUNT(*)(no documents materialized) and a cappedfetch_drawers, replacing two unboundedget(where=source_file)calls on the per-query path.Adversarial-review fixes
_enrich_closet_hits) counts first and skips the grep-overwrite for files larger than the fetch cap — a capped, unordered subset could otherwise omit the matched chunk and overwrite the hit with an unrelated early one. Oversized hits keep their own correct text and report the accurateCOUNT(*)total.COALESCEthe typed metadata columns so int/float/bool keys keep parity with the Python scan instead of collapsing into the missing-key bucket.Testing
tests/test_palace_stats.py: parity oracle (SQL == Python scan) for string, int, crosstab, and missing-key metadata; bounded source-file access;palace_path=Nonefallback.tests/test_closets.py; directSqliteExactRetrieverinterface test intests/test_hnsw_capacity.py.ruff check+ruff format --checkclean.No new dependencies. Default behavior unchanged; only performance and internal structure improve.
🤖 Generated with Claude Code