Skip to content

Commit 833f3d2

Browse files
kmruizCopilot
andauthored
chore: check that a vector search index exists with indexCheck (#693)
Co-authored-by: Copilot <[email protected]>
1 parent 34c9c68 commit 833f3d2

File tree

3 files changed

+117
-4
lines changed

3 files changed

+117
-4
lines changed

src/common/search/vectorSearchEmbeddingsManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,25 @@ export class VectorSearchEmbeddingsManager {
4848
this.embeddings.delete(embeddingDefKey);
4949
}
5050

51+
async indexExists({
52+
database,
53+
collection,
54+
indexName,
55+
}: {
56+
database: string;
57+
collection: string;
58+
indexName: string;
59+
}): Promise<boolean> {
60+
const provider = await this.atlasSearchEnabledProvider();
61+
if (!provider) {
62+
return false;
63+
}
64+
65+
const searchIndexesWithName = await provider.getSearchIndexes(database, collection, indexName);
66+
67+
return searchIndexesWithName.length >= 1;
68+
}
69+
5170
async embeddingsForNamespace({
5271
database,
5372
collection,

src/tools/mongodb/read/aggregate.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,27 @@ export class AggregateTool extends MongoDBToolBase {
100100

101101
// Check if aggregate operation uses an index if enabled
102102
if (this.config.indexCheck) {
103-
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
104-
return provider
105-
.aggregate(database, collection, pipeline, {}, { writeConcern: undefined })
106-
.explain("queryPlanner");
103+
const [usesVectorSearchIndex, indexName] = await this.isVectorSearchIndexUsed({
104+
database,
105+
collection,
106+
pipeline,
107107
});
108+
switch (usesVectorSearchIndex) {
109+
case "not-vector-search-query":
110+
await checkIndexUsage(provider, database, collection, "aggregate", async () => {
111+
return provider
112+
.aggregate(database, collection, pipeline, {}, { writeConcern: undefined })
113+
.explain("queryPlanner");
114+
});
115+
break;
116+
case "non-existent-index":
117+
throw new MongoDBError(
118+
ErrorCodes.AtlasVectorSearchIndexNotFound,
119+
`Could not find an index with name "${indexName}" in namespace "${database}.${collection}".`
120+
);
121+
case "valid-index":
122+
// nothing to do, everything is correct so ready to run the query
123+
}
108124
}
109125

110126
pipeline = await this.replaceRawValuesWithEmbeddingsIfNecessary({
@@ -279,6 +295,41 @@ export class AggregateTool extends MongoDBToolBase {
279295
return pipeline;
280296
}
281297

298+
private async isVectorSearchIndexUsed({
299+
database,
300+
collection,
301+
pipeline,
302+
}: {
303+
database: string;
304+
collection: string;
305+
pipeline: Document[];
306+
}): Promise<["valid-index" | "non-existent-index" | "not-vector-search-query", string?]> {
307+
// check if the pipeline contains a $vectorSearch stage
308+
let usesVectorSearch = false;
309+
let indexName: string = "default";
310+
311+
for (const stage of pipeline) {
312+
if ("$vectorSearch" in stage) {
313+
const { $vectorSearch: vectorSearchStage } = stage as z.infer<typeof VectorSearchStage>;
314+
usesVectorSearch = true;
315+
indexName = vectorSearchStage.index;
316+
break;
317+
}
318+
}
319+
320+
if (!usesVectorSearch) {
321+
return ["not-vector-search-query"];
322+
}
323+
324+
const indexExists = await this.session.vectorSearchEmbeddingsManager.indexExists({
325+
database,
326+
collection,
327+
indexName,
328+
});
329+
330+
return [indexExists ? "valid-index" : "non-existent-index", indexName];
331+
}
332+
282333
private generateMessage({
283334
aggResultsCount,
284335
documents,

tests/integration/tools/mongodb/read/aggregate.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,47 @@ describeWithMongoDB(
405405
await integration.mongoClient().db(integration.randomDbName()).collection("databases").drop();
406406
});
407407

408+
it("should throw an exception when using an index that does not exist", async () => {
409+
await waitUntilSearchIsReady(integration.mongoClient());
410+
411+
const collection = integration.mongoClient().db(integration.randomDbName()).collection("databases");
412+
413+
await collection.insertOne({ name: "mongodb", description_embedding: [1, 2, 3, 4] });
414+
await integration.connectMcpClient();
415+
const response = await integration.mcpClient().callTool({
416+
name: "aggregate",
417+
arguments: {
418+
database: integration.randomDbName(),
419+
collection: "databases",
420+
pipeline: [
421+
{
422+
$vectorSearch: {
423+
index: "non_existing",
424+
path: "description_embedding",
425+
queryVector: "example",
426+
numCandidates: 10,
427+
limit: 10,
428+
embeddingParameters: {
429+
model: "voyage-3-large",
430+
outputDimension: 256,
431+
},
432+
},
433+
},
434+
{
435+
$project: {
436+
description_embedding: 0,
437+
},
438+
},
439+
],
440+
},
441+
});
442+
443+
const responseContent = getResponseContent(response);
444+
expect(responseContent).toContain(
445+
`Error running aggregate: Could not find an index with name "non_existing" in namespace "${integration.randomDbName()}.databases".`
446+
);
447+
});
448+
408449
for (const [dataType, embedding] of Object.entries(DOCUMENT_EMBEDDINGS)) {
409450
for (const similarity of ["euclidean", "cosine", "dotProduct"]) {
410451
describe.skipIf(!process.env.TEST_MDB_MCP_VOYAGE_API_KEY)(
@@ -417,6 +458,7 @@ describeWithMongoDB(
417458
.mongoClient()
418459
.db(integration.randomDbName())
419460
.collection("databases");
461+
420462
await collection.insertOne({ name: "mongodb", description_embedding: embedding });
421463

422464
await createVectorSearchIndexAndWait(
@@ -686,6 +728,7 @@ describeWithMongoDB(
686728
previewFeatures: ["vectorSearch"],
687729
maxDocumentsPerQuery: -1,
688730
maxBytesPerQuery: -1,
731+
indexCheck: true,
689732
}),
690733
downloadOptions: { search: true },
691734
}

0 commit comments

Comments
 (0)