@mhalder/qdrant-mcp-server 1.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +5 -16
- package/.github/workflows/claude-code-review.yml +6 -5
- package/.nvmrc +1 -0
- package/.releaserc.json +8 -1
- package/CHANGELOG.md +40 -0
- package/README.md +106 -74
- package/build/embeddings/cohere.test.js +8 -6
- package/build/embeddings/cohere.test.js.map +1 -1
- package/build/embeddings/openai.test.js +12 -8
- package/build/embeddings/openai.test.js.map +1 -1
- package/build/index.js +22 -7
- package/build/index.js.map +1 -1
- package/build/qdrant/client.d.ts +1 -1
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js +2 -2
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +80 -24
- package/build/qdrant/client.test.js.map +1 -1
- package/compose.yaml +57 -0
- package/examples/README.md +5 -3
- package/package.json +24 -20
- package/src/embeddings/cohere.test.ts +9 -8
- package/src/embeddings/openai.test.ts +13 -10
- package/src/index.ts +134 -51
- package/src/qdrant/client.test.ts +220 -79
- package/src/qdrant/client.ts +27 -16
- package/tsconfig.json +5 -4
- package/vitest.config.ts +1 -0
- package/docker-compose.yml +0 -22
|
@@ -2,70 +2,112 @@ import { QdrantClient } from "@qdrant/js-client-rest";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { QdrantManager } from "./client.js";
|
|
4
4
|
|
|
5
|
+
const mockClient = {
|
|
6
|
+
createCollection: vi.fn().mockResolvedValue({}),
|
|
7
|
+
getCollection: vi.fn().mockResolvedValue({}),
|
|
8
|
+
getCollections: vi.fn().mockResolvedValue({ collections: [] }),
|
|
9
|
+
deleteCollection: vi.fn().mockResolvedValue({}),
|
|
10
|
+
upsert: vi.fn().mockResolvedValue({}),
|
|
11
|
+
search: vi.fn().mockResolvedValue([]),
|
|
12
|
+
retrieve: vi.fn().mockResolvedValue([]),
|
|
13
|
+
delete: vi.fn().mockResolvedValue({}),
|
|
14
|
+
query: vi.fn().mockResolvedValue({ points: [] }),
|
|
15
|
+
};
|
|
16
|
+
|
|
5
17
|
vi.mock("@qdrant/js-client-rest", () => ({
|
|
6
|
-
QdrantClient: vi.fn()
|
|
18
|
+
QdrantClient: vi.fn().mockImplementation(function () {
|
|
19
|
+
return mockClient;
|
|
20
|
+
}),
|
|
7
21
|
}));
|
|
8
22
|
|
|
9
23
|
describe("QdrantManager", () => {
|
|
10
24
|
let manager: QdrantManager;
|
|
11
|
-
let mockClient: any;
|
|
12
25
|
|
|
13
26
|
beforeEach(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// Reset mocks and restore default implementations
|
|
28
|
+
mockClient.createCollection.mockReset().mockResolvedValue({});
|
|
29
|
+
mockClient.getCollection.mockReset().mockResolvedValue({});
|
|
30
|
+
mockClient.getCollections
|
|
31
|
+
.mockReset()
|
|
32
|
+
.mockResolvedValue({ collections: [] });
|
|
33
|
+
mockClient.deleteCollection.mockReset().mockResolvedValue({});
|
|
34
|
+
mockClient.upsert.mockReset().mockResolvedValue({});
|
|
35
|
+
mockClient.search.mockReset().mockResolvedValue([]);
|
|
36
|
+
mockClient.retrieve.mockReset().mockResolvedValue([]);
|
|
37
|
+
mockClient.delete.mockReset().mockResolvedValue({});
|
|
38
|
+
mockClient.query.mockReset().mockResolvedValue({ points: [] });
|
|
39
|
+
vi.mocked(QdrantClient).mockClear();
|
|
27
40
|
manager = new QdrantManager("http://localhost:6333");
|
|
28
41
|
});
|
|
29
42
|
|
|
43
|
+
describe("constructor", () => {
|
|
44
|
+
it("should pass apiKey to QdrantClient when provided", () => {
|
|
45
|
+
new QdrantManager("http://localhost:6333", "test-api-key");
|
|
46
|
+
|
|
47
|
+
expect(QdrantClient).toHaveBeenCalledWith({
|
|
48
|
+
url: "http://localhost:6333",
|
|
49
|
+
apiKey: "test-api-key",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should work without apiKey for unauthenticated instances", () => {
|
|
54
|
+
new QdrantManager("http://localhost:6333");
|
|
55
|
+
|
|
56
|
+
expect(QdrantClient).toHaveBeenCalledWith({
|
|
57
|
+
url: "http://localhost:6333",
|
|
58
|
+
apiKey: undefined,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
30
63
|
describe("createCollection", () => {
|
|
31
64
|
it("should create a collection with default distance metric", async () => {
|
|
32
65
|
await manager.createCollection("test-collection", 1536);
|
|
33
66
|
|
|
34
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
67
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
68
|
+
"test-collection",
|
|
69
|
+
{
|
|
70
|
+
vectors: {
|
|
71
|
+
size: 1536,
|
|
72
|
+
distance: "Cosine",
|
|
73
|
+
},
|
|
38
74
|
},
|
|
39
|
-
|
|
75
|
+
);
|
|
40
76
|
});
|
|
41
77
|
|
|
42
78
|
it("should create a collection with custom distance metric", async () => {
|
|
43
79
|
await manager.createCollection("test-collection", 1536, "Euclid");
|
|
44
80
|
|
|
45
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
81
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
82
|
+
"test-collection",
|
|
83
|
+
{
|
|
84
|
+
vectors: {
|
|
85
|
+
size: 1536,
|
|
86
|
+
distance: "Euclid",
|
|
87
|
+
},
|
|
49
88
|
},
|
|
50
|
-
|
|
89
|
+
);
|
|
51
90
|
});
|
|
52
91
|
|
|
53
92
|
it("should create a hybrid collection with sparse vectors enabled", async () => {
|
|
54
93
|
await manager.createCollection("test-collection", 1536, "Cosine", true);
|
|
55
94
|
|
|
56
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
96
|
+
"test-collection",
|
|
97
|
+
{
|
|
98
|
+
vectors: {
|
|
99
|
+
dense: {
|
|
100
|
+
size: 1536,
|
|
101
|
+
distance: "Cosine",
|
|
102
|
+
},
|
|
61
103
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
104
|
+
sparse_vectors: {
|
|
105
|
+
text: {
|
|
106
|
+
modifier: "idf",
|
|
107
|
+
},
|
|
66
108
|
},
|
|
67
109
|
},
|
|
68
|
-
|
|
110
|
+
);
|
|
69
111
|
});
|
|
70
112
|
});
|
|
71
113
|
|
|
@@ -91,12 +133,20 @@ describe("QdrantManager", () => {
|
|
|
91
133
|
describe("listCollections", () => {
|
|
92
134
|
it("should return list of collection names", async () => {
|
|
93
135
|
mockClient.getCollections.mockResolvedValue({
|
|
94
|
-
collections: [
|
|
136
|
+
collections: [
|
|
137
|
+
{ name: "collection1" },
|
|
138
|
+
{ name: "collection2" },
|
|
139
|
+
{ name: "collection3" },
|
|
140
|
+
],
|
|
95
141
|
});
|
|
96
142
|
|
|
97
143
|
const collections = await manager.listCollections();
|
|
98
144
|
|
|
99
|
-
expect(collections).toEqual([
|
|
145
|
+
expect(collections).toEqual([
|
|
146
|
+
"collection1",
|
|
147
|
+
"collection2",
|
|
148
|
+
"collection3",
|
|
149
|
+
]);
|
|
100
150
|
});
|
|
101
151
|
|
|
102
152
|
it("should return empty array when no collections exist", async () => {
|
|
@@ -191,7 +241,9 @@ describe("QdrantManager", () => {
|
|
|
191
241
|
it("should delete a collection", async () => {
|
|
192
242
|
await manager.deleteCollection("test-collection");
|
|
193
243
|
|
|
194
|
-
expect(mockClient.deleteCollection).toHaveBeenCalledWith(
|
|
244
|
+
expect(mockClient.deleteCollection).toHaveBeenCalledWith(
|
|
245
|
+
"test-collection",
|
|
246
|
+
);
|
|
195
247
|
});
|
|
196
248
|
});
|
|
197
249
|
|
|
@@ -240,7 +292,7 @@ describe("QdrantManager", () => {
|
|
|
240
292
|
const normalizedId = calls[0][1].points[0].id;
|
|
241
293
|
// Check that it's a valid UUID format
|
|
242
294
|
expect(normalizedId).toMatch(
|
|
243
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
295
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
244
296
|
);
|
|
245
297
|
// Ensure it's not the original ID
|
|
246
298
|
expect(normalizedId).not.toBe("my-custom-id");
|
|
@@ -248,7 +300,9 @@ describe("QdrantManager", () => {
|
|
|
248
300
|
|
|
249
301
|
it("should preserve UUID format IDs without modification", async () => {
|
|
250
302
|
const uuidId = "123e4567-e89b-12d3-a456-426614174000";
|
|
251
|
-
const points = [
|
|
303
|
+
const points = [
|
|
304
|
+
{ id: uuidId, vector: [0.1, 0.2, 0.3], payload: { text: "test" } },
|
|
305
|
+
];
|
|
252
306
|
|
|
253
307
|
await manager.addPoints("test-collection", points);
|
|
254
308
|
|
|
@@ -276,8 +330,10 @@ describe("QdrantManager", () => {
|
|
|
276
330
|
},
|
|
277
331
|
});
|
|
278
332
|
|
|
279
|
-
await expect(
|
|
280
|
-
|
|
333
|
+
await expect(
|
|
334
|
+
manager.addPoints("test-collection", points),
|
|
335
|
+
).rejects.toThrow(
|
|
336
|
+
'Failed to add points to collection "test-collection": Vector dimension mismatch',
|
|
281
337
|
);
|
|
282
338
|
});
|
|
283
339
|
|
|
@@ -286,8 +342,10 @@ describe("QdrantManager", () => {
|
|
|
286
342
|
|
|
287
343
|
mockClient.upsert.mockRejectedValue(new Error("Network error"));
|
|
288
344
|
|
|
289
|
-
await expect(
|
|
290
|
-
|
|
345
|
+
await expect(
|
|
346
|
+
manager.addPoints("test-collection", points),
|
|
347
|
+
).rejects.toThrow(
|
|
348
|
+
'Failed to add points to collection "test-collection": Network error',
|
|
291
349
|
);
|
|
292
350
|
});
|
|
293
351
|
|
|
@@ -296,8 +354,10 @@ describe("QdrantManager", () => {
|
|
|
296
354
|
|
|
297
355
|
mockClient.upsert.mockRejectedValue("Unknown error");
|
|
298
356
|
|
|
299
|
-
await expect(
|
|
300
|
-
|
|
357
|
+
await expect(
|
|
358
|
+
manager.addPoints("test-collection", points),
|
|
359
|
+
).rejects.toThrow(
|
|
360
|
+
'Failed to add points to collection "test-collection": Unknown error',
|
|
301
361
|
);
|
|
302
362
|
});
|
|
303
363
|
});
|
|
@@ -325,7 +385,11 @@ describe("QdrantManager", () => {
|
|
|
325
385
|
{ id: 2, score: 0.85, payload: { text: "result2" } },
|
|
326
386
|
]);
|
|
327
387
|
|
|
328
|
-
const results = await manager.search(
|
|
388
|
+
const results = await manager.search(
|
|
389
|
+
"test-collection",
|
|
390
|
+
[0.1, 0.2, 0.3],
|
|
391
|
+
5,
|
|
392
|
+
);
|
|
329
393
|
|
|
330
394
|
expect(results).toEqual([
|
|
331
395
|
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
@@ -424,7 +488,9 @@ describe("QdrantManager", () => {
|
|
|
424
488
|
});
|
|
425
489
|
|
|
426
490
|
it("should handle null payload in results", async () => {
|
|
427
|
-
mockClient.search.mockResolvedValue([
|
|
491
|
+
mockClient.search.mockResolvedValue([
|
|
492
|
+
{ id: 1, score: 0.95, payload: null },
|
|
493
|
+
]);
|
|
428
494
|
|
|
429
495
|
const results = await manager.search("test-collection", [0.1, 0.2, 0.3]);
|
|
430
496
|
|
|
@@ -453,11 +519,19 @@ describe("QdrantManager", () => {
|
|
|
453
519
|
},
|
|
454
520
|
});
|
|
455
521
|
|
|
456
|
-
mockClient.search.mockResolvedValue([
|
|
522
|
+
mockClient.search.mockResolvedValue([
|
|
523
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
524
|
+
]);
|
|
457
525
|
|
|
458
|
-
const results = await manager.search(
|
|
526
|
+
const results = await manager.search(
|
|
527
|
+
"hybrid-collection",
|
|
528
|
+
[0.1, 0.2, 0.3],
|
|
529
|
+
5,
|
|
530
|
+
);
|
|
459
531
|
|
|
460
|
-
expect(results).toEqual([
|
|
532
|
+
expect(results).toEqual([
|
|
533
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
534
|
+
]);
|
|
461
535
|
expect(mockClient.search).toHaveBeenCalledWith("hybrid-collection", {
|
|
462
536
|
vector: { name: "dense", vector: [0.1, 0.2, 0.3] },
|
|
463
537
|
limit: 5,
|
|
@@ -480,11 +554,19 @@ describe("QdrantManager", () => {
|
|
|
480
554
|
},
|
|
481
555
|
});
|
|
482
556
|
|
|
483
|
-
mockClient.search.mockResolvedValue([
|
|
557
|
+
mockClient.search.mockResolvedValue([
|
|
558
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
559
|
+
]);
|
|
484
560
|
|
|
485
|
-
const results = await manager.search(
|
|
561
|
+
const results = await manager.search(
|
|
562
|
+
"standard-collection",
|
|
563
|
+
[0.1, 0.2, 0.3],
|
|
564
|
+
5,
|
|
565
|
+
);
|
|
486
566
|
|
|
487
|
-
expect(results).toEqual([
|
|
567
|
+
expect(results).toEqual([
|
|
568
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
569
|
+
]);
|
|
488
570
|
expect(mockClient.search).toHaveBeenCalledWith("standard-collection", {
|
|
489
571
|
vector: [0.1, 0.2, 0.3],
|
|
490
572
|
limit: 5,
|
|
@@ -495,7 +577,9 @@ describe("QdrantManager", () => {
|
|
|
495
577
|
|
|
496
578
|
describe("getPoint", () => {
|
|
497
579
|
it("should retrieve a point by id", async () => {
|
|
498
|
-
mockClient.retrieve.mockResolvedValue([
|
|
580
|
+
mockClient.retrieve.mockResolvedValue([
|
|
581
|
+
{ id: 1, payload: { text: "test" } },
|
|
582
|
+
]);
|
|
499
583
|
|
|
500
584
|
const point = await manager.getPoint("test-collection", 1);
|
|
501
585
|
|
|
@@ -566,7 +650,11 @@ describe("QdrantManager", () => {
|
|
|
566
650
|
],
|
|
567
651
|
});
|
|
568
652
|
|
|
569
|
-
const results = await manager.hybridSearch(
|
|
653
|
+
const results = await manager.hybridSearch(
|
|
654
|
+
"test-collection",
|
|
655
|
+
denseVector,
|
|
656
|
+
sparseVector,
|
|
657
|
+
);
|
|
570
658
|
|
|
571
659
|
expect(results).toEqual([
|
|
572
660
|
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
@@ -602,7 +690,12 @@ describe("QdrantManager", () => {
|
|
|
602
690
|
|
|
603
691
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
604
692
|
|
|
605
|
-
await manager.hybridSearch(
|
|
693
|
+
await manager.hybridSearch(
|
|
694
|
+
"test-collection",
|
|
695
|
+
denseVector,
|
|
696
|
+
sparseVector,
|
|
697
|
+
10,
|
|
698
|
+
);
|
|
606
699
|
|
|
607
700
|
expect(mockClient.query).toHaveBeenCalledWith(
|
|
608
701
|
"test-collection",
|
|
@@ -611,7 +704,7 @@ describe("QdrantManager", () => {
|
|
|
611
704
|
expect.objectContaining({ limit: 40 }), // 10 * 4
|
|
612
705
|
]),
|
|
613
706
|
limit: 10,
|
|
614
|
-
})
|
|
707
|
+
}),
|
|
615
708
|
);
|
|
616
709
|
});
|
|
617
710
|
|
|
@@ -622,7 +715,13 @@ describe("QdrantManager", () => {
|
|
|
622
715
|
|
|
623
716
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
624
717
|
|
|
625
|
-
await manager.hybridSearch(
|
|
718
|
+
await manager.hybridSearch(
|
|
719
|
+
"test-collection",
|
|
720
|
+
denseVector,
|
|
721
|
+
sparseVector,
|
|
722
|
+
5,
|
|
723
|
+
filter,
|
|
724
|
+
);
|
|
626
725
|
|
|
627
726
|
expect(mockClient.query).toHaveBeenCalledWith("test-collection", {
|
|
628
727
|
prefetch: [
|
|
@@ -664,7 +763,13 @@ describe("QdrantManager", () => {
|
|
|
664
763
|
|
|
665
764
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
666
765
|
|
|
667
|
-
await manager.hybridSearch(
|
|
766
|
+
await manager.hybridSearch(
|
|
767
|
+
"test-collection",
|
|
768
|
+
denseVector,
|
|
769
|
+
sparseVector,
|
|
770
|
+
5,
|
|
771
|
+
filter,
|
|
772
|
+
);
|
|
668
773
|
|
|
669
774
|
const call = mockClient.query.mock.calls[0][1];
|
|
670
775
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -674,11 +779,19 @@ describe("QdrantManager", () => {
|
|
|
674
779
|
it("should handle Qdrant format filter (should)", async () => {
|
|
675
780
|
const denseVector = [0.1, 0.2, 0.3];
|
|
676
781
|
const sparseVector = { indices: [1], values: [0.5] };
|
|
677
|
-
const filter = {
|
|
782
|
+
const filter = {
|
|
783
|
+
should: [{ key: "tag", match: { value: "important" } }],
|
|
784
|
+
};
|
|
678
785
|
|
|
679
786
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
680
787
|
|
|
681
|
-
await manager.hybridSearch(
|
|
788
|
+
await manager.hybridSearch(
|
|
789
|
+
"test-collection",
|
|
790
|
+
denseVector,
|
|
791
|
+
sparseVector,
|
|
792
|
+
5,
|
|
793
|
+
filter,
|
|
794
|
+
);
|
|
682
795
|
|
|
683
796
|
const call = mockClient.query.mock.calls[0][1];
|
|
684
797
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -688,11 +801,19 @@ describe("QdrantManager", () => {
|
|
|
688
801
|
it("should handle Qdrant format filter (must_not)", async () => {
|
|
689
802
|
const denseVector = [0.1, 0.2, 0.3];
|
|
690
803
|
const sparseVector = { indices: [1], values: [0.5] };
|
|
691
|
-
const filter = {
|
|
804
|
+
const filter = {
|
|
805
|
+
must_not: [{ key: "status", match: { value: "deleted" } }],
|
|
806
|
+
};
|
|
692
807
|
|
|
693
808
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
694
809
|
|
|
695
|
-
await manager.hybridSearch(
|
|
810
|
+
await manager.hybridSearch(
|
|
811
|
+
"test-collection",
|
|
812
|
+
denseVector,
|
|
813
|
+
sparseVector,
|
|
814
|
+
5,
|
|
815
|
+
filter,
|
|
816
|
+
);
|
|
696
817
|
|
|
697
818
|
const call = mockClient.query.mock.calls[0][1];
|
|
698
819
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -705,7 +826,13 @@ describe("QdrantManager", () => {
|
|
|
705
826
|
|
|
706
827
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
707
828
|
|
|
708
|
-
await manager.hybridSearch(
|
|
829
|
+
await manager.hybridSearch(
|
|
830
|
+
"test-collection",
|
|
831
|
+
denseVector,
|
|
832
|
+
sparseVector,
|
|
833
|
+
5,
|
|
834
|
+
{},
|
|
835
|
+
);
|
|
709
836
|
|
|
710
837
|
const call = mockClient.query.mock.calls[0][1];
|
|
711
838
|
expect(call.prefetch[0].filter).toBeUndefined();
|
|
@@ -720,7 +847,11 @@ describe("QdrantManager", () => {
|
|
|
720
847
|
points: [{ id: 1, score: 0.95, payload: null }],
|
|
721
848
|
});
|
|
722
849
|
|
|
723
|
-
const results = await manager.hybridSearch(
|
|
850
|
+
const results = await manager.hybridSearch(
|
|
851
|
+
"test-collection",
|
|
852
|
+
denseVector,
|
|
853
|
+
sparseVector,
|
|
854
|
+
);
|
|
724
855
|
|
|
725
856
|
expect(results).toEqual([{ id: 1, score: 0.95, payload: undefined }]);
|
|
726
857
|
});
|
|
@@ -738,9 +869,9 @@ describe("QdrantManager", () => {
|
|
|
738
869
|
});
|
|
739
870
|
|
|
740
871
|
await expect(
|
|
741
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
872
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
742
873
|
).rejects.toThrow(
|
|
743
|
-
'Hybrid search failed on collection "test-collection": Named vector not found'
|
|
874
|
+
'Hybrid search failed on collection "test-collection": Named vector not found',
|
|
744
875
|
);
|
|
745
876
|
});
|
|
746
877
|
|
|
@@ -751,8 +882,10 @@ describe("QdrantManager", () => {
|
|
|
751
882
|
mockClient.query.mockRejectedValue(new Error("Network timeout"));
|
|
752
883
|
|
|
753
884
|
await expect(
|
|
754
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
755
|
-
).rejects.toThrow(
|
|
885
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
886
|
+
).rejects.toThrow(
|
|
887
|
+
'Hybrid search failed on collection "test-collection": Network timeout',
|
|
888
|
+
);
|
|
756
889
|
});
|
|
757
890
|
|
|
758
891
|
it("should throw error with String(error) fallback", async () => {
|
|
@@ -762,8 +895,10 @@ describe("QdrantManager", () => {
|
|
|
762
895
|
mockClient.query.mockRejectedValue("Unknown error");
|
|
763
896
|
|
|
764
897
|
await expect(
|
|
765
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
766
|
-
).rejects.toThrow(
|
|
898
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
899
|
+
).rejects.toThrow(
|
|
900
|
+
'Hybrid search failed on collection "test-collection": Unknown error',
|
|
901
|
+
);
|
|
767
902
|
});
|
|
768
903
|
});
|
|
769
904
|
|
|
@@ -854,7 +989,7 @@ describe("QdrantManager", () => {
|
|
|
854
989
|
const normalizedId = calls[0][1].points[0].id;
|
|
855
990
|
// Check that it's a valid UUID format
|
|
856
991
|
expect(normalizedId).toMatch(
|
|
857
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
992
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
858
993
|
);
|
|
859
994
|
expect(normalizedId).not.toBe("my-doc-id");
|
|
860
995
|
});
|
|
@@ -904,8 +1039,10 @@ describe("QdrantManager", () => {
|
|
|
904
1039
|
},
|
|
905
1040
|
});
|
|
906
1041
|
|
|
907
|
-
await expect(
|
|
908
|
-
|
|
1042
|
+
await expect(
|
|
1043
|
+
manager.addPointsWithSparse("test-collection", points),
|
|
1044
|
+
).rejects.toThrow(
|
|
1045
|
+
'Failed to add points with sparse vectors to collection "test-collection": Sparse vector not configured',
|
|
909
1046
|
);
|
|
910
1047
|
});
|
|
911
1048
|
|
|
@@ -920,8 +1057,10 @@ describe("QdrantManager", () => {
|
|
|
920
1057
|
|
|
921
1058
|
mockClient.upsert.mockRejectedValue(new Error("Connection refused"));
|
|
922
1059
|
|
|
923
|
-
await expect(
|
|
924
|
-
|
|
1060
|
+
await expect(
|
|
1061
|
+
manager.addPointsWithSparse("test-collection", points),
|
|
1062
|
+
).rejects.toThrow(
|
|
1063
|
+
'Failed to add points with sparse vectors to collection "test-collection": Connection refused',
|
|
925
1064
|
);
|
|
926
1065
|
});
|
|
927
1066
|
|
|
@@ -936,8 +1075,10 @@ describe("QdrantManager", () => {
|
|
|
936
1075
|
|
|
937
1076
|
mockClient.upsert.mockRejectedValue("Unexpected error");
|
|
938
1077
|
|
|
939
|
-
await expect(
|
|
940
|
-
|
|
1078
|
+
await expect(
|
|
1079
|
+
manager.addPointsWithSparse("test-collection", points),
|
|
1080
|
+
).rejects.toThrow(
|
|
1081
|
+
'Failed to add points with sparse vectors to collection "test-collection": Unexpected error',
|
|
941
1082
|
);
|
|
942
1083
|
});
|
|
943
1084
|
});
|
package/src/qdrant/client.ts
CHANGED
|
@@ -23,8 +23,8 @@ export interface SparseVector {
|
|
|
23
23
|
export class QdrantManager {
|
|
24
24
|
private client: QdrantClient;
|
|
25
25
|
|
|
26
|
-
constructor(url: string = "http://localhost:6333") {
|
|
27
|
-
this.client = new QdrantClient({ url });
|
|
26
|
+
constructor(url: string = "http://localhost:6333", apiKey?: string) {
|
|
27
|
+
this.client = new QdrantClient({ url, apiKey });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -37,7 +37,8 @@ export class QdrantManager {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Check if already a valid UUID (8-4-4-4-12 format)
|
|
40
|
-
const uuidRegex =
|
|
40
|
+
const uuidRegex =
|
|
41
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
41
42
|
if (uuidRegex.test(id)) {
|
|
42
43
|
return id;
|
|
43
44
|
}
|
|
@@ -51,7 +52,7 @@ export class QdrantManager {
|
|
|
51
52
|
name: string,
|
|
52
53
|
vectorSize: number,
|
|
53
54
|
distance: "Cosine" | "Euclid" | "Dot" = "Cosine",
|
|
54
|
-
enableSparse: boolean = false
|
|
55
|
+
enableSparse: boolean = false,
|
|
55
56
|
): Promise<void> {
|
|
56
57
|
const config: any = {};
|
|
57
58
|
|
|
@@ -139,7 +140,7 @@ export class QdrantManager {
|
|
|
139
140
|
id: string | number;
|
|
140
141
|
vector: number[];
|
|
141
142
|
payload?: Record<string, any>;
|
|
142
|
-
}
|
|
143
|
+
}>,
|
|
143
144
|
): Promise<void> {
|
|
144
145
|
try {
|
|
145
146
|
// Normalize all IDs to ensure string IDs are in UUID format
|
|
@@ -153,8 +154,11 @@ export class QdrantManager {
|
|
|
153
154
|
points: normalizedPoints,
|
|
154
155
|
});
|
|
155
156
|
} catch (error: any) {
|
|
156
|
-
const errorMessage =
|
|
157
|
-
|
|
157
|
+
const errorMessage =
|
|
158
|
+
error?.data?.status?.error || error?.message || String(error);
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Failed to add points to collection "${collectionName}": ${errorMessage}`,
|
|
161
|
+
);
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
@@ -162,7 +166,7 @@ export class QdrantManager {
|
|
|
162
166
|
collectionName: string,
|
|
163
167
|
vector: number[],
|
|
164
168
|
limit: number = 5,
|
|
165
|
-
filter?: Record<string, any
|
|
169
|
+
filter?: Record<string, any>,
|
|
166
170
|
): Promise<SearchResult[]> {
|
|
167
171
|
// Convert simple key-value filter to Qdrant filter format
|
|
168
172
|
// Accepts either:
|
|
@@ -202,7 +206,7 @@ export class QdrantManager {
|
|
|
202
206
|
|
|
203
207
|
async getPoint(
|
|
204
208
|
collectionName: string,
|
|
205
|
-
id: string | number
|
|
209
|
+
id: string | number,
|
|
206
210
|
): Promise<{ id: string | number; payload?: Record<string, any> } | null> {
|
|
207
211
|
try {
|
|
208
212
|
const normalizedId = this.normalizeId(id);
|
|
@@ -223,7 +227,10 @@ export class QdrantManager {
|
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
229
|
|
|
226
|
-
async deletePoints(
|
|
230
|
+
async deletePoints(
|
|
231
|
+
collectionName: string,
|
|
232
|
+
ids: (string | number)[],
|
|
233
|
+
): Promise<void> {
|
|
227
234
|
// Normalize IDs to ensure string IDs are in UUID format
|
|
228
235
|
const normalizedIds = ids.map((id) => this.normalizeId(id));
|
|
229
236
|
|
|
@@ -243,7 +250,7 @@ export class QdrantManager {
|
|
|
243
250
|
sparseVector: SparseVector,
|
|
244
251
|
limit: number = 5,
|
|
245
252
|
filter?: Record<string, any>,
|
|
246
|
-
_semanticWeight: number = 0.7
|
|
253
|
+
_semanticWeight: number = 0.7,
|
|
247
254
|
): Promise<SearchResult[]> {
|
|
248
255
|
// Convert simple key-value filter to Qdrant filter format
|
|
249
256
|
let qdrantFilter;
|
|
@@ -293,8 +300,11 @@ export class QdrantManager {
|
|
|
293
300
|
payload: result.payload || undefined,
|
|
294
301
|
}));
|
|
295
302
|
} catch (error: any) {
|
|
296
|
-
const errorMessage =
|
|
297
|
-
|
|
303
|
+
const errorMessage =
|
|
304
|
+
error?.data?.status?.error || error?.message || String(error);
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Hybrid search failed on collection "${collectionName}": ${errorMessage}`,
|
|
307
|
+
);
|
|
298
308
|
}
|
|
299
309
|
}
|
|
300
310
|
|
|
@@ -308,7 +318,7 @@ export class QdrantManager {
|
|
|
308
318
|
vector: number[];
|
|
309
319
|
sparseVector: SparseVector;
|
|
310
320
|
payload?: Record<string, any>;
|
|
311
|
-
}
|
|
321
|
+
}>,
|
|
312
322
|
): Promise<void> {
|
|
313
323
|
try {
|
|
314
324
|
// Normalize all IDs to ensure string IDs are in UUID format
|
|
@@ -326,9 +336,10 @@ export class QdrantManager {
|
|
|
326
336
|
points: normalizedPoints,
|
|
327
337
|
});
|
|
328
338
|
} catch (error: any) {
|
|
329
|
-
const errorMessage =
|
|
339
|
+
const errorMessage =
|
|
340
|
+
error?.data?.status?.error || error?.message || String(error);
|
|
330
341
|
throw new Error(
|
|
331
|
-
`Failed to add points with sparse vectors to collection "${collectionName}": ${errorMessage}
|
|
342
|
+
`Failed to add points with sparse vectors to collection "${collectionName}": ${errorMessage}`,
|
|
332
343
|
);
|
|
333
344
|
}
|
|
334
345
|
}
|
package/tsconfig.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"module": "
|
|
5
|
-
"moduleResolution": "
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
6
|
"outDir": "./build",
|
|
7
7
|
"rootDir": "./src",
|
|
8
8
|
"strict": true,
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"resolveJsonModule": true,
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"declarationMap": true,
|
|
15
|
-
"sourceMap": true
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"noImplicitOverride": true
|
|
16
17
|
},
|
|
17
18
|
"include": ["src/**/*"],
|
|
18
19
|
"exclude": ["node_modules", "build"]
|