How-To: Build and use the relational SQL vector index (Virtuoso)

Learn how to create, query, and maintain a native HNSW-backed vector index on a relational text column in Virtuoso using SQL, with local ONNX or remote HTTP embedding models.

This guide covers building and using the native, model-bound, HNSW-backed relational SQL vector index in Virtuoso Open Source. It indexes a raw text column, the server owns embedding generation via a local ONNX model or a remote HTTP embedding provider, and similarity is queried with X_SIMILAR(...) or ORDER BY VECTOR_DISTANCE(...) TOP N, both driven by the HNSW index.

This article covers:

  • Prerequisites and build: Configuring Virtuoso with ONNX support
  • Creating a vector index: DDL syntax, HNSW parameters, and model binding
  • Remote embedding providers: OpenAI, Ollama, LM Studio, and OpenAI-compatible APIs
  • Querying: Text-based search, ANN-driven search, filter/rerank mode, and accuracy tuning
  • Maintenance: Automatic queue-based index updates
  • Troubleshooting: Common issues and health checks

Prerequisites

Before you start, make sure you have:

  • Virtuoso Open Source built with the ONNX plugin (--enable-onnxruntime)
  • The vonnx plugin loaded in your virtuoso.ini
  • An embedding model registered in DB.DBA.ONNX_MODELS (auto-provisioned at startup for the bundled granite-384-local model)

Verify the plugin and model at startup:

SQL
-- The vonnx plugin should load at startup; check the log for:
--   "SUCCESS plugin N: loaded from .../vonnx.so"

-- Verify the bundled model auto-provisioned:
SELECT model_name, model_type, handle
FROM DB.DBA.ONNX_MODELS
WHERE model_name = 'granite-384-local';
-- Expect: model_type = text_embedding, handle = 1 (non-null means the model is loaded)

-- Test embedding:
SELECT length (DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL('granite-384-local', 'hello world'));
-- Expect: 384

If the query returns no row, or handle is null, see Troubleshooting.


Build Virtuoso with ONNX support

Configure and build

From the repository root:

Shell
./autogen.sh
./configure \
  --prefix=/opt/virtuoso-opensource \
  --with-layout=openlink \
  --enable-onnxruntime=/path/to/onnxruntime \
  --enable-vvec-vad
make
make install
  • --enable-onnxruntime=<dir> builds the vonnx plugin and its onnx_helper program. <dir> is an ONNX Runtime installation prefix that holds the static library lib/libonnxruntime.a and the C API headers (in include/ or include/onnxruntime/). ONNX Runtime is linked into the plugin and the helper, so nothing from it has to be installed on the server at run time. README.Building.md lists ONNX Runtime 1.29.0 or later. Given without a directory, --enable-onnxruntime asks pkg-config for libonnxruntime. If no usable libonnxruntime.a is found, configure warns and the server is built without the plugin.
  • --enable-vvec-vad also builds vvec_dav.vad, the VVec console package (--enable-all-vads includes it).
  • On macOS with Homebrew, add --enable-openssl=$(brew --prefix openssl@3).

The bundled model's weights (model.onnx, about 60 MB) are not kept in the source repository. The first build downloads them from https://nexus.openlinksw.com/repository/onnx-models/ and checks them, together with the tokenizer files that ship with the source, against the recorded SHA256SUMS; a mismatch fails the build. A tree that already holds a verified copy never touches the network, so for an offline build place model.onnx in binsrc/hosting/onnx/onnx-models/granite-embedding-30m-english/ first. make install puts the model under <prefix>/hosting/onnx-models/granite-embedding-30m-english/.

Enable the plugin in virtuoso.ini

The virtuoso.ini that make install writes for the default database already loads the plugin when it was built, and says where the models live:

INI
[ONNX]
ModelRoot   = /opt/virtuoso-opensource/hosting/onnx-models
;HelperPath = /opt/virtuoso-opensource/hosting/onnx_helper

[Plugins]
LoadPath    = /opt/virtuoso-opensource/hosting
Load17      = plain, vonnx

For another database, add the vonnx line under [Plugins] (any free LoadN number) and the [ONNX] section. With HelperPath commented out the helper is found next to the plugin in LoadPath. On startup, confirm the log shows lines of this form:

Output
SUCCESS plugin N: loaded from <hosting directory>/vonnx.so }
PL LOG: ONNX bootstrap refreshed 1 model(s)

