dedupe and duplicate_pairs find these directly over a
vector index, without a pairwise scan.
Example data
Connect with an Enterprise connection. A small fixture keeps the distances easy to check by hand.Create the example table
Create the example table
curation_embeddings has eight records. Rows 10 and 20 are near-identical;
rows 30, 40 and 50 form one looser cluster; the rest are distinct.dedupe and duplicate_pairs read the vector index, so the column needs one
before either function runs; a query against unindexed fragments is rejected
rather than silently returning stale or wrong results.For an l2 index, distance_threshold and the distance column below are
both squared L2, not the plain L2 a plain SQL distance() call returns.Collapse near-duplicates
dedupe returns one representative row per cluster of records within
distance_threshold of each other — rows 10/20 collapse to one, 30/40/50
collapse to one, and the four distinct rows pass through unchanged, leaving
ids 10, 30, 60, 70 and 80:
dedupe returns full rows, not
just which ones matched. Clustering is transitive: 30 and 40 are within
distance_threshold of each other, and so are 40 and 50, so all three
collapse into one representative even though 30 and 50 alone sit outside the
threshold. Pass resolution => 'direct_representative'
for a simpler, non-transitive selection instead, if a chained cluster like that
should not merge into one representative.
Which row survives as the representative is not guaranteed to stay the same
across appends: a later row that bridges two previously separate clusters can
change which id represents the merged cluster. Do not treat a representative’s
id as a stable identifier for its cluster.
Inspect the pairs instead
duplicate_pairs returns the matching edges themselves, without collapsing
anything — useful for review, or for a threshold that is still being tuned:
(10, 20), (30, 40) and (40, 50) — note 30 and 50
are not a direct pair here (their squared L2 distance is 0.01, outside the threshold),
even though dedupe merges all three transitively above.
Keep it current with a materialized view
Wrap either call in a materialized view so the deduplicated set stays current as new records arrive, instead of recomputing it from scratch on every curation pass:At production scale
LeRobotDataset runs the samededupe
call over 27.6 million real embedding rows to remove near-static frames from a
robotics training set in under 30 seconds — the mechanism does not change
with scale, only the table it runs against.