This tutorial walks through a complete, real-world example: loading OpenLink's public FAQ documents (published as Turtle/RDF using schema.org vocabulary), creating a SPARQL vector index over the schema:Question entities, and performing semantic similarity search with bif:vector_search.
This tutorial covers:
- Loading RDF data: Bulk-loading FAQ Turtle documents via
SPARQL LOAD - Creating a vector index: Using a general selector with
schema:Questionentities - Refreshing and draining: Populating the HNSW graph with embeddings
- Semantic search: Querying FAQs by meaning, not keyword matching
- Interpreting results: Understanding scores and distances
Prerequisites
Before you start, make sure you have:
- Virtuoso Open Source built with ONNX support and the
vonnxplugin loaded - The bundled
granite-384-localmodel auto-provisioned at startup (384 dimensions) - Network access to
virtuoso.openlinksw.comandwww.openlinksw.comfor loading the FAQ documents - DBA access to the Virtuoso instance (via iSQL on port 1111 or similar)
Verify the model is loaded:
SELECT model_name, handle FROM DB.DBA.ONNX_MODELS WHERE model_name = 'granite-384-local';
-- Expect: handle = 1 (non-null means loaded)Step 1: Load the FAQ documents
OpenLink publishes a collection of FAQ documents as Turtle/RDF using the schema.org vocabulary. Each document contains schema:Question entities with schema:name (the question text) and associated schema:Answer entities.
Load all FAQ sources into a single graph (<urn:openlink:faqs>):
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-licensing-related-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-faq-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://www.openlinksw.com/data/turtle/general/virtuoso-cios-cdos-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://www.openlinksw.com/data/turtle/general/virtuoso-llms-for-cios-cdos-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-sys-monitoring-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/personal-assistant-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/VirtuosoHowTos.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/VirtuosoHowTos-Extra.ttl> INTO GRAPH <urn:openlink:faqs>;This query runs as follows:
SPARQL LOADfetches each Turtle document from the remote URL.INTO GRAPHplaces all triples into the named graph<urn:openlink:faqs>.- Multiple loads into the same graph accumulate — all FAQ sources merge into one searchable collection.
Verify the data loaded:
SPARQL DEFINE input:storage ""
SELECT (COUNT(*) AS ?total) WHERE { GRAPH <urn:openlink:faqs> { ?s ?p ?o } }
;SPARQL DEFINE input:storage ""
SELECT (COUNT(*) AS ?questions) WHERE {
GRAPH <urn:openlink:faqs> {
?q a <http://schema.org/Question> .
}
}
;Step 2: Create the vector index
Create a vector index over the schema:Question entities, embedding the question text from schema:name. This uses a general selector because we need a graph pattern to filter for schema:Question types specifically.
SPARQL DEFINE input:storage ""
CREATE VECTOR INDEX <urn:openlink:faqs:embeddings>
KEY ?question
EMBEDDING ?questionName
WHERE {
GRAPH <urn:openlink:faqs> {
?question a <http://schema.org/Question> ;
<http://schema.org/name> ?questionName .
}
}
USING MODEL 'granite-384-local'
WITH (dimensions = 384, distance = 'cosine')
;This query runs as follows:
key ?question— the subject IRI of each FAQ question becomes the searchable entity identifier.embedding ?questionName— theschema:nameliteral (the question text) is what gets embedded into a vector.where { ... }— the graph pattern selects onlyschema:Questionentities with aschema:namefrom the<urn:openlink:faqs>graph.using model 'granite-384-local'— binds the index to the bundled 384-dimensional granite model.with (dimensions = 384, distance = 'cosine')— cosine distance is the standard choice for text similarity.
Key concepts:
- The
KEYvariable must identify the entity IRI — here?question. - The
EMBEDDINGvariable must be the text literal to embed — here?questionName. - The two variables must be different.
- The catalog row is stored in
DB.DBA.SPARQL_VEC_INDEXwithSVI_STATE = 'active'.
Verify the index was created:
SELECT SVI_IRI, SVI_STATE, SVI_SELECTOR_TYPE, SVI_MODEL_NAME, SVI_DIMENSIONS
FROM DB.DBA.SPARQL_VEC_INDEX
WHERE SVI_IRI = 'urn:openlink:faqs:embeddings';Step 3: Refresh the index
After creating the index, run a refresh to evaluate the selector pattern, embed each question's text, and populate the HNSW graph:
SPARQL DEFINE input:storage ""
refresh VECTOR INDEX <urn:openlink:faqs:embeddings>
;Vector index <urn:openlink:faqs:embeddings> refreshed, 68 rows processed, 0 stale keys deletedRefresh evaluates the WHERE pattern, embeds each ?questionName value with the bound model, and inserts the resulting vectors into the HNSW graph — one row per question that has a name. For a local model like the bundled one, no network is involved: the embeddings come from helper processes on the same host. For a remote provider, the questions are sent in batches of HTTP requests.
Step 4: Search the FAQs
Now you can search the FAQ collection by semantic meaning. The bif:vector_search magic predicate embeds your query text with the same model and searches the HNSW graph for the most similar questions:
SPARQL
PREFIX schema: <http://schema.org/>
SELECT ?doc ?name ?score ?distance
WHERE {
?doc schema:name ?name ;
bif:vector_search "Virtuoso Data Virtualization Support"
option (
INDEX <urn:openlink:faqs:embeddings>,
score ?score,
distance ?distance,
score_limit 0.85,
distance_limit 0.30,
LIMIT 10,
ef_search 100
) .
}
ORDER BY DESC(?score)
;The first three of the ten rows it returned on 2026-10-20:
| ?name | ?score | ?distance |
|---|---|---|
| Does OpenLink Virtuoso support data virtualization? | 0.937 | 0.126 |
| How does Virtuoso aid digital transformation? | 0.903 | 0.194 |
| What are the key use cases for Virtuoso? | 0.899 | 0.202 |
This query runs as follows:
bif:vector_search "Virtuoso Data Virtualization Support"— the query text is embedded once using the index's bound model,granite-384-local.- HNSW search finds the 10 nearest neighbor questions in the vector space.
?docbinds to the matchingschema:QuestionIRI.?namebinds to the question'sschema:nametext.?scoreand?distancebind the similarity score and raw distance.score_limit 0.85filters out low-similarity results.ORDER BY DESC(?score)returns the most similar questions first.
Search options explained
| Option | Value | Purpose |
|---|---|---|
index <urn:openlink:faqs:embeddings> | Explicit index IRI | Required when multiple indexes exist |
score ?score | Bind variable | Higher = more similar, from 0 to 1; for cosine it is 1 - ?distance / 2 (1 = identical), the score X_SIMILAR gives in SQL |
distance ?distance | Bind variable | Lower = more similar (0 = identical) |
score_limit 0.85 | Threshold | Exclude results scoring below 0.85 |
distance_limit 0.30 | Threshold | Exclude results with distance > 0.30 |
limit 10 | Result count | Return at most 10 matches |
ef_search 100 | Search depth | HNSW beam width — higher = better recall |
Step 5: Try different queries
The power of semantic search is that it matches meaning, not keywords. Try these queries and observe how different phrasings find the same FAQs:
-- Search for scalability questions
SPARQL
PREFIX schema: <http://schema.org/>
SELECT ?doc ?name ?score WHERE {
?doc schema:name ?name ;
bif:vector_search "how to scale virtuoso for more data"
option (INDEX <urn:openlink:faqs:embeddings>, score ?score, LIMIT 5, ef_search 100) .
}
ORDER BY DESC(?score)
;-- Search for high availability
SPARQL
PREFIX schema: <http://schema.org/>
SELECT ?doc ?name ?score WHERE {
?doc schema:name ?name ;
bif:vector_search "disaster recovery and high availability"
option (INDEX <urn:openlink:faqs:embeddings>, score ?score, LIMIT 5, ef_search 100) .
}
ORDER BY DESC(?score)
;-- Search for RDF views / data virtualization
SPARQL
PREFIX schema: <http://schema.org/>
SELECT ?doc ?name ?score WHERE {
?doc schema:name ?name ;
bif:vector_search "expose SQL tables as RDF graphs"
option (INDEX <urn:openlink:faqs:embeddings>, score ?score, LIMIT 5, ef_search 100) .
}
ORDER BY DESC(?score)
;Key concepts:
- The model captures meaning, not words. With the bundled model on 2026-10-20, "expose SQL tables as RDF graphs" ranked "How do I Generate and Deploy RDF-based Linked Data from Tables in a Relational Database?" first (0.896) and "How do I use R2RML to map RDB to RDF with Virtuoso?" fourth (0.848) — neither says "expose" or "SQL". "disaster recovery and high availability" put the High-Availability and Disaster Recovery question first (0.918), well clear of the next (0.792).
ef_search 100provides good recall for most FAQ collections. Increase to 200+ for larger datasets or when you need maximum accuracy.
Step 6: Using a remote embedding model
To use a hosted model instead — for example OpenAI's text-embedding-3-small (1536 dimensions) in place of the bundled local model — first register the provider, then recreate the index:
-- Store the API key:
DB.DBA.ONNX_API_KEY_SET('ONNX_API_KEY_OPENAI', '<your-secret-key>');
-- Register the model:
DB.DBA.ONNX_REGISTER_HTTP_EMBED_MODEL(
'openai-small', 'openai',
'text-embedding-3-small', 1536,
NULL, 'ONNX_API_KEY_OPENAI', 30);
-- Drop the old index:
SPARQL DEFINE input:storage ""
DROP SILENT VECTOR INDEX <urn:openlink:faqs:embeddings>
;
-- Recreate with the OpenAI model:
SPARQL DEFINE input:storage ""
CREATE VECTOR INDEX <urn:openlink:faqs:embeddings>
KEY ?question
EMBEDDING ?questionName
WHERE {
GRAPH <urn:openlink:faqs> {
?question a <http://schema.org/Question> ;
<http://schema.org/name> ?questionName .
}
}
USING MODEL 'openai-small'
WITH (dimensions = 1536, distance = 'cosine', hnsw_m = 32, ef_construction = 400)
;
-- Refresh to embed all questions via OpenAI:
SPARQL DEFINE input:storage ""
refresh VECTOR INDEX <urn:openlink:faqs:embeddings>
;Cleanup
When you're done with the tutorial, you can drop the index and clear the graph:
SPARQL DEFINE input:storage ""
DROP SILENT VECTOR INDEX <urn:openlink:faqs:embeddings>
;
SPARQL DEFINE input:storage ""
CLEAR GRAPH <urn:openlink:faqs>
;Complete script
Here is the full end-to-end script you can paste into iSQL:
-- Step 1: Load FAQ documents
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-licensing-related-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-faq-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://www.openlinksw.com/data/turtle/general/virtuoso-cios-cdos-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://www.openlinksw.com/data/turtle/general/virtuoso-llms-for-cios-cdos-faq.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/virtuoso-sys-monitoring-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/personal-assistant-top-questions.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/VirtuosoHowTos.ttl> INTO GRAPH <urn:openlink:faqs>;
SPARQL LOAD <https://virtuoso.openlinksw.com/data/turtle/general/VirtuosoHowTos-Extra.ttl> INTO GRAPH <urn:openlink:faqs>;
-- Step 2: Create the vector index
SPARQL DEFINE input:storage ""
CREATE VECTOR INDEX <urn:openlink:faqs:embeddings>
KEY ?question
EMBEDDING ?questionName
WHERE {
GRAPH <urn:openlink:faqs> {
?question a <http://schema.org/Question> ;
<http://schema.org/name> ?questionName .
}
}
USING MODEL 'granite-384-local'
WITH (dimensions = 384, distance = 'cosine')
;
-- Step 3: Refresh to populate the HNSW graph
SPARQL DEFINE input:storage ""
refresh VECTOR INDEX <urn:openlink:faqs:embeddings>
;
-- Step 4: Semantic search
SPARQL
PREFIX schema: <http://schema.org/>
SELECT ?doc ?name ?score ?distance
WHERE {
?doc schema:name ?name ;
bif:vector_search "Virtuoso Data Virtualization Support"
option (
INDEX <urn:openlink:faqs:embeddings>,
score ?score,
distance ?distance,
score_limit 0.85,
distance_limit 0.30,
LIMIT 10,
ef_search 100
) .
}
ORDER BY DESC(?score)
;Next steps
- SPARQL vector index how-to — Complete reference for all SPARQL vector index features
- SQL vector index how-to — Use vector indexes with relational tables
- Vector index overview — Architecture and concepts
Related content
Reference:
- schema.org Question — The Question vocabulary used in the FAQ data
- OpenLink Virtuoso FAQ — The live FAQ page
- granite-embedding-30m-english — The bundled embedding model
PreviousQuickstart: Vector index for Virtuoso NextHow-To: Build and use the relational SQL vector index (Virtuoso)