---
title: "\"Managing Vector Indexes Visually with the VVec Console\""
description: "Learn how to use VVec, a Virtuoso Server Pages (VSP) console for creating SQL and SPARQL vector indexes, running similarity search, registering embedding models, and draining maintenance queues from your browser."
url: urn:osdi:this
dateModified: 2026-10-30T15:45:34Z
language: en
---
One page of the Vector Index documentation, MERGED by build_vector_index_docs.py from this template and the markdown in fixture/vector-index/. Do not edit the generated file: change this template or the generator and rebuild. It opens on the documentation header (dxshell.py), not on the site hero.

[Home](urn:osdi:site)/[Virtuoso](urn:osdi:sitevirtuoso/)/[Vector index](urn:osdi:sitevirtuoso/docs/vector-index/)/Managing vector indexes visually with the VVec Console

# Managing vector indexes visually with the VVec Console

Learn how to use VVec, a Virtuoso Server Pages (VSP) console for creating SQL and SPARQL vector indexes, running similarity search, registering embedding models, and draining maintenance queues from your browser.

Start  [Overview](urn:osdi:sitevirtuoso/docs/vector-index/) [Quickstart](urn:osdi:sitevirtuoso/docs/vector-index/quickstart/) [Tutorial: FAQ semantic search](urn:osdi:sitevirtuoso/docs/vector-index/tutorial-faq-semantic-search/)  Build  [SQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sql-vector-index/) [SPARQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sparql-vector-index/) [Search RDF literals (knnSimilar)](urn:osdi:sitevirtuoso/docs/vector-index/knn-similar/)  Configure  [Embedding models](urn:osdi:sitevirtuoso/docs/vector-index/embedding-models/) [Index parameters](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/) [Similarity metrics](urn:osdi:sitevirtuoso/docs/vector-index/similarity-metrics/)  Operate  VVec Console (web UI) [Troubleshooting](urn:osdi:sitevirtuoso/docs/vector-index/troubleshooting/)  Reference  [API reference](urn:osdi:sitevirtuoso/docs/vector-index/api-reference/)

