fix(ts-oss/redis): fall back to "*" for empty or all-null filters (#6014)
This commit is contained in:
@@ -133,6 +133,18 @@ function toSnakeCase(obj: Record<string, any>): Record<string, any> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a RediSearch pre-filter expression from filters. Returns "*" (match
|
||||||
|
// all) when there are no usable conditions — including an empty filters object
|
||||||
|
// or one whose values are all null/undefined — since an empty expression is an
|
||||||
|
// invalid RediSearch query (e.g. ` =>[KNN ...]`).
|
||||||
|
export function buildRedisFilterExpr(filters?: SearchFilters): string {
|
||||||
|
if (!filters) return "*";
|
||||||
|
const conditions = Object.entries(toSnakeCase(filters))
|
||||||
|
.filter(([, value]) => value !== null && value !== undefined)
|
||||||
|
.map(([key, value]) => `@${key}:{${escapeRedisTagValue(value)}}`);
|
||||||
|
return conditions.length > 0 ? conditions.join(" ") : "*";
|
||||||
|
}
|
||||||
|
|
||||||
// Utility function to convert object keys to camelCase
|
// Utility function to convert object keys to camelCase
|
||||||
function toCamelCase(obj: Record<string, any>): Record<string, any> {
|
function toCamelCase(obj: Record<string, any>): Record<string, any> {
|
||||||
if (typeof obj !== "object" || obj === null) return obj;
|
if (typeof obj !== "object" || obj === null) return obj;
|
||||||
@@ -406,13 +418,7 @@ export class RedisDB implements VectorStore {
|
|||||||
filters?: SearchFilters,
|
filters?: SearchFilters,
|
||||||
): Promise<VectorStoreResult[]> {
|
): Promise<VectorStoreResult[]> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
const filterExpr = buildRedisFilterExpr(filters);
|
||||||
const filterExpr = snakeFilters
|
|
||||||
? Object.entries(snakeFilters)
|
|
||||||
.filter(([_, value]) => value !== null && value !== undefined)
|
|
||||||
.map(([key, value]) => `@${key}:{${escapeRedisTagValue(value)}}`)
|
|
||||||
.join(" ")
|
|
||||||
: "*";
|
|
||||||
|
|
||||||
const queryVector = new Float32Array(query).buffer;
|
const queryVector = new Float32Array(query).buffer;
|
||||||
|
|
||||||
@@ -655,13 +661,7 @@ export class RedisDB implements VectorStore {
|
|||||||
topK: number = 100,
|
topK: number = 100,
|
||||||
): Promise<[VectorStoreResult[], number]> {
|
): Promise<[VectorStoreResult[], number]> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
const filterExpr = buildRedisFilterExpr(filters);
|
||||||
const filterExpr = snakeFilters
|
|
||||||
? Object.entries(snakeFilters)
|
|
||||||
.filter(([_, value]) => value !== null && value !== undefined)
|
|
||||||
.map(([key, value]) => `@${key}:{${escapeRedisTagValue(value)}}`)
|
|
||||||
.join(" ")
|
|
||||||
: "*";
|
|
||||||
|
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
SORTBY: "created_at",
|
SORTBY: "created_at",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { buildRedisFilterExpr } from "../src/vector_stores/redis";
|
||||||
|
|
||||||
|
describe("buildRedisFilterExpr", () => {
|
||||||
|
it("returns '*' when filters is undefined", () => {
|
||||||
|
expect(buildRedisFilterExpr(undefined)).toBe("*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '*' for an empty filters object", () => {
|
||||||
|
// Regression: previously produced "" -> invalid RediSearch query
|
||||||
|
// (` =>[KNN ...]`).
|
||||||
|
expect(buildRedisFilterExpr({})).toBe("*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns '*' when every filter value is null/undefined", () => {
|
||||||
|
expect(
|
||||||
|
buildRedisFilterExpr({ user_id: null as any, agent_id: undefined }),
|
||||||
|
).toBe("*");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds a tag clause for a single filter", () => {
|
||||||
|
expect(buildRedisFilterExpr({ user_id: "alice" })).toBe("@user_id:{alice}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("snake-cases keys and joins multiple clauses", () => {
|
||||||
|
expect(buildRedisFilterExpr({ userId: "alice", runId: "r1" })).toBe(
|
||||||
|
"@user_id:{alice} @run_id:{r1}",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops null/undefined values but keeps the rest", () => {
|
||||||
|
expect(
|
||||||
|
buildRedisFilterExpr({ user_id: "alice", agent_id: null as any }),
|
||||||
|
).toBe("@user_id:{alice}");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user