Repository navigation
fix(pgvector,supabase): negated ranges, zero vectors, and metric detection - #58
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 8d8fe98 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 27 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
commit: |
Add tests for three query defects that give wrong results without an error. not around a range filter skips a record that lacks the field, a cosine zero vector ranks first once an HNSW query is wrapped in an outer sort by score, and a euclidean table whose name contains an operator class name is read as that metric. The negated range cases run on both PGlite suites and in the filter differential corpus. A plain-path zero vector test is a control. 11 of the new tests fail at this commit and pass once the adapters are fixed.
A range leaf compiled to a jsonb_typeof check and a comparison. With the
field missing, the path is NULL, so the leaf was NULL and NOT of it was
NULL too, which dropped the record. not(gt("year", 2000)) skipped
records without a year, and delete by that filter left them behind,
against the docs.
The leaf now starts with an IS NOT NULL check on the path, so a missing
field makes it false and NOT of it true. The TypeScript compiler and
vecstore_filter_sql change together, and the SQL text expectations that
pin the leaf follow.
An HNSW query is wrapped so an outer sort restores the order after an iterative scan. That sort used the score, descending for cosine and dot. A zero vector has a cosine distance of NaN, so its score is NaN, and Postgres sorts NaN above every number. The zero vector came first, where the inner sort by distance and the plain statement put it last. The wrapped statement now selects the distance next to the score and the outer query sorts by it, ascending for every metric. The plain statement is unchanged. vecstore_query does the same, and the SCORE_ORDERS table and the score_order variable are gone.
The adapter and vecstore_metric read the metric by looking for an operator class name anywhere in an index definition of the table. Every definition contains the table name, so a euclidean table named x_vector_cosine_ops was read as cosine from its primary key. The SQL used like, where the underscore is also a wildcard. Both now match the exact text of the embedding column's index, such as (embedding vector_cosine_ops), and the SQL uses strpos so nothing is read as a pattern.
The range leaf now starts with an IS NOT NULL check on the path. Update the compiled SQL on the filters page and the pgvector and Supabase outputs of the Match and Range demo cards to show it.
3bae98f to
8d8fe98
Compare
Stacked on #57 (which sits on #55 and #54). Review the last six commits, from
test(pgvector,supabase): cover negated ranges, zero vectors, opclasses.Background
Three Postgres query defects gave wrong results with no error, all reproduced on PGlite (Postgres 18.3, pgvector 0.8.1). Each lives in both the pgvector adapter and the SQL functions the Supabase adapter calls, so they ship together and Supabase users re-run
sql/supabase.sqlonce.notaround a range filter skipped records that lack the field. The range leaf was(jsonb_typeof(path) = 'number' AND path > value); for a missing field that'sNULL, andNOT NULLis stillNULL.not(gt("year", 2000))skipped records without ayear, anddelete({ filter: not(gt("year", 2000)) })left them behind. The provider differences page says pgvector and Supabase match them.1 - NaN, and the outerORDER BY hits.score DESCadded for the HNSW iterative scan puts NaN above every number. The innerORDER BY distanceand the plain statement put it last. A zero vector is what a failed embedding call usually returns.like, where_is a wildcard), so a euclidean table namedx_vector_cosine_opswas read as cosine from its primary key.Summary
path IS NOT NULL, in the TypeScript compiler and invecstore_filter_sql.vecstore_querydoes the same.(embedding vector_cosine_ops)and friends exactly; the SQL usesstrpos.sql/supabase.sql.Verification
bun run testtwice in random order: 554 pass, 0 fail.bun run checkandbun run typecheckare clean.Checklist
vecstore-sdk(runbun run changesetin the project root)