---
title: "Similarity Metrics for Virtuoso Vector Index"
description: "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."
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/)/Similarity metrics for the Virtuoso vector index

# 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.

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  Operate  [VVec Console (web UI)](urn:osdi:sitevirtuoso/docs/vector-index/vvec-console/) [Troubleshooting](urn:osdi:sitevirtuoso/docs/vector-index/troubleshooting/)  Reference  [API reference](urn:osdi:sitevirtuoso/docs/vector-index/api-reference/)

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`

Important

The metric is fixed at index creation time. To change metrics, drop the index and create it again with the other metric — `distance_metric` in SQL, `distance` in SPARQL. A SQL index is filled as it is created; a SPARQL index on its next refresh.

---

## 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.

Tip

Because cosine ignores magnitude, it is robust to variations in how strongly a model responds to a passage — it only cares about *what* the text is about.

### 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.

Note

L2 is sensitive to vector magnitude. If your model **normalizes** its output vectors (all unit length), cosine and L2 produce the same ranking: for unit vectors the L2 distance is `sqrt(2 × cosine distance)`. With the bundled granite model, cosine distances 0.2010, 0.4188 and 0.4803 came back from an L2 index as 0.6340, 0.9152 and 0.9801. Only the scores differ, because each metric maps distance to score its own way. The rankings diverge only when vectors have meaningfully different lengths.

### 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.

Tip

A quick test: query a handful of known-similar and known-dissimilar pairs under both cosine and L2. If the gap between similar and dissimilar scores is larger and more stable with cosine, keep the default.

---

## 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](#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');
```

Note

`X_SIMILAR` and `VECTOR_DISTANCE` are computed from the same underlying distance, so ordering by `VECTOR_DISTANCE` and projecting `X_SIMILAR` always produces a consistent ranking.

### SPARQL bif:vector_search

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)
;
```

Note

A limit outside those ranges is an error, not an empty result: a `score_limit` below 0 or above 1 raises `VEC61` on a cosine index (`cosine score_limit must be between 0 and 1 inclusive`) and `VEC62` on an L1 or L2 one, a negative `distance_limit` raises `VEC65`, and a non-finite value raises `VEC60` or `VEC64`. Because the score is a cosine similarity rescaled to `[0, 1]`, a threshold copied from a tool that reports raw cosine similarity `s` corresponds to `score_limit (1 + s) / 2`.

---

## 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_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 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 |

Note

Score ranges differ between models. Both surfaces report the same score for the same distance, so a threshold calibrated in SQL holds in SPARQL; calibrate it for your model and your data.

---

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

### For SPARQL (bif:vector_search)

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)
;
```

Tip

Start with a low threshold (`score_limit 0.5`) to see the full distribution of scores, then tighten it based on where relevant results end and irrelevant ones begin. For cosine, `score_limit x` and `distance_limit 2 * (1 - x)` are the same test, and `score_limit x` is the floor `X_SIMILAR (...) >= x` sets in SQL.

---

## Next steps

- **[Index parameters](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/)** — Full reference for `CREATE VECTOR INDEX` options including `dimensions`, `distance_metric`, and HNSW tuning
- **[SQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sql-vector-index/)** — Complete guide for creating, querying, and maintaining SQL vector indexes
- **[SPARQL vector index how-to](urn:osdi:sitevirtuoso/docs/vector-index/howto-sparql-vector-index/)** — Complete guide for RDF vector indexes with `bif:vector_search`

## Related content

**Reference:**

- [Cosine similarity on Wikipedia](https://en.wikipedia.org/wiki/Cosine_similarity) — Background on the cosine distance metric
- [Efficient and robust approximate nearest neighbor search using HNSW graphs (Malkov & Yashunin)](https://arxiv.org/abs/1603.09320) — The paper that introduced HNSW, the graph the vector index is built on

[PreviousIndex parameters and tuning for the Virtuoso vector index](urn:osdi:sitevirtuoso/docs/vector-index/index-parameters/) [NextManaging vector indexes visually with the VVec Console](urn:osdi:sitevirtuoso/docs/vector-index/vvec-console/)

On this page

1. [Overview](#overview)
2. [Supported metrics](#supported-metrics)
3. [Comparison table](#comparison-table)
4. [Choosing a metric](#choosing-a-metric)
5. [How scores are computed](#how-scores-are-computed)
6. [Practical examples](#practical-examples)
7. [Setting thresholds](#setting-thresholds)
8. [Next steps](#next-steps)
9. [Related content](#related-content)
