@mhalder/qdrant-mcp-server 1.1.1 → 1.3.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/CHANGELOG.md +18 -0
- package/README.md +36 -0
- package/biome.json +34 -0
- package/build/embeddings/sparse.d.ts +40 -0
- package/build/embeddings/sparse.d.ts.map +1 -0
- package/build/embeddings/sparse.js +105 -0
- package/build/embeddings/sparse.js.map +1 -0
- package/build/embeddings/sparse.test.d.ts +2 -0
- package/build/embeddings/sparse.test.d.ts.map +1 -0
- package/build/embeddings/sparse.test.js +69 -0
- package/build/embeddings/sparse.test.js.map +1 -0
- package/build/index.js +333 -32
- package/build/index.js.map +1 -1
- package/build/qdrant/client.d.ts +21 -2
- package/build/qdrant/client.d.ts.map +1 -1
- package/build/qdrant/client.js +131 -17
- package/build/qdrant/client.js.map +1 -1
- package/build/qdrant/client.test.js +429 -21
- package/build/qdrant/client.test.js.map +1 -1
- package/build/transport.test.d.ts +2 -0
- package/build/transport.test.d.ts.map +1 -0
- package/build/transport.test.js +168 -0
- package/build/transport.test.js.map +1 -0
- package/examples/README.md +16 -1
- package/examples/basic/README.md +1 -0
- package/examples/hybrid-search/README.md +236 -0
- package/package.json +3 -1
- package/src/embeddings/sparse.test.ts +87 -0
- package/src/embeddings/sparse.ts +127 -0
- package/src/index.ts +393 -59
- package/src/qdrant/client.test.ts +544 -56
- package/src/qdrant/client.ts +162 -22
- package/src/transport.test.ts +202 -0
- package/vitest.config.ts +3 -3
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
describe("Transport Configuration", () => {
|
|
3
|
+
let originalEnv;
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
originalEnv = { ...process.env };
|
|
6
|
+
});
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
process.env = originalEnv;
|
|
9
|
+
});
|
|
10
|
+
describe("HTTP Port Validation", () => {
|
|
11
|
+
it("should accept valid port numbers", () => {
|
|
12
|
+
const validPorts = ["1", "80", "443", "3000", "8080", "65535"];
|
|
13
|
+
validPorts.forEach((port) => {
|
|
14
|
+
const parsed = parseInt(port, 10);
|
|
15
|
+
expect(parsed).toBeGreaterThanOrEqual(1);
|
|
16
|
+
expect(parsed).toBeLessThanOrEqual(65535);
|
|
17
|
+
expect(Number.isNaN(parsed)).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it("should reject invalid port numbers", () => {
|
|
21
|
+
const invalidCases = [
|
|
22
|
+
{ port: "0", reason: "port 0 is reserved" },
|
|
23
|
+
{ port: "-1", reason: "negative ports are invalid" },
|
|
24
|
+
{ port: "65536", reason: "exceeds maximum port" },
|
|
25
|
+
{ port: "99999", reason: "exceeds maximum port" },
|
|
26
|
+
{ port: "abc", reason: "non-numeric input" },
|
|
27
|
+
{ port: "", reason: "empty string" },
|
|
28
|
+
];
|
|
29
|
+
invalidCases.forEach(({ port, reason }) => {
|
|
30
|
+
const parsed = parseInt(port, 10);
|
|
31
|
+
const isValid = !Number.isNaN(parsed) && parsed >= 1 && parsed <= 65535;
|
|
32
|
+
expect(isValid, `Failed for ${port}: ${reason}`).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it("should use default port 3000 when HTTP_PORT is not set", () => {
|
|
36
|
+
const port = parseInt(process.env.HTTP_PORT || "3000", 10);
|
|
37
|
+
expect(port).toBe(3000);
|
|
38
|
+
});
|
|
39
|
+
it("should parse HTTP_PORT from environment", () => {
|
|
40
|
+
process.env.HTTP_PORT = "8080";
|
|
41
|
+
const port = parseInt(process.env.HTTP_PORT || "3000", 10);
|
|
42
|
+
expect(port).toBe(8080);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("Transport Mode Validation", () => {
|
|
46
|
+
it("should accept valid transport modes", () => {
|
|
47
|
+
const validModes = ["stdio", "http", "STDIO", "HTTP"];
|
|
48
|
+
validModes.forEach((mode) => {
|
|
49
|
+
const normalized = mode.toLowerCase();
|
|
50
|
+
expect(["stdio", "http"]).toContain(normalized);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it("should reject invalid transport modes", () => {
|
|
54
|
+
const invalidModes = ["tcp", "websocket", "grpc", ""];
|
|
55
|
+
invalidModes.forEach((mode) => {
|
|
56
|
+
const normalized = mode.toLowerCase();
|
|
57
|
+
expect(["stdio", "http"]).not.toContain(normalized);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it("should default to stdio when TRANSPORT_MODE is not set", () => {
|
|
61
|
+
const mode = (process.env.TRANSPORT_MODE || "stdio").toLowerCase();
|
|
62
|
+
expect(mode).toBe("stdio");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("Request Size Limits", () => {
|
|
66
|
+
it("should define request size limit for HTTP transport", () => {
|
|
67
|
+
const limit = "10mb";
|
|
68
|
+
expect(limit).toMatch(/^\d+mb$/);
|
|
69
|
+
});
|
|
70
|
+
it("should parse size limit correctly", () => {
|
|
71
|
+
const limit = "10mb";
|
|
72
|
+
const sizeInBytes = parseInt(limit, 10) * 1024 * 1024;
|
|
73
|
+
expect(sizeInBytes).toBe(10485760);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("HTTP Server Configuration", () => {
|
|
78
|
+
describe("Graceful Shutdown", () => {
|
|
79
|
+
it("should handle shutdown signals", async () => {
|
|
80
|
+
const mockServer = {
|
|
81
|
+
close: vi.fn((callback) => {
|
|
82
|
+
if (callback)
|
|
83
|
+
callback();
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
const shutdown = () => {
|
|
87
|
+
mockServer.close(() => {
|
|
88
|
+
// Shutdown callback
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
shutdown();
|
|
92
|
+
expect(mockServer.close).toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
it("should support timeout for forced shutdown", () => {
|
|
95
|
+
vi.useFakeTimers();
|
|
96
|
+
let forcedShutdown = false;
|
|
97
|
+
const timeout = setTimeout(() => {
|
|
98
|
+
forcedShutdown = true;
|
|
99
|
+
}, 10000);
|
|
100
|
+
vi.advanceTimersByTime(9999);
|
|
101
|
+
expect(forcedShutdown).toBe(false);
|
|
102
|
+
vi.advanceTimersByTime(1);
|
|
103
|
+
expect(forcedShutdown).toBe(true);
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
vi.useRealTimers();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("Error Handling", () => {
|
|
109
|
+
it("should return JSON-RPC 2.0 error format", () => {
|
|
110
|
+
const error = {
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
error: {
|
|
113
|
+
code: -32603,
|
|
114
|
+
message: "Internal server error",
|
|
115
|
+
},
|
|
116
|
+
id: null,
|
|
117
|
+
};
|
|
118
|
+
expect(error.jsonrpc).toBe("2.0");
|
|
119
|
+
expect(error.error.code).toBe(-32603);
|
|
120
|
+
expect(error.error.message).toBeTruthy();
|
|
121
|
+
expect(error.id).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
it("should use standard JSON-RPC error codes", () => {
|
|
124
|
+
const errorCodes = {
|
|
125
|
+
parseError: -32700,
|
|
126
|
+
invalidRequest: -32600,
|
|
127
|
+
methodNotFound: -32601,
|
|
128
|
+
invalidParams: -32602,
|
|
129
|
+
internalError: -32603,
|
|
130
|
+
};
|
|
131
|
+
expect(errorCodes.parseError).toBe(-32700);
|
|
132
|
+
expect(errorCodes.invalidRequest).toBe(-32600);
|
|
133
|
+
expect(errorCodes.methodNotFound).toBe(-32601);
|
|
134
|
+
expect(errorCodes.invalidParams).toBe(-32602);
|
|
135
|
+
expect(errorCodes.internalError).toBe(-32603);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("Transport Lifecycle", () => {
|
|
140
|
+
it("should close transport on response close", () => {
|
|
141
|
+
const mockTransport = {
|
|
142
|
+
close: vi.fn(),
|
|
143
|
+
};
|
|
144
|
+
const mockResponse = {
|
|
145
|
+
on: vi.fn((event, callback) => {
|
|
146
|
+
if (event === "close") {
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
mockResponse.on("close", () => {
|
|
152
|
+
mockTransport.close();
|
|
153
|
+
});
|
|
154
|
+
expect(mockTransport.close).toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
it("should handle transport connection", async () => {
|
|
157
|
+
const mockServer = {
|
|
158
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
159
|
+
};
|
|
160
|
+
const mockTransport = {
|
|
161
|
+
handleRequest: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
close: vi.fn(),
|
|
163
|
+
};
|
|
164
|
+
await mockServer.connect(mockTransport);
|
|
165
|
+
expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=transport.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.test.js","sourceRoot":"","sources":["../src/transport.test.ts"],"names":[],"mappings":"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"}
|
package/examples/README.md
CHANGED
|
@@ -31,6 +31,21 @@ Configure MCP server as described in [main README](../README.md).
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
### 🔀 [Hybrid Search](./hybrid-search/)
|
|
35
|
+
|
|
36
|
+
Combine semantic and keyword search for better results
|
|
37
|
+
|
|
38
|
+
- Understanding hybrid search benefits
|
|
39
|
+
- Creating hybrid-enabled collections
|
|
40
|
+
- Comparing semantic vs hybrid search
|
|
41
|
+
- Best practices for technical content
|
|
42
|
+
|
|
43
|
+
**Use cases:** Technical docs, product search, legal documents, code search
|
|
44
|
+
|
|
45
|
+
**Time:** 15-20 minutes | **Difficulty:** Intermediate
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
34
49
|
### ⚡ [Rate Limiting](./rate-limiting/)
|
|
35
50
|
|
|
36
51
|
Automatic rate limit handling for batch operations
|
|
@@ -77,7 +92,7 @@ Complex search filters with boolean logic
|
|
|
77
92
|
## Learning Path
|
|
78
93
|
|
|
79
94
|
```
|
|
80
|
-
Basic → Rate Limiting → Knowledge Base → Advanced Filtering
|
|
95
|
+
Basic → Hybrid Search → Rate Limiting → Knowledge Base → Advanced Filtering
|
|
81
96
|
```
|
|
82
97
|
|
|
83
98
|
## Common Patterns
|
package/examples/basic/README.md
CHANGED
|
@@ -52,6 +52,7 @@ Delete collection "my-first-collection"
|
|
|
52
52
|
|
|
53
53
|
Continue learning with these examples:
|
|
54
54
|
|
|
55
|
+
- **[Hybrid Search](../hybrid-search/)** - Combine semantic and keyword search
|
|
55
56
|
- **[Knowledge Base](../knowledge-base/)** - Metadata and content organization
|
|
56
57
|
- **[Advanced Filtering](../filters/)** - Complex search queries
|
|
57
58
|
- **[Rate Limiting](../rate-limiting/)** - Batch processing patterns
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Hybrid Search
|
|
2
|
+
|
|
3
|
+
Combine semantic vector search with keyword (BM25) search for more accurate and comprehensive results.
|
|
4
|
+
|
|
5
|
+
**Time:** 15-20 minutes | **Difficulty:** Intermediate
|
|
6
|
+
|
|
7
|
+
## What is Hybrid Search?
|
|
8
|
+
|
|
9
|
+
Hybrid search combines two search approaches:
|
|
10
|
+
|
|
11
|
+
1. **Semantic Search**: Understands meaning and context using vector embeddings
|
|
12
|
+
2. **Keyword Search**: Exact term matching using BM25 sparse vectors
|
|
13
|
+
|
|
14
|
+
The results are merged using **Reciprocal Rank Fusion (RRF)**, which combines rankings from both methods to produce the best overall results.
|
|
15
|
+
|
|
16
|
+
## When to Use Hybrid Search
|
|
17
|
+
|
|
18
|
+
Hybrid search is ideal for:
|
|
19
|
+
|
|
20
|
+
- **Technical documentation**: Users search for exact function names + concepts
|
|
21
|
+
- **Product search**: Match SKUs/model numbers + descriptions
|
|
22
|
+
- **Legal documents**: Exact citations + semantic context
|
|
23
|
+
- **Code search**: Function names + natural language descriptions
|
|
24
|
+
- **Mixed queries**: "authentication JWT" (semantic + exact keyword)
|
|
25
|
+
|
|
26
|
+
## Benefits
|
|
27
|
+
|
|
28
|
+
- **Best of both worlds**: Precision (keyword) + recall (semantic)
|
|
29
|
+
- **Better results for ambiguous queries**
|
|
30
|
+
- **Handles typos** (semantic) and **exact matches** (keyword)
|
|
31
|
+
- **More control** over result relevance
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
### 1. Create a Collection with Hybrid Search Enabled
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Create a collection named "technical_docs" with Cosine distance and enableHybrid set to true
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Important**: Set `enableHybrid: true` to enable hybrid search capabilities.
|
|
42
|
+
|
|
43
|
+
### 2. Add Documents
|
|
44
|
+
|
|
45
|
+
Documents are automatically indexed for both semantic and keyword search:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Add these documents to technical_docs:
|
|
49
|
+
- id: 1, text: "The authenticateUser function validates JWT tokens for user sessions",
|
|
50
|
+
metadata: {"category": "authentication", "type": "function"}
|
|
51
|
+
- id: 2, text: "JWT (JSON Web Token) is a compact URL-safe means of representing claims",
|
|
52
|
+
metadata: {"category": "authentication", "type": "definition"}
|
|
53
|
+
- id: 3, text: "OAuth2 provides authorization framework for third-party applications",
|
|
54
|
+
metadata: {"category": "authentication", "type": "protocol"}
|
|
55
|
+
- id: 4, text: "The login endpoint requires username and password credentials",
|
|
56
|
+
metadata: {"category": "authentication", "type": "endpoint"}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Perform Hybrid Search
|
|
60
|
+
|
|
61
|
+
Search using both semantic understanding and keyword matching:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Search technical_docs for "JWT authentication function" with limit 3 using hybrid_search
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Result**: Documents are ranked by combining:
|
|
68
|
+
|
|
69
|
+
- Semantic similarity to "authentication function"
|
|
70
|
+
- Exact keyword matches for "JWT"
|
|
71
|
+
|
|
72
|
+
### 4. Hybrid Search with Filters
|
|
73
|
+
|
|
74
|
+
Combine hybrid search with metadata filtering:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Search technical_docs for "JWT token validation" with limit 2 and filter {"type": "function"} using hybrid_search
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Comparison: Semantic vs Hybrid Search
|
|
81
|
+
|
|
82
|
+
### Semantic Search Only
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Search technical_docs for "JWT authentication" with limit 3 using semantic_search
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Result**: May miss documents with exact "JWT" match if they're not semantically similar.
|
|
89
|
+
|
|
90
|
+
### Hybrid Search
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Search technical_docs for "JWT authentication" with limit 3 using hybrid_search
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Result**: Finds both:
|
|
97
|
+
|
|
98
|
+
- Documents semantically related to authentication
|
|
99
|
+
- Documents with exact "JWT" keyword match
|
|
100
|
+
- Best combination ranked by RRF
|
|
101
|
+
|
|
102
|
+
## Example Scenarios
|
|
103
|
+
|
|
104
|
+
### Scenario 1: Exact Term + Context
|
|
105
|
+
|
|
106
|
+
**Query**: "authenticateUser JWT"
|
|
107
|
+
|
|
108
|
+
**Hybrid Search finds**:
|
|
109
|
+
|
|
110
|
+
1. Documents with `authenticateUser` function name (keyword match)
|
|
111
|
+
2. Documents about JWT authentication (semantic match)
|
|
112
|
+
3. Best combination of both
|
|
113
|
+
|
|
114
|
+
**Pure semantic search might miss**: Exact function name if using different terminology.
|
|
115
|
+
|
|
116
|
+
### Scenario 2: Acronym + Description
|
|
117
|
+
|
|
118
|
+
**Query**: "API rate limiting"
|
|
119
|
+
|
|
120
|
+
**Hybrid Search finds**:
|
|
121
|
+
|
|
122
|
+
1. Documents with "API" acronym (keyword match)
|
|
123
|
+
2. Documents about rate limiting concepts (semantic match)
|
|
124
|
+
3. Documents mentioning "API rate limiting" get highest score
|
|
125
|
+
|
|
126
|
+
### Scenario 3: Typos + Exact Terms
|
|
127
|
+
|
|
128
|
+
**Query**: "OAuth2 authentification"
|
|
129
|
+
|
|
130
|
+
**Hybrid Search finds**:
|
|
131
|
+
|
|
132
|
+
1. "OAuth2" exact matches (keyword - ignores typo in other term)
|
|
133
|
+
2. Authentication concepts (semantic - understands "authentification" ≈ "authentication")
|
|
134
|
+
|
|
135
|
+
## Technical Details
|
|
136
|
+
|
|
137
|
+
### How It Works
|
|
138
|
+
|
|
139
|
+
1. **Dense Vector Generation**: Your query is embedded using the configured embedding provider (Ollama, OpenAI, etc.)
|
|
140
|
+
2. **Sparse Vector Generation**: Query is tokenized and BM25 scores are calculated
|
|
141
|
+
3. **Parallel Search**: Both vectors are searched simultaneously
|
|
142
|
+
4. **Result Fusion**: RRF combines rankings from both searches
|
|
143
|
+
5. **Final Ranking**: Merged results with combined relevance scores
|
|
144
|
+
|
|
145
|
+
### BM25 Sparse Vectors
|
|
146
|
+
|
|
147
|
+
The server uses a lightweight BM25 implementation for sparse vectors:
|
|
148
|
+
|
|
149
|
+
- Tokenization: Lowercase + whitespace splitting
|
|
150
|
+
- IDF scoring: Inverse document frequency
|
|
151
|
+
- Configurable parameters: k1=1.2, b=0.75
|
|
152
|
+
|
|
153
|
+
### Reciprocal Rank Fusion (RRF)
|
|
154
|
+
|
|
155
|
+
RRF formula: `score = Σ(1 / (k + rank))` where k=60 (default)
|
|
156
|
+
|
|
157
|
+
Benefits:
|
|
158
|
+
|
|
159
|
+
- No score normalization needed
|
|
160
|
+
- Robust to differences in score scales
|
|
161
|
+
- Works well for combining different ranking methods
|
|
162
|
+
|
|
163
|
+
## Best Practices
|
|
164
|
+
|
|
165
|
+
1. **Enable hybrid for technical content**: Use when exact terms matter
|
|
166
|
+
2. **Use semantic for general content**: Natural language queries without technical terms
|
|
167
|
+
3. **Combine with filters**: Narrow down results by category or type
|
|
168
|
+
4. **Test both approaches**: Compare semantic vs hybrid for your use case
|
|
169
|
+
5. **Monitor performance**: Hybrid search requires more computation
|
|
170
|
+
|
|
171
|
+
## Performance Considerations
|
|
172
|
+
|
|
173
|
+
- **Storage**: Hybrid collections require more space (dense + sparse vectors)
|
|
174
|
+
- **Indexing**: Document indexing is slightly slower
|
|
175
|
+
- **Query time**: Hybrid search performs two searches and fusion
|
|
176
|
+
- **Scalability**: Qdrant optimizes both vector types efficiently
|
|
177
|
+
|
|
178
|
+
## Try It
|
|
179
|
+
|
|
180
|
+
Practice hybrid search with your own example:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
Create a collection named "my-hybrid-docs" with Cosine distance and enableHybrid set to true
|
|
184
|
+
|
|
185
|
+
Add these documents to my-hybrid-docs:
|
|
186
|
+
- id: "doc1", text: "The calculateTax function computes sales tax based on location and product type", metadata: {"category": "finance", "type": "function"}
|
|
187
|
+
- id: "doc2", text: "Tax calculation involves applying regional rates and exemptions", metadata: {"category": "finance", "type": "concept"}
|
|
188
|
+
- id: "doc3", text: "The generateInvoice method creates PDF invoices with itemized charges", metadata: {"category": "billing", "type": "function"}
|
|
189
|
+
- id: "doc4", text: "Invoice generation includes customer details, line items, and payment terms", metadata: {"category": "billing", "type": "concept"}
|
|
190
|
+
|
|
191
|
+
Search my-hybrid-docs for "calculateTax invoice" with limit 3 using hybrid_search
|
|
192
|
+
|
|
193
|
+
Search my-hybrid-docs for "tax calculation" with limit 2 using semantic_search
|
|
194
|
+
|
|
195
|
+
# Compare the results - notice how hybrid search finds both exact function names and concepts
|
|
196
|
+
|
|
197
|
+
Delete collection "my-hybrid-docs"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Troubleshooting
|
|
201
|
+
|
|
202
|
+
### "Collection does not have hybrid search enabled"
|
|
203
|
+
|
|
204
|
+
**Solution**: Create a new collection with `enableHybrid: true`. Existing collections cannot be converted.
|
|
205
|
+
|
|
206
|
+
### Poor results with hybrid search
|
|
207
|
+
|
|
208
|
+
**Try**:
|
|
209
|
+
|
|
210
|
+
1. Adjust query phrasing to include key terms
|
|
211
|
+
2. Use metadata filters to narrow scope
|
|
212
|
+
3. Increase `limit` to see more results
|
|
213
|
+
4. Compare with pure semantic search
|
|
214
|
+
|
|
215
|
+
### Slow query performance
|
|
216
|
+
|
|
217
|
+
**Solutions**:
|
|
218
|
+
|
|
219
|
+
1. Reduce prefetch limit (contact support for tuning)
|
|
220
|
+
2. Add filters to narrow search space
|
|
221
|
+
3. Use fewer documents or partition data
|
|
222
|
+
|
|
223
|
+
## Cleanup
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Delete collection "technical_docs"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Next Steps
|
|
230
|
+
|
|
231
|
+
Continue learning with these examples:
|
|
232
|
+
|
|
233
|
+
- **[Knowledge Base](../knowledge-base/)** - Searchable documentation with metadata
|
|
234
|
+
- **[Advanced Filtering](../filters/)** - Complex search queries
|
|
235
|
+
- **[Rate Limiting](../rate-limiting/)** - Batch processing patterns
|
|
236
|
+
- **[Basic Usage](../basic/)** - Fundamental operations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mhalder/qdrant-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "MCP server for semantic search using local Qdrant and Ollama (default) with support for OpenAI, Cohere, and Voyage AI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@qdrant/js-client-rest": "^1.12.0",
|
|
38
38
|
"bottleneck": "^2.19.5",
|
|
39
39
|
"cohere-ai": "^7.19.0",
|
|
40
|
+
"express": "^5.1.0",
|
|
40
41
|
"openai": "^4.77.3",
|
|
41
42
|
"zod": "^3.24.1"
|
|
42
43
|
},
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"@semantic-release/git": "^10.0.1",
|
|
48
49
|
"@semantic-release/github": "^11.0.6",
|
|
49
50
|
"@semantic-release/npm": "^12.0.2",
|
|
51
|
+
"@types/express": "^5.0.3",
|
|
50
52
|
"@types/node": "^22.10.5",
|
|
51
53
|
"@vitest/coverage-v8": "^2.1.8",
|
|
52
54
|
"@vitest/ui": "^2.1.8",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { BM25SparseVectorGenerator } from "./sparse.js";
|
|
3
|
+
|
|
4
|
+
describe("BM25SparseVectorGenerator", () => {
|
|
5
|
+
it("should generate sparse vectors for simple text", () => {
|
|
6
|
+
const generator = new BM25SparseVectorGenerator();
|
|
7
|
+
const result = generator.generate("hello world");
|
|
8
|
+
|
|
9
|
+
expect(result.indices).toBeDefined();
|
|
10
|
+
expect(result.values).toBeDefined();
|
|
11
|
+
expect(result.indices.length).toBeGreaterThan(0);
|
|
12
|
+
expect(result.values.length).toBe(result.indices.length);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should generate different vectors for different texts", () => {
|
|
16
|
+
const generator = new BM25SparseVectorGenerator();
|
|
17
|
+
const result1 = generator.generate("hello world");
|
|
18
|
+
const result2 = generator.generate("goodbye world");
|
|
19
|
+
|
|
20
|
+
// Different texts should have different sparse representations
|
|
21
|
+
expect(result1.indices).not.toEqual(result2.indices);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should generate consistent vectors for the same text", () => {
|
|
25
|
+
const generator = new BM25SparseVectorGenerator();
|
|
26
|
+
const result1 = generator.generate("hello world");
|
|
27
|
+
const result2 = generator.generate("hello world");
|
|
28
|
+
|
|
29
|
+
expect(result1.indices).toEqual(result2.indices);
|
|
30
|
+
expect(result1.values).toEqual(result2.values);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle empty strings", () => {
|
|
34
|
+
const generator = new BM25SparseVectorGenerator();
|
|
35
|
+
const result = generator.generate("");
|
|
36
|
+
|
|
37
|
+
expect(result.indices).toHaveLength(0);
|
|
38
|
+
expect(result.values).toHaveLength(0);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should handle special characters and punctuation", () => {
|
|
42
|
+
const generator = new BM25SparseVectorGenerator();
|
|
43
|
+
const result = generator.generate("hello, world! how are you?");
|
|
44
|
+
|
|
45
|
+
expect(result.indices).toBeDefined();
|
|
46
|
+
expect(result.values).toBeDefined();
|
|
47
|
+
expect(result.indices.length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should train on corpus and generate IDF scores", () => {
|
|
51
|
+
const generator = new BM25SparseVectorGenerator();
|
|
52
|
+
const corpus = ["the quick brown fox", "jumps over the lazy dog", "the fox is quick"];
|
|
53
|
+
|
|
54
|
+
generator.train(corpus);
|
|
55
|
+
const result = generator.generate("quick fox");
|
|
56
|
+
|
|
57
|
+
expect(result.indices).toBeDefined();
|
|
58
|
+
expect(result.values).toBeDefined();
|
|
59
|
+
expect(result.indices.length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should use static generateSimple method", () => {
|
|
63
|
+
const result = BM25SparseVectorGenerator.generateSimple("hello world");
|
|
64
|
+
|
|
65
|
+
expect(result.indices).toBeDefined();
|
|
66
|
+
expect(result.values).toBeDefined();
|
|
67
|
+
expect(result.indices.length).toBeGreaterThan(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should lowercase and tokenize text properly", () => {
|
|
71
|
+
const generator = new BM25SparseVectorGenerator();
|
|
72
|
+
const result1 = generator.generate("HELLO WORLD");
|
|
73
|
+
const result2 = generator.generate("hello world");
|
|
74
|
+
|
|
75
|
+
// Should produce same results due to lowercasing
|
|
76
|
+
expect(result1.indices).toEqual(result2.indices);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should generate positive values", () => {
|
|
80
|
+
const generator = new BM25SparseVectorGenerator();
|
|
81
|
+
const result = generator.generate("hello world");
|
|
82
|
+
|
|
83
|
+
result.values.forEach((value) => {
|
|
84
|
+
expect(value).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|