Skip to content

v4.0.0 — an embedding belongs to one graph

Choose a tag to compare

@tom-dyar tom-dyar released this 22 Sep 15:44
· 655 commits to main since this release
441ed1a

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.0

Upgrade 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 it

An 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 id column is gone.
    kg_NodeEmbeddings and kg_NodeEmbeddings_optimized are now
    emb_rowid BIGINT IDENTITY PRIMARY KEY, graph_id, node_id, emb, metadata with
    UNIQUE (graph_id, node_id). SELECT id returns the RowID, WHERE id = 'some:node'
    returns 0 rows with no error, and INSERT (id, emb) is SQLCODE -108. The silent
    middle case is the dangerous one.
  • nodes no longer declares UNIQUE (node_id). It is UNIQUE (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) became IN 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_FUSE takes seven parameters. A six-argument CALL is a runtime arity error.
  • Graph.KG.Edge is deleted; rdf_edges is DDL-declared everywhere. Probe
    Graph.KG.Eraser in %Dictionary.ClassDefinition to tell a deployed install from a
    DDL-only namespace.
  • Cypher n.id is n.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). Use label=,
    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 datetime scalar is now named DateTime. 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 in Graph_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 (and model_key where a route is selected) on twelve methods,
    including store_embedding, vector_search, kg_KNN_VEC, kg_RRF_FUSE and
    embedding_count. graph=None means the default graph (''), never every graph.
    vector_search aimed at a routed table with no graph refuses 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 are ambiguous_graph, no_node and
    resolver_declined. An operator places them one at a time with
    engine.place_quarantined(...), or a resolver= callback avoids the quarantine entirely.
  • engine.embedding_inventory() and ivg embeddings inventory. One row per route, plus
    one row per graph that has nodes and no route. index_state is read from
    %Dictionary.CompiledIndex, never from the registry or a row count.
  • examples/demo_per_graph_embeddings.py with 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 bare AND to a finished statement (SQLCODE -25).
  • The GraphQL endpoint never enforced the 10-level query depth limit spec 003 specified.
    IVG_GRAPHQL_MAX_DEPTH overrides 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, from IVG_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.