fix(ts-oss/cassandra): apply every operator in a compound field filter (#6511)
This commit is contained in:
@@ -455,44 +455,64 @@ export class CassandraDB implements VectorStore {
|
|||||||
return value.includes(payloadValue);
|
return value.includes(payloadValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every operator present in a compound condition must hold (AND), so check
|
||||||
|
// them all instead of returning on the first match. Returning early meant a
|
||||||
|
// range like { gte: 10, lte: 20 } only applied `gte`. Mirrors the databricks
|
||||||
|
// store's matcher.
|
||||||
|
let sawOperator = false;
|
||||||
|
|
||||||
if ("eq" in value) {
|
if ("eq" in value) {
|
||||||
return payloadValue === value.eq;
|
sawOperator = true;
|
||||||
|
if (payloadValue !== value.eq) return false;
|
||||||
}
|
}
|
||||||
if ("ne" in value) {
|
if ("ne" in value) {
|
||||||
return payloadValue !== value.ne;
|
sawOperator = true;
|
||||||
|
if (payloadValue === value.ne) return false;
|
||||||
}
|
}
|
||||||
if ("gt" in value) {
|
if ("gt" in value) {
|
||||||
return payloadValue > value.gt;
|
sawOperator = true;
|
||||||
|
if (!(payloadValue > value.gt)) return false;
|
||||||
}
|
}
|
||||||
if ("gte" in value) {
|
if ("gte" in value) {
|
||||||
return payloadValue >= value.gte;
|
sawOperator = true;
|
||||||
|
if (!(payloadValue >= value.gte)) return false;
|
||||||
}
|
}
|
||||||
if ("lt" in value) {
|
if ("lt" in value) {
|
||||||
return payloadValue < value.lt;
|
sawOperator = true;
|
||||||
|
if (!(payloadValue < value.lt)) return false;
|
||||||
}
|
}
|
||||||
if ("lte" in value) {
|
if ("lte" in value) {
|
||||||
return payloadValue <= value.lte;
|
sawOperator = true;
|
||||||
|
if (!(payloadValue <= value.lte)) return false;
|
||||||
}
|
}
|
||||||
if ("in" in value) {
|
if ("in" in value) {
|
||||||
return Array.isArray(value.in) && value.in.includes(payloadValue);
|
sawOperator = true;
|
||||||
|
if (!Array.isArray(value.in) || !value.in.includes(payloadValue))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if ("nin" in value) {
|
if ("nin" in value) {
|
||||||
return !Array.isArray(value.nin) || !value.nin.includes(payloadValue);
|
sawOperator = true;
|
||||||
|
if (Array.isArray(value.nin) && value.nin.includes(payloadValue))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if ("contains" in value) {
|
if ("contains" in value) {
|
||||||
return (
|
sawOperator = true;
|
||||||
typeof payloadValue === "string" &&
|
if (
|
||||||
payloadValue.includes(value.contains)
|
typeof payloadValue !== "string" ||
|
||||||
);
|
!payloadValue.includes(value.contains)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if ("icontains" in value) {
|
if ("icontains" in value) {
|
||||||
return (
|
sawOperator = true;
|
||||||
typeof payloadValue === "string" &&
|
if (
|
||||||
payloadValue.toLowerCase().includes(value.icontains.toLowerCase())
|
typeof payloadValue !== "string" ||
|
||||||
);
|
!payloadValue.toLowerCase().includes(value.icontains.toLowerCase())
|
||||||
|
)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return payloadValue === value;
|
return sawOperator ? true : payloadValue === value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterVector(
|
private filterVector(
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/// <reference types="jest" />
|
||||||
|
/**
|
||||||
|
* Cassandra vector store — filter matching unit tests.
|
||||||
|
*
|
||||||
|
* Cassandra has no server-side metadata filter, so search()/list() scan rows
|
||||||
|
* and apply filters in-app via matchFieldCondition(). These tests drive that
|
||||||
|
* matcher through the public list() API with an injected fake client.
|
||||||
|
*/
|
||||||
|
import { CassandraDB } from "../src/vector_stores/cassandra";
|
||||||
|
|
||||||
|
type Row = { id: string; payload: Record<string, any> };
|
||||||
|
|
||||||
|
// Minimal fake driver: CREATE statements during initialize() return nothing;
|
||||||
|
// a SELECT returns the seeded rows in one page (pageState undefined => stop).
|
||||||
|
function fakeClient(rows: Row[]) {
|
||||||
|
return {
|
||||||
|
async connect() {},
|
||||||
|
async execute(query: string) {
|
||||||
|
if (/^\s*SELECT/i.test(query)) {
|
||||||
|
return { rows, pageState: undefined };
|
||||||
|
}
|
||||||
|
return { rows: [], pageState: undefined };
|
||||||
|
},
|
||||||
|
async shutdown() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeStore(rows: Row[]) {
|
||||||
|
return new CassandraDB({
|
||||||
|
keyspace: "mem0",
|
||||||
|
collectionName: "mem0",
|
||||||
|
dimension: 3,
|
||||||
|
client: fakeClient(rows) as any,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CassandraDB filter matching", () => {
|
||||||
|
const rows: Row[] = [
|
||||||
|
{ id: "a", payload: { data: "a", age: 5 } },
|
||||||
|
{ id: "b", payload: { data: "b", age: 15 } },
|
||||||
|
{ id: "c", payload: { data: "c", age: 25 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
it("applies every operator in a compound range filter (not just the first)", async () => {
|
||||||
|
const store = makeStore(rows);
|
||||||
|
// age in [10, 20]: only "b" (15) qualifies. The old matcher returned on the
|
||||||
|
// first operator (gte), so "c" (25) leaked through because lte was ignored.
|
||||||
|
const [results] = await store.list({ age: { gte: 10, lte: 20 } });
|
||||||
|
expect(results.map((r) => r.id)).toEqual(["b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still matches a single-operator filter", async () => {
|
||||||
|
const store = makeStore(rows);
|
||||||
|
const [results] = await store.list({ age: { gte: 15 } });
|
||||||
|
expect(results.map((r) => r.id).sort()).toEqual(["b", "c"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a plain equality filter as before", async () => {
|
||||||
|
const store = makeStore(rows);
|
||||||
|
const [results] = await store.list({ age: 15 });
|
||||||
|
expect(results.map((r) => r.id)).toEqual(["b"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user