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
vonnxplugin loaded in yourvirtuoso.ini - An embedding model registered in
DB.DBA.ONNX_MODELS(auto-provisioned at startup for the bundledgranite-384-localmodel)
Verify the plugin and model at startup:
-- 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: 384If the query returns no row, or handle is null, see Troubleshooting.
Build Virtuoso with ONNX support
Configure and build
From the repository root:
./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 thevonnxplugin and itsonnx_helperprogram.<dir>is an ONNX Runtime installation prefix that holds the static librarylib/libonnxruntime.aand the C API headers (ininclude/orinclude/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.mdlists ONNX Runtime 1.29.0 or later. Given without a directory,--enable-onnxruntimeaskspkg-configforlibonnxruntime. If no usablelibonnxruntime.ais found,configurewarns and the server is built without the plugin.--enable-vvec-vadalso buildsvvec_dav.vad, the VVec console package (--enable-all-vadsincludes 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:
[ONNX]
ModelRoot = /opt/virtuoso-opensource/hosting/onnx-models
;HelperPath = /opt/virtuoso-opensource/hosting/onnx_helper
[Plugins]
LoadPath = /opt/virtuoso-opensource/hosting
Load17 = plain, vonnxFor 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:
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 type | Description | Handle required? |
|---|---|---|
text_embedding | Local ONNX model (e.g. the bundled granite-384-local) | Yes — loaded at startup |
openai | OpenAI API (text-embedding-3-small, etc.) | No — HTTP provider |
ollama | Local Ollama server | No — HTTP provider |
lmstudio | Local LM Studio server | No — HTTP provider |
openai_compatible | Any OpenAI-compatible API | No — HTTP provider |
Embed text manually (for testing)
-- 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).
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:
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:
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:
CREATE TABLEdefines a table with an integer PK and a text column.INSERTpopulates rows before index creation so they get backfilled.CREATE VECTOR INDEXbinds the index to thegranite-384-localmodel and builds the HNSW graph.
Key concepts:
USING 'model_name'binds the index to a specific model inONNX_MODELS.dimensionsmust match the model's output dimension exactly (unlessreduce = 'truncate'is used, in which case it's the reduced dimension). It can also be left out — see the short form above.distance_metriccan becosine(default),l2, orl1.hnsw_mandef_constructioncontrol HNSW graph quality.reducecan 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
- Validates the model binding and dimension match.
- Creates a hidden managed embedding table.
- Creates the native HNSW index over the managed embeddings.
- Creates
AFTER INSERT/UPDATE/DELETEmaintenance triggers. - Backfills existing rows — every row of the table, embedded inside the
CREATEstatement; the index is populated and searchable whenCREATEreturns.
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
| Parameter | Default | Accuracy-focused | Effect |
|---|---|---|---|
hnsw_m | 16 | 32 | Max neighbors per node per layer |
ef_construction | 200 | 400 | Search 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:
registry_set ('__vvec_passthrough_column', 'on');With the flag off, a source_kind = 'vector' create fails with VEC83:
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:
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'replacesUSING— supplying both is contradictory and fails withVEC84; omitting both fails withVEC12.dimensionsis 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
NULLcolumn 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 markederrorwith the message (for example22023: VEC31: Embedding dimension 2 does not match vector index dimension 384.), counted inDB.DBA.VEC_SQL_WORKER_STATUS (), and not retried. At create time such a row fails theCREATEwith 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:
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;| id | score |
|---|---|
| 3 | 0.925 |
| 1 | 0.750 |
| 2 | 0.746 |
A text query against a passthrough index raises a catchable VEC88:
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)
DB.DBA.ONNX_API_KEY_SET ('ONNX_API_KEY_OPENAI', '<your-secret-key>');Step 2: Register the provider model
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:
-- 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
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
-- 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.
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:
| id | content | distance | score |
|---|---|---|---|
| 4 | How do I change the dba password? | 0.252 | 0.874 |
| 1 | How do I Install Virtuoso on Linux? | 0.473 | 0.763 |
| 2 | How do I install virtuoso on Windows? | 0.481 | 0.759 |
| 3 | How do I install virtuoso on macOS? | 0.495 | 0.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)
Leading-index (HNSW-driven) search
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:
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:
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?Using the k parameter for ANN search
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:
-- 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
| Goal | k value | Effect |
|---|---|---|
| Fast, approximate | about the TOP n you want (e.g. 10) | Quick search, may miss some matches |
| Balanced | 50–100 | Good recall for most use cases |
| Maximum accuracy | 200+ | Explores more of the graph, highest recall |
| Exact | omit both k and TOP | Every 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:
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)
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_nodewithmode=ann - Filter mode: output contains a call to
__VEC_INDEX_ROW_SIMILARITY,mode=row_distance, and novvec_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.
-- 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:
-- 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 = okName 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
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
| To | A non-DBA account needs |
|---|---|
| Create or drop a vector index | A 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 table | The 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 text | SELECT 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 RAM | Swap | Notes |
|---|---|---|
| 0.5 GB | 2 GB (essential) | Below the practical floor: a stock server and its two helpers measured ~840 MB at rest |
| 1 GB | 2 GB (recommended) | Covers model-load spike + batch drain |
| 2 GB | 2–4 GB (safety net) | Fine with tuned RAM |
Tuning for small instances
- Cut
NumberOfBuffers— budget ~64 MB per 8000 buffers - Limit ONNX Runtime threads on 1–2 vCPU nodes
- Budget two or more helpers per local model — up to about 285 MB each at rest with the bundled model
- Keep embedding batches and
__vec_embed_concurrencymodest
Limitations
- The table needs a single-column integer primary key (
INTEGER,BIGINT,SMALLINTorNUMERIC); composite and character keys are refused withVEC11 - The source column must be a character column (
VARCHAR,NVARCHAR,LONG VARCHAR,LONG NVARCHAR), orANY/LONG ANY/(LONG) VARBINARYfor precomputed vectors dimensionsmust match the model's output dimension exactly (or be the reduced dimension withreduce = '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.
VectorANNCostBiasin the[Parameters]section ofvirtuoso.ini(default1.0) scales that cost; values below 1 favour ANN plans. Check the choice with EXPLAIN
Troubleshooting
| Symptom | Cause & fix |
|---|---|
SR185: Undefined procedure DB.DBA.ONNX_EMBED_TEXT | The 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_MODELS | The 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 fails | Verify ONNX health: SELECT model_name, handle FROM DB.DBA.ONNX_MODELS; (handle must be non-null). |
dimensions mismatch on create | WITH (dimensions = N) must equal the model's output (bundled granite-384-local = 384, OpenAI text-embedding-3-small = 1536). |
| Query returns 0 rows | The 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 401 | The 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 search | Drain the queue: SELECT DB.DBA.VEC_SQL_QUEUE_DRAIN(1000); |
VEC33: Malformed query vector | The 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 error | Use DROP INDEX <name>; — no ON <table> clause. |
Health check
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
- SPARQL vector index how-to — Use vector indexes with RDF data and SPARQL queries
- Vector index overview — Architecture and concepts
Related content
Reference:
- ONNX Runtime — Microsoft ONNX Runtime project
- granite-embedding-30m-english — Bundled embedding model
- HNSW algorithm — Hierarchical Navigable Small World graphs
PreviousTutorial: Semantic search over OpenLink FAQs with a SPARQL vector index NextHow-To: Build and use the SPARQL vector index (Virtuoso)