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
|
||||
function toCamelCase(obj: Record<string, any>): Record<string, any> {
|
||||
if (typeof obj !== "object" || obj === null) return obj;
|
||||
@@ -406,13 +418,7 @@ export class RedisDB implements VectorStore {
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[]> {
|
||||
await this.initialize();
|
||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
||||
const filterExpr = snakeFilters
|
||||
? Object.entries(snakeFilters)
|
||||
.filter(([_, value]) => value !== null && value !== undefined)
|
||||
.map(([key, value]) => `@${key}:{${escapeRedisTagValue(value)}}`)
|
||||
.join(" ")
|
||||
: "*";
|
||||
const filterExpr = buildRedisFilterExpr(filters);
|
||||
|
||||
const queryVector = new Float32Array(query).buffer;
|
||||
|
||||
@@ -655,13 +661,7 @@ export class RedisDB implements VectorStore {
|
||||
topK: number = 100,
|
||||
): Promise<[VectorStoreResult[], number]> {
|
||||
await this.initialize();
|
||||
const snakeFilters = filters ? toSnakeCase(filters) : undefined;
|
||||
const filterExpr = snakeFilters
|
||||
? Object.entries(snakeFilters)
|
||||
.filter(([_, value]) => value !== null && value !== undefined)
|
||||
.map(([key, value]) => `@${key}:{${escapeRedisTagValue(value)}}`)
|
||||
.join(" ")
|
||||
: "*";
|
||||
const filterExpr = buildRedisFilterExpr(filters);
|
||||
|
||||
const searchOptions = {
|
||||
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