Repository navigation
Conversation
The offline pipeline embedded `type: <Type>\n<field>: <value>` while a text
`nearest($v, $q)` embeds `$q` verbatim, so a record whose source equaled the
query text did not rank at distance zero: unrelated vectors under the mock
provider, a systematic skew under a real model.
`@embed("source")` declares `vector = embed(source)` with one String source
(RFC 0015), and a query string carries no type or field to template, so the
pipeline is the side that changes: it now embeds the source value unchanged,
which is what the query path already does. The spec's `fields` must name
exactly that one property; a record with an absent, null or blank source gets
no vector instead of one embedded from its type name, and a re-embed removes
such a record's stale vector; a non-string source is refused.
The no-source path of `omnigraph embed` kept the target whenever `embedding_missing` held, which is also true for an empty list: a record with a blank source and `embedding: []` came out unchanged, with `cleaned_records: 0`, and its load then failed with "expects 4 dimensions, got 0". Selecting which records need embedding is not the same contract as deciding what a record without source text may keep. The no-source path now removes whatever the target holds (a stale vector, a null, an empty placeholder) and reports `Removed` exactly when it removed something, which `--clean` already counts the same way. The existing `a_record_without_source_text_keeps_no_vector` test covers all three target values; before this change it failed on the `[]` case with `Unchanged` versus `Removed`.
…d` declarations
`omnigraph embed` took a hand-written spec (`{"dimension": N, "types": {"T":
{"target": …, "fields": […]}}}`, or a seed manifest's `embeddings:` entry)
that restated what `embedding: Vector(N) @embed("title", model="…")` already
declares, and nothing compared the two. A spec naming another source or
dimension silently wrote vectors a text `nearest()` could not match, unknown
spec keys were ignored, and a recorded `@embed` model was never checked
against the provider, although the query path refuses that mismatch.
The command now takes `--schema <schema.pg>` (a seed manifest names it with
`schema:`) and fills every `@embed` vector the catalog declares, with that
declaration's source and dimension. A type may carry several vectors of
different dimensions, which the spec could not express. `--spec` and the
manifest's `embeddings` entry are removed, and the manifest refuses keys it
does not read.
Before reading input or creating output, a run refuses a recorded model the
provider does not resolve, through `EmbeddingConfig::embeds_with_model`,
which `resolve_nearest_query_vec` now uses too. It also refuses a `--type`
without an `@embed` vector, a schema without one, and, by line, a selected
record without source text whose vector is a required `Vector(N)`. JSON
output reports `vectors` and `schema` in place of `dimension`.
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: request changes before merge for the one P2 finding below. This is a COMMENT review because the authenticated account is the PR author.
Reviewed 3f437a6670a60c176968138134959681546b0a50 against its parent 3721e4640c0944c0b45e3cd830b851b8daa103a6 in a clean isolated checkout. This covers the final schema-authority commit. The prerequisite #929 is now merged.
The old file pipeline repeated the schema in an embedding spec. The two could disagree about the source, dimension, or model. This change reads targets from the compiler catalog, checks recorded models, and generates each declared vector. Users must replace --spec and the manifest's embeddings section with a schema path. It remains a file preparation command. Loading the result remains a separate graph operation.
Required change
The model check couples every vector on a node to one provider, even though the schema permits different models per property. Both model choices then refuse, and --type cannot select between properties. The inline comment gives the reproduction and the required regression. Preserve schema authority while allowing a run to select the declared vector it will generate. Do not restore a second source/dimension/model mapping or silently ignore a mismatched target.
Correctness, tradeoffs, and liability
The central fix is sound: remove duplicate declarations at the CLI boundary. The catalog-derived plan covers each node's declarations and dimensions. It uses the same model comparison as text nearest. Strict manifest decoding closes the ignored-key class. Required-vector refusal agrees with the loader.
This removes a configuration format and its drift. It adds an in-memory target plan, a small public model-comparison method, and a changed JSON summary. It adds no durable state, queue, or publication rule. Those costs are justified. Five future vector options can derive from the catalog instead of adding five parallel spec fields. The unresolved target-selection gap would otherwise force users to maintain reduced schema copies, which recreates the drift this change removes.
The intended workload is offline seed preparation. The loop streams records but still makes serial provider calls for each vector it generates. More declared targets can mean more calls. I did not measure throughput. The caller must supply the intended schema file. Existing vectors remain trusted in fill-missing mode; this is not a provenance audit or automatic re-embedding on source changes.
I surveyed the other 29 open PRs. The relevant connections are #793's vector-space contracts, #884's compiler-owned type expansion, #951's proposed views, and the skill edits in #811/#892. Keeping this work in the catalog and offline CLI fits those boundaries. Merge the overlapping skill edits carefully. The planner, merge/cache, deployment/admission, GQT corpus, and release/documentation work does not require another embedding authority. RFC 0015's future graph reconciler can reuse the same declarations and provider rule without moving network calls into ordinary writes.
I consulted the repository guides and the complete relevant Lance type, array, vector-search, and index documentation. I checked stock Lance 11.0.0 source (ab6b5bbe46009ed78746b444df8db59a8bc5d842). OmniGraph maps vectors to FixedSizeList<Float32>; Lance's scanner checks query dimensions. Neither check establishes an embedding model's identity. That remains OmniGraph's responsibility. The current Lance 14 beta does not justify a substrate dependency for this CLI fix. No storage or deployment behavior changes here.
Local validation and before/after proof
All Cargo commands used RUSTUP_TOOLCHAIN=1.97.1, CARGO_TARGET_DIR=/tmp/review-928/target, and RUSTFLAGS='--cfg tokio_unstable --cfg tokio_unstable'.
| Command | Parent | Reviewed head |
|---|---|---|
cargo test -p omnigraph-cli --locked --bin omnigraph embed |
9 passed | 14 passed |
cargo test -p omnigraph-cli --locked --test integration cli_data::embed_ |
4 passed | 5 passed |
cargo test -p omnigraph-engine --locked --test search nearest_string_ |
Not run | 4 passed |
cargo test -p omnigraph-cli --locked --test integration cli_data::embed_review_ |
4 behavioral failures | 4 passed |
cargo test -p omnigraph-cli --locked --bin omnigraph review_seed_source_refuses_an_unread_model |
1 behavioral failure | 1 passed |
The last two rows used temporary tests in the existing CLI owners. The four CLI cases were embed_review_schema_owns_source, embed_review_schema_owns_dimension, embed_review_recorded_model_mismatch_refuses, and embed_review_required_vector_without_source_refuses. The parent wrote the wrong source vector, wrote eight dimensions instead of four, accepted another model, and installed a row without its required vector. The fifth test showed that the parent accepted an unread sources.model key. All five passed on the reviewed head.
The same test source and behavioral assertions ran on both commits. One compatibility branch selected the parent's --spec interface or the head's --schema interface from embed --help. An unchanged command would only prove an unknown-flag failure. No compile, setup, skipped, or zero-test result counts as this proof. GQT cannot exercise the file CLI, and its runner explicitly refuses @embed schemas, so the existing Rust owners are appropriate. All temporary source/test edits were restored. No changes were pushed.
cargo fmt --all --check, git diff HEAD^ HEAD --check, changed-file typos, python scripts/check-docs.py (253 files), and bash scripts/check-agents-md.sh passed. The macOS linker emitted a compact-unwind size warning, but tests ran.
CI and limits
The exact head has green Fix Regression Gate and PR Title checks. These are not runtime test evidence. I found no workspace/GQT build checks on this draft head. I did not run the full workspace, live provider calls, cloud tests, or performance measurements. No separate optional finding.
| fn check_model(&self, config: &EmbeddingConfig) -> Result<()> { | ||
| for target in self.types.values().flatten() { | ||
| if let Some(recorded) = &target.model | ||
| && !config.embeds_with_model(recorded) |
There was a problem hiding this comment.
[P2] Let a run select one declared vector when a type uses several models
This loop requires every vector on a node to use the same provider model. The schema permits vec_a: Vector(4)? @embed("text", model="model-a") and vec_b: Vector(4)? @embed("text", model="model-b") on one Doc. With one row containing text: "hello", embed --schema schema.pg --input raw.jsonl --output out.jsonl --type Doc refuses vec_b under model-a and refuses vec_a under model-b. I reproduced both with the mock provider on this head. Changing --type cannot help, and the check also blocks a missing vector when the other model's vector is already present. On parent 3721e464, two runs with a spec selecting each target populated both properties.
Please preserve the ability to process an individual schema-declared target. Its source, dimension, and recorded model must still come from the catalog. A target selector can do that without restoring the parallel spec or adding automatic provider dispatch. Extend the existing CLI embedding case with two differently modeled vectors on one node: run each target with its matching model, preserve the other vector, and refuse a wrong model for the selected target. The invariant belongs at the selected vector, not at the entire node type.
…uthority Keep main's v0.14.0 note for ModernRelay#929 as released, move the embed summary's human writer into embed.rs with main's yaml_serde, update main's embed report test to the schema form, keep main's condensed skill reference, shorten this change's notes to the composer's limits and regenerate the v0.15.0 snapshot.
…ed vectors The model check held every vector on a node type to one provider model, so a type declaring vectors of two models could be filled by neither run, and --type could not choose between its properties. --vector names the declared @embed vectors a run fills or cleans. Each must be declared, on a type --type selects when both are given, and every --type must keep one. The source, dimension and model still come from the declaration, the model check covers only the selected vectors, and the record's other vectors stay as they were.
|
Addressed the review of [P2] Let a run select one declared vector when a type uses several models (
Bringing the branch up to date: merge commit 6d0d7ae. The branch still carried #929's original commits, which
Checks run locally on the merged tree:
I updated the PR description where decisions changed: the released #929 note, |
ragnorc
left a comment
There was a problem hiding this comment.
Recommendation: ready after one mechanical fix (regenerate the v0.15.0 release-note snapshot; see Required). This is a COMMENT review because the authenticated reviewer is the PR author.
Reviewed 00ad792787c4a5a407ada619422acd6efa3c530b in a clean, isolated worktree. The previous review covered 3f437a66. This review covers the delta: the merge 6d0d7ae3 (main at ea4a16d5) and 00ad7927 (--vector Type.property). The merge changes only files this PR already owns.
Why, what, how, impact
Why. The previous review found one defect. The model check held every vector on a node type to one provider model. When one type declares two vectors with different recorded models, no run could fill either vector. --type could not select between them.
What. --vector Type.property (repeatable) names the declared @embed vectors that a run fills or cleans. The source, dimension and model still come from the schema declaration. The model check covers only the selected vectors. Other vectors in the record stay unchanged.
How. EmbedPlan::restrict_to_vectors (embed.rs#L223-L272) filters the catalog-derived plan. It refuses a malformed name, an undeclared vector, a vector on a type that --type excludes, and a --type that keeps no vector. check_model then runs over the filtered plan.
Impact. The P2 is fixed at the layer that owns it: the plan, which is the one source of what a run fills. No second mapping of source, dimension or model comes back. There is no automatic per-model provider dispatch. A type with two models now needs one run per provider, and each run is explicit.
Required before merge
- The generated v0.15.0 snapshot conflicts with main.
git merge-tree origin/main 00ad7927shows one conflict:docs/releases/v0.15.0.md. Main regenerated that snapshot afterea4a16d5. Do not merge the file by hand. Merge main, then runpython3 scripts/release_notes.py snapshot --target HEAD --date 2026-11-06 --replaceandverify. At the reviewed head,verify --version v0.15.0 --target HEADpasses. A simulated squash commit onea4a16d5with this diff also passesverify. Ifv0.15.0is tagged before this PR merges, remove the snapshot edit. The twochangelog.d/embed-schema-authority.*notes then belong to the next cycle. Either way, the breaking--specremoval must ship in a release whose notes name it.
Optional
-
One owner for "which types this run processes" (inline comment). Row selection reads the raw
--typeset (EmbedJob::type_filter). The vectors come from the plan. The two can disagree, and the--jsonsummary shows it. WithIssueandTagvectors and three rows (oneIssue, twoTag), the same work gives two different reports:Arguments selected_recordsembedded_recordsvectors--vector Issue.embedding3 1 Issue.embedding--type Issue --vector Issue.embedding1 1 Issue.embedding--type Issue1 1 Issue.embeddingThe cause predates this commit: without
--type, rows of a type with no@embedvector also count as selected.--vectormakes it easier to reach. A row is processed only when the plan has targets for its type. So select the row from the plan (job.plan.types.contains_key(type_name), then the--selectpredicates), and removeEmbedJob::type_filter. Then one structure answers both questions, and the next selector (for example a per-field recipe) cannot add a third answer. If you keep the current meaning, documentselected_recordsas "rows of a selected type, whether or not it has a selected vector".
Correctness, tradeoffs and liability
- Correctness. The refusals are complete for the inputs the flag accepts.
split_once('.')is safe: type and property names cannot contain.. The filter runs afterrestrict_to, so error order is stable: an unknown--typeis reported first. Clean mode removes only the selected vectors. Fill-missing and re-embed leave the other vectors byte-identical (verified below). An unselected required vector is not checked. That is correct for a selector, andloadstill refuses a record without it. - Liability added. One repeatable flag, one filter pass (about 50 lines), three error messages and docs/skill lines. No new state, format, protocol or publication rule. The flag names a declared field and restates nothing, so it cannot drift from the schema.
- Liability removed. Users no longer need a reduced copy of the schema per model. That copy would recreate the drift this PR removes.
- Five more changes of this kind. Each future per-field option can filter the same plan.
EmbeddingConfig::embeds_with_modelis the one comparison that the textnearest()and this pipeline share. RFC #1302 (GQ 3) proposes@embedrecipes: the model revision plus encoding choices, possibly from a schema default. When that lands, extendembeds_with_modelto compare the resolved recipe descriptor, not the model label. Then the CLI andnearest()stay on one rule.--vectorselects a field, which is the unit a recipe binds to, so it fits that direction. Today the check compares only the model string. A matching label does not prove one embedding space. That limit predates this PR, and the RFC states it. - Workload. This is an offline seed-preparation command. Selecting fewer vectors makes fewer provider calls. Two models now need two passes over the file. That cost is small and explicit. I did not measure throughput.
Other open PRs: only #1302 (the recipe text above) and RFC drafts #884, #1015 and #1101 mention @embed. None conflicts with this flag. The skill edits in references/commands.md and references/search.md match the clap help.
Lance: the change does not touch storage. I read the Lance data-types guide (vectors are FixedSizeList<Float32, n>), the arrays guide, the vector-search quick start and the vector-index format page. Lance checks dimensions and stores no embedding-model identity, so the model and selection rules stay in Omnigraph's catalog and CLI.
Validation
All commands ran in the isolated worktree with CARGO_TARGET_DIR set to a shared warm target. Rust toolchain from rust-toolchain.toml.
| Command | 6d0d7ae3 (parent) |
00ad7927 (head) |
|---|---|---|
cargo test -p omnigraph-cli --locked --lib embed |
not run | 19 passed |
cargo test -p omnigraph-cli --locked --test integration cli_data::embed_ |
not run | 5 passed |
cargo test -p omnigraph-cli --locked --test integration cli_data::embed_review_two_models_on_one_type_fill_each_vector -- --exact (temporary) |
1 failed: Decision.vec_b records @embed(model="model-b"), but the embedding provider resolves to model 'model-a' |
1 passed |
The temporary test extended the existing cli_data owner. Schema: one Decision with vec_a (model-a) and vec_b (model-b), both from intent. It fills vec_a with the model-a provider, then fills vec_b with the model-b provider from that output, and requires vec_a to stay identical. It used the same assertions on both commits. It selected with --vector when embed --help lists it, and with --type Decision on the parent, which has no other selector. So the parent failure is the behavioral refusal, not an unknown flag. GQT cannot express this: the file pipeline is a CLI process, and the GQT runner refuses @embed schemas. The PR's own extension of embed_reads_vectors_from_the_schema_and_refuses_another_model already covers this case, so no extra test is needed. I restored the temporary edit, and the worktree is clean.
Also passed at the head: cargo fmt --all --check, git diff --check ea4a16d5 HEAD, python3 scripts/check-docs.py (471 files) and python3 scripts/release_notes.py verify --version v0.15.0 --target HEAD. typos was not installed on this host. I did not run it.
CI on this exact head is green, including Test Workspace, Lint (clippy) and Format, GQ Logic Tests and Fix Regression Gate. I did not run the full workspace, live embedding providers or cloud backends.
| }; | ||
| let plan = EmbedPlan::from_schema_file(&schema)? | ||
| .restrict_to(&type_filter)? | ||
| .restrict_to_vectors(&args.vectors, &type_filter)?; |
There was a problem hiding this comment.
[Optional] The plan is filtered here, but row selection (L468) still reads the raw --type set. The two can disagree. With Issue and Tag vectors and rows Issue, Tag, Tag, --vector Issue.embedding reports selected_records: 3, while --type Issue --vector Issue.embedding reports 1 for the same work (one Issue.embedding filled in both). Without --type, rows of a type with no @embed vector also count. Suggest selecting the row from the plan (job.plan.types.contains_key(type_name), then the --select predicates) and removing EmbedJob::type_filter. Then the plan alone says which types a run processes.
What & why
omnigraph embedfilled vectors from a hand-written spec ({"dimension": N, "types": {"T": {"target": "embedding", "fields": ["title"]}}}, or a seed manifest'sembeddings:entry). After #929 that spec restates exactly what the schema already declares:embedding: Vector(N) @embed("title", model="…")gives the target, the source, the dimension and the model. Nothing compared the two, so:bodyfor a schema declaring@embed("title"), or a wrong dimension, silently wrote vectors a textnearest()cannot match;embeddings.model:that did nothing);@embed(model=…)was never compared with the provider model the pipeline resolves, although the query path refuses that mismatch (resolve_nearest_query_vec).This PR makes the schema the only statement of what to embed:
--schema <schema.pg>replaces--spec. Every@embeddeclaration in the catalog (NodeType::embed_sources) is a vector the run fills, with that declaration's source property and itsVector(N)dimension. A type can now carry several vectors of different dimensions, which the spec could not express (one globaldimension, one target per type).schema:.embeddings:is gone, and the manifest now refuses keys it does not read (deny_unknown_fields, nested sections included).EmbeddingConfig::embeds_with_modelis the comparison the query path already made;resolve_nearest_query_vecnow calls it, andomnigraph embedcalls it for every selected vector with a recorded model, before it reads input or creates output.--vector Type.propertyselects declared vectors (repeatable). A run fills or cleans only the named@embedvectors, and the model check covers only those, so a type declaring vectors of two models is filled by one run per provider while each run leaves the other vector as it was. Each name must be declared, on a type--typeselects when both are given, and every--typemust keep one. The source, dimension and model still come from the declaration; no spec comes back.--typenaming a type with no@embedvector; a schema with none; and, by line, a selected record without source text whose vector is a requiredVector(N)(a load would refuse that record anyway;--cleanis unchanged).vectors(each processed vector'stype,property,source,dimension) andschemareplacedimension, which has no single value once a schema declares several vectors.Why refuse the old spec instead of validating it
Validating a spec against the schema keeps two sources of truth plus a comparison, and the spec format cannot even describe valid schemas.
--specis removed outright, following the precedent of--allow-data-loss(scripts get clap's unknown-argument error; the breaking note says what to do). #929's breaking note shipped in v0.14.0, whose snapshot pins its digest, so this PR no longer edits it; this change's own breaking note says what replaces the spec. Main had already prepared the v0.15.0 snapshot, so, as later notes onmaindo, this PR regeneratesdocs/releases/v0.15.0.md(release_notes.py snapshot --replace, the recorded release date) and both notes land in that preview; both were shortened to the composer's 400- and 200-character limits, with the detail in the embeddings guide.Why a schema file, not the graph's accepted schema
embedis alocal-plane command (RFC 0010/0011): it reads and writes files and takes no graph address. RFC 0015 plansomnigraph embed --graph <scope>as a reconciler that writes to the graph, so a read-only graph flag now would collide with it. Seed files are often prepared beforeinit, when no graph exists.omnigraph schema showprints a graph's accepted schema for anyone without the file; the docs say so.Backing issue / RFC
omnigraph embedembed the exact@embedsource text #929's description names. I can open one if a maintainer wants it to back the PR.@embedderivation RFC 0015 declares (docs/rfcs/0015-ingest-embeddings.md, "Embeddings are derived state, keyed off@embed", and its "model mismatch refuses loudly, reusing the query-path same-space validation"), though RFC 0015 is a draft.Checklist
docs/user/search/embeddings.md(offline pipeline, seed manifests, the model and required-vector refusals),docs/dev/execution.md,skills/omnigraph/(SKILL.md, references/search.md, references/commands.md); changelogembed-schema-authority.breaking.mdand.fixed.mdLocal verification
cargo test -p omnigraph-cli --locked --bin omnigraph embed: 9 passed;cargo test -p omnigraph-cli --locked --test integration -- cli_data::embed_ cli_data::optimize_json_succeeds_on_local_graph: 5 passed.cargo test -p omnigraph-cli --locked: unit 147 passed (6 ignored), integration 295 passed (8 ignored). The first run failed 57 server-backed tests because a-p omnigraph-clirun does not buildomnigraph-server; aftercargo build -p omnigraph-server --locked, those 57 passed with--exact.cargo test -p omnigraph-engine --locked --test search nearest_string_: 4 passed (the query path's model checks through the shared rule).check_modeldisabled,a_run_embeds_each_declared_source_at_its_declared_dimensionfails; with the required-vector refusal disabled,a_required_vector_without_source_text_is_refusedfails. Both pass restored.cargo clippy -p omnigraph-cli -p omnigraph-engine --all-targets --locked -- -D warnings -W clippy::dbg_macro: clean.cargo fmt --all --check: clean.python3 scripts/check-docs.py,bash scripts/check-agents-md.sh,typoson the changed files,python3 scripts/release_notes.py preview --working-tree: clean; the notes render.@embedschemas, so no case reaches this code; CI runs both.Notes for reviewers
@embedschemas because they resolve a provider from the process environment, so the regression tests are in-source incrates/omnigraph-cli/src/embed.rs.run_embed_jobnow takes the client as a parameter, so a whole run is tested against temp files with a mock client that resolves an explicit model: each stored vector must equalembed_query_text(source, N)at the declared dimension, and a provider resolving another model must be refused before the output file exists. One CLI test (cli_data::embed_reads_vectors_from_the_schema_and_refuses_another_model) covers the env-to-refusal wiring and that--specis no longer accepted.schema.pg, and its pre-existing vector is 4-dimensional; it was 2-dimensional under a 4-dimension spec, which a load would have refused.--typerefusal is a behavior change: before, a--typenaming a type the spec did not cover was a silent no-op.--vector, not--target:--targetis the CLI's global target flag. The model refusal now suggests--vector Type.propertyinstead of--type, since--typecannot choose between two vectors of one type.--specerror text: clap's "a similar argument exists: '--select'" tip on the removed flag is unhelpful; I left it rather than add a hidden flag only to refuse it, consistent with--allow-data-loss.parse_schema+build_catalog(admission mode), the same path aslint --schema.