18 August 2026 · 2 min
A note is always its own nearest neighbour
Threadly finds notes related to the one you are looking at. It shows five. The query asks for six.
That is not an off-by-one. It is the fix for one.
The problem
Related notes are found by embedding every note on write, then asking pgvector for the closest vectors to the current note's own embedding. Cosine distance, nearest first.
The trouble is that the note you are searching from is in the index too. It is not merely close to itself, it is at distance zero, so it is guaranteed to come back first, every single time.
So the results have to drop it:
const results = neighbors
.filter((r) => r.id !== id)
.slice(0, 5);And the moment you write that filter, asking for five is wrong. You get five, throw one away, and render four. Silently. Forever.
const { data: neighbors } = await supabase.rpc("match_notes", {
query_embedding: embedding,
match_count: 6,
});Six in, one dropped, five out.
The part worth keeping
The bug never throws. There is no error, no warning, no failed request. The feature just quietly does ten percent less than it says it does, and it would have survived indefinitely because four related notes look exactly as plausible as five.
Anywhere you post-filter a result set, the size you request and the size you display are two different numbers, and the gap between them is however many rows the filter can remove. Write them down as different numbers, because that is what they are.
The same shape shows up in a lot of retrieval code. If you filter by score threshold after fetching
k, you can get anywhere from zero to k back. If you deduplicate by document after fetching k
chunks, the same. Fetching exactly what you intend to show only works when nothing between the index
and the render can drop a row.