Repository navigation
v4.0.0 — an embedding belongs to one graph
An embedding is a graph-scoped, per-model resource now. Through 3.2.0 a vector was
namespace-wide and graph-blind, so a KNN in a shared namespace could return a neighbour
belonging to another graph, and two graphs could not hold two models, because one row was
being overwritten. Fixing that moves rows rather than altering columns, so this release
breaks the storage layout.
Install
pip install --upgrade iris-vector-graph==4.0.0Upgrade before writing anything
Order matters: initialize_schema() first, then the migration.
engine.initialize_schema() # warns, defers kg_KNN_VEC
from iris_vector_graph.migrations import migrate_to_graph_scoped_embeddings
report = migrate_to_graph_scoped_embeddings(engine.conn, dry_run=True) # predict
report = migrate_to_graph_scoped_embeddings(engine.conn) # do itAn upgraded package meeting a 3.2.0 table cannot compile kg_KNN_VEC, because the 4.0.0
body reads n.node_id and n.graph_id. Server-side vector search is unavailable between
the two calls; nothing else is. The migration is re-runnable, so if it is killed, run it
again. report.rows_accounted must equal the pre-upgrade row count, with zero deleted.
Full upgrade path, including the one silent break:
docs/migration/v4.0.0.md.
Breaking
- The embedding tables are re-keyed and the
idcolumn is gone.
kg_NodeEmbeddingsandkg_NodeEmbeddings_optimizedare now
emb_rowid BIGINT IDENTITY PRIMARY KEY, graph_id, node_id, emb, metadatawith
UNIQUE (graph_id, node_id).SELECT idreturns the RowID,WHERE id = 'some:node'
returns0rows with no error, andINSERT (id, emb)isSQLCODE -108. The silent
middle case is the dangerous one. nodesno longer declaresUNIQUE (node_id). It isUNIQUE (graph_id, node_id).
A node ID may exist in more than one graph, which is what makes two models possible.
An edge requires both endpoints in its own graph.kg_KNN_VEC's fourth argument changed meaning and still compiles.
IN embeddingConfig VARCHAR(128)becameIN graphId VARCHAR(256). A 3.2.0 call passing
a model name now searches a graph of that name, finds nothing, and reports no error.
Audit every four-argument call site before upgrading.kg_RRF_FUSEtakes seven parameters. A six-argumentCALLis a runtime arity error.Graph.KG.Edgeis deleted;rdf_edgesis DDL-declared everywhere. Probe
Graph.KG.Eraserin%Dictionary.ClassDefinitionto tell a deployed install from a
DDL-only namespace.- Cypher
n.idisn.node_id. - The snapshot NDJSON node record changed.
{"id": ...}became
{"graph_id": ..., "node_id": ...}. Reads accept both, so a 3.2.0 snapshot restores into
4.0.0; a 4.0.0 snapshot does not restore into 3.2.0. embed_nodes(where=...)is removed (deprecated in 2.18.0). Uselabel=,
node_ids=,exclude_pattern=,missing_only=.get_procedures_sql_list(embedding_dimension=...)is removed. Inert since it was
added, deprecated in 3.2.0.- The GraphQL
datetimescalar is now namedDateTime. Clients generating types from
introspection see the rename; queries and responses are unchanged.
Added
- Routed tables, one per
(graph_id, model_key). Named
kg_emb_<first 16 hex of sha256(graph_id + "\0" + (model_key or ""))>, created by the
first write that names the pair, width taken from the vector being stored, one HNSW index
each, registered inGraph_KG.embedding_registry. The registry row is the only authority
on a route's name; never derive one to find a route. A pair with no routed table does not
fall back to a namespace-wide scan. - A keyword-only
graph(andmodel_keywhere a route is selected) on twelve methods,
includingstore_embedding,vector_search,kg_KNN_VEC,kg_RRF_FUSEand
embedding_count.graph=Nonemeans the default graph (''), never every graph.
vector_searchaimed at a routed table with nographrefuses rather than defaulting. Graph_KG.embedding_quarantine. Vectors the migration could not place in a graph go
here rather than into the default graph. Reasons areambiguous_graph,no_nodeand
resolver_declined. An operator places them one at a time with
engine.place_quarantined(...), or aresolver=callback avoids the quarantine entirely.engine.embedding_inventory()andivg embeddings inventory. One row per route, plus
one row per graph that has nodes and no route.index_stateis read from
%Dictionary.CompiledIndex, never from the registry or a row count.examples/demo_per_graph_embeddings.pywith a walkthrough in
docs/demos/PER_GRAPH_EMBEDDINGS_DEMO.md.
Fixed
kg_PERSONALIZED_PAGERANK(bidirectional=True)walked forward edges only, with no error,
on all three paths that run on a deployed install.- A snapshot omitted the BM25 corpus (
Graph_KG.docs) and the two-hop counts
(^KG("deg2p"),^KG("deg2p_exact")) while reporting every store as complete. MATCH (a), (b) MERGE (a)-[:R]->(b)could not be prepared: the idempotency rewrite
appended a bareANDto a finished statement (SQLCODE -25).- The GraphQL endpoint never enforced the 10-level query depth limit spec 003 specified.
IVG_GRAPHQL_MAX_DEPTHoverrides it; a bad value falls back to the default rather than
disabling the limit. - A recorded vector width could go stale against its column.
- Demos resolved their connection by discovery and could land on another project's IRIS
container. The container is resolved by name now, fromIVG_TEST_CONTAINER.
Measured on this release
Against ivg-iris-enterprise (irishealth:2026.3.0AI.113.0, port 31972):
| Gate | Result |
|---|---|
| Unit tests | 10088 passed, 0 failed, 15 skipped |
| Integration tests | 2452 passed, 0 failed, 55 skipped (eight chunks) |
| Integration (GraphQL) | 19 passed, 2 skipped |
| Story E2Es | 31 passed (US1-US5); spec 227's own E2E set 62 passed |
| Coverage | 90% (25390 statements, 2435 missed) |
| ObjectScript | 53/53 classes compiled, 0 errors |
| Migration rehearsal | real 3.2.0 install, populated, migrated |
| Routed-table ceiling | 50 graphs x 2 models = 100 tables, created 12.4 s, erased 2.0 s |
| Recall@10 | 1.0000 on both routes of the two-model fixture |
Seven tests in tests/unit fail when the suite runs straight after the integration
chunks and pass alone. The cause is two fixtures that reshape shared container state, and
it is recorded in docs/KNOWN_ISSUES.md.
Known gaps
The IVF leg stays graph-blind and refuses rather than guessing: Graph.KG.IVFIndex is
keyed by index name and holds no graph, so search_nodes_by_vector rejects a call that
names both a graph and an ivf_name.
^KG("deg2p_exact_merged") is Arno's interned sketch and stays graph-blind, because
^NKG's interning has no graph dimension. A snapshot carries the per-graph counts and
rebuilds the merged sketch.
A graph ID is not an authorisation boundary
Client-supplied graph IDs are collision avoidance: they keep two datasets from overwriting
each other's rows. Nothing stops a caller who can reach the engine from naming another
caller's graph. For tenant isolation use a separate IRIS namespace per tenant, which IRIS
enforces.
Full notes, with the failure modes and the reasoning behind each break, in
CHANGELOG.md.