Models

Embeddings come from models resolved through DB.DBA.ONNX_MODELS. The bundled granite-384-local model (IBM granite-embedding-30m-english, 384 dimensions) is auto-provisioned at startup — you do not create or populate the table by hand.

Available model types

Model typeDescriptionHandle required?
text_embeddingLocal ONNX model (e.g. the bundled granite-384-local)Yes — loaded at startup
openaiOpenAI API (text-embedding-3-small, etc.)No — HTTP provider
ollamaLocal Ollama serverNo — HTTP provider
lmstudioLocal LM Studio serverNo — HTTP provider
openai_compatibleAny OpenAI-compatible APINo — HTTP provider

Embed text manually (for testing)

SQL
-- By model name (recommended):
SELECT DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL('granite-384-local', 'hello world');

-- By handle (lower-level plugin BIF):
SELECT ONNX_EMBED_TEXT(
  (SELECT handle FROM DB.DBA.ONNX_MODELS WHERE model_name = 'granite-384-local'),
  'hello world'
);

Create a vector index

The source table needs a single-column integer primary key (INTEGER, BIGINT, SMALLINT or NUMERIC) and a character column (VARCHAR, NVARCHAR, LONG VARCHAR or LONG NVARCHAR).

SQL
CREATE TABLE DB.DBA.DOCS (
  id      INTEGER NOT NULL PRIMARY KEY,
  content LONG VARCHAR
);

INSERT INTO DB.DBA.DOCS VALUES (1, 'How do I Install Virtuoso on Linux?');
INSERT INTO DB.DBA.DOCS VALUES (2, 'How do I install virtuoso on Windows?');
INSERT INTO DB.DBA.DOCS VALUES (3, 'How do I install virtuoso on macOS?');
INSERT INTO DB.DBA.DOCS VALUES (4, 'How do I change the dba password?');

CREATE VECTOR INDEX DOCS_VEC
ON DB.DBA.DOCS (content)
USING 'granite-384-local'
WITH (
  dimensions      = 384,
  distance_metric = 'cosine',
  hnsw_m          = 16,
  ef_construction = 200
);

The short form

Every WITH option above has a default, and dimensions is knowable from the model, so the same index can be created with none of them. The column can be left out too when the table has exactly one string column:

SQL
CREATE VECTOR INDEX DOCS_VEC ON DB.DBA.DOCS USING 'granite-384-local';

That gives dimensions 384 from the model, cosine, hnsw_m 16 and ef_construction 200 — identical to the explicit statement above. Spell the options out when you want something other than those defaults.

If the table has more than one character column, naming it is required: the server refuses rather than choosing for you, because indexing the wrong column succeeds quietly and only shows up later as poor search results:

Output
VEC11: Cannot infer a source column for vector index LIM_TWO_VEC: table DB.DBA.LIM_TWO
has 2 candidates (title, body). Name one explicitly: ON DB.DBA.LIM_TWO (<column>).

This query runs as follows:

  1. CREATE TABLE defines a table with an integer PK and a text column.
  2. INSERT populates rows before index creation so they get backfilled.
  3. CREATE VECTOR INDEX binds the index to the granite-384-local model and builds the HNSW graph.

Key concepts:

  • USING 'model_name' binds the index to a specific model in ONNX_MODELS.
  • dimensions must match the model's output dimension exactly (unless reduce = 'truncate' is used, in which case it's the reduced dimension). It can also be left out — see the short form above.
  • distance_metric can be cosine (default), l2, or l1.
  • hnsw_m and ef_construction control HNSW graph quality.
  • reduce can be 'none' (default) or 'truncate' — enables Matryoshka dimension reduction for MRL-trained models like OpenAI text-embedding-3-small. See Index parameters: Matryoshka reduction.

What CREATE VECTOR INDEX does

  1. Validates the model binding and dimension match.
  2. Creates a hidden managed embedding table.
  3. Creates the native HNSW index over the managed embeddings.
  4. Creates AFTER INSERT/UPDATE/DELETE maintenance triggers.
  5. Backfills existing rows — every row of the table, embedded inside the CREATE statement; the index is populated and searchable when CREATE returns.

