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) 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: 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.
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 for the build.
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:
./configure … --enable-vvec-vad
makeIf you have already built the tree and only need the package, invoke the packaging rule directly — it does not depend on the option:
make -C binsrc/vvec vvec_dav.vadIt prints INFO: Vadpacker completed without errors on success.
Install the VAD
Install it from the Conductor (System Admin → Packages), or from isql as dba:
isql <host>:<port> dba <password>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:
http://<host>:<http-port>/vvec/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.
- Refresh (SPARQL rows) re-evaluates the selector and re-embeds changed rows.
- Drop removes the index (relational
DROP INDEXorsparql drop vector index), after a confirmation prompt.
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.
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.
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 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.
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.
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_constructionandreduceare editable. - SPARQL surface — the index IRI is fixed. The selector (Simple or Custom, with its graph/predicate or key/embedding variables,
WHEREpattern and dataset clause), model, dimensions, distance,hnsw_m,ef_construction,maintenance,reduceand 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.
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.
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 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:
DB.DBA.XML_SET_NS_DECL ('schema', 'http://schema.org/', 2);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:
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 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.
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.
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.
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 thevvec-labelsrule 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 overschema:descriptionmatches 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_MODELbehind 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/embeddingsfor OpenAI,http://127.0.0.1:11434/v1/embeddingsfor Ollama,http://127.0.0.1:1234/v1/embeddingsfor 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_compatiblehas no default, so it stays blank and you must supply one. An endpoint must usehttps://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.onnxfile, unload the old handle first — see Embedding models. - Test a model by embedding a sample string and previewing the vector — its dimension and first eight components.
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 callsDB.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 everySchedulerIntervalminutes ([Parameters]invirtuoso.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. 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.
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.
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> 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
OriginorReferer, or a non-POSTmethod returns 403 and performs no action. TheSameSite=Strictcookie 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
WHEREpattern — is checked structurally before it is placed in the DDL: balanced braces, no comments or control characters, no update or DDL keywords, and nodefinepragmas. - Do not grant application users execute rights on the
VEC_INDEX_*management procedures.
Next steps
- SQL vector index how-to — the SQL surface in depth, from the command line.
- SPARQL vector index how-to — the RDF surface and
bif:vector_search. - Tutorial: FAQ semantic search — an end-to-end SPARQL example you can also drive from VVec.
Related content
Reference:
- API reference — every SQL and SPARQL entry point VVec calls
- Index parameters — HNSW build- and query-time tuning
- Maintenance — queues, workers, and keeping indexes current
PreviousSimilarity metrics for the Virtuoso vector index NextTroubleshooting the Virtuoso vector index