Skip to main content

Vector Functions

Vector-Distance Functions​

These functions measure how close two equal-length vectors are — the basis of vector search, where a query embedding is compared against the embeddings stored in a column.

Every function and operator here is a plain scalar: pass any two vectors and get the distance back, in any expression, with or without an index. The operators (<->, <+>, <=>, <#>) are short forms of the named functions. When a query orders by — or filters on — the distance between an indexed column and a constant query vector, as in ORDER BY embedding <-> :query LIMIT k, the planner answers it from the column's IVF index and returns the approximate k nearest neighbours — far faster than scoring every row. Each function computes one fixed metric and only uses an index whose configured metric matches it (l2, l1, cosine or ip).

Lower distance means more similar. cosine_similarity and inner_product are the exceptions — there, higher means more similar. negative_inner_product flips the sign so that lower is again more similar, which is what the ip index metric needs.

Function / operatorMetricDirectionDescription
l2_distance(a, b)L2lower = closerEuclidean (straight-line) distance.
l2_sqr_distance(a, b)L2²lower = closerSquared Euclidean distance (skips the square root).
l1_distance(a, b)L1lower = closerManhattan distance.
cosine_distance(a, b)cosinelower = closer1 − cosine_similarity.
cosine_similarity(a, b)cosinehigher = closerCosine of the angle between the vectors.
inner_product(a, b)dothigher = closerDot product.
negative_inner_product(a, b)iplower = closer−inner_product; backs the ip index metric.
a <-> bL2lower = closerSame as l2_distance.
a <+> bL1lower = closerSame as l1_distance.
a <=> bcosinelower = closerSame as cosine_distance.
a <#> biplower = closerSame as negative_inner_product.
l2_norm(a)L2—Vector magnitude.
l1_norm(a)L1—Vector magnitude.
l2_normalize(a)L2—Scale to a unit vector.
l1_normalize(a)L1—Scale to a unit vector.

DuckDB's names for the same functions take lists and arrays alike, and use the index the same way: list_distance and array_distance are l2_distance; list_cosine_distance and array_cosine_distance are cosine_distance; list_cosine_similarity and array_cosine_similarity are cosine_similarity; list_inner_product, array_inner_product, list_dot_product and array_dot_product are inner_product; list_negative_inner_product, array_negative_inner_product, list_negative_dot_product and array_negative_dot_product are negative_inner_product; vector_norm is l2_norm.

Choosing a metric​

The four metrics answer different questions about two vectors x and y of length n:

  • Euclidean / L2 — sqrt(Σ (xᵢ − yᵢ)²). Straight-line distance through space. It accounts for both direction and magnitude, so it is the natural default when the absolute size of the components is meaningful (e.g. raw feature vectors, coordinates).
  • Squared Euclidean / L2² — Σ (xᵢ − yᵢ)². The same ordering as L2 with the square root dropped, so it is cheaper whenever you only rank or threshold and never need the true distance value.
  • Manhattan / L1 — Σ |xᵢ − yᵢ|. Sums the per-axis differences instead of combining them with Pythagoras. Less sensitive to a single large-deviation dimension than L2, and a common choice for sparse or high-dimensional data.
  • Cosine — 1 − (x · y) / (‖x‖ · ‖y‖). Compares only the direction of the vectors and ignores their length. This is the usual choice for text and embedding models, where two documents about the same topic point the same way regardless of length. A zero vector has no direction: its cosine similarity with any vector is 0 and its cosine distance 1, the same values the index scores it with.
  • Inner product (dot) — Σ xᵢ yᵢ. Rewards vectors that are both aligned and large. For vectors that are already L2-normalized to unit length, inner product equals cosine similarity, so many embedding pipelines normalize once and then use the cheaper dot product. Because higher means more similar, the index uses negative inner product (negative_inner_product) so that, like every other metric, a smaller value sorts first.

Operators, metrics and the IVF index​

Each operator is bound to exactly one metric and is independent of how a column happens to be indexed — a <-> b always computes L2 even if a comes from a cosine-indexed column. To get acceleration, the operator's metric must match the index's metric:

OperatorMetricEquivalent functionMatching index metric
a <-> bL2l2_distancel2
a <+> bL1l1_distancel1
a <=> bcosinecosine_distancecosine
a <#> bipnegative_inner_productip

A query of the shape

sql
SELECT id FROM index_name ORDER BY emb <-> $query LIMIT k;

lets the planner probe the IVF index's clusters closest to $query and return the approximate k nearest neighbours, touching only a small fraction of the rows. Use the operator whose metric matches the index you built; mixing them (for example <=> against an l2 index) still returns correct distances but cannot use the index, so it falls back to scanning every row. For tiny tables the planner may scan anyway because a sequential pass is cheaper than probing clusters — the speed-up matters at scale.

Distance functions​

l2_distance(a, b)​

Euclidean (L2) distance between two equal-length vectors — the straight-line distance in space.

OperandType
a, bvectors of the same length
SQL
SELECT l2_distance([3, 4, 0]::FLOAT[3], [0, 0, 0]::FLOAT[3]) AS distance;
 distance
----------
        5

l2_sqr_distance(a, b)​

The squared L2 distance. It preserves the same ordering as l2_distance while skipping the square root, so it is cheaper when you only need to rank or threshold.

OperandType
a, bvectors of the same length
SQL
SELECT l2_sqr_distance([3, 4, 0]::FLOAT[3], [0, 0, 0]::FLOAT[3]) AS distance;
 distance
----------
       25

l1_distance(a, b)​

Manhattan (L1) distance — the sum of the absolute per-component differences.

OperandType
a, bvectors of the same length
SQL
SELECT l1_distance([3, 4, 0]::FLOAT[3], [0, 0, 0]::FLOAT[3]) AS distance;
 distance
----------
        7

cosine_distance(a, b)​

Cosine distance, defined as 1 − cosine_similarity. It ignores vector magnitude and depends only on direction, which makes it the usual choice for normalized text/embedding models. Lies in [0, 2].

OperandType
a, bvectors of the same length
SQL
SELECT cosine_distance([1, 0, 0]::FLOAT[3], [1, 1, 0]::FLOAT[3]) AS distance;
 distance
------------
 0.29289323

cosine_similarity(a, b)​

The cosine of the angle between the vectors, in [−1, 1]. Unlike the distance functions, higher values mean more similar. For unit-length vectors it equals inner_product.

OperandType
a, bvectors of the same length
SQL
SELECT cosine_similarity([1, 0, 0]::FLOAT[3], [1, 1, 0]::FLOAT[3]) AS similarity;
 similarity
------------
 0.70710677

inner_product(a, b)​

The dot product of the two vectors. Higher means more similar; for unit-length vectors it equals cosine_similarity.

OperandType
a, bvectors of the same length
SQL
SELECT inner_product([1, 2, 3]::FLOAT[3], [4, 5, 6]::FLOAT[3]) AS dot;
 dot
-----
  32

negative_inner_product(a, b)​

−inner_product, so that lower values mean more similar — the form used by the ip index metric, where the index orders neighbours by ascending distance. It is the function behind the <#> operator.

OperandType
a, bvectors of the same length
SQL
SELECT negative_inner_product([1, 2, 3]::FLOAT[3], [4, 5, 6]::FLOAT[3]) AS neg_dot;
 neg_dot
---------
     -32

Distance operators: <->, <+>, <=>, <#>​

Each operator maps to one metric and to a named function, and like it only accelerates a query when its metric matches the index (see Operators, metrics and the IVF index). On GEOMETRY operands <-> is instead the planar distance, as in PostGIS — see Geo Functions.

OperatorMetricEquivalent function
a <-> bl2l2_distance
a <+> bl1l1_distance
a <=> bcosinecosine_distance
a <#> bipnegative_inner_product

kNN: the k closest vectors​

Order by the operator and LIMIT to the number of neighbours you want. The query below asks the l2-metric index for the two nearest rows to the origin and projects the distance alongside the id:

SQL
SELECT id, emb <-> [0, 0, 0]::FLOAT[3] AS distanceFROM vecs_l2ORDER BY distance, idLIMIT 2;
 id | distance
----+----------
  1 |        1
  3 |        1

Range search: every vector within a radius​

Compare the same operator in a WHERE clause to return all vectors closer than a threshold, rather than a fixed count:

SQL
SELECT id, emb <-> [0, 0, 0]::FLOAT[3] AS distanceFROM vecs_l2WHERE emb <-> [0, 0, 0]::FLOAT[3] < 2ORDER BY distance, id;
 id | distance
----+----------
  1 |        1
  3 |        1

The two forms combine: add ORDER BY emb <-> $query LIMIT k to a range query to take the closest k within the radius.

The other metrics​

The same pattern works with each metric's operator against an index built for that metric.

Manhattan (<+>, l1 index):

SQL
SELECT id, emb <+> [0, 0, 0]::FLOAT[3] AS distanceFROM vecs_lORDER BY distance, idLIMIT 2;
 id | distance
----+----------
  1 |        1
  3 |        1

Cosine (<=>, cosine index) — id 1 is the query direction itself, so its distance is 0:

SQL
SELECT id, emb <=> [1, 0, 0]::FLOAT[3] AS distanceFROM vecs_cORDER BY distance, idLIMIT 2;
 id | distance
----+------------
  1 |          0
  3 | 0.29289323

Inner product (<#>, ip index) — the operator returns the negative dot product, so the largest dot product (id 3, the longest aligned vector) sorts first:

SQL
SELECT id, emb <#> [1, 1, 1]::FLOAT[3] AS distanceFROM vecs_iORDER BY distance, idLIMIT 3;
 id | distance
----+----------
  3 |       -6
  2 |       -5
  1 |       -1

Norms and normalization​

l2_norm and l1_norm return a vector's magnitude; l2_normalize and l1_normalize scale it to a unit vector under the corresponding norm. Normalizing before indexing with cosine (or comparing with inner_product) keeps comparisons magnitude-independent — once vectors are unit-length, inner_product and cosine_similarity coincide.

FunctionOperandReturns
l2_norm(a)vectorscalar magnitude under the L2 norm
l1_norm(a)vectorscalar magnitude under the L1 norm
l2_normalize(a)vectorunit vector of the same type under the L2 norm; a zero vector stays zero
l1_normalize(a)vectorunit vector of the same type under the L1 norm; a zero vector stays zero
SQL
SELECT l2_norm([3, 4, 0]::FLOAT[3]) AS norm;
 norm
------
    5
SQL
SELECT l1_norm([3, 4, 0]::FLOAT[3]) AS norm;
 norm
------
    7
SQL
SELECT l2_normalize([3, 4, 0]::FLOAT[3]) AS unit;
 unit
-------------
 {0.6,0.8,0}
SQL
SELECT l1_normalize([3, 4, 0]::FLOAT[3]) AS unit;
 unit
--------------------------
 {0.42857146,0.5714286,0}

Coming from Elasticsearch​

Elasticsearch kNN search over a dense_vector field maps onto a SereneDB ivf index queried with a distance operator:

Elasticsearch / OpenSearchSereneDB
dense_vector field + index: ivfFLOAT[N] column with an ivf operator class
similarity: l2_norm<-> / l2_distance (metric l2)
similarity: cosine<=> / cosine_distance (metric cosine)
similarity: dot_product / max_inner_product<#> / negative_inner_product (metric ip)
knn query (k, num_candidates)ORDER BY emb <-> $q LIMIT k
knn with filtera WHERE clause beside the ORDER BY (Hybrid Search)

Notable differences. SereneDB adds a Manhattan metric (<+> / l1_distance) that Elasticsearch lacks, while Elasticsearch's l_inf / Hamming (binary-vector) similarities have no SereneDB equivalent. Cluster count (nlist) and vector compression (quant) are set at index build time, while sdb_ivf_search_nprobe — SereneDB's analogue of num_candidates — and sdb_rerank_factor tune recall per session.

See also​