Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit d1411d6

Browse files
fix(qdrant): fix pre-filter mapping and ingestion loop bug (#2035)
1 parent ba24377 commit d1411d6

3 files changed

Lines changed: 282 additions & 208 deletions

File tree

.changeset/smooth-pandas-stop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@llamaindex/qdrant": patch
3+
---
4+
5+
fix: correctly map NE and NIN filter operators to Qdrant must_not clauses and preserve numeric types in IN filters

packages/providers/storage/qdrant/src/QdrantVectorStore.ts

Lines changed: 54 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
1-
import type { BaseNode, Metadata } from "@llamaindex/core/schema";
1+
import { type BaseNode, type Metadata } from "@llamaindex/core/schema";
22
import {
33
BaseVectorStore,
44
FilterCondition,
55
FilterOperator,
6+
metadataDictToNode,
7+
nodeToMetadata,
68
type MetadataFilters,
79
type VectorStoreBaseParams,
810
type VectorStoreQuery,
911
type VectorStoreQueryResult,
1012
} from "@llamaindex/core/vector-store";
11-
12-
import {
13-
metadataDictToNode,
14-
nodeToMetadata,
15-
} from "@llamaindex/core/vector-store";
1613
import type { QdrantClientParams, Schemas } from "@qdrant/js-client-rest";
1714
import { QdrantClient } from "@qdrant/js-client-rest";
1815

1916
type QdrantFilter = Schemas["Filter"];
2017
type QdrantMustConditions = QdrantFilter["must"];
2118
type QdrantQueryResult = Schemas["QueryResponse"];
2219
type QdrantSearchParams = Schemas["SearchParams"];
20+
type QdrantCondition = Schemas["Condition"]; // Added for type safety
2321

2422
type PointStruct = {
2523
id: string;
26-
payload: Record<string, string>;
24+
payload: Metadata; // Use Metadata type instead of any
2725
vector: number[];
2826
};
2927

@@ -35,6 +33,14 @@ type QdrantParams = {
3533
batchSize?: number;
3634
} & VectorStoreBaseParams;
3735

36+
/**
37+
* Interface for Qdrant-specific query options to avoid 'any' casting.
38+
*/
39+
interface QdrantQueryOptions {
40+
qdrant_filters?: QdrantFilter;
41+
qdrant_search_params?: QdrantSearchParams;
42+
}
43+
3844
/**
3945
* Qdrant vector store.
4046
*/
@@ -144,7 +150,7 @@ export class QdrantVectorStore extends BaseVectorStore {
144150
const points: PointStruct[] = [];
145151
const ids = [];
146152

147-
for (let i = 0; i < nodes.length; i++) {
153+
for (let i = 0; i < nodes.length; ) {
148154
const nodeIds = [];
149155
const vectors = [];
150156
const payloads = [];
@@ -269,14 +275,9 @@ export class QdrantVectorStore extends BaseVectorStore {
269275
query: VectorStoreQuery<QdrantSearchParams | undefined>,
270276
options?: object,
271277
): Promise<VectorStoreQueryResult> {
272-
const qdrantFilters =
273-
options && "qdrant_filters" in options
274-
? options.qdrant_filters
275-
: undefined;
276-
const qdrantSearchParams =
277-
options && "qdrant_search_params" in options
278-
? options.qdrant_search_params
279-
: undefined;
278+
const qdrantOptions = options as QdrantQueryOptions; // Cast to specific interface
279+
const qdrantFilters = qdrantOptions?.qdrant_filters;
280+
const qdrantSearchParams = qdrantOptions?.qdrant_search_params;
280281

281282
let queryFilters: QdrantFilter | undefined;
282283
let searchParams: QdrantSearchParams | undefined;
@@ -317,7 +318,7 @@ export class QdrantVectorStore extends BaseVectorStore {
317318
function buildQueryFilter(query: VectorStoreQuery): QdrantFilter | undefined {
318319
if (!query.docIds && !query.queryStr && !query.filters) return undefined;
319320

320-
const mustConditions: QdrantMustConditions = [];
321+
const mustConditions: QdrantCondition[] = []; // Explicitly typed
321322
if (query.docIds) {
322323
mustConditions.push({
323324
key: "doc_id",
@@ -327,10 +328,14 @@ function buildQueryFilter(query: VectorStoreQuery): QdrantFilter | undefined {
327328

328329
const metadataFilters = toQdrantMetadataFilters(query.filters);
329330
if (metadataFilters) {
330-
mustConditions.push(metadataFilters);
331+
if (metadataFilters.must) {
332+
mustConditions.push(...metadataFilters.must);
333+
} else {
334+
mustConditions.push(metadataFilters);
335+
}
331336
}
332337

333-
return { must: mustConditions };
338+
return mustConditions.length > 0 ? { must: mustConditions } : undefined;
334339
}
335340

336341
function buildSearchParams(
@@ -355,74 +360,46 @@ function toQdrantMetadataFilters(
355360
): QdrantFilter | undefined {
356361
if (!subFilters?.filters.length) return undefined;
357362

358-
const conditions: QdrantMustConditions = [];
363+
const conditions: QdrantCondition[] = []; // Explicitly typed
359364

360365
for (const subfilter of subFilters.filters) {
361-
if (subfilter.operator === FilterOperator.EQ) {
362-
if (typeof subfilter.value === "number") {
363-
conditions.push({
364-
key: subfilter.key,
365-
range: {
366-
gte: subfilter.value,
367-
lte: subfilter.value,
368-
},
369-
});
366+
const { key, value, operator } = subfilter;
367+
368+
if (operator === FilterOperator.EQ) {
369+
if (typeof value === "number") {
370+
conditions.push({ key, range: { gte: value, lte: value } });
370371
} else {
371372
conditions.push({
372-
key: subfilter.key,
373-
match: { value: subfilter.value },
373+
key,
374+
match: { value: value as string | number | boolean },
374375
});
375376
}
376-
} else if (subfilter.operator === FilterOperator.LT) {
377-
conditions.push({
378-
key: subfilter.key,
379-
range: { lt: subfilter.value },
380-
});
381-
} else if (subfilter.operator === FilterOperator.GT) {
382-
conditions.push({
383-
key: subfilter.key,
384-
range: { gt: subfilter.value },
385-
});
386-
} else if (subfilter.operator === FilterOperator.GTE) {
387-
conditions.push({
388-
key: subfilter.key,
389-
range: { gte: subfilter.value },
390-
});
391-
} else if (subfilter.operator === FilterOperator.LTE) {
392-
conditions.push({
393-
key: subfilter.key,
394-
range: { lte: subfilter.value },
395-
});
396-
} else if (subfilter.operator === FilterOperator.TEXT_MATCH) {
397-
conditions.push({
398-
key: subfilter.key,
399-
match: { text: subfilter.value },
400-
});
401-
} else if (subfilter.operator === FilterOperator.NE) {
402-
conditions.push({
403-
key: subfilter.key,
404-
match: { except: [subfilter.value] },
405-
});
406-
} else if (subfilter.operator === FilterOperator.IN) {
407-
const values = Array.isArray(subfilter.value)
408-
? subfilter.value.map(String)
409-
: String(subfilter.value).split(",");
410-
conditions.push({
411-
key: subfilter.key,
412-
match: { any: values },
413-
});
414-
} else if (subfilter.operator === FilterOperator.NIN) {
415-
const values = Array.isArray(subfilter.value)
416-
? subfilter.value.map(String)
417-
: String(subfilter.value).split(",");
377+
} else if (operator === FilterOperator.LT) {
378+
conditions.push({ key, range: { lt: value as number } });
379+
} else if (operator === FilterOperator.GT) {
380+
conditions.push({ key, range: { gt: value as number } });
381+
} else if (operator === FilterOperator.GTE) {
382+
conditions.push({ key, range: { gte: value as number } });
383+
} else if (operator === FilterOperator.LTE) {
384+
conditions.push({ key, range: { lte: value as number } });
385+
} else if (operator === FilterOperator.TEXT_MATCH) {
386+
conditions.push({ key, match: { text: value as string } });
387+
} else if (operator === FilterOperator.NE) {
418388
conditions.push({
419-
key: subfilter.key,
420-
match: { except: values },
389+
must_not: [
390+
{ key, match: { value: value as string | number | boolean } },
391+
],
421392
});
422-
} else if (subfilter.operator === FilterOperator.IS_EMPTY) {
393+
} else if (operator === FilterOperator.IN) {
394+
const values = Array.isArray(value) ? value : [value];
395+
conditions.push({ key, match: { any: values as (string | number)[] } });
396+
} else if (operator === FilterOperator.NIN) {
397+
const values = Array.isArray(value) ? value : [value];
423398
conditions.push({
424-
is_empty: { key: subfilter.key },
399+
must_not: [{ key, match: { any: values as (string | number)[] } }],
425400
});
401+
} else if (operator === FilterOperator.IS_EMPTY) {
402+
conditions.push({ is_empty: { key } });
426403
}
427404
}
428405

0 commit comments

Comments
 (0)