A similarity metric measures how close two vectors are. When you query a vector index, Virtuoso ranks results by the metric you chose at index creation time. The metric you pick affects search quality and must match how your embedding model was trained — most text embedding models (including the bundled granite model and OpenAI's) are designed for cosine similarity.
This article covers:
- Supported metrics: cosine, L2 (Euclidean), and L1 (Manhattan) distance
- How scores are computed:
VECTOR_DISTANCE,X_SIMILAR, andbif:vector_search - Choosing a metric: guidance for text, image, and magnitude-sensitive embeddings
- Practical examples: interpreting scores with the bundled granite model, measured
- Setting thresholds: filtering results with
score_limitanddistance_limit
Overview
Virtuoso supports three similarity metrics:
- Cosine similarity (default) — the angle between two vectors, ignoring magnitude
- L2 / Euclidean distance — straight-line distance between two points
- L1 / Manhattan distance — sum of absolute differences per dimension
Each metric produces a distance (lower = more similar), which both search surfaces — X_SIMILAR in SQL, the score of bif:vector_search in SPARQL — turn into the same normalized similarity score in [0, 1] (higher = more similar), so a document scores the same on both. The choice of metric affects search quality and must match how the embedding model was trained. Most text embedding models — including the bundled granite model and OpenAI's — are designed for cosine similarity, which is why cosine is the default.
Supported metrics
Cosine similarity (default)
Cosine similarity measures the angle between two vectors, ignoring their magnitude. It captures direction — two vectors pointing the same way are considered similar regardless of how long they are.
| Property | Value |
|---|---|
| Distance formula | 1 - (dot(A, B) / (|A| * |B|)) |
| Distance range | [0, 2] — 0 = identical direction, 1 = orthogonal, 2 = opposite |
Similarity (X_SIMILAR, bif:vector_search score) | 1 - (distance / 2), range [0, 1] |
| Keyword | 'cosine' in WITH (distance_metric = 'cosine') |
Best for: text embeddings. Most text models normalize their output vectors (or are magnitude-invariant), so cosine captures pure semantic direction.
L2 / Euclidean distance
L2 distance measures the straight-line distance between two points in vector space. Unlike cosine, it is sensitive to vector magnitude.
| Property | Value |
|---|---|
| Distance formula | sqrt(sum((A[i] - B[i])^2)) |
| Distance range | [0, infinity) |
Similarity (X_SIMILAR, bif:vector_search score) | 1 / (1 + distance), range [0, 1] |
| Keyword | 'l2' in WITH (distance_metric = 'l2') |
Best for: embeddings where magnitude carries meaning — for example, some image embeddings where vector length reflects confidence or feature strength.
L1 / Manhattan distance
L1 distance measures the sum of absolute differences per dimension — the distance you would walk along a grid of axis-aligned streets.
| Property | Value |
|---|---|
| Distance formula | sum(|A[i] - B[i]|) |
| Distance range | [0, infinity) |
Similarity (X_SIMILAR, bif:vector_search score) | 1 / (1 + distance), range [0, 1] |
| Keyword | 'l1' in WITH (distance_metric = 'l1') |
Best for: high-dimensional data where L2 may concentrate distance in a few dominant dimensions. L1 spreads differences across dimensions more evenly and is less sensitive to outliers than L2.
Comparison table
| Metric | Keyword | Distance range | Similarity formula | Similarity range | Magnitude-sensitive? | Best for |
|---|---|---|---|---|---|---|
| Cosine | 'cosine' | [0, 2] | 1 - (d / 2) | [0, 1] | No | Text embeddings |
| L2 | 'l2' | [0, inf) | 1 / (1 + d) | [0, 1] | Yes | Image / some embeddings |
| L1 | 'l1' | [0, inf) | 1 / (1 + d) | [0, 1] | Yes | Robust to outliers |
Choosing a metric
For text embeddings (recommended: cosine)
Most text embedding models — the bundled granite model, OpenAI, Cohere, and others — are designed for cosine similarity. Cosine ignores magnitude and focuses on semantic direction, which is exactly how these models are trained to represent meaning. Even when a model does not explicitly normalize its output, cosine typically gives the best ranking for text.
For image embeddings
Some image models produce embeddings where magnitude reflects confidence or the strength of detected features. In those cases L2 may be appropriate, and the model's documentation usually says so. If you are unsure, start with cosine and compare the results against L2 on a labeled set of queries.
When magnitude matters
If you need to distinguish between "strong" and "weak" matches based on vector magnitude, use L2 — it rewards vectors that are both close in direction and similar in length. For pure semantic similarity (does this text mean the same thing?), cosine is almost always the right choice.
How scores are computed
Virtuoso exposes the metric through three surfaces, each returning either the raw distance or a normalized similarity score.
VECTOR_DISTANCE function
VECTOR_DISTANCE returns the raw distance value, where lower means more similar. The exact meaning depends on the metric:
- Cosine: returns
1 - cosine_similarity, so0= identical,1= orthogonal,2= opposite direction. Real text seldom gets near1: with the bundled granite model, unrelated sentences measure about0.5(see Practical examples) - L2: returns the Euclidean distance
- L1: returns the Manhattan distance
SELECT TOP 10 id, content, VECTOR_DISTANCE(content, 'query text') AS distance
FROM DB.DBA.DOCS
ORDER BY VECTOR_DISTANCE(content, 'query text');X_SIMILAR function
X_SIMILAR returns a normalized similarity score in [0, 1], where higher means more similar. The normalization depends on the metric:
- Cosine:
similarity = 1 - (distance / 2) - L2 / L1:
similarity = 1 / (1 + distance)
A score of 1.0 means identical. For cosine, 0.5 means orthogonal and 0.0 opposite, so unrelated text does not score near 0 — with the bundled granite model it measures about 0.72–0.76.
SELECT TOP 10 id, content, X_SIMILAR(content, 'query text') AS score
FROM DB.DBA.DOCS
ORDER BY VECTOR_DISTANCE(content, 'query text');SPARQL bif:vector_search
The bif:vector_search magic predicate exposes both values through its options:
scoreoption: binds the same higher-is-better score asX_SIMILAR, in[0, 1]:1 - distance / 2for cosine,1 / (1 + distance)for L1 and L2. Measured with the bundled granite model, the query "api credentials" over the same three texts — "How to rotate API credentials", "Password reset steps for the customer portal" and "Billing escalation and invoice dispute workflow" — indexed once in SQL and once in SPARQL: cosine distances0.2010,0.4188and0.4803, and scores0.8995,0.7906and0.7598from bothX_SIMILARandbif:vector_searchdistanceoption: binds the raw distance (same value asVECTOR_DISTANCE)score_limit: the minimum score to return, in[0, 1]for every metric.distance_limit: the maximum distance to return. Must be finite and non-negative.
SPARQL
SELECT ?doc ?score ?distance WHERE {
?doc bif:vector_search "query text"
option (INDEX <urn:idx>, score ?score, distance ?distance,
score_limit 0.85, distance_limit 0.30, LIMIT 10) .
}
ORDER BY DESC(?score)
;Practical examples
Interpreting cosine scores with the bundled granite-384-local model
Measured 2026-10-20 with the bundled granite-384-local (384-dim): each pair embedded with DB.DBA.ONNX_EMBED_TEXT_FOR_MODEL, then compared with X_SIMILAR and VECTOR_DISTANCE (cosine). bif:vector_search binds the same score for the same distance.
| Pair | Kind | Score (X_SIMILAR) | Cosine distance |
|---|---|---|---|
| "how to install" / "how to install" | identical | 1.0000 | 0.0000 |
| "The cat sat on the mat." / "A cat was sitting on the mat." | paraphrase | 0.9881 | 0.0239 |
| "install virtuoso" / "How to install Virtuoso on Linux" | near-identical intent | 0.9780 | 0.0440 |
| "How do I reset my password?" / "I forgot my password, how can I change it?" | paraphrase | 0.9666 | 0.0669 |
| "install" / "setup guide" | closely related | 0.8976 | 0.2047 |
| "how to install" / "How do I install virtuoso on Windows?" | closely related | 0.8752 | 0.2496 |
| "SPARQL query" / "RDF graph query language" | closely related | 0.8533 | 0.2934 |
| "stock market" / "interest rates" | topically related | 0.8030 | 0.3940 |
| "database" / "Virtuoso installation" | loosely related | 0.7572 | 0.4857 |
| "database" / "sun is yellow" | unrelated | 0.7563 | 0.4874 |
| "quarterly earnings report" / "a recipe for banana bread" | unrelated | 0.7496 | 0.5008 |
| "reset password" / "the sun is yellow" | unrelated | 0.7457 | 0.5086 |
| "How do I bake bread?" / "Kubernetes pod scheduling" | unrelated | 0.7341 | 0.5319 |
| "Cricket is a bat-and-ball game played between two teams." / "The SPARQL endpoint speaks over HTTP and returns JSON." | unrelated | 0.7253 | 0.5495 |
What these pairs show:
- Unrelated text does not score near 0. It measured
X_SIMILAR0.72–0.76, a cosine distance of about 0.5. Paraphrases measured above 0.96, closely related pairs 0.85–0.90, a topically related pair 0.80. - Very short queries compress the scale. "database" scored the same against "Virtuoso installation" as against "sun is yellow" (0.7572 and 0.7563): one word carries too little to separate topical from unrelated. Give queries a few words.
- On these pairs, a score floor between 0.80 and 0.85 — an
X_SIMILARfloor in SQL, ascore_limitin SPARQL — separated related from unrelated text. Fourteen pairs are a sketch, not a calibration: measure your own data.
Scores move with the model: the same pair of texts can score far apart under two models, and where unrelated text lands differs from model to model. Re-calibrate every threshold when you change models.
Interpreting cosine scores with OpenAI text-embedding-3-small (1536-dim)
Typical ranges, not measured on this build — calibrate before relying on them. They are usually quoted as cosine similarity (1 - distance); the second column is the score Virtuoso reports for the same range (1 - distance / 2).
| Cosine similarity | Score (X_SIMILAR, bif:vector_search) | Interpretation |
|---|---|---|
| 0.90–1.0 | 0.95–1.0 | Near-identical |
| 0.75–0.90 | 0.875–0.95 | Highly related |
| 0.50–0.75 | 0.75–0.875 | Topically related |
| 0.30–0.50 | 0.65–0.75 | Weakly related |
| 0.00–0.30 | 0.50–0.65 | Unrelated |
Setting thresholds
Thresholds let you filter out weak matches so you only return results that are genuinely similar to your query.
For SQL (X_SIMILAR)
-- Only return results with X_SIMILAR above 0.85:
SELECT TOP 10 id, content FROM DB.DBA.DOCS
WHERE X_SIMILAR(content, 'query text') > 0.85;With the bundled granite model, a floor of 0.70 filters nothing. On a table of six rows — "How do I reset my password?", "Password reset steps for the customer portal", "Quarterly billing escalation workflow" and three "How do I install Virtuoso on …?" questions — X_SIMILAR(content, 'reset password') > 0.70 returned all six, where > 0.85 returned only the two about passwords (0.951 and 0.901); the others scored 0.729–0.740 (measured 2026-10-20).
For SPARQL (bif:vector_search)
-- Only return results with score >= 0.85 and distance <= 0.30
-- (for cosine, the same test twice):
SPARQL
SELECT ?doc ?score ?distance WHERE {
?doc bif:vector_search "query text"
option (INDEX <urn:idx>, score ?score, distance ?distance,
score_limit 0.85, distance_limit 0.30, LIMIT 10) .
}
ORDER BY DESC(?score)
;Next steps
- Index parameters — Full reference for
CREATE VECTOR INDEXoptions includingdimensions,distance_metric, and HNSW tuning - SQL vector index how-to — Complete guide for creating, querying, and maintaining SQL vector indexes
- SPARQL vector index how-to — Complete guide for RDF vector indexes with
bif:vector_search
Related content
Reference:
- Cosine similarity on Wikipedia — Background on the cosine distance metric
- Efficient and robust approximate nearest neighbor search using HNSW graphs (Malkov & Yashunin) — The paper that introduced HNSW, the graph the vector index is built on
PreviousIndex parameters and tuning for the Virtuoso vector index NextManaging vector indexes visually with the VVec Console