v0.36.3.0 feat: dynamic embedding column selection for search#1164
Open
garrytan wants to merge 10 commits into
Open
v0.36.3.0 feat: dynamic embedding column selection for search#1164garrytan wants to merge 10 commits into
garrytan wants to merge 10 commits into
Conversation
Schema migration ALTERs eval_candidates to add a nullable embedding_column TEXT column. Per-row capture metadata so `gbrain eval replay` reproduces the same column the capture ran against (D16 / CDX-10). NULL-tolerant: pre-v0.36 rows fall back to current default. Renumbered v67→v68 because master claimed v67 for facts_typed_claim_columns during this branch's lifetime. PGLite parity via sqlFor.pglite — same ALTER IF NOT EXISTS.
…nes) The read-path foundation for routing search through any populated embedding column, not just OpenAI 1536. src/core/search/embedding-column.ts (new) is the canonical seam. Single source of truth for column → provider/dim/type lookup. Validates registry keys via regex (/^[a-z_][a-z0-9_]*$/), uses Object.create(null) + Object.hasOwn so 'constructor' and other inherited names can't masquerade as registered columns. Identifier-quoting on SQL interpolation as defense in depth. src/core/types.ts widens SearchOpts.embeddingColumn to accept ResolvedColumn descriptors at the engine boundary; adds EmbeddingColumnConfig + ResolvedColumn exports. src/core/config.ts merges embedding_columns + search_embedding_column from the DB plane via loadConfigWithEngine, mirroring the existing embedding_multimodal_model pattern. Handles the no-file case so env-only Postgres installs see DB-plane overrides (codex /ship #3). src/core/ai/gateway.ts: embedQuery(text, opts) + embed(texts, opts) accept embeddingModel + dimensions overrides. isAvailable(touchpoint, modelOverride?) so hybrid asks 'is the active column's provider reachable?' not 'is the global default reachable?' (CDX-4 / D10). Engines: searchVector accepts ResolvedColumn descriptors via normalizeEngineColumn; engine code is config-free and unit-testable. getEmbeddingsByChunkIds(ids, column?) so cosineReScore hydrates from the active column instead of always 'embedding' (CDX-3 / D9). Identifier-quoting belt at the SQL boundary. src/core/eval-capture.ts threads embedding_column from hybridSearch meta into the persisted capture row.
Wires the resolver into hybridSearch, the query op, doctor, and the config command. src/core/search/hybrid.ts: resolves the column once at the boundary, threads the descriptor into engine calls, routes embedQuery through the resolved column's provider/dims, and calls isCacheSafe (not isDefaultColumn) for cache skip so user overrides of the 'embedding' builtin can't leak across vector spaces (CDX-4). cosineReScore now hydrates from the active column. src/core/search/mode.ts: KNOBS_HASH_VERSION 2→3, append-only new fields col= and prov= alongside floor_ratio. Cache rows from different columns or providers now sit in different keyspaces — cross-column contamination impossible. src/core/operations.ts: query op accepts embedding_column param for per-call A/B benchmarking. search op (keyword-only) deliberately does NOT (CDX-9 / D15) — would be silent UX. src/commands/doctor.ts: new embedding_column_registry check. Batch format_type probe (D13) catches dim drift that information_schema.columns.udt_name can't. Batch pg_indexes probe (D5) warns on missing HNSW. Coverage % on active column, gates at <90% (D14), short-circuits on empty brains (codex /ship #5). src/commands/config.ts: validates embedding_columns JSON shape at set time, runs the coverage gate when setting search_embedding_column, uses Object.hasOwn for the registry lookup. src/commands/eval-replay.ts: replay re-runs queries against the captured embedding_column so post-flip-config replays don't surface as false-positive regressions.
50 unit cases for the resolver (resolution chain, registry
merge, validation, prototype pollution, descriptor
passthrough, isCacheSafe, normalizeEngineColumn).
8 gateway override cases — embeddingModel + dimensions
flow into providerOptions, isAvailable(touchpoint, override)
routes to the right recipe, unknown models throw clean.
4 cosineReScore + 6 ops + 5 knobs-hash + 7 mode + 9 PGLite
E2E + 7 Postgres E2E + 5 eval-replay column metadata.
Postgres E2E (gated on DATABASE_URL) covers halfvec(2560)
end-to-end on real pgvector, EXPLAIN-visible HNSW index
on the alternate column, format_type-based dim drift catch,
and the <90% coverage gate.
Pins every codex /ship fix: prototype-pollution rejection
('constructor' as column name), descriptor passthrough
validation (rejects SQL-shaped strings in dimensions),
isCacheSafe semantics (space-based, not name-based).
Total: 141 new + extended cases, all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add CLAUDE.md key-files entry for src/core/search/embedding-column.ts. Annotate hybrid.ts, gateway.ts, doctor.ts, and migrate.ts entries with v0.36.3.0 wave changes (ResolvedColumn threading, embedQuery model override, embedding_column_registry check, migration v68). Document knobs_hash v=2 → v=3 bump under the Search Mode section. Regenerate llms-full.txt from the updated CLAUDE.md so the auto-checked bundle matches source (build-llms.test.ts CI guard). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # VERSION # package.json
# Conflicts: # CHANGELOG.md # VERSION # package.json
1. test/loadConfig-merge.test.ts: update the 'returns null when base config is null' contract test. Pre-v0.36 the function returned null for null base; the codex /ship #3 fix changed that to synthesize a minimal `{ engine: 'postgres' }` so env-only installs see DB-plane overrides. Test now pins the new contract + adds a round-trip case asserting the merge actually surfaces `embedding_columns` / `search_embedding_column` set via gbrain config set on a null base. 2. test/schema-bootstrap-coverage.test.ts was failing because eval_candidates.embedding_column (added by migration v68) wasn't covered by applyForwardReferenceBootstrap. Fix: add the column to PGLITE_SCHEMA_SQL's eval_candidates CREATE TABLE definition (and src/schema.sql for parity) so fresh installs get it natively. The coverage test's third tier (schemaCreateTableCols) now finds it. Regenerated schema-embedded.ts via bun run build:schema. Schema-blob path is cleaner than COLUMN_EXEMPTIONS — fresh installs skip the migration entirely; upgrade installs still run v68.
# Conflicts: # CHANGELOG.md # VERSION # package.json # src/core/migrate.ts
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.
Summary
Search now routes through any embedding column you've populated, not just OpenAI 1536.
gbrain config set search_embedding_column embedding_voyageflips your whole brain to Voyage in a single line. ThequeryMCP op acceptsembedding_columnper-call for A/B benchmarking. Newembedding_columnsconfig registry declares which columns exist, what provider produced them, and what dim/type they use.Closes the gap from PR #1106 (closed; spec-only): users with multiple embedding providers backfilled but couldn't route search through them.
Substantive changes (excluding VERSION/CHANGELOG bookkeeping):
eval_candidates.embedding_column TEXTso replay reproduces the column the capture ran against.src/core/search/embedding-column.ts), widenedSearchOpts.embeddingColumnto acceptResolvedColumndescriptors, DB-plane config merge forembedding_columns+search_embedding_column, gatewayembedQuery/embed/isAvailablemodel overrides, enginesearchVectortakes descriptor +getEmbeddingsByChunkIdstakes column param.hybridSearchresolves at the boundary and threads descriptor + model + dims into engine + embedQuery + cosineReScore.knobsHashv=2→v=3 folds in(col, prov).queryMCP op acceptsembedding_column.gbrain doctoraddsembedding_column_registrycheck (batch format_type probe, HNSW visibility, coverage gate).gbrain config setvalidates registry shape + coverage gate.Test Coverage
Coverage: ~85% of new code paths. 8 gaps documented (mostly CLI-surface tests for the new
gbrain config setpath). All 5 codex /ship findings have regression tests.Test count: 537 → 544 test files (+7). All 141 new cases green. Existing parallel-runner shows 4 pre-existing PGLite WASM cold-start timeouts on shard 8 under heavy contention; the same tests pass 6/6 in 5s isolated — pre-existing infrastructure flakiness, unrelated to this branch.
Pre-Landing Review
/plan-eng-reviewran during planning + caught 5 findings (D2-D5 in the plan).Codex outside voice ran on the plan + caught 10 more (D7-D16).
Codex
/shipreview ran on the diff + caught 5 more (#1-#5 in the codex-fixes commit).All 20 findings applied before merge. The five codex
/shipfixes:Object.create(null)+Object.hasOwnsoconstructorandtoStringcan't masquerade as registered columns.ResolvedColumnget full validation; SQL-shaped strings indimensionsrejected.gbrain config set search_embedding_column X.isCacheSafe(resolved, cfg)compares full embedding space (name + dim + model), not just name. User overrides of theembeddingbuiltin no longer leak across vector spaces.Plan Completion
14/14 implementation tasks DONE. 16/16 decisions applied. Plan file:
~/.claude/plans/system-instruction-you-are-working-sparkling-sun.md.Documentation
src/core/search/embedding-column.ts. Appended v0.36.3.0 annotations tohybrid.ts,gateway.ts,doctor.ts, andmigrate.tsentries.bun run build:llmsso the committed bundle matches the updated CLAUDE.md (CI shard 1 enforces).Test plan
bun run verify— 15 checks pass (privacy, jsonb, source-id, test-isolation, wasm, admin-build, scope-drift, cli-exec, system-of-record, eval-glossary, typecheck, etc).gbrain config set search_embedding_column embedding_voyageround-trip with coverage gate fires correctly.🤖 Generated with Claude Code