Similarity metrics for the Virtuoso vector index

Learn about the cosine, L2 (Euclidean), and L1 (Manhattan) similarity metrics supported by Virtuoso's vector index, how each score is computed, and how to choose and calibrate the right metric for your embedding model.

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, and bif: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_limit and distance_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.

PropertyValue
Distance formula1 - (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.

PropertyValue
Distance formulasqrt(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.

PropertyValue
Distance formulasum(|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

MetricKeywordDistance rangeSimilarity formulaSimilarity rangeMagnitude-sensitive?Best for
Cosine'cosine'[0, 2]1 - (d / 2)[0, 1]NoText embeddings
L2'l2'[0, inf)1 / (1 + d)[0, 1]YesImage / some embeddings
L1'l1'[0, inf)1 / (1 + d)[0, 1]YesRobust to outliers

Choosing a metric

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, so 0 = identical, 1 = orthogonal, 2 = opposite direction. Real text seldom gets near 1: with the bundled granite model, unrelated sentences measure about 0.5 (see Practical examples)
  • L2: returns the Euclidean distance
  • L1: returns the Manhattan distance
SQL
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.

SQL
SELECT TOP 10 id, content, X_SIMILAR(content, 'query text') AS score
FROM DB.DBA.DOCS
ORDER BY VECTOR_DISTANCE(content, 'query text');

The bif:vector_search magic predicate exposes both values through its options:

  • score option: binds the same higher-is-better score as X_SIMILAR, in [0, 1]: 1 - distance / 2 for 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 distances 0.2010, 0.4188 and 0.4803, and scores 0.8995, 0.7906 and 0.7598 from both X_SIMILAR and bif:vector_search
  • distance option: binds the raw distance (same value as VECTOR_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.
SQL
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.

PairKindScore (X_SIMILAR)Cosine distance
"how to install" / "how to install"identical1.00000.0000
"The cat sat on the mat." / "A cat was sitting on the mat."paraphrase0.98810.0239
"install virtuoso" / "How to install Virtuoso on Linux"near-identical intent0.97800.0440
"How do I reset my password?" / "I forgot my password, how can I change it?"paraphrase0.96660.0669
"install" / "setup guide"closely related0.89760.2047
"how to install" / "How do I install virtuoso on Windows?"closely related0.87520.2496
"SPARQL query" / "RDF graph query language"closely related0.85330.2934
"stock market" / "interest rates"topically related0.80300.3940
"database" / "Virtuoso installation"loosely related0.75720.4857
"database" / "sun is yellow"unrelated0.75630.4874
"quarterly earnings report" / "a recipe for banana bread"unrelated0.74960.5008
"reset password" / "the sun is yellow"unrelated0.74570.5086
"How do I bake bread?" / "Kubernetes pod scheduling"unrelated0.73410.5319
"Cricket is a bat-and-ball game played between two teams." / "The SPARQL endpoint speaks over HTTP and returns JSON."unrelated0.72530.5495

What these pairs show:

  • Unrelated text does not score near 0. It measured X_SIMILAR 0.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_SIMILAR floor in SQL, a score_limit in 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 similarityScore (X_SIMILAR, bif:vector_search)Interpretation
0.90–1.00.95–1.0Near-identical
0.75–0.900.875–0.95Highly related
0.50–0.750.75–0.875Topically related
0.30–0.500.65–0.75Weakly related
0.00–0.300.50–0.65Unrelated

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)

SQL
-- 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).

SQL
-- 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

Reference:

PreviousIndex parameters and tuning for the Virtuoso vector index NextManaging vector indexes visually with the VVec Console