Repository navigation
Take a KNN query's vector, k and mmr_lambda from a joined table - #14
Conversation
… joined table A join that supplied k or mmr_lambda from another table failed with the LIMIT error or silently dropped mmr_lambda, and one that supplied the query vector could return no rows, because vec0BestIndex accepted plans that scan vec0 before that table. This ports the MATCH, k and mmr_lambda part of vlasky#14; its refusal of plans without joined filters is left out, because it would turn a lookup join into k rows per joined row.
|
Claude and I ported the MATCH/ These checks fix a LIMIT error for joined The filter checks change this lookup join: create virtual table v using vec0(embedding float[2], cat integer);
with recursive points(i) as (
values (1) union all select i + 1 from points where i < 30
)
insert into v(rowid, embedding, cat)
select i, '[' || i || ',0]', (i - 1) % 3 + 1 from points;
create table categories(id integer primary key, name text);
insert into categories values (1, 'a'), (2, 'b'), (3, 'c');
select v.rowid, c.name from v join categories c on c.id = v.cat
where v.embedding match '[0,0]' and v.k = 3;On SQLite 3.54.1 and 3.50.4, this returns 3 rows on The PR forces one KNN search per category. For per-category searches, this already works on main: select c.id,
(select group_concat(v.rowid) from v
where v.embedding match '[0,0]' and v.k = 3 and v.cat = c.id) as nearest
from categories c; |
A join that supplied k from another table failed with "A LIMIT or 'k = ?' constraint is required on vec0 knn queries.", one that supplied mmr_lambda returned no rows, and one that supplied the query vector through a lookup could return no rows. xBestIndex skipped an unusable constraint, and when a KNN argument reads another table of the query, SQLite also weighs plans that scan vec0 before that table, in which the argument is unusable. Six points on a line and a one-row table w: `select v.rowid from v, w where v.embedding match '[0,0]' and v.k = w.n` failed in either FROM order, and `v.k = 2 and v.mmr_lambda = w.lambda` returned nothing. When a MATCH on a vector column is present, xBestIndex now returns SQLITE_CONSTRAINT for a plan in which the MATCH, k or a hidden parameter column declared after k (mmr_lambda among them) is unusable, the way json_each refuses a plan that cannot supply its argument, so the planner takes one that gives vec0 every argument. Filters on rowid, partition key, metadata and distance columns are left to the plan. vec0 cannot tell a lookup join from a filter join, and refusing the plans without such a filter turned the lookup `select v.rowid, c.name from v join categories c on c.id = v.cat where v.embedding match '[0,0]' and v.k = 3` from the 3 nearest rows into 3 rows per category, as Matthew McEachen measured on SQLite 3.54.1 and 3.50.4 (vlasky#14 (comment)) and as it measures here on 3.46.1. ARCHITECTURE.md's idxStr section says when such a filter applies before the k all the same. The scope and the tests follow his port, photostructure@64db37c tests/test-knn-joined-constraints.py takes the query vector, k and mmr_lambda from another table in both FROM orders, checks that a table that supplies one of them filters before the k, and that a lookup join returns k rows in either FROM order. Ten of its sixteen cases fail on e67755f, and the four lookup cases fail with filters refused as well.
9339147 to
db4345a
Compare
A join that supplied k from another table failed with "A LIMIT or 'k = ?' constraint is required on vec0 knn queries.", and one that supplied the query vector through a lookup could return no rows. xBestIndex skipped an unusable constraint, and when a KNN argument reads another table of the query, SQLite also weighs plans that scan vec0 before that table, in which the argument is unusable. Six points on a line and a one-row table w: `select v.rowid from v, w where v.embedding match '[0,0]' and v.k = w.n` failed in either FROM order. When a MATCH on a vector column is present, xBestIndex now returns SQLITE_CONSTRAINT for a plan in which the MATCH, k or a hidden parameter column declared after k is unusable, the way json_each refuses a plan that cannot supply its argument, so the planner takes one that gives vec0 every argument. Filters on rowid, partition key, metadata and distance columns are left to the plan. vec0 cannot tell a lookup join from a filter join, and refusing the plans without such a filter turned the lookup `select v.rowid, c.name from v join categories c on c.id = v.cat where v.embedding match '[0,0]' and v.k = 3` from the 3 nearest rows into 3 rows per category, as Matthew McEachen measured on SQLite 3.54.1 and 3.50.4 (vlasky#14 (comment)) and as it measures here on 3.46.1. ARCHITECTURE.md's idxStr section says when such a filter applies before the k all the same. The scope and the tests follow his port, photostructure@64db37c tests/test-knn-joined-constraints.py takes the query vector and k from another table in both FROM orders, checks that a table that supplies one of them filters before the k, and that a lookup join returns k rows in either FROM order. Six of its twelve cases fail on 04d28bd.
|
Your reproduction gives the same here on SQLite 3.46.1: 3 rows on e67755f, 9 on 9339147. The branch now has your scope (db4345a): The tests follow yours. |
|
Thanks for this. The approach is the one SQLite documents for required virtual-table parameters, and in testing it fixes more than the description claims ( The fix: a duplicate select v.rowid from v, w where v.embedding match '[0,0]' and v.k = 2 and v.k = w.n;
-- main: [1, 2] this branch: []Every plan with Two claims in the new ARCHITECTURE.md paragraph did not hold up, so I reworded them:
The commit also notes that a cycle (the argument's table reads |
A KNN query whose
kcomes from another table fails, and one whose query vector ormmr_lambdacomes from it can return no rows:vec0BestIndexskips an unusable constraint, and when a KNN argument reads another table of the query, SQLite also weighs plans that scanvec0before that table, in which the argument is unusable. Aktaken from a one-row table fails the same way in either FROM order, and anmmr_lambdataken from one returns no rows.When a MATCH on a vector column is present,
vec0BestIndexnow returnsSQLITE_CONSTRAINTfor a plan in which the MATCH,kor a hidden parameter column declared afterk(mmr_lambdaamong them) is unusable, the wayjson_eachrefuses a plan that cannot supply its argument, so the planner takes one that givesvec0every argument. Each query above then returns the 2 nearest in each category,(1, 1), (1, 2), (2, 4), (2, 5): withcategoriesread first,v.tag = c.idis usable too.Filters on rowid, partition key, metadata and distance columns are left to the plan, as in @mceachen's port, photostructure/sqlite-vec@64db37c.
vec0cannot tell a lookup join from a filter join, and refusing the plans without such a filter turns his lookup from the 3 nearest rows into 3 rows per category. ARCHITECTURE.md's idxStr section says when a filter applies before the k all the same: the other table supplies a KNN argument, aCROSS JOINnames it first,vec0is on the right of aLEFT JOINwith the filter inON, or the KNN is a correlated subquery whose filter reads the outer query's columns.tests/test-knn-joined-constraints.pytakes the query vector,kandmmr_lambdafrom another table in both FROM orders, checks that a table that supplies one of them filters before the k, and that a lookup join returns k rows in either FROM order. 10 of its 16 cases fail on main and all pass here; refusing the filters as well fails the 4 lookup cases. The full suite passes (159 passed, 4 skipped, 259 snapshots).The same change for asg017/sqlite-vec is asg017#331.