The backfill reads the table in key order and embeds and inserts it in batches of 32 (__vec_embed_batch_size). An HTTP provider gets each batch in one request. A local model gets one text per call — its helper is no faster per text on a batch — so for a local model CREATE takes as long as embedding the table: 4,000 FiQA passages (704 characters on average) took 180 s with granite-384-local, about 22 a second, on a 10-core arm64 machine (Apple M1 Pro), 2026-10-10. Give the model more threads to go faster — see Helper thread counts. A row that cannot be embedded fails the whole CREATE, naming the rows of its batch.

By default the backfill links each row into the graph as it goes, and an incremental graph is one 8 KB page per node: once it outgrows the buffer pool, every link is a disk read. For a large table, create the index with build = 'bulk' — every vector is staged first and the graph derived in one parallel pass, in packed pages — or size the buffer pool to hold the graph (Maintenance). Measured with a synthetic 384-dimension test model (no embedding cost) and the stock virtuoso.ini (10,000 buffers): 40,000 rows took 4.3 s with build = 'bulk'; the default build was still reading graph pages from disk after 12 minutes and was stopped. See Index parameters: build.

Tuning HNSW parameters for accuracy

ParameterDefaultAccuracy-focusedEffect
hnsw_m1632Max neighbors per node per layer
ef_construction200400Search depth during index build

For production use with 1536-dimensional OpenAI embeddings, hnsw_m = 32 and ef_construction = 400 are recommended.


Bring your own vectors (precomputed embeddings)

If your table column already holds embedding vectors — you compute them in your own pipeline — you can index the column directly, with no model. This is the source_kind = 'vector' (passthrough) mode. CREATE VECTOR INDEX builds the HNSW graph from the column's vectors, and ordinary INSERT / UPDATE / DELETE keep the index in sync from the column value.

Enable the mode (DBA-only)

Passthrough is opt-in, behind a registry flag that a DBA sets once:

SQL
registry_set ('__vvec_passthrough_column', 'on');

With the flag off, a source_kind = 'vector' create fails with VEC83:

Output
VEC83: Precomputed-vector passthrough column mode (source_kind='vector') is not enabled.
A DBA can enable it with: registry_set ('__vvec_passthrough_column', 'on').

The source column

The source column must be able to hold a boxed vector value — ANY, LONG ANY, or (LONG) VARBINARY. A plain VARCHAR/NVARCHAR column is rejected at create time with VEC87 (it cannot faithfully hold a vector). Each row's value may be either:

  • a native float array — the canonical, most compact form, which is what the embedding functions return, or a vector (0.1, 0.2, …) of numbers; or
  • a serialized blob/string — a stored serialize (vector (…)); the index deserializes it for you.

Prefer the native float-array form in storage (smallest, no per-element boxing).

Create the index

Omit the USING '<model>' clause entirely and pass source_kind = 'vector'. In this example the vectors come from the bundled model, standing in for whatever pipeline produces yours:

SQL
CREATE TABLE DB.DBA.DOCS_V (
  id  INTEGER NOT NULL PRIMARY KEY,
  emb ANY                       -- holds a 384-d embedding per row
);

INSERT INTO DB.DBA.DOCS_V VALUES (1, DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL ('granite-384-local', 'How do I Install Virtuoso on Linux?'));
INSERT INTO DB.DBA.DOCS_V VALUES (2, DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL ('granite-384-local', 'How do I install virtuoso on Windows?'));
INSERT INTO DB.DBA.DOCS_V VALUES (3, DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL ('granite-384-local', 'Steps to reset a forgotten password'));

CREATE VECTOR INDEX DOCS_V_IDX
ON DB.DBA.DOCS_V (emb)
WITH (
  dimensions      = 384,
  distance_metric = 'cosine',
  hnsw_m          = 16,
  ef_construction = 200,
  source_kind     = 'vector'    -- no USING clause; the column IS the vector
);

Rules:

  • source_kind = 'vector' replaces USING — supplying both is contradictory and fails with VEC84; omitting both fails with VEC12.
  • dimensions is the vector length; every row's vector must match it.
  • reduce = 'truncate' is not available (it needs a model) → VEC86.
  • Everything else (distance_metric, hnsw_m, ef_construction, rebuild) works exactly as in the model path, and existing rows are backfilled on create.

Maintenance (upsert from the column)

