Add new pg_bm25 full-text-search postgres extension #18061
Replies: 5 comments 13 replies
|
pg_search would be awesome! Then I could try not adding elasticsearch to the stack. |
|
Hi everyone! Phil here from @paradedb. You can get "ParadeDB on Supabase" today by doing logical replication. Supabase's docs: https://supabase.com/docs/guides/database/postgres/setup-replication-external |
|
Hey all, |
|
While waiting for native pg_search/pg_bm25 support, you can achieve solid hybrid search using PostgreSQL's built-in full-text search combined with pgvector. I've been running this in production with ~1M rows and text-dense chunks (1-20 paragraphs per row). Table StructureAdd a CREATE TABLE your_doc_vec (
id serial PRIMARY KEY,
chunk_text text NOT NULL,
embedding vector NOT NULL,
-- Auto-maintained tsvector column
chunk_text_tsv tsvector GENERATED ALWAYS AS (to_tsvector('danish', chunk_text)) STORED
);
-- GIN index for fast text search
CREATE INDEX idx_chunk_text_tsv_gin ON your_doc_vec USING GIN (chunk_text_tsv);
-- HNSW index for vector search
CREATE INDEX idx_embedding_hnsw ON your_doc_vec
USING hnsw (embedding vector_cosine_ops) WITH (m = 32, ef_construction = 128);The RRF Score FunctionUse Reciprocal Rank Fusion to combine vector and text search results: CREATE OR REPLACE FUNCTION rrf_score(rank_val bigint, k integer DEFAULT 60)
RETURNS numeric
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
SELECT CASE WHEN rank_val IS NULL THEN 0.0
ELSE 1.0 / (rank_val::numeric + k::numeric) END;
$$;Hybrid Search FunctionCREATE OR REPLACE FUNCTION search_hybrid(
p_embedding vector,
p_search_text text,
p_limit integer DEFAULT 30,
p_rrf_k integer DEFAULT 60
)
RETURNS TABLE(id integer, chunk_text text, score numeric)
LANGUAGE plpgsql AS $$
BEGIN
SET LOCAL work_mem = '256MB'; -- helps with large tables
RETURN QUERY
WITH vector_results AS (
SELECT doc.id, doc.chunk_text,
row_number() OVER (ORDER BY doc.embedding <=> p_embedding) AS rank
FROM your_doc_vec doc
ORDER BY doc.embedding <=> p_embedding
LIMIT p_limit
),
text_results AS (
SELECT doc.id, doc.chunk_text,
row_number() OVER (ORDER BY ts_rank_cd(doc.chunk_text_tsv,
plainto_tsquery('danish', p_search_text)) DESC) AS rank
FROM your_doc_vec doc
WHERE plainto_tsquery('danish', p_search_text) @@ doc.chunk_text_tsv
ORDER BY ts_rank_cd(doc.chunk_text_tsv, plainto_tsquery('danish', p_search_text)) DESC
LIMIT p_limit
),
combined AS (
SELECT * FROM vector_results
UNION ALL
SELECT * FROM text_results
)
SELECT c.id, c.chunk_text, sum(rrf_score(c.rank, p_rrf_k)) AS score
FROM combined c
GROUP BY c.id, c.chunk_text
ORDER BY score DESC
LIMIT p_limit;
END;
$$;How It Works
The function runs both searches in parallel, ranks each result set, then combines them using RRF. Documents appearing in both result sets get boosted scores.
Key Points
This approach gives you hybrid search with full RLS support today, no external services needed. Feel free to reach out if you have any questions — happy to help! |
|
@ElectricCodeGuy's version is the right shape and matches what I ended up with. Two things in it are worth flagging for anyone copying it, because both fail silently rather than erroring.
On a twelve row test corpus a three word query matched exactly one row under AND. If you are running this in production, worth checking how often your text CTE is actually returning rows. The fix is to OR the terms and let ranking do the work, since The window has to sit outside the LIMIT. In SELECT doc.id, row_number() OVER (ORDER BY ...) AS rank
FROM your_doc_vec doc
ORDER BY ... LIMIT p_limitthe SELECT id, row_number() OVER (ORDER BY distance) AS rank
FROM (
SELECT id, embedding <=> p_embedding AS distance
FROM your_doc_vec
ORDER BY distance, id
LIMIT p_limit
) c1.19ms against 0.85ms on 100k rows in my measurements, and the gap widens with the table. The On the RLS point @Rubenburdin raised: that is the strongest argument against the replication workaround and it does not apply to this approach at all. The query runs on your own connection against your own table, so RLS applies exactly as it does everywhere else. No second instance, no replication lag, no policy to reimplement. None of this beats real BM25. I packaged the above after hitting all of it: pghybrid generates the statement with OR semantics, the window outside the limit, tiebreakers on both sides, and a |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
A new, powerful full-text-search extension for postgres has been released recently called pg_bm25.
I think it would be a huge benefit to add support for this new interesting extension to the supabase environment (docker image & cloud).
Besides the basic pg_bm25 extension, there's also the pg_search extension to combine bm25 with vector searches which would probably make sense to add as well.
All reactions