**VVec** is a lightweight, self-contained web console for the Virtuoso vector index. It runs entirely inside Virtuoso as a set of [Virtuoso Server Pages (VSP)](https://docs.openlinksw.com/virtuoso/vsp/) served from a WebDAV collection — no external framework, build step, or client-side dependency. It gives operators a point-and-click surface over the same SQL and SPARQL entry points documented in the [API reference](urn:osdi:sitevirtuoso/docs/vector-index/api-reference/): `CREATE VECTOR INDEX`, `X_SIMILAR` / `VECTOR_DISTANCE`, `sparql create vector index`, `bif:vector_search`, the `DB.DBA.ONNX_MODELS` registry, and the maintenance queues.

*Figure: The vvec Overview page: four stat cards counting total, SQL and SPARQL indexes and registered embedding models, each linking on to the page that manages them*

With VVec you can:

- **Browse** every vector index on the instance — relational (SQL) and RDF (SPARQL) — with model, dimensions, metric, and state at a glance.
- **Create** an index on the **SQL side** (a relational text column) or the **SPARQL side** (an RDF predicate, with a **Simple** or **Custom** selector).
- **Search by meaning** — type natural-language text, pick one or more indexes, and get the nearest matches ranked by similarity.
- **Edit** an index — change any part of its definition and save; the index is dropped and rebuilt from the new definition. Its name and description change without a rebuild.
- **Manage namespace prefixes** — list, add, rebind and remove the instance's persistent SPARQL prefixes, which the graph and predicate fields accept as prefixed names (`schema:name`).
- **Manage embedding models and API keys** — inspect `DB.DBA.ONNX_MODELS`, register and edit remote HTTP providers, store their keys, reload local ONNX models, and test embeddings.
- **Choose label properties** — decide which properties name an entity in SPARQL results.
- **Grant annotation rights** — let non-DBA accounts name and describe the indexes they own.
- **Drive maintenance** — drain the SQL and SPARQL queues and control their workers.

Note

VVec is part of Virtuoso Open Source. Its source is in `binsrc/vvec/` (the VSP pages in `vsp/`, `css/vvec.css`, `sql/vvec_install.sql`, and a README), and it is delivered as a **VAD package**, `vvec_dav.vad`. It is an operator tool that runs with DBA privileges — see [Access and security](#access-and-security).

---

## Deploy and open the console

VVec is installed as a **VAD package** (`vvec_dav.vad`) — not by running a SQL script directly. Installing it uploads the VSP pages and stylesheet into the `/DAV/VAD/vvec/` WebDAV collection, runs the bundled `sql/vvec_install.sql` — which defines the `DB.DBA.VVEC_*` procedures, creates the sign-in session store and seeds the label properties — and maps the `/vvec/` virtual directory, behind the console's sign-in, to that collection.

The console drives the vector index and its embedding models, so the server needs them too: Virtuoso built with the ONNX plugin (`--enable-onnxruntime=DIR`) and `vonnx` loaded, with at least one model in `DB.DBA.ONNX_MODELS` — the bundled `granite-384-local` is registered at startup. See the [SQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sql-vector-index/) for the build.

Important

Running `sql/vvec_install.sql` by hand from `isql` is **not** enough. It defines the procedures and maps `/vvec/`, but it does not upload the pages into WebDAV, so the console redirects to a sign-in page that answers **404 Not Found**. The pages are uploaded by the VAD, so install the VAD.

### Build the VAD

The VVec VAD is not built by default. Configure Virtuoso with `--enable-vvec-vad` (just this package) or `--enable-all-vads` (every VAD package); `make` then produces `binsrc/vvec/vvec_dav.vad`, and `make install` copies it into the installation's VAD directory with the other packages:

Shell

```
./configure … --enable-vvec-vad
make
```

Important

Get the option name exactly right — `--enable-vvec-vad` or `--enable-all-vads` (note `enable`, not `with`, and the trailing **s** on `all-vads`). An option that does not exist does **not** stop `configure`; it only adds a `WARNING: unrecognized options: …` line at the very end of the run, which is easy to scroll past. The build then completes with no `.vad` file and no error, because `binsrc/vvec/Makefile` was generated with its `nodist_vad_DATA = vvec_dav.vad` line commented out.

To confirm the option took effect before building:

Shell

```
grep -n 'nodist_vad_DATA' binsrc/vvec/Makefile
```

The line must be live — a leading `#` means the VAD is still disabled.

If you have already built the tree and only need the package, invoke the packaging rule directly — it does not depend on the option:

Shell

```
make -C binsrc/vvec vvec_dav.vad
```

It prints `INFO: Vadpacker completed without errors` on success.

### Install the VAD

Install it from the **Conductor** (**System Admin → Packages**), or from `isql` as `dba`:

Shell

```
isql <host>:<port> dba <password>
```

SQL

```
vad_install ('<path>/vvec_dav.vad', 0);
```

On success it reports `Installation of "VVec" is complete.` Then open the console in a browser; it takes you to the sign-in page, where a **DBA** account is required:

Output

```
http://<host>:<http-port>/vvec/
```

Tip

For the `isql` install, the `.vad` file must sit in a directory listed in `DirsAllowed` in `virtuoso.ini` — otherwise `vad_install` cannot read it. The `virtuoso.ini` that `make install` writes already lists the VAD directory, which is where the package is installed. The uploaded pages themselves need no `DirsAllowed` entry; they are served from WebDAV.

---

## Overview

**Overview** is the landing page, and it is only a summary: four stat cards — total, SQL and SPARQL index counts, and the registered embedding models — each linking through to the page that can act on it. The catalogue itself lives on its own page.

The sidebar groups the pages by what you come to do: **Use** (Create Index, Similarity Search), **Manage** (Overview, Manage Indexes) and **Configure** (Embedding Models, API Keys, Namespace Prefixes, Labels, Permissions, Maintenance).

---

## Manage Indexes

**Manage Indexes** lists every relational and RDF index together, tagged `SQL` or `SPARQL`, with its target, bound model, dimension, distance metric, and state.

**State** reads `ready` for every healthy index on both surfaces, and **`empty`** in amber for one that is built but holds nothing — an RDF selector that matched no text, or a relational column with no values. Such an index still creates cleanly and reports `active`, so without this the catalogue would show a green pill on an index that answers every search with nothing. Searches against it return no rows until the source has data and the index is refreshed. The engine keeps two vocabularies for the ready condition — a relational index rests in `READY_FOR_INTERNAL_LOAD` or `READY`, an RDF one in `active` — and the console shows a single word, because there is no difference between them to act on. The states that *do* differ keep their own names: `creating`, `refreshing`, `draining` and `dropping` in amber, `error` in red. A catalogue query still sees the engine's own values.

The target is shown on two lines — the container above the thing inside it: table above column for a relational index, graph above predicate for a Simple RDF one. A **Custom** selector has no second half, because it is an arbitrary query: its first line shows the graphs named in its `FROM` / `FROM NAMED` clause (a dash when it has none), and a **(query)** link below it opens the stored selector in full.

The **Name** column uses the same two lines for an RDF index that has been given one: the IRI on top, because that is what everything addresses the index by, and its name underneath. An index with no name shows only the IRI rather than an empty second line.

### Acting on several at once

Each row has a checkbox, and the header one selects every index. As soon as anything is selected a bar appears above the table with the count and **Search**, **Refresh**, **Drop** and **Deselect all**.

**Edit** disappears once more than one index is selected — editing is a single-index act. At exactly one selected it stays.

Two limits are shown in the bar rather than discovered after a click:

- **Search** refuses a mixed selection: a search cannot combine SQL and SPARQL indexes yet, so one search runs against one surface. (The two surfaces score a document alike — the same model and metric give the same score on both.)
- **Refresh** is an RDF operation: a relational index has no selector to re-evaluate. Relational rows in a selection are skipped, and the bar says how many of the chosen are refreshable.

A bulk action runs each index on its own, so one failure does not stop the rest; the banner reports how many succeeded and names the first that did not.

Each row also has inline actions:

- **Search** jumps to the similarity-search page with that index preselected. The **Try a similarity search** link on the Create page does the same with the index you have just built, so you can type a query and search without opening the picker.
- **Edit** opens the index definition for change — see [Edit an index](#edit-an-index).
- **Refresh** (SPARQL rows) re-evaluates the selector and re-embeds changed rows.
- **Drop** removes the index (relational `DROP INDEX` or `sparql drop vector index`), after a confirmation prompt.

Note

The hidden backing index of a SPARQL vector index is intentionally omitted from the SQL list — it is an internal implementation detail of the RDF surface.

---

## Create a vector index

The **Create Index** page defines a new index. A segmented control switches between the **SQL** and **SPARQL** surfaces; the form adapts to the surface you choose.

*Figure: The Create Vector Index form on the SQL surface, with index name, table, text column, embedding model, dimensions, distance metric, and HNSW parameters*

**SQL surface.** Type the base table's name — qualified (`DB.DBA.DOCS`) or bare (`DOCS`, looked up under `DB.DBA`); the field suggests the `DB.DBA` tables that qualify. A second after you stop typing, the console looks the table up: one with a single-column `INTEGER` primary key and at least one `VARCHAR` / `LONG VARCHAR` column fills the **Text column** list with those columns, and one that does not qualify is reported with the reason. (The form is stricter than `CREATE VECTOR INDEX`, which also takes a `SMALLINT`, `BIGINT` or `NUMERIC` key and `NVARCHAR` columns; create such an index in SQL.) Pick an embedding model — the dimension fills in from it — and adjust the distance metric (`cosine`, `l2`, `l1`), `hnsw_m`, `ef_construction`, and Matryoshka `reduce` if you need to. Submitting runs `CREATE VECTOR INDEX … ON <table>(<column>)` and backfills existing rows.

**SPARQL surface.** Choose **Simple** (one graph + one predicate) or **Custom** (a `KEY`/`EMBEDDING` variable pair plus a `WHERE` pattern, so the selector can join and filter), then a model and the same HNSW options plus a `maintenance` mode (`manual` or `async`) and a **Build method**. Submitting runs `sparql create vector index …` and then a `refresh` to populate the graph.

The two fields under the index IRI — **Name** and **Description** — are optional and describe the index rather than configuring it. They are what the search picker shows when you are choosing between several indexes, and they are published as `rdfs:label` and `rdfs:comment` in `<urn:vvec:indexes>`. See Naming an index.

Note

**You do not have to wait on this page.** The build runs inside the request — embedding every source row, which is minutes on a large source — but it finishes on the server whether or not the browser is still connected, so closing the tab or letting it time out does not abort it. [Manage Indexes](#manage-indexes) shows the index as `refreshing` until it is `ready`. What you lose by leaving is the result banner, which is the only place that reports an index that ended in `error`.

Note

Simple and Custom are what the two selector forms are called in the console. In the engine and in `DB.DBA.SPARQL_VEC_INDEX.SVI_SELECTOR_TYPE` they are `shorthand` and `general`, which is what you will see in the SQL reference and in a catalogue query.

**Build method** (SPARQL surface only) selects how the first backfill constructs the graph — `auto (incremental)`, the default; `incremental`; or `bulk`, which embeds everything first and then derives the whole graph in one parallel pass. Bulk is much faster and smaller on a large first build, and it matters most for exactly the kind of build someone starts from a console. It is not the default because the parallel builder is non-deterministic: two builds of the same data give different graphs, which on a corpus of near-duplicates can rank an exact match below one. See [Index parameters: build](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/#build-default-auto-which-means-incremental).

The console's SQL form does not offer the control. SQL `CREATE VECTOR INDEX` takes the option in its `WITH` clause — `build = 'bulk'` — for the backfill it runs over the rows already in the table.

Both the **Graph IRI** and **Predicate IRI** fields accept either a full IRI or a **prefixed name** — `schema:name` is expanded to `http://schema.org/name` from the instance's declared prefixes before the DDL is built, and the expanded IRI is what the confirmation shows and what the catalog stores. `schema` is not one of the prefixes Virtuoso declares out of the box, so add it on [Namespace prefixes](#namespace-prefixes) first. A value whose prefix is *not* declared is left exactly as typed, as is a full IRI or a `urn:`. Each field has its own picker, and together they let you define a Simple selector without typing an IRI. **Search graphs**, beside **Graph IRI**, lists the named graphs on the instance and searches them as you type. **List predicates**, beside **Predicate IRI**, then lists the predicates *in that graph* whose objects are string literals — the ones a Simple index can embed — so numeric and date predicates are left out. Each is shown as its prefixed name, `schema:name`, with the full IRI beneath it in small type, and falls back to the plain IRI when no declared prefix covers it; the filter matches either spelling. Clicking an entry puts the full IRI in the field.

Tip

**List predicates** reads the graph currently in the Graph IRI field, so fill that in first; it says so if you have not. A prefixed name works there too. To stay quick on a very large graph it looks at the first 200,000 of the graph's triples, so a predicate that only appears beyond them is not listed — type its IRI instead.

Note

The graph list comes from `DB.DBA.SPARQL_SELECT_KNOWN_GRAPHS`, which walks the `G` index only, so it stays fast on a large store — its cost tracks the number of graphs rather than the number of triples. If you enumerate graphs yourself, note that `select distinct ?g where { graph ?g { ?s ?p ?o } }` is rewritten to that same call, but adding a `FILTER` to the query — inside the `GRAPH` group or after it — defeats the rewrite and turns it back into a full quad scan. Filter the list afterwards instead, as the console does.

Tip

VVec counts the subjects the selector produced and reports them: a successful create says how many were indexed. A selector whose IRIs exist but match no text — an IRI-valued predicate, say, or a graph whose data is not loaded yet — still creates cleanly and goes `active`; the page warns in amber that 0 subjects were indexed, and [Manage Indexes](#manage-indexes) shows the index as `empty`, so every search against it returns no rows. A prefixed name whose prefix is not declared is the other usual cause of an index that finds nothing: it is kept as typed, no triple uses that IRI, and the create ends in `error` with `… is not a known IRI, so the selector matches nothing`.

Inputs are validated against the catalog before any DDL is built (the table and column must exist; the index name and IRIs must pass strict character checks), and the page reports the exact error if creation or the follow-up refresh leaves the index in a non-active state.

Tip

For the meaning of each parameter, see [Index parameters](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/) and [Similarity metrics](urn:osdi:sitevirtuoso/docs/vector-index/similarity-metrics/). For the model choices, see [Embedding models](urn:osdi:sitevirtuoso/docs/vector-index/embedding-models/).

---

## Edit an index

A vector index definition is **immutable in the engine** — there is no `ALTER VECTOR INDEX`. The **Edit** action on a Manage Indexes row therefore does the only thing that can change a definition: it **drops the index and creates it again** from the edited form, which discards the stored HNSW graph and re-embeds the source data. The page says so in a banner and asks for confirmation before it runs, and the rebuild takes as long as the original build did — searches against the index fail while it is in flight.

The form opens prefilled with the index's current definition, read back out of the catalog. Everything the Create page offers is editable, with two exceptions that define *which* index this is:

- **SQL surface** — the index name and the base table are fixed. The text column, model, dimensions, distance metric, `hnsw_m`, `ef_construction` and `reduce` are editable.
- **SPARQL surface** — the index IRI is fixed. The selector (Simple or Custom, with its graph/predicate or key/embedding variables, `WHERE` pattern and dataset clause), model, dimensions, distance, `hnsw_m`, `ef_construction`, `maintenance`, `reduce` and build method are editable.

**Name and description are edited separately, and cost nothing.** On an RDF index they sit in their own card above the definition, with their own Save, because they describe the index rather than define it — there is no reason to re-embed a corpus to fix a typo in a label. That Save calls `DB.DBA.VEC_INDEX_SET_METADATA`, which writes the two catalogue columns and touches nothing else: the HNSW graph is left alone and searches keep answering throughout. Clearing a box removes the value.

Rebuilding the definition preserves them: the regenerated DDL carries whatever the catalogue holds, including on the restore-on-failure replay.

Two properties the form does not expose are carried through from the catalog rather than dropped on the floor: a Custom selector's captured `PREFIX` preamble (shown read-only, so you can see which prefixes its `WHERE` pattern resolves against) and `source_kind = 'vector'` for a precomputed-vector passthrough index.

Important

If the edited definition fails to create *after* the drop has already gone through, VVec replays the original definition so the instance is not left without the index, and the banner says whether that restore succeeded. It is still a rebuild either way — the restored index is repopulated from scratch, not recovered.

An RDF index can be edited only while it is `ready` or in `error`; one that is creating, refreshing, draining or dropping must settle first. A stored definition that the console cannot reproduce verbatim — for example a selector written by hand from `isql` that does not pass the console's checks — is refused, with a pointer to edit it from `isql` instead.

---

## API keys

The **API Keys** page manages the provider credentials that HTTP embedding models reference. Keys are not a table: they live in the SQL registry, and a model stores the *name* of its key rather than the key itself, so replacing a value takes effect immediately for every model referencing that name.

The list is the union of the names models reference and any registry entry following the `ONNX_API_KEY_` convention, so a key nothing uses yet is still manageable and a model pointing at a *missing* key is still visible. Each row shows whether a value is set and which models use it, with **Replace** and **Delete** — or **Set key** for a name that has no value yet. A form below the list adds a key under any name, or replaces the value stored under an existing one.

Note

Nothing about a stored value is read back or measured — set-ness comes from whether the name appears in the registry at all. Deleting a key that models still reference leaves them registered but unable to embed until a value is stored again; the confirmation names them.

---

## Namespace prefixes

The **Namespace Prefixes** page manages `DB.DBA.SYS_XML_PERSISTENT_NS_DECL` — the instance-wide set of SPARQL/XML namespace prefixes. It is the same set the built-in [SPARQL query editor](https://docs.openlinksw.com/virtuoso/rdfsparql/) lists under *Namespace Prefixes*, so a prefix added here is in scope for every SPARQL request that does not declare its own, including the graph and predicate IRIs of a vector index and a Custom selector's `WHERE` pattern.

From the page you can:

- **Add or rebind** a prefix — the form calls `DB.DBA.XML_SET_NS_DECL (prefix, iri, 2)`, so re-using an existing prefix name points it at the new IRI.
- **Edit** a row — copies it into the form so you can change the IRI.
- **Remove** a prefix — calls `DB.DBA.XML_REMOVE_NS_BY_PREFIX (prefix, 2)`.

The equivalent from `isql` is:

SQL

```
DB.DBA.XML_SET_NS_DECL ('schema', 'http://schema.org/', 2);
```

Note

Virtuoso re-declares a set of well-known prefixes (`rdf`, `rdfs`, `owl`, `xsd`, `foaf`, `dc`, `dcterms`, `skos`, …) at every startup, so removing one of those from this page is effective only until the server restarts.

A query can always override the shared set with its own declaration, which is what a Custom selector created with a local `PREFIX` does — VVec preserves that preamble across an edit.

Prefixes declared here are also what the **Graph IRI** and **Predicate IRI** fields on the Create and Edit pages expand a prefixed name against, so adding `schema` here is what makes `schema:name` work as a predicate there.

---

## Labels

A SPARQL result names a subject by its IRI, which is an address rather than a name. Every dataset says the name differently — `rdfs:label`, `schema:name`, `skos:prefLabel`, `dcterms:title`, `foaf:name` — so rather than the console holding a list, the properties are declared `rdfs:subPropertyOf rdfs:label` in the graph `urn:vvec:labels`, that graph is registered as the inference rule set **`vvec-labels`** with `rdfs_rule_set()`, and the search page asks for `rdfs:label` under it:

SPARQL

```
DEFINE input:inference <vvec-labels>
SELECT ?s ?l
 WHERE { VALUES ?s { ...the subjects ON this page... }
         GRAPH ?g { ?s rdfs:label ?l } }
```

One query per result page, driven from a `VALUES` list so each subject is an index lookup.

The **Labels** page lists the properties and lets you add or remove one. You give just the property — a full IRI, or a qname whose prefix is registered on the [Prefixes](#namespace-prefixes) page — and the console writes the `rdfs:subPropertyOf` statement and rebuilds the rule set. `rdfs:label` itself is always matched and is not in the editable list: it is what the others resolve to.

The defaults — `schema:name`, `skos:prefLabel`, `skos:altLabel`, `foaf:name`, `dcterms:title` and `dc:title` — are seeded on install, and only into an empty graph, so an upgrade never hands back a list you have pruned. **Restore defaults** follows the same rule: it reseeds the list when it is empty and leaves a non-empty list as it is, so to put back a single default you removed, add it again by hand.

Note

Changing the list affects searches run afterwards. A query the engine has already compiled keeps the rule set it was compiled with.

This is the same mechanism the Faceted Browser uses for its own labels, which builds `urn:fct:facets` and registers it as `facets`.

---

## Permissions

**Permissions** administers who, beyond a DBA account, may set an index's **Name** and **Description**. It manages exactly one thing so far: SQL `EXECUTE` on `DB.DBA.VEC_INDEX_SET_METADATA`.

The list is read straight out of `DB.DBA.SYS_GRANTS`, so a grant made from `isql` appears here too, and the page administers the SQL grant itself rather than a parallel notion of permission the console invents and then has to keep in step.

**A fresh install lists no granted account.** Only DBA accounts can annotate an index until someone is added deliberately.

DBA accounts are listed first, marked *always — by being DBA*, and have no **Revoke** button: they hold the right by being DBA rather than by a grant, and a button that could not take it away would be a lie. A grant to an SQL **role**, or to **PUBLIC**, is shown as what it is rather than hidden — a grant the page cannot see is the one that gets left behind.

**Grant** picks from a `<select>` of the instance's enabled, SQL-enabled, non-role, non-DBA accounts that do not already hold it. There is no free-text box: a mistyped username in `GRANT` is how someone ends up believing they granted something they did not, and one of the real accounts on every Virtuoso is called `nobody` — the identity unauthenticated requests get — so a typo can land somewhere much worse than nowhere. `nobody` is offered in the list like any other account, labelled for what it is. **Revoke** is behind the usual confirmation dialog, since it takes an ability away from someone.

The page states the rule it administers, because a permission UI that does not say what the permission means is how people over-grant: the grant lets an account annotate the indexes **it created** and nothing else, while a DBA account can annotate every index on the instance. Both gates are described in Naming an index.

Note

**Reading** the names is a separate question this page does not answer. They are published as `rdfs:label` and `rdfs:comment` in `<urn:vvec:indexes>`, and who may read that is decided by SQL `SELECT` grants on `DB.DBA.SPARQL_VEC_INDEX` — none by default — and by which SPARQL endpoint you let see the graph.

---

## Similarity search

The **Similarity Search** page embeds a natural-language query with each chosen index's own model and ranks the nearest matches. Type a query, open the index picker, tick one or more indexes, choose a result count (5 to 100), and search.

In the picker, an index that has been given a name leads with it and keeps its IRI underneath, and its description follows on up to two lines. This is where a description earns its room: someone choosing between six indexes is exactly who needs to know what each one covers. An unnamed index reads IRI first.

A search needs at least one index; searching with none ticked is refused rather than quietly widened to the whole instance. **Select all** in the picker covers a surface in one click, so a broad search is still one action — just an explicit one. The page opens on whichever surface actually has indexes.

The picker is a dialog with a surface control — **SQL**, **SPARQL**, and **All**, which is not available yet. Ticking several indexes searches all of them and merges the hits into **one ranking**, rather than a section per index; a result's own index is shown in its row. Switching surface clears the ticks, because a single search cannot span SQL and RDF indexes yet.

Every row shows the index it came from, that index's **model**, and its **rank within that index**, and an **Order by** control flips the whole list between two orderings without re-running the search:

- **Relevance rank** — each hit's position within its own index first. Use this when the indexes do not share a model, because it compares like with like.
- **Raw score** — highest similarity number first, whatever produced it.

When more than one index was searched, filter chips above the table narrow the ranking to particular indexes, also without re-querying.

Important

Raw similarity scores are **not calibrated between embedding models**, which is why rank ordering is the default as soon as the chosen indexes do not share a model and metric — the page then says so above the results. Each model has its own floor: `granite-384-local` scores text that has nothing to do with the query at about 0.73 (see the [Quickstart](urn:osdi:sitevirtuoso/docs/vector-index/quickstart/)), and a model whose floor sits higher would put *every* one of its hits above *every* hit from the other in a raw-score ranking — not because they matched better. When the chosen indexes do share a model and metric the scores are the same measurement, and raw score is the default.

Note

If one index fails — say it is mid-refresh — the others still return, and the failure is reported above the results.

*Figure: The Similarity Search page for a SPARQL index, with the Subject column, an Indexed text column, score bars, and a Raw URL / Entity label toggle*

Every result shows the **indexed text** — the exact string that was embedded — alongside a normalized **score** (higher is closer, with a bar) and the raw **distance** (lower is closer).

- For a **SQL** index, results show the row primary key and the indexed text, driven by `X_SIMILAR` / `ORDER BY VECTOR_DISTANCE`. The query text is embedded once with the index's bound model.
- For a **SPARQL** index, results show the subject IRI, driven by `bif:vector_search`, with the indexed text retrieved from the index's selector (the Simple selector's predicate, or a Custom selector's embedding variable). A **Raw URL / Entity label** slider toggles the Subject column between the raw IRI and a hyperlink whose visible text is the entity's **name**, resolved through the [`vvec-labels`](#labels) rule set. It **starts on Entity label** — the labels are fetched on the same request, and a column of IRIs is rarely what you are scanning for. That is rarely the indexed string: an index built over `schema:description` matches on the description, but the row is still labelled with the entity's name. A subject with no matching label property falls back to the indexed text, and then to its IRI. The indexed-text column is shown either way.

### Resizing the results

The results table gives under the hand. Drag the right edge of any column heading but the last to widen or narrow that column — useful when a run of long IRIs wants the Subject column wide, or a run of short ones wants the room given to the text instead. Drag a row's bottom edge — anywhere along it; a hairline appears when the pointer is on the row — to make that row taller, and the indexed text fills the height you gave it. So dragging down is how you read a long document's match in place. Double-click either grip to put it back.

Nothing is remembered. This is a reading posture for the answer on screen, not a setting, and a width restored onto a different result set would be wrong more often than right.

RDF indexes that are not `active` appear in the picker greyed out, labelled with their state and *not searchable*, and cannot be ticked. A search that reaches one anyway — from the **Search** link on its Manage Indexes row, say — reports the state and the index's last error instead of results.

---

## Embedding models

The **Embedding Models** page lists everything registered in `DB.DBA.ONNX_MODELS` — the bundled local ONNX model and any remote HTTP providers — with type, dimension, status, and endpoint. Status is `loaded` (or `not loaded`) for an ONNX model, `remote` for an HTTP provider, and `degraded` in red once a provider has failed three times in a row. An ONNX model reads `in-process` in the **Endpoint** column: it runs on the server host, in helper processes the server starts, and is never called over the network, so it has no endpoint to show.

*Figure: The Embedding Models page listing registered models with type, dimensions, status, and endpoint*

From here you can:

- **Register an HTTP provider** (OpenAI, Ollama, LM Studio, or any OpenAI-compatible endpoint) by calling `DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL` behind the form. The form takes the **API key itself**, not just the registry name it will be filed under: leave the name blank and the key is stored under the provider's conventional one, so the model and its key cannot drift apart. Keys are stored in the server registry, and the console never displays them back.Choosing a provider fills the **Endpoint** field with that provider's default — `https://api.openai.com/v1/embeddings` for OpenAI, `http://127.0.0.1:11434/v1/embeddings` for Ollama, `http://127.0.0.1:1234/v1/embeddings` for LM Studio — which is the same value the server would have inferred had you left the field blank. It is filled in rather than left empty because for a local runtime that default is a port worth seeing, and often worth changing: edit it freely, and a value you have typed is never overwritten by a later change of provider. `openai_compatible` has no default, so it stays blank and you must supply one. An endpoint must use `https://` unless it is on a loopback or private-network address (`localhost`, `127.*`, `10.*`, `172.16.*`–`172.31.*`, `192.168.*`); the server refuses to send requests, and keys, in clear text anywhere else.
- **Edit a provider** — the *Edit* action on an HTTP-provider row reopens its settings (provider, provider model, dimensions, endpoint, key registry name, timeout, description) prefilled, with the API key field empty. Saving re-registers it under the same name and resets its failure counters. Leaving the API key blank keeps the stored one; type a new value only to replace it. The model name itself is fixed, because indexes bind to it, and changing the dimensions does not re-embed existing indexes.
- **Reload** a local ONNX model — `DB.DBA.ONNX_MODEL_REFRESH` — which re-reads its registry row and loads it if it is not loaded, for example after its files were missing at startup, without restarting the server. A model already loaded from the same file with the same options keeps its handle, so to pick up a replaced `.onnx` file, unload the old handle first — see [Embedding models](urn:osdi:sitevirtuoso/docs/vector-index/embedding-models/#onnx_model_refresh).
- **Test** a model by embedding a sample string and previewing the vector — its dimension and first eight components.

Caution

If the console is not on **HTTPS**, submitting a form that carries an API key is interrupted by a confirmation: the value would cross the network in clear text, where any proxy, gateway or machine on the same network can read it. Put the instance behind HTTPS whenever the whole path is not trusted — or skip the browser entirely and make the same call over `isql`, ODBC or JDBC, each of which can be TLS-encrypted:

SQL

```
DB.DBA.ONNX_API_KEY_SET ('ONNX_API_KEY_OPENAI', 'sk-...');
```

The prompt only appears when a key field actually has something in it, so editing a provider without changing its key is not interrupted. It also does not appear on a **secure context** — which includes `http://localhost`, since loopback traffic never leaves the machine.

*Do not warn me again in this browser* is remembered in that browser's local storage for that origin, never on the server, so it does not silence the warning for your colleagues. It takes effect only if you go on to continue; ticking it and cancelling changes nothing. While it is in force the **API Keys** page says so and offers to turn the warning back on.

Important

An HTTP provider's endpoint is called at query time to embed the query text, so query latency includes the provider round-trip. See [Embedding models](urn:osdi:sitevirtuoso/docs/vector-index/embedding-models/) for the full model lifecycle.

---

## Maintenance

The **Maintenance** page surfaces the two independent index-maintenance queues and their workers.

*Figure: The Index Maintenance page with the SQL and SPARQL queue panels, pending/error counts, and worker controls*

Base-table writes and RDF quad changes enqueue work instead of embedding inline. The controls that process it differ between the two panels:

- The **SQL queue** panel (`DB.DBA.VEC_SQL_QUEUE`) shows pending and error counts and the last run. **Drain** processes up to the number of rows you give, now. **Start worker** calls `DB.DBA.VEC_SQL_WORKER_START`, which schedules a drain of up to 100 rows as a server scheduled event, and **Stop worker** removes it. The interval is stored as the event's interval, which the scheduler counts in **minutes** — although the field is labelled seconds — and the scheduler itself runs only every `SchedulerInterval` minutes (`[Parameters]` in `virtuoso.ini`; 10 in the shipped file). Use **Drain** when you want new rows searchable now.
- The **SPARQL queue** panel (`DB.DBA.SPARQL_VEC_QUEUE`) has **Drain**, **Run once**, **Start** / **Stop**, **Suspend queue** / **Resume**, and the worker's batch size and idle delay. **Drain** processes up to the number of rows you give, whenever you press it. **Start** only marks the worker enabled — nothing drains the queue on a schedule by itself — and **Run once** processes one batch of the configured size, and only while the worker is enabled; with it stopped, Run once processes nothing. The idle delay is stored and shown, but nothing reads it.

For manual-maintenance SPARQL indexes, use the **Refresh** action on [Manage Indexes](#manage-indexes). See Maintenance for the queue model and operational guidance.

---

## How the console reports work in progress

Actions that take real time — creating or rebuilding an index, embedding, a queue drain — replace their button with what is happening and run a rule under the form until the response arrives.

Actions that are *irreversible but instant* get the same treatment for a different reason. Dropping an index, deleting an API key and removing a prefix are single statements whose reply can be back before the click has finished. A page that simply changes underneath you reads as one where nothing happened, so the confirmation dialog takes the commit instead of vanishing on it: the buttons disable, the action button says what it is doing, a rule runs at the foot, and Escape and the backdrop stop dismissing it. It holds a short beat before submitting, then stays up until the response replaces the page — so a slow drop shows its full duration, and a fast one is still visible.

Storing an API key and registering a provider hold the same beat without a dialog, because they put a secret on the wire and the write itself is instant.

Results land the same way: the outcome banner settles into place on arrival rather than merely being there.

**Similarity search has none of this, deliberately.** It is the high-frequency surface — no dialog, no working label, no landing banner. The results are the feedback, and an indicator on a sub-second action only makes it feel slower. All of it collapses under `prefers-reduced-motion`.

---

## Light and dark

The **Light / Dark** switch at the foot of the sidebar navigation changes the palette. **Light is the default.** The console deliberately does not follow `prefers-color-scheme` — a dark desktop does not decide what this console looks like.

The choice is remembered in `localStorage`, per browser and per origin. It is never stored on the server, so one operator's preference is not an instance-wide setting, and a shared `dba` login does not mean a shared theme.

---

## Signing in and out

Opening any console page while signed out takes you to `/vvec/login`, carrying where you were headed so you land there afterwards. Sign in with a **DBA** account: a valid but non-DBA account is refused, because this is an operator tool.

**Your password does not cross the wire.** Every render of the sign-in page mints a single-use nonce; the browser submits `MD5(nonce || password)` and blanks the field you typed into, and the server recomputes the same digest from the stored credential. The nonce is deleted the moment it is redeemed, so a captured sign-in cannot be replayed. This is the same challenge the Virtuoso Conductor uses. With JavaScript turned off the form can only submit the password in clear, which the console accepts **over HTTPS only**.

A successful sign-in writes a row to `DB.DBA.VVEC_SESSION` and sets a `vvec_sid` cookie — `HttpOnly`, `SameSite=Strict`, `Secure` when the request arrived over HTTPS, and carrying no `Expires`, so the browser drops it on exit. The session is pinned to the client IP it was created from. Because a session is a row, it can really end: signing out, deleting the row, or rebuilding the database ends it.

### When a session ends

Three windows must all hold. Whichever lapses first ends the session:

| Window | Default | What it means | To change |
| --- | --- | --- | --- |
| **Page open** | 150 s | No heartbeat from an open page. | `DB.DBA.VVEC_SES_BEAT_S ()` |
| **Idle** | 30 min | No page request, however long a page stays open. | `DB.DBA.VVEC_SES_IDLE_MIN ()` |
| **Absolute** | 12 h | Total lifetime, however active. | `DB.DBA.VVEC_SES_MAX_H ()` |

Each is a one-line function defined by the console's install SQL; redefine one from `isql` to change that window without touching the procedures that read it. Reinstalling the VAD puts the defaults back.

SQL

```
CREATE function DB.DBA.VVEC_SES_IDLE_MIN () returns INTEGER { RETURN 60; };
```

**Closing the page signs you out.** Every open console page sends a heartbeat to `/vvec/ping` every 30 seconds. When the beats stop — tab closed, browser quit, machine asleep, network gone — the session goes stale within the page-open window and the next request is bounced to the sign-in page. HTTP has no close event, so missed heartbeats are the only signal that covers every way a page can stop existing, including a crash or a force-quit.

Note

The **idle** clock is advanced only by real page requests, never by the heartbeat. A console left open on an unattended screen still times out after 30 minutes; a heartbeat proves the page still exists, not that anyone is using it.

The sidebar's foot shows the account you are signed in as. Clicking it opens a menu with **Sign out**, which deletes the session row — the id the browser still holds then matches nothing. A DBA can end every session at once from `isql`:

SQL

```
SQL> DELETE FROM DB.DBA.VVEC_SESSION;
```

Expired rows are swept hourly by `DB.DBA.VVEC_SESSION_GC ()`; expiry itself is enforced on every request, so the sweep is only housekeeping.

---

## Access and security

VVec runs its pages with `vsp_user => 'dba'`, so the install always guards `/vvec/`: the vhost `auth_fn` (`DB.DBA.VVEC_AUTH`) validates a server-side session against `DB.DBA.VVEC_SESSION` and authorizes only **DBA accounts**. A valid but non-DBA sign-in is refused. There is no unauthenticated deployment mode.

- The sign-in challenge keeps the password off the wire, but the session that follows is not encrypted — put the instance behind **HTTPS** for any non-localhost use.
- Every state-changing form carries a per-session CSRF token, checked as the first act of each POST handler; a missing or wrong token, a cross-origin `Origin` or `Referer`, or a non-`POST` method returns **403** and performs no action. The `SameSite=Strict` cookie means a cross-site request carries no session at all, so the token is a second line rather than the only one.
- All user-influenced output is HTML- or URL-escaped. Identifiers used in DDL are validated against the catalog before use, and query text is bound as a parameter (SQL) or escaped into a SPARQL string literal rather than spliced into a statement.
- The one free-form input — a Custom selector's `WHERE` pattern — is checked structurally before it is placed in the DDL: balanced braces, no comments or control characters, no update or DDL keywords, and no `define` pragmas.
- Do not grant application users execute rights on the `VEC_INDEX_*` management procedures.

---

## Next steps

- **[SQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sql-vector-index/)** — the SQL surface in depth, from the command line.
- **[SPARQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sparql-vector-index/)** — the RDF surface and `bif:vector_search`.
- **[Tutorial: FAQ semantic search](urn:osdi:sitevirtuoso/docs/vector-index/tutorial-faq-semantic-search/)** — an end-to-end SPARQL example you can also drive from VVec.

## Related content

**Reference:**

- [API reference](urn:osdi:sitevirtuoso/docs/vector-index/api-reference/) — every SQL and SPARQL entry point VVec calls
- [Index parameters](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/) — HNSW build- and query-time tuning
- Maintenance — queues, workers, and keeping indexes current

[PreviousSimilarity metrics for the Virtuoso vector index](urn:osdi:sitevirtuoso/docs/vector-index/similarity-metrics/) [NextTroubleshooting the Virtuoso vector index](urn:osdi:sitevirtuoso/docs/vector-index/troubleshooting/)

On this page

1. [Deploy and open the console](#deploy-and-open-the-console)
2. [Overview](#overview)
3. [Manage Indexes](#manage-indexes)
4. [Create a vector index](#create-a-vector-index)
5. [Edit an index](#edit-an-index)
6. [API keys](#api-keys)
7. [Namespace prefixes](#namespace-prefixes)
8. [Labels](#labels)
9. [Permissions](#permissions)
10. [Similarity search](#similarity-search)
11. [Embedding models](#embedding-models)
12. [Maintenance](#maintenance)
13. [How the console reports work in progress](#how-the-console-reports-work-in-progress)
14. [Light and dark](#light-and-dark)
15. [Signing in and out](#signing-in-and-out)
16. [Access and security](#access-and-security)
17. [Next steps](#next-steps)
18. [Related content](#related-content)