INSERT / UPDATE / DELETE on the base table maintain the index from the column value — no model is invoked:

  • a new/changed vector is upserted into the graph;
  • a NULL column value removes the row from the index;
  • a malformed value (VEC30) or a wrong-dimension value (VEC31) is a hard, deterministic per-row error: the queue row is marked error with the message (for example 22023: VEC31: Embedding dimension 2 does not match vector index dimension 384.), counted in DB.DBA.VEC_SQL_WORKER_STATUS (), and not retried. At create time such a row fails the CREATE with an actionable message; fix the row and recreate.

The hidden managed embedding table stays server-owned exactly as in the model path — the VEC82 guard still blocks any non-DBA write to it.

Querying a passthrough index

Query by vector — the index has no model, so it cannot embed query text:

SQL
SELECT TOP 3 id,
       X_SIMILAR (emb, DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL ('granite-384-local', 'I forgot my password'), 10) AS score
  FROM DB.DBA.DOCS_V
 WHERE X_SIMILAR (emb, DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL ('granite-384-local', 'I forgot my password'), 10) > 0.5
 ORDER BY 2 DESC;
idscore
30.925
10.750
20.746

A text query against a passthrough index raises a catchable VEC88:

Output
VEC88: Vector index DOCS_V_IDX has no embedding model (source_kind='vector'); query it
by vector with X_SIMILAR (col, <vector>, k), not by text.

__VEC_INDEX_REBUILD (including 'fast') works normally.


Using a remote HTTP embedding provider

Instead of the local ONNX model, you can bind an index to a remote embedding API.

Step 1: Store the API key (DBA-only)

SQL
DB.DBA.ONNX_API_KEY_SET ('ONNX_API_KEY_OPENAI', '<your-secret-key>');

Step 2: Register the provider model

SQL
DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL (
  _model_name,     -- alias for CREATE VECTOR INDEX
  _provider,       -- 'openai', 'ollama', 'lmstudio', or 'openai_compatible'
  _remote_model,   -- the provider's model name
  _dimensions,     -- must match the provider model's output dimension
  _endpoint        := NULL,   -- null = provider default
  _api_key_name    := NULL,   -- the registry name from step 1 (NOT the key itself)
  _timeout         := 30,     -- seconds, 1 to 300
  _description     := NULL,
  _insecure        := 0);

The endpoint must be https:// unless it is on a loopback or private-network host; a plain-http public endpoint is refused at registration so an API key is never sent in cleartext. See Embedding models for every parameter.

Examples:

SQL
-- OpenAI text-embedding-3-small (1536 dimensions)
DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL (
  'openai-small', 'openai',
  'text-embedding-3-small', 1536,
  NULL, 'ONNX_API_KEY_OPENAI', 30);

-- Local Ollama (no key needed)
DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL (
  'ollama-mxbai', 'ollama',
  'mxbai-embed-large', 1024);

-- Any OpenAI-compatible endpoint
DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL (
  'my-remote', 'openai_compatible',
  'bge-large-en', 1024,
  'https://my-host/v1/embeddings', 'ONNX_API_KEY_MYHOST', 30);

Step 3: Create an index over it

SQL
CREATE VECTOR INDEX DOCS_VEC ON DB.DBA.DOCS (content)
USING 'openai-small'
WITH (dimensions = 1536, distance_metric = 'cosine',
      hnsw_m = 32, ef_construction = 400);

Automatic maintenance

After creation, base-table writes keep the index current without running embedding generation in the write transaction:

  • INSERT / UPDATE / DELETE triggers enqueue work into DB.DBA.VEC_SQL_QUEUE
  • Hash-skip detects unchanged text and skips re-embedding
  • Deletes remove the managed row and the HNSW node

Draining the queue

SQL
-- One-shot manual drain (up to N items); returns the number processed:
SELECT DB.DBA.VEC_SQL_QUEUE_DRAIN(1000);

-- Or schedule a worker (the argument is the scheduler interval, in minutes):
DB.DBA.VEC_SQL_WORKER_START(1);
SELECT obj2json (DB.DBA.VEC_SQL_WORKER_STATUS());
-- ["pending",0,"error",0,"last_run",...,"last_count",...,"last_error",null]
DB.DBA.VEC_SQL_WORKER_STOP();

VEC_SQL_WORKER_START registers a scheduled event, VEC_SQL_WORKER, that drains up to 100 queued rows per run (and feeds new RDF literals to any RDF literal index). Virtuoso's scheduler counts the interval in minutes and only wakes every SchedulerInterval minutes ([Parameters] in virtuoso.ini, 10 in the shipped file), so a scheduled drain can lag a write by that long. Call VEC_SQL_QUEUE_DRAIN yourself when a write has to be searchable at once.


