@@ -14,6 +14,7 @@ import { AGG_COUNT_MAX_TIME_MS_CAP, ONE_MB, CURSOR_LIMITS_TO_LLM_TEXT } from "..
1414import { zEJSON } from "../../args.js" ;
1515import { LogId } from "../../../common/logger.js" ;
1616import { zSupportedEmbeddingParameters } from "../../../common/search/embeddingsProvider.js" ;
17+ import { collectFieldsFromVectorSearchFilter } from "../../../helpers/collectFieldsFromVectorSearchFilter.js" ;
1718
1819const AnyStage = zEJSON ( ) ;
1920const VectorSearchStage = z . object ( {
@@ -97,6 +98,7 @@ export class AggregateTool extends MongoDBToolBase {
9798 try {
9899 const provider = await this . ensureConnected ( ) ;
99100 await this . assertOnlyUsesPermittedStages ( pipeline ) ;
101+ await this . assertVectorSearchFilterFieldsAreIndexed ( database , collection , pipeline ) ;
100102
101103 // Check if aggregate operation uses an index if enabled
102104 if ( this . config . indexCheck ) {
@@ -202,6 +204,74 @@ export class AggregateTool extends MongoDBToolBase {
202204 }
203205 }
204206
207+ private async assertVectorSearchFilterFieldsAreIndexed (
208+ database : string ,
209+ collection : string ,
210+ pipeline : Record < string , unknown > [ ]
211+ ) : Promise < void > {
212+ if ( ! ( await this . session . isSearchSupported ( ) ) ) {
213+ return ;
214+ }
215+
216+ const searchIndexesWithFilterFields = await this . searchIndexesWithFilterFields ( database , collection ) ;
217+ for ( const stage of pipeline ) {
218+ if ( "$vectorSearch" in stage ) {
219+ const { $vectorSearch : vectorSearchStage } = stage as z . infer < typeof VectorSearchStage > ;
220+ const allowedFilterFields = searchIndexesWithFilterFields [ vectorSearchStage . index ] ;
221+ if ( ! allowedFilterFields ) {
222+ this . session . logger . warning ( {
223+ id : LogId . toolValidationError ,
224+ context : "aggregate tool" ,
225+ message : `Could not assert if filter fields are indexed - No filter fields found for index ${ vectorSearchStage . index } ` ,
226+ } ) ;
227+ return ;
228+ }
229+
230+ const filterFieldsInStage = collectFieldsFromVectorSearchFilter ( vectorSearchStage . filter ) ;
231+ const filterFieldsNotIndexed = filterFieldsInStage . filter (
232+ ( field ) => ! allowedFilterFields . includes ( field )
233+ ) ;
234+ if ( filterFieldsNotIndexed . length ) {
235+ throw new MongoDBError (
236+ ErrorCodes . AtlasVectorSearchInvalidQuery ,
237+ `Vector search stage contains filter on fields are not indexed by index ${ vectorSearchStage . index } - ${ filterFieldsNotIndexed . join ( ", " ) } `
238+ ) ;
239+ }
240+ }
241+ }
242+ }
243+
244+ private async searchIndexesWithFilterFields (
245+ database : string ,
246+ collection : string
247+ ) : Promise < Record < string , string [ ] > > {
248+ const searchIndexes = ( await this . session . serviceProvider . getSearchIndexes ( database , collection ) ) as Array < {
249+ name : string ;
250+ latestDefinition : {
251+ fields : Array <
252+ | {
253+ type : "vector" ;
254+ }
255+ | {
256+ type : "filter" ;
257+ path : string ;
258+ }
259+ > ;
260+ } ;
261+ } > ;
262+
263+ return searchIndexes . reduce < Record < string , string [ ] > > ( ( indexFieldMap , searchIndex ) => {
264+ const filterFields = searchIndex . latestDefinition . fields
265+ . map < string | undefined > ( ( field ) => {
266+ return field . type === "filter" ? field . path : undefined ;
267+ } )
268+ . filter ( ( filterField ) => filterField !== undefined ) ;
269+
270+ indexFieldMap [ searchIndex . name ] = filterFields ;
271+ return indexFieldMap ;
272+ } , { } ) ;
273+ }
274+
205275 private async countAggregationResultDocuments ( {
206276 provider,
207277 database,
0 commit comments