@mhalder/qdrant-mcp-server 1.6.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/.nvmrc +1 -0
- package/CHANGELOG.md +24 -0
- package/README.md +85 -76
- 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 +20 -6
- package/build/index.js.map +1 -1
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +64 -24
- package/build/qdrant/client.test.js.map +1 -1
- package/compose.yaml +57 -0
- package/examples/README.md +5 -3
- package/package.json +22 -19
- package/src/embeddings/cohere.test.ts +9 -8
- package/src/embeddings/openai.test.ts +13 -10
- package/src/index.ts +132 -50
- package/src/qdrant/client.test.ts +200 -79
- package/src/qdrant/client.ts +25 -14
- package/tsconfig.json +5 -4
- package/vitest.config.ts +1 -0
- package/docker-compose.yml +0 -22
|
@@ -2,28 +2,41 @@ 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
|
|
|
@@ -51,41 +64,50 @@ describe("QdrantManager", () => {
|
|
|
51
64
|
it("should create a collection with default distance metric", async () => {
|
|
52
65
|
await manager.createCollection("test-collection", 1536);
|
|
53
66
|
|
|
54
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
68
|
+
"test-collection",
|
|
69
|
+
{
|
|
70
|
+
vectors: {
|
|
71
|
+
size: 1536,
|
|
72
|
+
distance: "Cosine",
|
|
73
|
+
},
|
|
58
74
|
},
|
|
59
|
-
|
|
75
|
+
);
|
|
60
76
|
});
|
|
61
77
|
|
|
62
78
|
it("should create a collection with custom distance metric", async () => {
|
|
63
79
|
await manager.createCollection("test-collection", 1536, "Euclid");
|
|
64
80
|
|
|
65
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
82
|
+
"test-collection",
|
|
83
|
+
{
|
|
84
|
+
vectors: {
|
|
85
|
+
size: 1536,
|
|
86
|
+
distance: "Euclid",
|
|
87
|
+
},
|
|
69
88
|
},
|
|
70
|
-
|
|
89
|
+
);
|
|
71
90
|
});
|
|
72
91
|
|
|
73
92
|
it("should create a hybrid collection with sparse vectors enabled", async () => {
|
|
74
93
|
await manager.createCollection("test-collection", 1536, "Cosine", true);
|
|
75
94
|
|
|
76
|
-
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
expect(mockClient.createCollection).toHaveBeenCalledWith(
|
|
96
|
+
"test-collection",
|
|
97
|
+
{
|
|
98
|
+
vectors: {
|
|
99
|
+
dense: {
|
|
100
|
+
size: 1536,
|
|
101
|
+
distance: "Cosine",
|
|
102
|
+
},
|
|
81
103
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
sparse_vectors: {
|
|
105
|
+
text: {
|
|
106
|
+
modifier: "idf",
|
|
107
|
+
},
|
|
86
108
|
},
|
|
87
109
|
},
|
|
88
|
-
|
|
110
|
+
);
|
|
89
111
|
});
|
|
90
112
|
});
|
|
91
113
|
|
|
@@ -111,12 +133,20 @@ describe("QdrantManager", () => {
|
|
|
111
133
|
describe("listCollections", () => {
|
|
112
134
|
it("should return list of collection names", async () => {
|
|
113
135
|
mockClient.getCollections.mockResolvedValue({
|
|
114
|
-
collections: [
|
|
136
|
+
collections: [
|
|
137
|
+
{ name: "collection1" },
|
|
138
|
+
{ name: "collection2" },
|
|
139
|
+
{ name: "collection3" },
|
|
140
|
+
],
|
|
115
141
|
});
|
|
116
142
|
|
|
117
143
|
const collections = await manager.listCollections();
|
|
118
144
|
|
|
119
|
-
expect(collections).toEqual([
|
|
145
|
+
expect(collections).toEqual([
|
|
146
|
+
"collection1",
|
|
147
|
+
"collection2",
|
|
148
|
+
"collection3",
|
|
149
|
+
]);
|
|
120
150
|
});
|
|
121
151
|
|
|
122
152
|
it("should return empty array when no collections exist", async () => {
|
|
@@ -211,7 +241,9 @@ describe("QdrantManager", () => {
|
|
|
211
241
|
it("should delete a collection", async () => {
|
|
212
242
|
await manager.deleteCollection("test-collection");
|
|
213
243
|
|
|
214
|
-
expect(mockClient.deleteCollection).toHaveBeenCalledWith(
|
|
244
|
+
expect(mockClient.deleteCollection).toHaveBeenCalledWith(
|
|
245
|
+
"test-collection",
|
|
246
|
+
);
|
|
215
247
|
});
|
|
216
248
|
});
|
|
217
249
|
|
|
@@ -260,7 +292,7 @@ describe("QdrantManager", () => {
|
|
|
260
292
|
const normalizedId = calls[0][1].points[0].id;
|
|
261
293
|
// Check that it's a valid UUID format
|
|
262
294
|
expect(normalizedId).toMatch(
|
|
263
|
-
/^[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,
|
|
264
296
|
);
|
|
265
297
|
// Ensure it's not the original ID
|
|
266
298
|
expect(normalizedId).not.toBe("my-custom-id");
|
|
@@ -268,7 +300,9 @@ describe("QdrantManager", () => {
|
|
|
268
300
|
|
|
269
301
|
it("should preserve UUID format IDs without modification", async () => {
|
|
270
302
|
const uuidId = "123e4567-e89b-12d3-a456-426614174000";
|
|
271
|
-
const points = [
|
|
303
|
+
const points = [
|
|
304
|
+
{ id: uuidId, vector: [0.1, 0.2, 0.3], payload: { text: "test" } },
|
|
305
|
+
];
|
|
272
306
|
|
|
273
307
|
await manager.addPoints("test-collection", points);
|
|
274
308
|
|
|
@@ -296,8 +330,10 @@ describe("QdrantManager", () => {
|
|
|
296
330
|
},
|
|
297
331
|
});
|
|
298
332
|
|
|
299
|
-
await expect(
|
|
300
|
-
|
|
333
|
+
await expect(
|
|
334
|
+
manager.addPoints("test-collection", points),
|
|
335
|
+
).rejects.toThrow(
|
|
336
|
+
'Failed to add points to collection "test-collection": Vector dimension mismatch',
|
|
301
337
|
);
|
|
302
338
|
});
|
|
303
339
|
|
|
@@ -306,8 +342,10 @@ describe("QdrantManager", () => {
|
|
|
306
342
|
|
|
307
343
|
mockClient.upsert.mockRejectedValue(new Error("Network error"));
|
|
308
344
|
|
|
309
|
-
await expect(
|
|
310
|
-
|
|
345
|
+
await expect(
|
|
346
|
+
manager.addPoints("test-collection", points),
|
|
347
|
+
).rejects.toThrow(
|
|
348
|
+
'Failed to add points to collection "test-collection": Network error',
|
|
311
349
|
);
|
|
312
350
|
});
|
|
313
351
|
|
|
@@ -316,8 +354,10 @@ describe("QdrantManager", () => {
|
|
|
316
354
|
|
|
317
355
|
mockClient.upsert.mockRejectedValue("Unknown error");
|
|
318
356
|
|
|
319
|
-
await expect(
|
|
320
|
-
|
|
357
|
+
await expect(
|
|
358
|
+
manager.addPoints("test-collection", points),
|
|
359
|
+
).rejects.toThrow(
|
|
360
|
+
'Failed to add points to collection "test-collection": Unknown error',
|
|
321
361
|
);
|
|
322
362
|
});
|
|
323
363
|
});
|
|
@@ -345,7 +385,11 @@ describe("QdrantManager", () => {
|
|
|
345
385
|
{ id: 2, score: 0.85, payload: { text: "result2" } },
|
|
346
386
|
]);
|
|
347
387
|
|
|
348
|
-
const results = await manager.search(
|
|
388
|
+
const results = await manager.search(
|
|
389
|
+
"test-collection",
|
|
390
|
+
[0.1, 0.2, 0.3],
|
|
391
|
+
5,
|
|
392
|
+
);
|
|
349
393
|
|
|
350
394
|
expect(results).toEqual([
|
|
351
395
|
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
@@ -444,7 +488,9 @@ describe("QdrantManager", () => {
|
|
|
444
488
|
});
|
|
445
489
|
|
|
446
490
|
it("should handle null payload in results", async () => {
|
|
447
|
-
mockClient.search.mockResolvedValue([
|
|
491
|
+
mockClient.search.mockResolvedValue([
|
|
492
|
+
{ id: 1, score: 0.95, payload: null },
|
|
493
|
+
]);
|
|
448
494
|
|
|
449
495
|
const results = await manager.search("test-collection", [0.1, 0.2, 0.3]);
|
|
450
496
|
|
|
@@ -473,11 +519,19 @@ describe("QdrantManager", () => {
|
|
|
473
519
|
},
|
|
474
520
|
});
|
|
475
521
|
|
|
476
|
-
mockClient.search.mockResolvedValue([
|
|
522
|
+
mockClient.search.mockResolvedValue([
|
|
523
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
524
|
+
]);
|
|
477
525
|
|
|
478
|
-
const results = await manager.search(
|
|
526
|
+
const results = await manager.search(
|
|
527
|
+
"hybrid-collection",
|
|
528
|
+
[0.1, 0.2, 0.3],
|
|
529
|
+
5,
|
|
530
|
+
);
|
|
479
531
|
|
|
480
|
-
expect(results).toEqual([
|
|
532
|
+
expect(results).toEqual([
|
|
533
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
534
|
+
]);
|
|
481
535
|
expect(mockClient.search).toHaveBeenCalledWith("hybrid-collection", {
|
|
482
536
|
vector: { name: "dense", vector: [0.1, 0.2, 0.3] },
|
|
483
537
|
limit: 5,
|
|
@@ -500,11 +554,19 @@ describe("QdrantManager", () => {
|
|
|
500
554
|
},
|
|
501
555
|
});
|
|
502
556
|
|
|
503
|
-
mockClient.search.mockResolvedValue([
|
|
557
|
+
mockClient.search.mockResolvedValue([
|
|
558
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
559
|
+
]);
|
|
504
560
|
|
|
505
|
-
const results = await manager.search(
|
|
561
|
+
const results = await manager.search(
|
|
562
|
+
"standard-collection",
|
|
563
|
+
[0.1, 0.2, 0.3],
|
|
564
|
+
5,
|
|
565
|
+
);
|
|
506
566
|
|
|
507
|
-
expect(results).toEqual([
|
|
567
|
+
expect(results).toEqual([
|
|
568
|
+
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
569
|
+
]);
|
|
508
570
|
expect(mockClient.search).toHaveBeenCalledWith("standard-collection", {
|
|
509
571
|
vector: [0.1, 0.2, 0.3],
|
|
510
572
|
limit: 5,
|
|
@@ -515,7 +577,9 @@ describe("QdrantManager", () => {
|
|
|
515
577
|
|
|
516
578
|
describe("getPoint", () => {
|
|
517
579
|
it("should retrieve a point by id", async () => {
|
|
518
|
-
mockClient.retrieve.mockResolvedValue([
|
|
580
|
+
mockClient.retrieve.mockResolvedValue([
|
|
581
|
+
{ id: 1, payload: { text: "test" } },
|
|
582
|
+
]);
|
|
519
583
|
|
|
520
584
|
const point = await manager.getPoint("test-collection", 1);
|
|
521
585
|
|
|
@@ -586,7 +650,11 @@ describe("QdrantManager", () => {
|
|
|
586
650
|
],
|
|
587
651
|
});
|
|
588
652
|
|
|
589
|
-
const results = await manager.hybridSearch(
|
|
653
|
+
const results = await manager.hybridSearch(
|
|
654
|
+
"test-collection",
|
|
655
|
+
denseVector,
|
|
656
|
+
sparseVector,
|
|
657
|
+
);
|
|
590
658
|
|
|
591
659
|
expect(results).toEqual([
|
|
592
660
|
{ id: 1, score: 0.95, payload: { text: "result1" } },
|
|
@@ -622,7 +690,12 @@ describe("QdrantManager", () => {
|
|
|
622
690
|
|
|
623
691
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
624
692
|
|
|
625
|
-
await manager.hybridSearch(
|
|
693
|
+
await manager.hybridSearch(
|
|
694
|
+
"test-collection",
|
|
695
|
+
denseVector,
|
|
696
|
+
sparseVector,
|
|
697
|
+
10,
|
|
698
|
+
);
|
|
626
699
|
|
|
627
700
|
expect(mockClient.query).toHaveBeenCalledWith(
|
|
628
701
|
"test-collection",
|
|
@@ -631,7 +704,7 @@ describe("QdrantManager", () => {
|
|
|
631
704
|
expect.objectContaining({ limit: 40 }), // 10 * 4
|
|
632
705
|
]),
|
|
633
706
|
limit: 10,
|
|
634
|
-
})
|
|
707
|
+
}),
|
|
635
708
|
);
|
|
636
709
|
});
|
|
637
710
|
|
|
@@ -642,7 +715,13 @@ describe("QdrantManager", () => {
|
|
|
642
715
|
|
|
643
716
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
644
717
|
|
|
645
|
-
await manager.hybridSearch(
|
|
718
|
+
await manager.hybridSearch(
|
|
719
|
+
"test-collection",
|
|
720
|
+
denseVector,
|
|
721
|
+
sparseVector,
|
|
722
|
+
5,
|
|
723
|
+
filter,
|
|
724
|
+
);
|
|
646
725
|
|
|
647
726
|
expect(mockClient.query).toHaveBeenCalledWith("test-collection", {
|
|
648
727
|
prefetch: [
|
|
@@ -684,7 +763,13 @@ describe("QdrantManager", () => {
|
|
|
684
763
|
|
|
685
764
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
686
765
|
|
|
687
|
-
await manager.hybridSearch(
|
|
766
|
+
await manager.hybridSearch(
|
|
767
|
+
"test-collection",
|
|
768
|
+
denseVector,
|
|
769
|
+
sparseVector,
|
|
770
|
+
5,
|
|
771
|
+
filter,
|
|
772
|
+
);
|
|
688
773
|
|
|
689
774
|
const call = mockClient.query.mock.calls[0][1];
|
|
690
775
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -694,11 +779,19 @@ describe("QdrantManager", () => {
|
|
|
694
779
|
it("should handle Qdrant format filter (should)", async () => {
|
|
695
780
|
const denseVector = [0.1, 0.2, 0.3];
|
|
696
781
|
const sparseVector = { indices: [1], values: [0.5] };
|
|
697
|
-
const filter = {
|
|
782
|
+
const filter = {
|
|
783
|
+
should: [{ key: "tag", match: { value: "important" } }],
|
|
784
|
+
};
|
|
698
785
|
|
|
699
786
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
700
787
|
|
|
701
|
-
await manager.hybridSearch(
|
|
788
|
+
await manager.hybridSearch(
|
|
789
|
+
"test-collection",
|
|
790
|
+
denseVector,
|
|
791
|
+
sparseVector,
|
|
792
|
+
5,
|
|
793
|
+
filter,
|
|
794
|
+
);
|
|
702
795
|
|
|
703
796
|
const call = mockClient.query.mock.calls[0][1];
|
|
704
797
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -708,11 +801,19 @@ describe("QdrantManager", () => {
|
|
|
708
801
|
it("should handle Qdrant format filter (must_not)", async () => {
|
|
709
802
|
const denseVector = [0.1, 0.2, 0.3];
|
|
710
803
|
const sparseVector = { indices: [1], values: [0.5] };
|
|
711
|
-
const filter = {
|
|
804
|
+
const filter = {
|
|
805
|
+
must_not: [{ key: "status", match: { value: "deleted" } }],
|
|
806
|
+
};
|
|
712
807
|
|
|
713
808
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
714
809
|
|
|
715
|
-
await manager.hybridSearch(
|
|
810
|
+
await manager.hybridSearch(
|
|
811
|
+
"test-collection",
|
|
812
|
+
denseVector,
|
|
813
|
+
sparseVector,
|
|
814
|
+
5,
|
|
815
|
+
filter,
|
|
816
|
+
);
|
|
716
817
|
|
|
717
818
|
const call = mockClient.query.mock.calls[0][1];
|
|
718
819
|
expect(call.prefetch[0].filter).toEqual(filter);
|
|
@@ -725,7 +826,13 @@ describe("QdrantManager", () => {
|
|
|
725
826
|
|
|
726
827
|
mockClient.query.mockResolvedValue({ points: [] });
|
|
727
828
|
|
|
728
|
-
await manager.hybridSearch(
|
|
829
|
+
await manager.hybridSearch(
|
|
830
|
+
"test-collection",
|
|
831
|
+
denseVector,
|
|
832
|
+
sparseVector,
|
|
833
|
+
5,
|
|
834
|
+
{},
|
|
835
|
+
);
|
|
729
836
|
|
|
730
837
|
const call = mockClient.query.mock.calls[0][1];
|
|
731
838
|
expect(call.prefetch[0].filter).toBeUndefined();
|
|
@@ -740,7 +847,11 @@ describe("QdrantManager", () => {
|
|
|
740
847
|
points: [{ id: 1, score: 0.95, payload: null }],
|
|
741
848
|
});
|
|
742
849
|
|
|
743
|
-
const results = await manager.hybridSearch(
|
|
850
|
+
const results = await manager.hybridSearch(
|
|
851
|
+
"test-collection",
|
|
852
|
+
denseVector,
|
|
853
|
+
sparseVector,
|
|
854
|
+
);
|
|
744
855
|
|
|
745
856
|
expect(results).toEqual([{ id: 1, score: 0.95, payload: undefined }]);
|
|
746
857
|
});
|
|
@@ -758,9 +869,9 @@ describe("QdrantManager", () => {
|
|
|
758
869
|
});
|
|
759
870
|
|
|
760
871
|
await expect(
|
|
761
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
872
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
762
873
|
).rejects.toThrow(
|
|
763
|
-
'Hybrid search failed on collection "test-collection": Named vector not found'
|
|
874
|
+
'Hybrid search failed on collection "test-collection": Named vector not found',
|
|
764
875
|
);
|
|
765
876
|
});
|
|
766
877
|
|
|
@@ -771,8 +882,10 @@ describe("QdrantManager", () => {
|
|
|
771
882
|
mockClient.query.mockRejectedValue(new Error("Network timeout"));
|
|
772
883
|
|
|
773
884
|
await expect(
|
|
774
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
775
|
-
).rejects.toThrow(
|
|
885
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
886
|
+
).rejects.toThrow(
|
|
887
|
+
'Hybrid search failed on collection "test-collection": Network timeout',
|
|
888
|
+
);
|
|
776
889
|
});
|
|
777
890
|
|
|
778
891
|
it("should throw error with String(error) fallback", async () => {
|
|
@@ -782,8 +895,10 @@ describe("QdrantManager", () => {
|
|
|
782
895
|
mockClient.query.mockRejectedValue("Unknown error");
|
|
783
896
|
|
|
784
897
|
await expect(
|
|
785
|
-
manager.hybridSearch("test-collection", denseVector, sparseVector)
|
|
786
|
-
).rejects.toThrow(
|
|
898
|
+
manager.hybridSearch("test-collection", denseVector, sparseVector),
|
|
899
|
+
).rejects.toThrow(
|
|
900
|
+
'Hybrid search failed on collection "test-collection": Unknown error',
|
|
901
|
+
);
|
|
787
902
|
});
|
|
788
903
|
});
|
|
789
904
|
|
|
@@ -874,7 +989,7 @@ describe("QdrantManager", () => {
|
|
|
874
989
|
const normalizedId = calls[0][1].points[0].id;
|
|
875
990
|
// Check that it's a valid UUID format
|
|
876
991
|
expect(normalizedId).toMatch(
|
|
877
|
-
/^[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,
|
|
878
993
|
);
|
|
879
994
|
expect(normalizedId).not.toBe("my-doc-id");
|
|
880
995
|
});
|
|
@@ -924,8 +1039,10 @@ describe("QdrantManager", () => {
|
|
|
924
1039
|
},
|
|
925
1040
|
});
|
|
926
1041
|
|
|
927
|
-
await expect(
|
|
928
|
-
|
|
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',
|
|
929
1046
|
);
|
|
930
1047
|
});
|
|
931
1048
|
|
|
@@ -940,8 +1057,10 @@ describe("QdrantManager", () => {
|
|
|
940
1057
|
|
|
941
1058
|
mockClient.upsert.mockRejectedValue(new Error("Connection refused"));
|
|
942
1059
|
|
|
943
|
-
await expect(
|
|
944
|
-
|
|
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',
|
|
945
1064
|
);
|
|
946
1065
|
});
|
|
947
1066
|
|
|
@@ -956,8 +1075,10 @@ describe("QdrantManager", () => {
|
|
|
956
1075
|
|
|
957
1076
|
mockClient.upsert.mockRejectedValue("Unexpected error");
|
|
958
1077
|
|
|
959
|
-
await expect(
|
|
960
|
-
|
|
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',
|
|
961
1082
|
);
|
|
962
1083
|
});
|
|
963
1084
|
});
|
package/src/qdrant/client.ts
CHANGED
|
@@ -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"]
|
package/vitest.config.ts
CHANGED
package/docker-compose.yml
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# No version field needed for Docker Compose v1.27.0+
|
|
2
|
-
services:
|
|
3
|
-
qdrant:
|
|
4
|
-
image: qdrant/qdrant:latest
|
|
5
|
-
container_name: qdrant-mcp
|
|
6
|
-
ports:
|
|
7
|
-
- "6333:6333"
|
|
8
|
-
- "6334:6334"
|
|
9
|
-
volumes:
|
|
10
|
-
- ./qdrant_storage:/qdrant/storage
|
|
11
|
-
environment:
|
|
12
|
-
- QDRANT__SERVICE__GRPC_PORT=6334
|
|
13
|
-
restart: unless-stopped
|
|
14
|
-
|
|
15
|
-
ollama:
|
|
16
|
-
image: ollama/ollama:latest
|
|
17
|
-
container_name: ollama
|
|
18
|
-
ports:
|
|
19
|
-
- "11434:11434"
|
|
20
|
-
volumes:
|
|
21
|
-
- ./ollama_storage:/root/.ollama
|
|
22
|
-
restart: unless-stopped
|