Querying

Query with text (primary form)

X_SIMILAR and VECTOR_DISTANCE accept the query as plain text. The server resolves the column's vector index, embeds the text with that index's own bound model, and drives the HNSW search.

SQL
SELECT TOP 10 id, content,
       VECTOR_DISTANCE(content, 'how to reset my password') AS distance,
       X_SIMILAR(content, 'how to reset my password')       AS score
FROM DB.DBA.DOCS
ORDER BY VECTOR_DISTANCE(content, 'how to reset my password');

Example output:

idcontentdistancescore
4How do I change the dba password?0.2520.874
1How do I Install Virtuoso on Linux?0.4730.763
2How do I install virtuoso on Windows?0.4810.759
3How do I install virtuoso on macOS?0.4950.753

The password question ranks first although it shares only one word with the query. The three install questions match the query poorly, yet each still scores about 0.75: scores are on the model's own scale, and with the bundled model text that shares little with the query lands well above 0.5. Judge relevance by the gap — here 0.87 against 0.75 — and set any threshold from your own data. See Similarity metrics.

Key concepts:

  • VECTOR_DISTANCE = raw distance (lower = closer)
  • X_SIMILAR = similarity in [0, 1] (higher = closer)
  • For cosine: score = 1 - distance / 2. Cosine distance runs 0..2, so halving maps it onto [0, 1]: identical vectors score 1.0, orthogonal vectors 0.5, and only opposed vectors approach 0.0. Where unrelated text lands depends on the model — about 0.75 for the install rows above.
  • For L1/L2: score = 1 / (1 + distance)

When the table's key is otherwise unbound and the query states how many rows it wants — a TOP n, or X_SIMILAR's third argument k — ORDER BY VECTOR_DISTANCE(...) or X_SIMILAR(...) in the WHERE clause drives the HNSW ANN node directly:

SQL
SELECT TOP 10 id, content
FROM DB.DBA.DOCS
WHERE X_SIMILAR(content, 'reset password') > 0.8;
-- 4  How do I change the dba password?

With a threshold of 0.7 the same query would also return the three install rows, which score about 0.74 against 'reset password'.

Filter / rerank mode

When the table is already constrained by another selective predicate, or the query gives no bound (no TOP, no k), X_SIMILAR scores the incoming rows exactly instead of leading — every row that clears the threshold is returned, with no approximation:

SQL
SELECT id, content
FROM DB.DBA.DOCS
WHERE id < 1000
  AND X_SIMILAR(content, 'reset password') > 0.8;
-- 4  How do I change the dba password?

The third argument to X_SIMILAR is k — the number of candidates to retrieve from the HNSW graph, which also sets how widely the search explores. Without it, the query's TOP n is used; with neither, the query is answered by exact scoring rather than the ANN search. A higher k improves recall:

SQL
-- k=100 retrieves 100 candidates from HNSW, then TOP 10 picks the best:
SELECT TOP 10 id, content,
       X_SIMILAR(content, 'reset password', 100) AS score
FROM DB.DBA.DOCS
WHERE X_SIMILAR(content, 'reset password', 100) > 0.0
ORDER BY X_SIMILAR(content, 'reset password', 100) DESC;

Tuning accuracy at query time

Goalk valueEffect
Fast, approximateabout the TOP n you want (e.g. 10)Quick search, may miss some matches
Balanced50–100Good recall for most use cases
Maximum accuracy200+Explores more of the graph, highest recall
Exactomit both k and TOPEvery row is scored; no approximation, cost grows with the table

Avoiding duplicate embedding calls with remote providers

With a local model, embedding a short query text costs a few milliseconds, so repeating the text literal is cheap. With a remote model every embedding is an HTTP request, and a TOP n query that ranks with ORDER BY VECTOR_DISTANCE (col, 'text') makes two of them, with or without a projected distance or score (WHERE X_SIMILAR (col, 'text') > … makes one, and two once it also projects the score). Embed once and reuse the vector:

SQL
SELECT TOP 10 id, content,
       VECTOR_DISTANCE(content, qv.v) AS distance,
       X_SIMILAR(content, qv.v)       AS score
FROM DB.DBA.DOCS
CROSS JOIN (SELECT DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL('openai-small',
  'how to reset my password') AS v) qv
