Drag the orange query or any grey vector (keyboard: focus a point, use arrow keys). The three nearest are highlighted. Contours show the distance of the 1st, 3rd and 5th nearest.
Drag the orange query or any grey vector (keyboard: focus a point, use arrow keys). The three nearest are highlighted. Contours show the distance of the 1st, 3rd and 5th nearest.
| Operator | Distance | "Equally close" looks like | Index operator class | Use when |
|---|---|---|---|---|
<-> | L2 (Euclidean) | Circles around the query | vector_l2_ops | Magnitude carries meaning (image features, coordinates) |
<=> | Cosine distance = 1 − cos θ | Rays from the origin — only the angle matters | vector_cosine_ops | Text embeddings whose length you want to ignore |
<#> | Negative inner product | Straight lines perpendicular to the query — longer vectors win | vector_ip_ops | Normalised embeddings (fastest), or recommendation scores |
<+> | L1 (taxicab) | Diamonds | vector_l1_ops (HNSW only) | Sparse-ish features, robustness to outliers |
Tick Normalise and every point snaps to the unit circle. Now cosine, inner product and L2 give the same order: for unit vectors, ‖a − b‖² = 2 − 2·cos θ and a·b = cos θ. That's why the choice barely matters for OpenAI embeddings (we checked: all 500,000 vectors in our benchmark set have length 1 ± 2 × 10⁻⁷) — what matters is that the index opclass and the query operator match, or the index is ignored (see the plans).
The SQL panel reproduces the current ranking in pgvector. Values agree with the table to about six significant digits — pgvector stores float4 elements and returns float8 distances, while this page computes in JavaScript doubles. Here are the four operators on a fixed example, run on our server:
demo=# SELECT l2_distance('[1,2,3]'::vector, '[4,5,6]') AS l2,
demo-# inner_product('[1,2,3]'::vector, '[4,5,6]') AS ip,
demo-# cosine_distance('[1,2,3]'::vector, '[4,5,6]') AS cosine,
demo-# l1_distance('[1,2,3]'::vector, '[4,5,6]') AS l1;
l2 | ip | cosine | l1
-------------------+----+----------------------+----
5.196152422706632 | 32 | 0.025368153802923787 | 9
(1 row)
demo=# SELECT vector_dims('[1,2,3]'::vector) AS dims,
demo-# vector_norm('[3,4]'::vector) AS norm,
demo-# l2_normalize('[3,4]'::vector) AS normalized,
demo-# subvector('[1,2,3,4,5]'::vector, 2, 3) AS sub;
dims | norm | normalized | sub
------+------+------------+---------
3 | 5 | [0.6,0.8] | [2,3,4]
(1 row)
demo=# SELECT '[1,2]'::vector + '[3,4]' AS added, '[1,2]'::vector - '[3,4]' AS subtracted,
demo-# '[1,2]'::vector * '[3,4]' AS multiplied, '[1,2]'::vector || '[3]' AS concatenated;
added | subtracted | multiplied | concatenated
-------+------------+------------+--------------
[4,6] | [-2,-2] | [3,8] | [1,2,3]
(1 row)If your embeddings are normalised to length 1 — OpenAI, Cohere and most sentence-transformer models are, or can be — they produce the same ranking, and inner product (<#> with vector_ip_ops) does slightly less arithmetic. If vector lengths vary and you don't want length to count, use cosine (<=>). Toggle Normalise all vectors above to see the rankings converge.
<#> returns the negative inner product so that, like every other pgvector operator, a smaller value means closer and ORDER BY … ASC works with an index. Multiply by −1 to get the actual inner product.
Cosine similarity = 1 − cosine distance: SELECT 1 - (embedding <=> $1) AS similarity. Keep the ORDER BY on the distance itself so the index can be used.