Mongoose Atlas Search 完整实战指南:从 Schema 搜索索引到 $search、向量搜索与混合检索
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
Mongoose 对 MongoDB Atlas Search 提供了端到端的官方支持:既可以在 Schema 定义阶段声明 Atlas Search 索引结构,也可以通过Model静态方法完成索引的创建、查看、更新与删除,还能在聚合管道中直接使用$search、$vectorSearch与$rankFusion完成文本检索、语义检索和混合检索。读完本文,你将掌握如何在 Mongoose 应用中声明与运维 Atlas Search 索引、编写相关性排序的文本查询、接入向量语义搜索,并遵循一套面向生产环境的索引与查询最佳实践。
本文以 docs/atlas-search.md 为骨架,结合仓库源码(lib/schema.js、lib/model.js、lib/aggregate.js)与测试用例(test/model.test.js)展开讲解,帮助你在理解 API 用法的同时,看清其底层实现链路。
概述:Mongoose 中的 Atlas Search 能力
Atlas Search 是基于 Apache Lucene 的全文检索能力,运行在 MongoDB Atlas 集群上,允许你以细粒度的文本索引方式为数据建立检索能力,并构建快速、基于相关性的搜索体验。
Mongoose 对 Atlas Search 的支持分为两大块:
- 索引管理:通过
schema.searchIndex()在 Schema 中声明搜索索引,用Model.createSearchIndexes()等静态方法落地索引; - 查询:通过聚合管道的
$search阶段执行文本搜索,并通过$vectorSearch与$rankFusion支持语义检索与混合检索。
从源码看,Mongoose 将搜索索引声明保存在 Schema 内部数组_searchIndexes中(见 lib/schema.js#L125),并在Schema.prototype.searchIndex()中追加声明后返回 Schema 实例以支持链式调用(见 lib/schema.js#L1196-L1200)。所有索引管理方法最终都委托给底层 node-mongodb-native driver 的Collection对应方法执行,Mongoose 层负责封装与上下文校验。
创建搜索索引
在 Schema 中声明搜索索引
使用schema.searchIndex()将 Atlas Search 索引定义直接写入 Schema,实现"索引即代码":
const movieSchema = new mongoose.Schema({ title: String, fullplot: String, genres: [String], cast: [String], year: Number }); // 定义基础的文本搜索索引 movieSchema.searchIndex({ name: 'movie_search', definition: { mappings: { dynamic: false, fields: { title: { type: 'string' }, fullplot: { type: 'string' }, cast: { type: 'string' }, year: { type: 'number' } } } } }); const Movie = mongoose.model('Movie', movieSchema); await Movie.createSearchIndexes(); // 创建索引searchIndex()接收一个描述对象,包含两个关键字段:
name:索引名称,后续查询、更新与删除时都通过该名称引用;definition:Atlas Search 索引定义,遵循 Atlas Search 索引规范,其中mappings描述字段到索引类型的映射关系。
在mappings中,dynamic: false表示只索引显式声明的字段;dynamic: true则自动索引所有受支持的字段。文档明确指出,dynamic: true不推荐用于生产环境,因为它会带来不必要的存储开销。
让模型初始化时自动创建索引
若希望模型初始化时自动创建 Schema 中声明的搜索索引,可以开启autoSearchIndex选项。该选项的取值链路在源码中清晰可见:Model.init()内部的_createSearchIndexes通过utils.getOption('autoSearchIndex', ...)依次从 Schema 选项、连接配置与 Mongoose 全局选项中解析(见 lib/model.js#L1142-L1154):
const _createSearchIndexes = async () => { const autoSearchIndex = utils.getOption( 'autoSearchIndex', this.schema.options, conn.config, conn.base.options ); if (!autoSearchIndex) { return; } return await this.createSearchIndexes(); };Model.init()会按顺序执行createCollection()→ensureIndexes()→createSearchIndexes()(见 lib/model.js#L1181-L1183),因此开启后无需显式调用createSearchIndexes()。
注意:所有 Atlas Search 索引 API 仅在连接 MongoDB Atlas 集群时可用。
选择文本分析器(Analyzer)
Atlas Search 基于 Apache Lucene 的分析器完成文本的切词(tokenization)、过滤与索引。通过analyzer选项可以精确控制每个字段的索引方式。常用的分析器包括:
lucene.standard:通用文本分析,按空白与标点切词;lucene.english:英语语言分析,带词干提取(stemming);lucene.keyword:将整个字段值视为单个 token,适合精确匹配。
完整的分析器列表与配置方式见 MongoDB 官方 Atlas Search Analyzers 文档。下面为不同字段配置差异化分析器:
movieSchema.searchIndex({ name: 'movie_search', definition: { mappings: { dynamic: false, fields: { title: { type: 'string', analyzer: 'lucene.standard' // 按空白/标点切词 }, fullplot: { type: 'string', analyzer: 'lucene.english' // 英语分析 + 词干提取 }, genres: { type: 'string', analyzer: 'lucene.keyword' // 整值匹配(不切词) }, cast: { type: 'string', analyzer: 'lucene.standard' }, year: { type: 'number' } } } } });分析器的选择直接影响召回质量:例如对fullplot这类长文本使用lucene.english可获得词形归一化(复数、时态归一)能力;对genres这类离散枚举值使用lucene.keyword可避免被拆成多个 token 导致误匹配。
管理搜索索引
Mongoose 在Model上提供了 5 个搜索索引管理方法,全部标注为"仅对 Atlas 集群有效"。它们统一先调用_checkContext()做上下文校验(防止new Model.xxx()误用,见 lib/model.js#L1062),随后委托给驱动层Collection的同名方法。
创建索引
// 创建 Schema 中声明的全部搜索索引 await Movie.createSearchIndexes(); // 程序化创建一个单独的索引 await Movie.createSearchIndex({ name: 'my_index', definition: { mappings: { dynamic: true } } });Model.createSearchIndexes()的实现会遍历this.schema._searchIndexes数组,逐个调用createSearchIndex()并收集结果(见 lib/model.js#L1854-L1861):
Model.createSearchIndexes = async function createSearchIndexes() { _checkContext(this, 'createSearchIndexes'); const results = []; for (const searchIndex of this.schema._searchIndexes) { results.push(await this.createSearchIndex(searchIndex)); } return results; };createSearchIndex则直接透传到底层集合(见 lib/model.js#L1375-L1379):
Model.createSearchIndex = async function createSearchIndex(description) { _checkContext(this, 'createSearchIndex'); return await this.$__collection.createSearchIndex(description); };仓库测试 test/model.test.js#L9805-L9830(issue gh-15465)验证了"为 Schema 中每个搜索索引各创建一个索引"的行为:定义name与description两个字符串字段的索引后,createSearchIndexes()返回['test'],随后listSearchIndexes()能查询到该索引。
列出索引
const indexes = await Movie.listSearchIndexes(); for (const index of indexes) { console.log(`${index.name}: ${index.status}`); }listSearchIndexes()先从驱动层拿到游标,再转换为数组返回(见 lib/model.js#L1441-L1447)。返回的每个索引对象包含id、name、status、queryable以及latestDefinition等字段。其中queryable表示索引是否已可用于查询——创建后索引需要异步构建,测试用例中正是通过轮询listSearchIndexes()直到queryable === true再执行查询(见 test/model.test.js#L9897-L9903)。
更新索引
await Movie.updateSearchIndex('movie_search', { mappings: { dynamic: false, fields: { title: { type: 'string' }, fullplot: { type: 'string' }, cast: { type: 'string' }, year: { type: 'number' } } } });updateSearchIndex(name, definition)接收索引名与新的definition,同样委托给底层集合方法(见 lib/model.js#L1397-L1401)。更新会触发 Atlas 重建索引,期间可能出现不可查询的窗口期,建议在低峰期执行。
删除索引
await Movie.dropSearchIndex('old_index');按名称删除索引(见 lib/model.js#L1418-L1422)。删除后该名称对应的搜索能力立即失效,请确认没有正在运行的查询依赖它。
文本搜索查询:$search 聚合阶段
索引就绪后,即可在聚合管道中以$search作为第一个阶段执行文本搜索。Mongoose 的聚合构建器还提供了链式辅助方法Aggregate.prototype.search(options),其实现就是this.append({ $search: options })(见 lib/aggregate.js#L1012-L1014),因此以下两种写法等价:
// 写法一:管道数组 const results = await Movie.aggregate([ { $search: { index: 'movie_search', text: { query: 'eternal sunshine', path: 'title' } } }, { $limit: 10 } ]); // 写法二:链式调用(等价于追加 $search 阶段) const results = await Movie.aggregate(). search({ text: { query: 'eternal sunshine', path: 'title' } }). limit(10);基础文本搜索的完整示例:
// 示例:展示不同的文本搜索选项 const results = await Movie.aggregate([ { $search: { index: 'movie_search', text: { query: 'eternal sunshine', path: 'title' // 单字段搜索 // path: ['title', 'fullplot', 'genres'] // 多字段:跨多个字段搜索 // fuzzy: { maxEdits: 2 } // 模糊匹配:容忍最多 2 处字符差异 } } }, { $limit: 10 } ]);几个关键参数说明:
index:指定使用哪个搜索索引(对应searchIndex()声明时的name);path:搜索路径,可以是单个字段名,也可以是字段名数组实现跨字段检索;fuzzy:启用拼写容错,maxEdits表示允许的最大编辑距离(通常取 1 或 2);$limit:尽早限制返回文档数,减少后续管道阶段的处理量。
复合查询:must / should / filter 与相关性评分
当需要组合多个检索条件时,使用compound操作符。它支持三类子句:
must:文档必须满足的条件;should:满足则加分(提升相关性),不满足不排除;filter:过滤条件,只影响是否命中,不参与评分。
// 查找标题包含 'mission'、2000 年后上映的电影,按相关性排序, // 主演含 Tom Cruise 的电影获得评分加成。 // Top 3 结果应为:Mission: Impossible II、Mission: Impossible - Ghost Protocol、 // Mission: Impossible III const results = await Movie.aggregate([ { $search: { index: 'movie_search', compound: { must: [ { text: { query: 'mission', path: 'title' } } ], should: [ { text: { query: 'tom cruise', path: 'cast', score: { boost: { value: 5 } }, // 主演 Tom Cruise 的电影评分翻倍(加权) matchCriteria: 'all' // 仅当所有词都匹配时才加分 } } ], filter: [ { range: { path: 'year', gte: 2000 // 仅包含 2000 年及之后上映的电影 } } ] } } }, { $project: { title: 1, cast: 1, fullplot: 1, score: { $meta: 'searchScore' } // 在结果中包含相关性评分 } }, { $match: { score: { $gte: 0.5 } // 从一个较低的阈值开始,根据实际数据调整 } } ]);要点:
- 用
$meta: 'searchScore'将 Atlas Search 相关性评分投影到score字段,供后续$sort或$match使用; score.boost.value控制加权倍数,matchCriteria: 'all'要求子句中的所有词都命中才应用加权;- Atlas Search 的评分是相对数据集的,不同索引、不同数据分布下分数绝对值差异很大,因此
$match阈值应从低值起步,观察真实分数分布后再收紧。
向量搜索:$vectorSearch
对于基于向量嵌入(embedding)的语义搜索,使用$vectorSearch聚合阶段。完整的生成嵌入与向量索引配置示例参见仓库内的向量搜索指南 docs/atlas-vector-search.md。
在 Schema 中声明向量搜索索引时,需要在searchIndex()描述对象中指定type: 'vectorSearch',并在definition.fields中描述向量字段。仓库测试 test/model.test.js#L9866-L9916 给出了一个可直接对照的完整流程:
const schema = new mongoose.Schema({ name: String, myVector: [Number] }); schema.searchIndex({ name: 'vector_index', type: 'vectorSearch', definition: { fields: [ { type: 'vector', numDimensions: 2, // 向量维度,必须与嵌入模型输出维度一致 path: 'myVector', // 存放向量数据的字段路径 similarity: 'dotProduct', // 相似度度量:dotProduct | cosine | euclidean quantization: 'scalar' // 量化方式,可选 'none' 或 'scalar' } ] } }); const TestModel = db.model('Test', schema); await TestModel.init(); const results = await TestModel.createSearchIndexes(); // results === ['vector_index']索引构建完成后,用$vectorSearch查询最相似的文档:
const [doc] = await TestModel.aggregate([ { $vectorSearch: { index: 'vector_index', path: 'myVector', queryVector: [0, 100], // 查询向量,通常来自同一嵌入模型 numCandidates: 10, // 候选集大小,越大越精确但越慢 limit: 1 // 返回条数 } } ]); // 测试断言:doc.name === 'Test1'(因为 [0, 100] 与 [0, 99] 的点积最大)该测试还演示了生产环境的关键一环:向量索引创建后并非立即可查询,需要轮询listSearchIndexes()直到queryable === true,再执行$vectorSearch(见 test/model.test.js#L9897-L9903)。
混合搜索:$rankFusion 融合文本与向量结果
混合搜索同时利用关键词相关性与语义相似度。使用$rankFusion将$vectorSearch与$search作为两个独立的子管道并行执行,再通过**倒数排名融合(Reciprocal Rank Fusion,RRF)**合并排序结果。
需要特别注意:$search必须是其子管道中的第一个阶段,正因如此,它不能紧跟在$vectorSearch之后出现在同一管道中——这正是$rankFusion存在的意义。
下面的示例使用generateEmbedding()函数生成查询向量(函数定义参见 docs/atlas-vector-search.md 中关于第三方嵌入模型的章节):
// 生成查询向量(详见向量搜索指南) const queryEmbedding = await generateEmbedding('charming animals with adventurous tone'); const results = await Movie.aggregate([ { $rankFusion: { input: { pipelines: { // 语义搜索子管道 vector: [ { $vectorSearch: { index: 'vector_index', // 向量搜索索引名 path: 'plot_embedding_voyage_3_large', // 存放向量的字段 queryVector: queryEmbedding, numCandidates: 100, limit: 50 } } ], // 关键词搜索子管道 text: [ { $search: { index: 'movie_search', text: { query: 'adventure animals', path: 'fullplot' } } }, { $limit: 50 } ] } }, combination: { weights: { vector: 0.7, // 语义相关性权重 70% text: 0.3 // 关键词相关性权重 30% } } } }, { $limit: 10 } ]);combination.weights用于调节各子管道在最终排名中的占比;RRF 融合后,两个子管道各自的排序位置共同决定最终顺序。混合搜索适合"既要求关键词精确命中、又希望语义相关文档获得曝光"的场景,例如电商搜索、内容平台推荐等。
最佳实践
索引管理
- 开发环境开启
autoSearchIndex: true:随 Schema 自动创建搜索索引,减少开发期手工操作; - 生产环境手动管理索引:通过
Model.createSearchIndexes()、Atlas 控制台、MongoDB CLI 或部署脚本管理,避免应用发布时意外变更线上索引; - 监控索引状态:创建后始终用
listSearchIndexes()确认索引已就绪(queryable: true)再放量查询。
Schema 设计
// 推荐:在 Schema 中声明索引,纳入版本控制 movieSchema.searchIndex({ name: 'movie_search', definition: { mappings: { dynamic: false, fields: { /* ... */ } } } }); // 同样推荐:生产环境将索引管理独立成脚本 const createProductionIndexes = async () => { await Article.createSearchIndex({ /* definition */ }); };查询优化
- 尽早使用
$limit:减少后续管道阶段的文档处理量; $search必须是管道第一阶段:在$search之前使用$match会直接报错。需要先过滤文档时,改用compound操作符内的filter子句;- 只投影必要字段:用
$project缩小返回数据体积; - 索引正确的字段:生产环境避免
dynamic: true——动态映射会索引所有字段,应使用静态映射只索引需要被搜索的字段; - 可以利用 MongoDB 官方的 Agent Skills 包辅助优化查询语句。
在 Mongoose 之外管理索引
生产部署中,索引也可以完全在 Mongoose 之外管理:
- Atlas UI:通过 MongoDB Atlas Web 界面创建与管理索引;
- MongoDB Compass:图形化索引管理工具(MongoDB 7.0+);
- MongoDB CLI / mongosh:脚本化执行索引操作;
- Atlas Admin API:通过 API 以编程方式管理索引。
若选择外部管理,务必在生产环境关闭autoSearchIndex,防止应用部署期间自动触发索引变更。
总结与延伸阅读
Mongoose 将 Atlas Search 的索引生命周期管理与查询能力完整收编进其 Schema / Model / Aggregate 三层 API:schema.searchIndex()负责声明、Model静态方法负责运维、聚合管道负责查询,三者配合即可在 Mongoose 应用中落地一套从文本检索、语义检索到混合检索的完整搜索方案。
仓库内可继续深入阅读的相关内容:
- 向量搜索专题指南:docs/atlas-vector-search.md
- Schema 层
searchIndex()实现:lib/schema.js#L1196-L1200 - Model 层索引管理方法实现:lib/model.js#L1375-L1447
Model.createSearchIndexes()遍历实现:lib/model.js#L1854-L1861autoSearchIndex自动创建逻辑:lib/model.js#L1142-L1154- 聚合
search()辅助方法:lib/aggregate.js#L1012-L1014 - 文本索引与向量索引的端到端测试用例:test/model.test.js#L9805-L9916
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考