Use when working with embeddings, semantic similarity, vector search, or the <-> <#> <=> operators. Load for pgvector queries, HNSW index creation, embedding storage, or similarity calculations...
Vector similarity search patterns for semantic matching.
Announce: "I'm using postgres-vectors to implement vector similarity correctly."
Embeddings are stored in the embeddings table:
CREATE TABLE embeddings (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
source_text text NOT NULL,
embedding extensions.vector(384) NOT NULL, -- 384 dimensions from gte-small
created_at timestamptz DEFAULT now()
);
-- Unique constraint prevents duplicate embeddings
CREATE UNIQUE INDEX idx_embeddings_source_text
ON embeddings (source_text);
Key points:
extensions.vector(384) - pgvector is in extensions schema| Operator | Distance Type | Use Case |
|---|---|---|
<-> |
L2 (Euclidean) | General purpose |
<#> |
Inner product (negative) | Normalized vectors (fastest) |
<=> |
Cosine distance | When normalization varies |
For this project: Use <=> (cosine) since we want similarity regardless of vector magnitude.
-- Cosine similarity = 1 - cosine_distance
SELECT
id,
1 - (embedding <=> query_embedding) AS similarity
FROM embeddings
WHERE 1 - (embedding <=> query_embedding) > 0.5 -- 0.5 threshold
ORDER BY embedding <=> query_embedding -- ASC for distance
LIMIT 10;
-- HNSW: Good recall, slightly slower builds
CREATE INDEX idx_embeddings_hnsw
ON embeddings
USING hnsw (embedding extensions.vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
Use HNSW when:
<=>)-- IVFFlat: Faster builds, requires tuning
CREATE INDEX idx_embeddings_ivfflat
ON embeddings
USING ivfflat (embedding extensions.vector_cosine_ops)
WITH (lists = 100);
-- Query with probes
SET ivfflat.probes = 10;
Use IVFFlat when:
lists and probesWe prioritize accuracy for semantic matching.
-- Find traits similar to a query
SELECT
t.id,
t.canonical_text,
1 - (e.embedding <=> v_query_embedding) AS similarity
FROM traits t
JOIN embeddings e ON e.source_text = t.canonical_text
WHERE 1 - (e.embedding <=> v_query_embedding) > 0.6
ORDER BY e.embedding <=> v_query_embedding
LIMIT 20;
-- Calculate similarity for multiple places at once
WITH place_similarities AS (
SELECT
pt.place_id,
1 - (e.embedding <=> v_description_embedding) AS trait_similarity
FROM place_traits pt
JOIN embeddings e ON e.id = pt.embedding_id
)
SELECT
place_id,
AVG(trait_similarity) AS avg_similarity,
MAX(trait_similarity) AS max_similarity
FROM place_similarities
GROUP BY place_id
ORDER BY avg_similarity DESC;
-- WRONG: Index can't help with post-filter
SELECT * FROM embeddings
ORDER BY embedding <=> query
LIMIT 100
WHERE some_condition; -- Filter after ORDER BY
-- CORRECT: Filter first, then order
SELECT * FROM embeddings
WHERE some_condition
ORDER BY embedding <=> query
LIMIT 100;
-- WRONG: Index is cosine_ops but query uses L2
CREATE INDEX ... USING hnsw (embedding vector_cosine_ops);
SELECT * FROM embeddings ORDER BY embedding <-> query; -- L2!
-- CORRECT: Match operator to index
SELECT * FROM embeddings ORDER BY embedding <=> query; -- Cosine
-- WRONG: Will fail
embedding vector(384)
-- CORRECT: Prefix with extensions
embedding extensions.vector(384)
Embeddings come from edge function, stored via database function:
-- Called by edge function after generating embedding
INSERT INTO embeddings (source_text, embedding)
VALUES (v_text, v_embedding)
ON CONFLICT (source_text) DO UPDATE
SET embedding = EXCLUDED.embedding;
See references/similarity-queries.md for more query examples.