perf(backends): O(N) streaming scan() primitive for full-corpus iteration (#1797) - #1
Draft
trek-e wants to merge 1 commit into
Draft
perf(backends): O(N) streaming scan() primitive for full-corpus iteration (#1797)#1trek-e wants to merge 1 commit into
trek-e wants to merge 1 commit into
Conversation
…tion (MemPalace#1797) Adds `BaseCollection.scan(*, where, include, limit) -> Iterator[(id, document, metadata)]` — the single linear traversal the rebuild / dedup / migrate / wake-up paths share — and routes the offset-paginated callers through it. Offset pagination was O(N²): ChromaDB's `offset` is scan-and-discard at the SQLite layer, so page k re-skips k·B rows. On a 623K-drawer palace the last page re-skipped ~622K rows. - base.py: `scan()` default paginates `get()` (correct; backends override). - chroma.py: `_sql_scan()` override streams `embeddings LEFT JOIN embedding_metadata ORDER BY e.id` in ONE cursor — each drawer's rows are contiguous, so it yields with O(1) accumulator memory, no offset re-skip and no HNSW load (reads chroma.sqlite3 by path, like extract_via_sqlite). The LEFT JOIN surfaces drawers with no user metadata; `chroma:document` is excluded from the join unless documents are requested. A single `{key: value}` `where` is pushed to SQL; richer filters fall back to the base scan. SQLite errors during streaming propagate — a rebuild fails loud, never silently truncates. - Rewired: repair._extract_drawers, dedup.get_source_groups, migrate._iter_collection_items, palace.prefetch_mined_set/bulk_check_mined, layers.Layer1.generate (MAX_SCAN → limit). EmbeddingCollection forwards scan(). Stacked on MemPalace#1664 (uses its SqliteExactRetriever / base.py seam). dedup_source_group is unchanged (by-id fetch, not a scan). Full suite: 2826 passed, 5 skipped; ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Stacked on MemPalace#1664 — base is the
perf/1657-read-path-o1branch, so this diff shows only thescan()work and requires MemPalace#1664 to merge first. After MemPalace#1664 lands indevelop, this retargets todevelopfor upstream review.Closes MemPalace#1797.
What
Adds
BaseCollection.scan(*, where, include, limit) -> Iterator[(id, document, metadata)]— the single linear traversal the rebuild / dedup / migrate / wake-up paths share — and routes the offset-paginated callers through it.Offset pagination was O(N²): ChromaDB's
offsetis scan-and-discard at the SQLite layer, so page k re-skips k·B rows. On a 623K-drawer palace the last page re-skipped ~622K rows.scan()default paginatesget()(correct; backends override)._sql_scan()streamsembeddings LEFT JOIN embedding_metadata ORDER BY e.idin one cursor — each drawer's rows are contiguous, so it yields with O(1) accumulator memory, no offset re-skip, and no HNSW load (readschroma.sqlite3by path, likeextract_via_sqlite). The LEFT JOIN surfaces drawers with no user metadata;chroma:documentis excluded from the join unless documents are requested. A single{key: value}whereis pushed to SQL; richer filters fall back to the base scan. SQLite errors during streaming propagate — a rebuild fails loud, never silently truncates.repair._extract_drawers,dedup.get_source_groups,migrate._iter_collection_items,palace.prefetch_mined_set/bulk_check_mined,layers.Layer1.generate(MAX_SCAN→limit).EmbeddingCollectionforwardsscan().dedup_source_groupis unchanged (by-id fetch, not a scan).Testing
New
tests/test_scan.py(10 tests): parity (SQL override ==get()), metadata-only excludes documents, documents included,wherefilter,limit, empty collection, complex-wherefallback, drawer-with-no-metadata still yielded, base-default parity, wrapper delegation. Full suite: 2826 passed, 5 skipped; ruff clean.Builds on the
SqliteExactRetriever/base.pyaggregation seam from MemPalace#1664.