Deduplicating feature requests with pgvector: the threshold is a trap
Two of our users asked for the same thing last month. One wrote βplease add a way to export the report as a spreadsheet.β The other wrote βCSV download for reports?β I filed them as separate requests, because I read them three weeks apart and did not connect them.
That is the whole problem with a feedback board. Duplicates do not announce themselves. They arrive slowly, in different words, and by the time you notice, the votes are split three ways and the one thing everybody wants looks like three things nobody cares much about.
I spent a weekend building semantic dedup for this, mostly to understand how it works before deciding whether to adopt something that already does it. Here is what I got wrong.
String matching does not survive contact with real users
The obvious first attempt is trigram similarity, which Postgres gives you for free:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
SELECT title, similarity(title, 'CSV download for reports')
FROM feature_request
ORDER BY similarity(title, 'CSV download for reports') DESC
LIMIT 5;
On my two examples this scores about 0.13. They share almost no characters. Meanwhile βexport report to PDFβ scores higher than either, because it shares the words export and report while asking for something completely different.
Lexical similarity measures how things are spelled. Duplicate feature requests are duplicated in meaning and almost never in spelling, because two people describing the same frustration will reliably reach for different words.
Embeddings, and the setup that actually matters
The fix is to compare meaning, which means embeddings and pgvector:
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE feature_request ADD COLUMN embedding vector(1536);
CREATE INDEX ON feature_request USING hnsw (embedding vector_cosine_ops);
Two notes on that index, both of which cost me time.
- HNSW builds slower and eats more memory than IVFFlat, but it does not need training data to be present before you build it. On a table that starts empty and grows one request at a time, IVFFlatβs list centroids are computed from whatever happens to be there at build time, which for a new board is nothing useful. Use HNSW unless you are backfilling a large corpus in one shot.
- The operator
Comments
No comments yet. Start the discussion.