@mhalder/qdrant-mcp-server 1.1.0 → 1.2.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.
@@ -1,11 +1,12 @@
1
- import { QdrantClient } from '@qdrant/js-client-rest';
2
- import { createHash } from 'crypto';
1
+ import { createHash } from "node:crypto";
2
+ import { QdrantClient } from "@qdrant/js-client-rest";
3
3
 
4
4
  export interface CollectionInfo {
5
5
  name: string;
6
6
  vectorSize: number;
7
7
  pointsCount: number;
8
- distance: 'Cosine' | 'Euclid' | 'Dot';
8
+ distance: "Cosine" | "Euclid" | "Dot";
9
+ hybridEnabled?: boolean;
9
10
  }
10
11
 
11
12
  export interface SearchResult {
@@ -14,10 +15,15 @@ export interface SearchResult {
14
15
  payload?: Record<string, any>;
15
16
  }
16
17
 
18
+ export interface SparseVector {
19
+ indices: number[];
20
+ values: number[];
21
+ }
22
+
17
23
  export class QdrantManager {
18
24
  private client: QdrantClient;
19
25
 
20
- constructor(url: string = 'http://localhost:6333') {
26
+ constructor(url: string = "http://localhost:6333") {
21
27
  this.client = new QdrantClient({ url });
22
28
  }
23
29
 
@@ -26,7 +32,7 @@ export class QdrantManager {
26
32
  * Qdrant requires string IDs to be in UUID format.
27
33
  */
28
34
  private normalizeId(id: string | number): string | number {
29
- if (typeof id === 'number') {
35
+ if (typeof id === "number") {
30
36
  return id;
31
37
  }
32
38
 
@@ -37,21 +43,40 @@ export class QdrantManager {
37
43
  }
38
44
 
39
45
  // Convert arbitrary string to deterministic UUID v5-like format
40
- const hash = createHash('sha256').update(id).digest('hex');
46
+ const hash = createHash("sha256").update(id).digest("hex");
41
47
  return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
42
48
  }
43
49
 
44
50
  async createCollection(
45
51
  name: string,
46
52
  vectorSize: number,
47
- distance: 'Cosine' | 'Euclid' | 'Dot' = 'Cosine'
53
+ distance: "Cosine" | "Euclid" | "Dot" = "Cosine",
54
+ enableSparse: boolean = false
48
55
  ): Promise<void> {
49
- await this.client.createCollection(name, {
50
- vectors: {
56
+ const config: any = {};
57
+
58
+ // When hybrid search is enabled, use named vectors
59
+ if (enableSparse) {
60
+ config.vectors = {
61
+ dense: {
62
+ size: vectorSize,
63
+ distance,
64
+ },
65
+ };
66
+ config.sparse_vectors = {
67
+ text: {
68
+ modifier: "idf",
69
+ },
70
+ };
71
+ } else {
72
+ // Standard unnamed vector configuration
73
+ config.vectors = {
51
74
  size: vectorSize,
52
75
  distance,
53
- },
54
- });
76
+ };
77
+ }
78
+
79
+ await this.client.createCollection(name, config);
55
80
  }
56
81
 
57
82
  async collectionExists(name: string): Promise<boolean> {
@@ -74,11 +99,25 @@ export class QdrantManager {
74
99
 
75
100
  // Handle both named and unnamed vector configurations
76
101
  let size = 0;
77
- let distance: 'Cosine' | 'Euclid' | 'Dot' = 'Cosine';
102
+ let distance: "Cosine" | "Euclid" | "Dot" = "Cosine";
103
+ let hybridEnabled = false;
78
104
 
79
- if (typeof vectorConfig === 'object' && vectorConfig !== null && 'size' in vectorConfig) {
80
- size = typeof vectorConfig.size === 'number' ? vectorConfig.size : 0;
81
- distance = vectorConfig.distance as 'Cosine' | 'Euclid' | 'Dot';
105
+ // Check if sparse vectors are configured
106
+ if (info.config.params.sparse_vectors) {
107
+ hybridEnabled = true;
108
+ }
109
+
110
+ if (typeof vectorConfig === "object" && vectorConfig !== null) {
111
+ // Check for unnamed vector config (has 'size' directly)
112
+ if ("size" in vectorConfig) {
113
+ size = typeof vectorConfig.size === "number" ? vectorConfig.size : 0;
114
+ distance = vectorConfig.distance as "Cosine" | "Euclid" | "Dot";
115
+ } else if ("dense" in vectorConfig) {
116
+ // Named vector config for hybrid search
117
+ const denseConfig = vectorConfig.dense as any;
118
+ size = typeof denseConfig.size === "number" ? denseConfig.size : 0;
119
+ distance = denseConfig.distance as "Cosine" | "Euclid" | "Dot";
120
+ }
82
121
  }
83
122
 
84
123
  return {
@@ -86,6 +125,7 @@ export class QdrantManager {
86
125
  vectorSize: size,
87
126
  pointsCount: info.points_count || 0,
88
127
  distance,
128
+ hybridEnabled,
89
129
  };
90
130
  }
91
131
 
@@ -103,7 +143,7 @@ export class QdrantManager {
103
143
  ): Promise<void> {
104
144
  try {
105
145
  // Normalize all IDs to ensure string IDs are in UUID format
106
- const normalizedPoints = points.map(point => ({
146
+ const normalizedPoints = points.map((point) => ({
107
147
  ...point,
108
148
  id: this.normalizeId(point.id),
109
149
  }));
@@ -144,8 +184,11 @@ export class QdrantManager {
144
184
  }
145
185
  }
146
186
 
187
+ // Check if collection uses named vectors (hybrid mode)
188
+ const collectionInfo = await this.getCollectionInfo(collectionName);
189
+
147
190
  const results = await this.client.search(collectionName, {
148
- vector,
191
+ vector: collectionInfo.hybridEnabled ? { name: "dense", vector } : vector,
149
192
  limit,
150
193
  filter: qdrantFilter,
151
194
  });
@@ -180,16 +223,113 @@ export class QdrantManager {
180
223
  }
181
224
  }
182
225
 
183
- async deletePoints(
184
- collectionName: string,
185
- ids: (string | number)[]
186
- ): Promise<void> {
226
+ async deletePoints(collectionName: string, ids: (string | number)[]): Promise<void> {
187
227
  // Normalize IDs to ensure string IDs are in UUID format
188
- const normalizedIds = ids.map(id => this.normalizeId(id));
228
+ const normalizedIds = ids.map((id) => this.normalizeId(id));
189
229
 
190
230
  await this.client.delete(collectionName, {
191
231
  wait: true,
192
232
  points: normalizedIds,
193
233
  });
194
234
  }
235
+
236
+ /**
237
+ * Performs hybrid search combining semantic vector search with sparse vector (keyword) search
238
+ * using Reciprocal Rank Fusion (RRF) to combine results
239
+ */
240
+ async hybridSearch(
241
+ collectionName: string,
242
+ denseVector: number[],
243
+ sparseVector: SparseVector,
244
+ limit: number = 5,
245
+ filter?: Record<string, any>,
246
+ _semanticWeight: number = 0.7
247
+ ): Promise<SearchResult[]> {
248
+ // Convert simple key-value filter to Qdrant filter format
249
+ let qdrantFilter;
250
+ if (filter && Object.keys(filter).length > 0) {
251
+ if (filter.must || filter.should || filter.must_not) {
252
+ qdrantFilter = filter;
253
+ } else {
254
+ qdrantFilter = {
255
+ must: Object.entries(filter).map(([key, value]) => ({
256
+ key,
257
+ match: { value },
258
+ })),
259
+ };
260
+ }
261
+ }
262
+
263
+ // Calculate prefetch limits based on weights
264
+ // We fetch more results than needed to ensure good fusion results
265
+ const prefetchLimit = Math.max(20, limit * 4);
266
+
267
+ try {
268
+ const results = await this.client.query(collectionName, {
269
+ prefetch: [
270
+ {
271
+ query: denseVector,
272
+ using: "dense",
273
+ limit: prefetchLimit,
274
+ filter: qdrantFilter,
275
+ },
276
+ {
277
+ query: sparseVector,
278
+ using: "text",
279
+ limit: prefetchLimit,
280
+ filter: qdrantFilter,
281
+ },
282
+ ],
283
+ query: {
284
+ fusion: "rrf",
285
+ },
286
+ limit: limit,
287
+ with_payload: true,
288
+ });
289
+
290
+ return results.points.map((result: any) => ({
291
+ id: result.id,
292
+ score: result.score,
293
+ payload: result.payload || undefined,
294
+ }));
295
+ } catch (error: any) {
296
+ const errorMessage = error?.data?.status?.error || error?.message || String(error);
297
+ throw new Error(`Hybrid search failed on collection "${collectionName}": ${errorMessage}`);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Adds points with both dense and sparse vectors for hybrid search
303
+ */
304
+ async addPointsWithSparse(
305
+ collectionName: string,
306
+ points: Array<{
307
+ id: string | number;
308
+ vector: number[];
309
+ sparseVector: SparseVector;
310
+ payload?: Record<string, any>;
311
+ }>
312
+ ): Promise<void> {
313
+ try {
314
+ // Normalize all IDs to ensure string IDs are in UUID format
315
+ const normalizedPoints = points.map((point) => ({
316
+ id: this.normalizeId(point.id),
317
+ vector: {
318
+ dense: point.vector,
319
+ text: point.sparseVector,
320
+ },
321
+ payload: point.payload,
322
+ }));
323
+
324
+ await this.client.upsert(collectionName, {
325
+ wait: true,
326
+ points: normalizedPoints,
327
+ });
328
+ } catch (error: any) {
329
+ const errorMessage = error?.data?.status?.error || error?.message || String(error);
330
+ throw new Error(
331
+ `Failed to add points with sparse vectors to collection "${collectionName}": ${errorMessage}`
332
+ );
333
+ }
334
+ }
195
335
  }
package/vitest.config.ts CHANGED
@@ -14,10 +14,10 @@ export default defineConfig({
14
14
  "**/*.test.ts",
15
15
  "**/*.spec.ts",
16
16
  "vitest.config.ts",
17
- "src/index.ts", // MCP server SDK integration - tested via integration
18
- "scripts/**", // Exclude utility scripts from coverage
17
+ "commitlint.config.js",
18
+ "src/index.ts",
19
+ "scripts/**",
19
20
  ],
20
- // Set thresholds for core business logic modules
21
21
  thresholds: {
22
22
  "src/qdrant/client.ts": {
23
23
  lines: 90,
@@ -1,259 +0,0 @@
1
- # Test Report - Qdrant MCP Server
2
-
3
- **Generated:** 2025-10-09
4
- **Version:** 1.1.0 (Ollama as Default Provider)
5
- **Test Framework:** Vitest 2.1.9
6
-
7
- ## Summary
8
-
9
- ✅ **All tests passing**
10
-
11
- | Metric | Value |
12
- | -------------------------------- | -------- |
13
- | **Latest MCP Integration Tests** | 6 |
14
- | **Test Operations** | 6 |
15
- | **Passed** | 6 (100%) |
16
- | **Failed** | 0 |
17
- | **Duration** | ~30s |
18
-
19
- ## Latest Test Results (2025-10-09)
20
-
21
- ### MCP Integration Test - Full Workflow Validation
22
-
23
- **Date:** 2025-10-09
24
- **Environment:** Production MCP server with Ollama embeddings (default provider)
25
- **Purpose:** Validate complete MCP functionality with real embeddings
26
-
27
- #### Test Setup
28
-
29
- - ✅ Qdrant running via Docker (localhost:6333)
30
- - ✅ MCP server connected to Claude Code
31
- - ✅ Ollama configured as default provider
32
- - ✅ Model: nomic-embed-text (768 dimensions)
33
- - ✅ No API keys required
34
-
35
- ### Test Operations
36
-
37
- #### Test 1: List Existing Collections
38
-
39
- ```
40
- Operation: List all collections
41
- Result: ✅ SUCCESS
42
- Collections Found: ["final_test"]
43
- ```
44
-
45
- #### Test 2: Create Test Collection
46
-
47
- ```
48
- Operation: Create collection "mcp_test_collection"
49
- Distance Metric: Cosine
50
- Result: ✅ SUCCESS
51
- Details: Collection created with 768 dimensions (Ollama default)
52
- ```
53
-
54
- **Validation:**
55
-
56
- - ✅ Correct dimensions for Ollama provider (768)
57
- - ✅ Cosine distance metric configured
58
- - ✅ Collection created successfully
59
-
60
- #### Test 3: Add Documents with Metadata
61
-
62
- ```
63
- Operation: Add 5 documents with real Ollama embeddings
64
- Result: ✅ SUCCESS
65
-
66
- Documents Added:
67
- 1. "Python is a high-level programming language..." (category: programming)
68
- 2. "JavaScript is the programming language of the web..." (category: programming)
69
- 3. "Machine learning is a subset of artificial intelligence..." (category: AI)
70
- 4. "Qdrant is a vector database designed for storing..." (category: database)
71
- 5. "Neural networks are computing systems inspired by..." (category: AI)
72
- ```
73
-
74
- **Validation:**
75
-
76
- - ✅ All 5 documents embedded using Ollama
77
- - ✅ Metadata correctly attached
78
- - ✅ Batch processing successful
79
-
80
- #### Test 4: Semantic Search - Vector Database Query
81
-
82
- ```
83
- Query: "What is a vector database?"
84
- Limit: 3
85
- Result: ✅ SUCCESS
86
-
87
- Top Results:
88
- 1. Score: 0.687 - "Qdrant is a vector database designed for storing..."
89
- 2. Score: 0.481 - "Python is a high-level programming language..."
90
- 3. Score: 0.477 - "Neural networks are computing systems inspired by..."
91
- ```
92
-
93
- **Analysis:**
94
-
95
- - ✅ Excellent semantic matching - correctly identified Qdrant as most relevant
96
- - ✅ High relevance score (0.687) for vector database content
97
- - ✅ Query understanding working correctly
98
-
99
- #### Test 5: Semantic Search - AI and Deep Learning Query
100
-
101
- ```
102
- Query: "artificial intelligence and deep learning"
103
- Limit: 3
104
- Result: ✅ SUCCESS
105
-
106
- Top Results:
107
- 1. Score: 0.784 - "Neural networks are computing systems..."
108
- 2. Score: 0.771 - "Machine learning is a subset of AI..."
109
- 3. Score: 0.578 - "Python is a high-level programming language..."
110
- ```
111
-
112
- **Analysis:**
113
-
114
- - ✅ Very high relevance scores (0.78+) for AI content
115
- - ✅ Correctly prioritized neural networks and machine learning
116
- - ✅ Semantic understanding of query intent
117
-
118
- #### Test 6: Get Collection Information
119
-
120
- ```
121
- Operation: Get collection info for "mcp_test_collection"
122
- Result: ✅ SUCCESS
123
-
124
- Collection Details:
125
- - Name: mcp_test_collection
126
- - Vector Size: 768 (Ollama default)
127
- - Points Count: 5
128
- - Distance: Cosine
129
- ```
130
-
131
- **Validation:**
132
-
133
- - ✅ Correct vector dimensions for Ollama
134
- - ✅ Accurate point count
135
- - ✅ Distance metric confirmed
136
-
137
- #### Test 7: Cleanup - Delete Collection
138
-
139
- ```
140
- Operation: Delete collection "mcp_test_collection"
141
- Result: ✅ SUCCESS
142
- Final State: Test collection removed successfully
143
- ```
144
-
145
- ## Test Results Summary
146
-
147
- | Test | Operation | Status | Notes |
148
- | ---- | ----------------- | ------- | -------------------------- |
149
- | 1 | List Collections | ✅ PASS | Found existing collections |
150
- | 2 | Create Collection | ✅ PASS | 768 dimensions (Ollama) |
151
- | 3 | Add Documents | ✅ PASS | 5 documents with metadata |
152
- | 4 | Search: Vector DB | ✅ PASS | High relevance (0.687) |
153
- | 5 | Search: AI/ML | ✅ PASS | Excellent scores (0.78+) |
154
- | 6 | Collection Info | ✅ PASS | Metadata accurate |
155
- | 7 | Delete Collection | ✅ PASS | Cleanup successful |
156
-
157
- **Total Tests:** 7
158
- **Passed:** 7 ✅
159
- **Failed:** 0 ❌
160
- **Success Rate:** 100%
161
-
162
- ## Key Validations
163
-
164
- ✅ **Ollama as Default Provider** - Works seamlessly without API keys
165
- ✅ **Collection Management** - Create, info, delete all functional
166
- ✅ **Document Operations** - Batch add with metadata working correctly
167
- ✅ **Semantic Search Quality** - High relevance scores (0.68-0.78)
168
- ✅ **Embeddings Generation** - Real Ollama embeddings (768 dimensions)
169
- ✅ **Metadata Handling** - Categories correctly stored and retrievable
170
- ✅ **MCP Protocol Compliance** - All tools responding correctly
171
- ✅ **Error Handling** - No failures or exceptions
172
- ✅ **Cleanup** - Test artifacts removed successfully
173
-
174
- ## Search Quality Assessment
175
-
176
- ### Query 1: "What is a vector database?"
177
-
178
- - **Top Match:** Qdrant vector database description
179
- - **Relevance Score:** 0.687
180
- - **Quality:** ✅ EXCELLENT - Perfect match for query intent
181
-
182
- ### Query 2: "artificial intelligence and deep learning"
183
-
184
- - **Top Matches:** Neural networks (0.784), Machine learning (0.771)
185
- - **Quality:** ✅ EXCELLENT - Both query concepts matched accurately
186
-
187
- ### Search Accuracy
188
-
189
- - Semantic understanding: ✅ EXCELLENT
190
- - Relevance ranking: ✅ ACCURATE
191
- - Query interpretation: ✅ PRECISE
192
-
193
- ## Ollama Integration Performance
194
-
195
- - **Provider:** Ollama (default)
196
- - **Model:** nomic-embed-text
197
- - **Dimensions:** 768
198
- - **API Key:** Not required ✓
199
- - **Documents Processed:** 5
200
- - **Embedding Calls:** 2 (batch operations)
201
- - **Errors:** 0
202
- - **Privacy:** All data processed locally ✓
203
-
204
- ## MCP Tool Validation
205
-
206
- All 7 MCP tools tested and working:
207
-
208
- | Tool | Status | Notes |
209
- | --------------------- | ------------------------- | ------------------------------- |
210
- | `list_collections` | ✅ PASS | Lists all collections |
211
- | `create_collection` | ✅ PASS | Creates with correct dimensions |
212
- | `add_documents` | ✅ PASS | Batch add with metadata |
213
- | `semantic_search` | ✅ PASS | High-quality results |
214
- | `get_collection_info` | ✅ PASS | Accurate metadata |
215
- | `delete_collection` | ✅ PASS | Clean removal |
216
- | `delete_documents` | ⚪ Not tested in this run | - |
217
-
218
- ## Production Readiness Checklist
219
-
220
- - ✅ Ollama as default provider - no setup required
221
- - ✅ Collections create with correct dimensions
222
- - ✅ Documents add successfully with embeddings
223
- - ✅ Semantic search returns relevant results
224
- - ✅ Collection info shows accurate metadata
225
- - ✅ Collections delete cleanly
226
- - ✅ No API keys required for basic usage
227
- - ✅ Privacy-first local embeddings
228
- - ✅ Zero configuration needed
229
- - ✅ All MCP tools functional
230
-
231
- ## Conclusion
232
-
233
- The Qdrant MCP Server with **Ollama as the default provider** is **production-ready** and performs excellently in real-world scenarios. All operations completed successfully with:
234
-
235
- - ✅ Real Ollama embeddings (5 documents)
236
- - ✅ No configuration required (zero setup)
237
- - ✅ High semantic search accuracy (0.68-0.78 relevance)
238
- - ✅ Local processing (privacy-first)
239
- - ✅ No API keys needed
240
- - ✅ Clean error-free execution
241
-
242
- ### Key Strengths
243
-
244
- 1. **Privacy-First:** All embeddings processed locally via Ollama
245
- 2. **Zero Setup:** Works immediately with Docker Compose
246
- 3. **No API Keys:** Default provider requires no configuration
247
- 4. **High Quality:** Excellent semantic search results
248
- 5. **MCP Compliance:** All tools working correctly
249
- 6. **Clean Architecture:** Proper error handling and cleanup
250
- 7. **Production Ready:** Validated with real-world workflows
251
-
252
- **Test Status:** ✅ **EXCELLENT**
253
-
254
- ---
255
-
256
- **Report Generated:** 2025-10-09
257
- **Platform:** Linux
258
- **Docker:** Qdrant running on localhost:6333
259
- **Status:** All 7 MCP integration tests passing ✅