@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.
- package/CHANGELOG.md +87 -69
- package/CONTRIBUTING.md +81 -92
- package/README.md +99 -634
- 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 +130 -30
- 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/examples/README.md +78 -253
- package/examples/basic/README.md +19 -72
- package/examples/filters/README.md +55 -155
- package/examples/hybrid-search/README.md +199 -0
- package/examples/knowledge-base/README.md +36 -98
- package/examples/rate-limiting/README.md +81 -290
- package/package.json +1 -1
- package/src/embeddings/sparse.test.ts +87 -0
- package/src/embeddings/sparse.ts +127 -0
- package/src/index.ts +161 -57
- package/src/qdrant/client.test.ts +544 -56
- package/src/qdrant/client.ts +162 -22
- package/vitest.config.ts +3 -3
- package/docs/test_report.md +0 -259
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 Sparse Vector Generator
|
|
3
|
+
*
|
|
4
|
+
* This module provides a simple BM25-like sparse vector generation for keyword search.
|
|
5
|
+
* For production use, consider using a proper BM25 implementation or Qdrant's built-in
|
|
6
|
+
* sparse vector generation via FastEmbed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { SparseVector } from "../qdrant/client.js";
|
|
10
|
+
|
|
11
|
+
interface TokenFrequency {
|
|
12
|
+
[token: string]: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class BM25SparseVectorGenerator {
|
|
16
|
+
private vocabulary: Map<string, number>;
|
|
17
|
+
private idfScores: Map<string, number>;
|
|
18
|
+
private documentCount: number;
|
|
19
|
+
private k1: number;
|
|
20
|
+
private b: number;
|
|
21
|
+
|
|
22
|
+
constructor(k1: number = 1.2, b: number = 0.75) {
|
|
23
|
+
this.vocabulary = new Map();
|
|
24
|
+
this.idfScores = new Map();
|
|
25
|
+
this.documentCount = 0;
|
|
26
|
+
this.k1 = k1;
|
|
27
|
+
this.b = b;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tokenize text into words (simple whitespace tokenization + lowercase)
|
|
32
|
+
*/
|
|
33
|
+
private tokenize(text: string): string[] {
|
|
34
|
+
return text
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^\w\s]/g, " ")
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.filter((token) => token.length > 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate term frequency for a document
|
|
43
|
+
*/
|
|
44
|
+
private getTermFrequency(tokens: string[]): TokenFrequency {
|
|
45
|
+
const tf: TokenFrequency = {};
|
|
46
|
+
for (const token of tokens) {
|
|
47
|
+
tf[token] = (tf[token] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
return tf;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build vocabulary from training documents (optional pre-training step)
|
|
54
|
+
* In a simple implementation, we can skip this and use on-the-fly vocabulary
|
|
55
|
+
*/
|
|
56
|
+
train(documents: string[]): void {
|
|
57
|
+
this.documentCount = documents.length;
|
|
58
|
+
const documentFrequency = new Map<string, number>();
|
|
59
|
+
|
|
60
|
+
// Calculate document frequency for each term
|
|
61
|
+
for (const doc of documents) {
|
|
62
|
+
const tokens = this.tokenize(doc);
|
|
63
|
+
const uniqueTokens = new Set(tokens);
|
|
64
|
+
|
|
65
|
+
for (const token of uniqueTokens) {
|
|
66
|
+
if (!this.vocabulary.has(token)) {
|
|
67
|
+
this.vocabulary.set(token, this.vocabulary.size);
|
|
68
|
+
}
|
|
69
|
+
documentFrequency.set(token, (documentFrequency.get(token) || 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Calculate IDF scores
|
|
74
|
+
for (const [token, df] of documentFrequency.entries()) {
|
|
75
|
+
const idf = Math.log((this.documentCount - df + 0.5) / (df + 0.5) + 1.0);
|
|
76
|
+
this.idfScores.set(token, idf);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate sparse vector for a query or document
|
|
82
|
+
* Returns indices and values for non-zero dimensions
|
|
83
|
+
*/
|
|
84
|
+
generate(text: string, avgDocLength: number = 50): SparseVector {
|
|
85
|
+
const tokens = this.tokenize(text);
|
|
86
|
+
const tf = this.getTermFrequency(tokens);
|
|
87
|
+
const docLength = tokens.length;
|
|
88
|
+
|
|
89
|
+
const indices: number[] = [];
|
|
90
|
+
const values: number[] = [];
|
|
91
|
+
|
|
92
|
+
// Calculate BM25 score for each term
|
|
93
|
+
for (const [token, freq] of Object.entries(tf)) {
|
|
94
|
+
// Ensure token is in vocabulary
|
|
95
|
+
if (!this.vocabulary.has(token)) {
|
|
96
|
+
// For unseen tokens, add them to vocabulary dynamically
|
|
97
|
+
this.vocabulary.set(token, this.vocabulary.size);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const index = this.vocabulary.get(token)!;
|
|
101
|
+
|
|
102
|
+
// Use a default IDF if not trained
|
|
103
|
+
const idf = this.idfScores.get(token) || 1.0;
|
|
104
|
+
|
|
105
|
+
// BM25 formula
|
|
106
|
+
const numerator = freq * (this.k1 + 1);
|
|
107
|
+
const denominator = freq + this.k1 * (1 - this.b + this.b * (docLength / avgDocLength));
|
|
108
|
+
const score = idf * (numerator / denominator);
|
|
109
|
+
|
|
110
|
+
if (score > 0) {
|
|
111
|
+
indices.push(index);
|
|
112
|
+
values.push(score);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { indices, values };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Simple static method for generating sparse vectors without training
|
|
121
|
+
* Useful for quick implementation
|
|
122
|
+
*/
|
|
123
|
+
static generateSimple(text: string): SparseVector {
|
|
124
|
+
const generator = new BM25SparseVectorGenerator();
|
|
125
|
+
return generator.generate(text);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
8
|
import {
|
|
@@ -8,24 +11,18 @@ import {
|
|
|
8
11
|
ListToolsRequestSchema,
|
|
9
12
|
ReadResourceRequestSchema,
|
|
10
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
-
import { QdrantManager } from "./qdrant/client.js";
|
|
12
|
-
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
13
14
|
import { z } from "zod";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
16
|
+
import { BM25SparseVectorGenerator } from "./embeddings/sparse.js";
|
|
17
|
+
import { QdrantManager } from "./qdrant/client.js";
|
|
17
18
|
|
|
18
19
|
// Read package.json for version
|
|
19
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const pkg = JSON.parse(
|
|
21
|
-
readFileSync(join(__dirname, "../package.json"), "utf-8"),
|
|
22
|
-
);
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
23
22
|
|
|
24
23
|
// Validate environment variables
|
|
25
24
|
const QDRANT_URL = process.env.QDRANT_URL || "http://localhost:6333";
|
|
26
|
-
const EMBEDDING_PROVIDER = (
|
|
27
|
-
process.env.EMBEDDING_PROVIDER || "ollama"
|
|
28
|
-
).toLowerCase();
|
|
25
|
+
const EMBEDDING_PROVIDER = (process.env.EMBEDDING_PROVIDER || "ollama").toLowerCase();
|
|
29
26
|
|
|
30
27
|
// Check for required API keys based on provider
|
|
31
28
|
if (EMBEDDING_PROVIDER !== "ollama") {
|
|
@@ -47,15 +44,13 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
47
44
|
break;
|
|
48
45
|
default:
|
|
49
46
|
console.error(
|
|
50
|
-
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama
|
|
47
|
+
`Error: Unknown embedding provider "${EMBEDDING_PROVIDER}". Supported providers: openai, cohere, voyage, ollama.`
|
|
51
48
|
);
|
|
52
49
|
process.exit(1);
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
if (!apiKey) {
|
|
56
|
-
console.error(
|
|
57
|
-
`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`,
|
|
58
|
-
);
|
|
53
|
+
console.error(`Error: ${requiredKeyName} is required for ${EMBEDDING_PROVIDER} provider.`);
|
|
59
54
|
process.exit(1);
|
|
60
55
|
}
|
|
61
56
|
}
|
|
@@ -64,8 +59,7 @@ if (EMBEDDING_PROVIDER !== "ollama") {
|
|
|
64
59
|
async function checkOllamaAvailability() {
|
|
65
60
|
if (EMBEDDING_PROVIDER === "ollama") {
|
|
66
61
|
const baseUrl = process.env.EMBEDDING_BASE_URL || "http://localhost:11434";
|
|
67
|
-
const isLocalhost =
|
|
68
|
-
baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
|
|
62
|
+
const isLocalhost = baseUrl.includes("localhost") || baseUrl.includes("127.0.0.1");
|
|
69
63
|
|
|
70
64
|
try {
|
|
71
65
|
const response = await fetch(`${baseUrl}/api/version`);
|
|
@@ -78,7 +72,7 @@ async function checkOllamaAvailability() {
|
|
|
78
72
|
const { models } = await tagsResponse.json();
|
|
79
73
|
const modelName = process.env.EMBEDDING_MODEL || "nomic-embed-text";
|
|
80
74
|
const modelExists = models.some(
|
|
81
|
-
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
|
|
75
|
+
(m: any) => m.name === modelName || m.name.startsWith(`${modelName}:`)
|
|
82
76
|
);
|
|
83
77
|
|
|
84
78
|
if (!modelExists) {
|
|
@@ -141,7 +135,7 @@ const server = new Server(
|
|
|
141
135
|
tools: {},
|
|
142
136
|
resources: {},
|
|
143
137
|
},
|
|
144
|
-
}
|
|
138
|
+
}
|
|
145
139
|
);
|
|
146
140
|
|
|
147
141
|
// Tool schemas
|
|
@@ -151,6 +145,10 @@ const CreateCollectionSchema = z.object({
|
|
|
151
145
|
.enum(["Cosine", "Euclid", "Dot"])
|
|
152
146
|
.optional()
|
|
153
147
|
.describe("Distance metric (default: Cosine)"),
|
|
148
|
+
enableHybrid: z
|
|
149
|
+
.boolean()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Enable hybrid search with sparse vectors (default: false)"),
|
|
154
152
|
});
|
|
155
153
|
|
|
156
154
|
const AddDocumentsSchema = z.object({
|
|
@@ -158,15 +156,13 @@ const AddDocumentsSchema = z.object({
|
|
|
158
156
|
documents: z
|
|
159
157
|
.array(
|
|
160
158
|
z.object({
|
|
161
|
-
id: z
|
|
162
|
-
.union([z.string(), z.number()])
|
|
163
|
-
.describe("Unique identifier for the document"),
|
|
159
|
+
id: z.union([z.string(), z.number()]).describe("Unique identifier for the document"),
|
|
164
160
|
text: z.string().describe("Text content to embed and store"),
|
|
165
161
|
metadata: z
|
|
166
162
|
.record(z.any())
|
|
167
163
|
.optional()
|
|
168
164
|
.describe("Optional metadata to store with the document"),
|
|
169
|
-
})
|
|
165
|
+
})
|
|
170
166
|
)
|
|
171
167
|
.describe("Array of documents to add"),
|
|
172
168
|
});
|
|
@@ -174,10 +170,7 @@ const AddDocumentsSchema = z.object({
|
|
|
174
170
|
const SemanticSearchSchema = z.object({
|
|
175
171
|
collection: z.string().describe("Name of the collection to search"),
|
|
176
172
|
query: z.string().describe("Search query text"),
|
|
177
|
-
limit: z
|
|
178
|
-
.number()
|
|
179
|
-
.optional()
|
|
180
|
-
.describe("Maximum number of results (default: 5)"),
|
|
173
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
181
174
|
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
182
175
|
});
|
|
183
176
|
|
|
@@ -191,9 +184,14 @@ const GetCollectionInfoSchema = z.object({
|
|
|
191
184
|
|
|
192
185
|
const DeleteDocumentsSchema = z.object({
|
|
193
186
|
collection: z.string().describe("Name of the collection"),
|
|
194
|
-
ids: z
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
ids: z.array(z.union([z.string(), z.number()])).describe("Array of document IDs to delete"),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const HybridSearchSchema = z.object({
|
|
191
|
+
collection: z.string().describe("Name of the collection to search"),
|
|
192
|
+
query: z.string().describe("Search query text"),
|
|
193
|
+
limit: z.number().optional().describe("Maximum number of results (default: 5)"),
|
|
194
|
+
filter: z.record(z.any()).optional().describe("Optional metadata filter"),
|
|
197
195
|
});
|
|
198
196
|
|
|
199
197
|
// List available tools
|
|
@@ -203,7 +201,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
203
201
|
{
|
|
204
202
|
name: "create_collection",
|
|
205
203
|
description:
|
|
206
|
-
"Create a new vector collection in Qdrant. The collection will be configured with the embedding provider's dimensions automatically.",
|
|
204
|
+
"Create a new vector collection in Qdrant. The collection will be configured with the embedding provider's dimensions automatically. Set enableHybrid to true to enable hybrid search combining semantic and keyword search.",
|
|
207
205
|
inputSchema: {
|
|
208
206
|
type: "object",
|
|
209
207
|
properties: {
|
|
@@ -216,6 +214,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
216
214
|
enum: ["Cosine", "Euclid", "Dot"],
|
|
217
215
|
description: "Distance metric (default: Cosine)",
|
|
218
216
|
},
|
|
217
|
+
enableHybrid: {
|
|
218
|
+
type: "boolean",
|
|
219
|
+
description: "Enable hybrid search with sparse vectors (default: false)",
|
|
220
|
+
},
|
|
219
221
|
},
|
|
220
222
|
required: ["name"],
|
|
221
223
|
},
|
|
@@ -323,8 +325,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
323
325
|
},
|
|
324
326
|
{
|
|
325
327
|
name: "delete_documents",
|
|
326
|
-
description:
|
|
327
|
-
"Delete specific documents from a collection by their IDs.",
|
|
328
|
+
description: "Delete specific documents from a collection by their IDs.",
|
|
328
329
|
inputSchema: {
|
|
329
330
|
type: "object",
|
|
330
331
|
properties: {
|
|
@@ -343,6 +344,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
343
344
|
required: ["collection", "ids"],
|
|
344
345
|
},
|
|
345
346
|
},
|
|
347
|
+
{
|
|
348
|
+
name: "hybrid_search",
|
|
349
|
+
description:
|
|
350
|
+
"Perform hybrid search combining semantic vector search with keyword search using BM25. This provides better results by combining the strengths of both approaches. The collection must be created with enableHybrid set to true.",
|
|
351
|
+
inputSchema: {
|
|
352
|
+
type: "object",
|
|
353
|
+
properties: {
|
|
354
|
+
collection: {
|
|
355
|
+
type: "string",
|
|
356
|
+
description: "Name of the collection to search",
|
|
357
|
+
},
|
|
358
|
+
query: {
|
|
359
|
+
type: "string",
|
|
360
|
+
description: "Search query text",
|
|
361
|
+
},
|
|
362
|
+
limit: {
|
|
363
|
+
type: "number",
|
|
364
|
+
description: "Maximum number of results (default: 5)",
|
|
365
|
+
},
|
|
366
|
+
filter: {
|
|
367
|
+
type: "object",
|
|
368
|
+
description: "Optional metadata filter",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
required: ["collection", "query"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
346
374
|
],
|
|
347
375
|
};
|
|
348
376
|
});
|
|
@@ -354,14 +382,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
354
382
|
try {
|
|
355
383
|
switch (name) {
|
|
356
384
|
case "create_collection": {
|
|
357
|
-
const { name, distance } = CreateCollectionSchema.parse(args);
|
|
385
|
+
const { name, distance, enableHybrid } = CreateCollectionSchema.parse(args);
|
|
358
386
|
const vectorSize = embeddings.getDimensions();
|
|
359
|
-
await qdrant.createCollection(name, vectorSize, distance);
|
|
387
|
+
await qdrant.createCollection(name, vectorSize, distance, enableHybrid || false);
|
|
388
|
+
|
|
389
|
+
let message = `Collection "${name}" created successfully with ${vectorSize} dimensions and ${distance || "Cosine"} distance metric.`;
|
|
390
|
+
if (enableHybrid) {
|
|
391
|
+
message += " Hybrid search is enabled for this collection.";
|
|
392
|
+
}
|
|
393
|
+
|
|
360
394
|
return {
|
|
361
395
|
content: [
|
|
362
396
|
{
|
|
363
397
|
type: "text",
|
|
364
|
-
text:
|
|
398
|
+
text: message,
|
|
365
399
|
},
|
|
366
400
|
],
|
|
367
401
|
};
|
|
@@ -370,7 +404,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
370
404
|
case "add_documents": {
|
|
371
405
|
const { collection, documents } = AddDocumentsSchema.parse(args);
|
|
372
406
|
|
|
373
|
-
// Check if collection exists
|
|
407
|
+
// Check if collection exists and get info
|
|
374
408
|
const exists = await qdrant.collectionExists(collection);
|
|
375
409
|
if (!exists) {
|
|
376
410
|
return {
|
|
@@ -384,21 +418,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
384
418
|
};
|
|
385
419
|
}
|
|
386
420
|
|
|
421
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
422
|
+
|
|
387
423
|
// Generate embeddings for all documents
|
|
388
424
|
const texts = documents.map((doc) => doc.text);
|
|
389
425
|
const embeddingResults = await embeddings.embedBatch(texts);
|
|
390
426
|
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
427
|
+
// If hybrid search is enabled, generate sparse vectors and use appropriate method
|
|
428
|
+
if (collectionInfo.hybridEnabled) {
|
|
429
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
430
|
+
|
|
431
|
+
// Prepare points with both dense and sparse vectors
|
|
432
|
+
const points = documents.map((doc, index) => ({
|
|
433
|
+
id: doc.id,
|
|
434
|
+
vector: embeddingResults[index].embedding,
|
|
435
|
+
sparseVector: sparseGenerator.generate(doc.text),
|
|
436
|
+
payload: {
|
|
437
|
+
text: doc.text,
|
|
438
|
+
...doc.metadata,
|
|
439
|
+
},
|
|
440
|
+
}));
|
|
400
441
|
|
|
401
|
-
|
|
442
|
+
await qdrant.addPointsWithSparse(collection, points);
|
|
443
|
+
} else {
|
|
444
|
+
// Standard dense-only vectors
|
|
445
|
+
const points = documents.map((doc, index) => ({
|
|
446
|
+
id: doc.id,
|
|
447
|
+
vector: embeddingResults[index].embedding,
|
|
448
|
+
payload: {
|
|
449
|
+
text: doc.text,
|
|
450
|
+
...doc.metadata,
|
|
451
|
+
},
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
await qdrant.addPoints(collection, points);
|
|
455
|
+
}
|
|
402
456
|
|
|
403
457
|
return {
|
|
404
458
|
content: [
|
|
@@ -411,8 +465,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
411
465
|
}
|
|
412
466
|
|
|
413
467
|
case "semantic_search": {
|
|
414
|
-
const { collection, query, limit, filter } =
|
|
415
|
-
SemanticSearchSchema.parse(args);
|
|
468
|
+
const { collection, query, limit, filter } = SemanticSearchSchema.parse(args);
|
|
416
469
|
|
|
417
470
|
// Check if collection exists
|
|
418
471
|
const exists = await qdrant.collectionExists(collection);
|
|
@@ -432,12 +485,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
432
485
|
const { embedding } = await embeddings.embed(query);
|
|
433
486
|
|
|
434
487
|
// Search
|
|
435
|
-
const results = await qdrant.search(
|
|
436
|
-
collection,
|
|
437
|
-
embedding,
|
|
438
|
-
limit || 5,
|
|
439
|
-
filter,
|
|
440
|
-
);
|
|
488
|
+
const results = await qdrant.search(collection, embedding, limit || 5, filter);
|
|
441
489
|
|
|
442
490
|
return {
|
|
443
491
|
content: [
|
|
@@ -500,6 +548,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
500
548
|
};
|
|
501
549
|
}
|
|
502
550
|
|
|
551
|
+
case "hybrid_search": {
|
|
552
|
+
const { collection, query, limit, filter } = HybridSearchSchema.parse(args);
|
|
553
|
+
|
|
554
|
+
// Check if collection exists
|
|
555
|
+
const exists = await qdrant.collectionExists(collection);
|
|
556
|
+
if (!exists) {
|
|
557
|
+
return {
|
|
558
|
+
content: [
|
|
559
|
+
{
|
|
560
|
+
type: "text",
|
|
561
|
+
text: `Error: Collection "${collection}" does not exist.`,
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
isError: true,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Check if collection has hybrid search enabled
|
|
569
|
+
const collectionInfo = await qdrant.getCollectionInfo(collection);
|
|
570
|
+
if (!collectionInfo.hybridEnabled) {
|
|
571
|
+
return {
|
|
572
|
+
content: [
|
|
573
|
+
{
|
|
574
|
+
type: "text",
|
|
575
|
+
text: `Error: Collection "${collection}" does not have hybrid search enabled. Create a new collection with enableHybrid set to true.`,
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
isError: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Generate dense embedding for query
|
|
583
|
+
const { embedding } = await embeddings.embed(query);
|
|
584
|
+
|
|
585
|
+
// Generate sparse vector for query
|
|
586
|
+
const sparseGenerator = new BM25SparseVectorGenerator();
|
|
587
|
+
const sparseVector = sparseGenerator.generate(query);
|
|
588
|
+
|
|
589
|
+
// Perform hybrid search
|
|
590
|
+
const results = await qdrant.hybridSearch(
|
|
591
|
+
collection,
|
|
592
|
+
embedding,
|
|
593
|
+
sparseVector,
|
|
594
|
+
limit || 5,
|
|
595
|
+
filter
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: JSON.stringify(results, null, 2),
|
|
603
|
+
},
|
|
604
|
+
],
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
503
608
|
default:
|
|
504
609
|
return {
|
|
505
610
|
content: [
|
|
@@ -513,8 +618,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
513
618
|
}
|
|
514
619
|
} catch (error: any) {
|
|
515
620
|
// Enhanced error details for debugging
|
|
516
|
-
const errorDetails =
|
|
517
|
-
error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
621
|
+
const errorDetails = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
|
|
518
622
|
|
|
519
623
|
console.error("Tool execution error:", {
|
|
520
624
|
tool: name,
|