ORDER BY VECTOR_DISTANCE(content, qv.v);

Confirm which mode (EXPLAIN)

SQL
EXPLAIN('SELECT TOP 10 id, content,
  VECTOR_DISTANCE(content, ''reset password'') AS distance
FROM DB.DBA.DOCS
ORDER BY VECTOR_DISTANCE(content, ''reset password'')');
  • Leading mode: output contains vvec_search_node with mode=ann
  • Filter mode: output contains a call to __VEC_INDEX_ROW_SIMILARITY, mode=row_distance, and no vvec_search_node

Inspect and operate

Each of these returns a name/value vector; obj2json prints it readably in isql, and get_keyword ('graph_nodes', …) reads a single field.

SQL
-- Index metrics (node/insert/deleted counts, dimension, hnsw_m, state):
SELECT obj2json (__VEC_INDEX_METRICS ('DOCS_VEC'));
-- ["dimension",384,"distance_metric","cosine","hnsw_m",16,"managed_rows",4,
--  "graph_nodes",4,"deleted_nodes",0,"insert_count",4,"search_count",4,
--  "search_usec",0,"bytes_written",65536,"layer0_hit_rate",1,
--  "state","READY_FOR_INTERNAL_LOAD"]

-- Page stats (node pages, entry point, max layer, page format):
SELECT obj2json (__VEC_INDEX_PAGE_STATS ('DOCS_VEC'));
-- ["metadata_pages",1,"node_pages",4,"deleted_pages",0,"entry_point_dp",38864,
--  "max_layer",0,"inline_dim_limit",1742,"overflow_pages",0,
--  "deleted_overflow_pages",0,"physical_node_pages",4,"format","legacy"]

-- Exact-vs-ANN recall validation and graph/embedding consistency:
SELECT obj2json (__VEC_INDEX_VALIDATE ('DOCS_VEC'));
-- ["queries_tested",4,"recall_at_10",1,"distance_metric","cosine","dimension",384,
--  "consistent",1,"consistency_detail","graph agrees with the stored embeddings",
--  "graph_epoch",4,"data_epoch",4,"active_nodes",4,"embedding_rows",4]

-- Queue/worker health:
SELECT obj2json (DB.DBA.VEC_SQL_WORKER_STATUS ());

Rebuild and compact

__VEC_INDEX_REBUILD rebuilds the HNSW graph from the durable stored embeddings — no re-embedding or model call is needed. Use it to recover an index that was marked needs_rebuild after an unclean shutdown (a VEC80 error on search), or to compact an index that has grown into the legacy page format through many incremental inserts:

SQL
-- Check the current page format:
SELECT get_keyword ('format', __VEC_INDEX_PAGE_STATS ('DOCS_VEC'));   -- legacy

-- Fast rebuild → compacts to the packed page format (~1.3× amplification at 384-d):
SELECT __VEC_INDEX_REBUILD ('DOCS_VEC', 'fast');                       -- 4 (embeddings)
SELECT get_keyword ('format', __VEC_INDEX_PAGE_STATS ('DOCS_VEC'));   -- packed

-- Confirm health:
SELECT __VEC_INDEX_HEALTH_RECHECK ('DOCS_VEC');  -- 1 = ok

Name the method: called with only the index name, __VEC_INDEX_REBUILD uses the index's rebuild option, whose default auto chooses the fast builder only from 10,000 embeddings and otherwise replays the graph incrementally — which leaves a small index in the legacy format.

For the rebuild methods (replay/fast/auto), durability epochs, and crash recovery, see Maintenance: durability and recovery.


Drop an index

SQL
DROP INDEX DOCS_VEC;

This removes the hidden managed table, maintenance triggers, and the native HNSW index. It does not drop the base table or the registered model.


Permissions

ToA non-DBA account needs
Create or drop a vector indexA DBA account. A non-DBA is refused even on a table it owns — the create reads the model registry and runs DBA-owned maintenance procedures.
Insert, update or delete rows of an indexed tableThe ordinary table privileges. The maintenance triggers enqueue the change on the account's behalf; draining the queue is a DBA operation.
Search with X_SIMILAR / VECTOR_DISTANCE by textSELECT on the table, and read access to the model registry: the SPARQL_SELECT role, or GRANT SELECT (model_id, model_name, model_type, handle) ON DB.DBA.ONNX_MODELS TO "<account>". Without the latter, a text search fails with SQ033: SELECT access denied for column model_id of table DB.DBA.ONNX_MODELS.

Deployment and sizing

A server starts two ONNX helper processes per loaded local model; a SPARQL refresh vector index raises the pool to its embedding concurrency (__vec_embed_concurrency, default 4), and a pool does not shrink while the server runs. With the bundled model each helper holds 265–285 MB at rest (265 MB freshly loaded, 276–285 MB after a few short texts), and its inference arenas grow with long inputs — 547–620 MB after one batch of 32 long passages. On top of that sits Virtuoso's buffer pool (NumberOfBuffers * 8 KB).

Swap recommendations

Instance RAMSwapNotes
0.5 GB2 GB (essential)Below the practical floor: a stock server and its two helpers measured ~840 MB at rest
1 GB2 GB (recommended)Covers model-load spike + batch drain
2 GB2–4 GB (safety net)Fine with tuned RAM

Tuning for small instances

  1. Cut NumberOfBuffers — budget ~64 MB per 8000 buffers
  2. Limit ONNX Runtime threads on 1–2 vCPU nodes
  3. Budget two or more helpers per local model — up to about 285 MB each at rest with the bundled model
  4. Keep embedding batches and __vec_embed_concurrency modest

Limitations

  • The table needs a single-column integer primary key (INTEGER, BIGINT, SMALLINT or NUMERIC); composite and character keys are refused with VEC11
  • The source column must be a character column (VARCHAR, NVARCHAR, LONG VARCHAR, LONG NVARCHAR), or ANY / LONG ANY / (LONG) VARBINARY for precomputed vectors
  • dimensions must match the model's output dimension exactly (or be the reduced dimension with reduce = 'truncate')
  • The choice between leading ANN search and filter/rerank is the optimizer's: another selective predicate on the table keeps a query in filter mode, and otherwise the ANN path is costed from the index's own search statistics. VectorANNCostBias in the [Parameters] section of virtuoso.ini (default 1.0) scales that cost; values below 1 favour ANN plans. Check the choice with EXPLAIN

Troubleshooting

SymptomCause & fix
SR185: Undefined procedure DB.DBA.ONNX_EMBED_TEXTThe vonnx plugin isn't loaded. Add it to [Plugins] in virtuoso.ini, restart. The server log then shows ONNX bootstrap disabled granite-384-local: … and DB.DBA.ONNX_MODELS has no row for it.
VEC18: Model granite-384-local is not present in DB.DBA.ONNX_MODELSThe plugin did not load, or — on a new database — the bootstrap could not find the model files. Check the server log for the ONNX bootstrap line, which names the paths it tried, and set [ONNX] ModelRoot / HelperPath to absolute paths.
CREATE VECTOR INDEX failsVerify ONNX health: SELECT model_name, handle FROM DB.DBA.ONNX_MODELS; (handle must be non-null).
dimensions mismatch on createWITH (dimensions = N) must equal the model's output (bundled granite-384-local = 384, OpenAI text-embedding-3-small = 1536).
Query returns 0 rowsThe HNSW graph may be empty. Check get_keyword ('graph_nodes', __VEC_INDEX_METRICS ('DOCS_VEC')) — it should be > 0. If 0, drop and recreate.
HTTP provider answers 401The key is missing, or the 6th registration argument was the key value instead of its registry name. Store the key with DB.DBA.ONNX_API_KEY_SET and register with its name (or null for openai).
New rows don't appear in searchDrain the queue: SELECT DB.DBA.VEC_SQL_QUEUE_DRAIN(1000);
VEC33: Malformed query vectorThe query text was bound as a parameter; only a text literal is embedded. Embed the parameter with DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL and pass the vector — see Query with text.
DROP VECTOR INDEX syntax errorUse DROP INDEX <name>; — no ON <table> clause.

Health check

SQL
SELECT model_name, handle FROM DB.DBA.ONNX_MODELS WHERE handle IS NOT NULL;
SELECT length (DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL('granite-384-local', 'health check'));  -- 384
SELECT obj2json (__VEC_INDEX_METRICS ('DOCS_VEC'));

Next steps

Reference:

PreviousTutorial: Semantic search over OpenLink FAQs with a SPARQL vector index NextHow-To: Build and use the SPARQL vector index (Virtuoso)