您可以使用MongoDB Vector Search实现多租户,以便应用程序的单个实例为多个租户提供服务。本页介绍专门应用于MongoDB Vector Search 的设计建议。这些建议与我们为Atlas提供的多租户建议不同。
建议
在为MongoDB Vector Search 设计多租户架构时,请参阅以下建议。
重要
本指导假设您可以在单个VPC中并置租户。否则,您必须为每个租户维护单独的项目,我们不建议对MongoDB Vector Search 这样做。
一个集合适用于所有租户
我们建议将所有租户数据存储在单个集合、单个数据库和集群中。您可以通过在每个文档中包含 tenant_id 字段来区分不同租户。此字段可以是租户的任何唯一标识符,例如 UUID 或租户名称。您可以在 MongoDB 向量搜索索引和查询中使用此字段作为 预过滤器。
这种集中式方法具有以下优点:
易于建模和扩展。
简化维护操作。
通过
tenant_id预过滤以实现高效的查询路由。注意
此过滤器的 ACID 一致性保证确保查询仅返回与此过滤器匹配的租户的数据。
每个租户一个集合或每个租户一个数据库
由于以下原因,我们不建议将每个租户存储在单独的集合或数据库中:
性能影响:此方法可能会导致变更流负载发生变化,具体取决于集合数量,这可能会对性能和监控功能产生负面影响。
无需额外隔离:Atlas中的隔离性ACID 一致性保证应用于数据库级别。在同一数据库中使用单独的集合不会提供额外的数据隔离性优势。使用单独的数据库会带来操作复杂性,对于大多数使用案例来说,没有任何有意义的安全优势。
相反,对所有租户使用一个集合。有关如何从每个租户一个集合模型迁移到单个集合模型的示例,请参阅从每个租户一个集合模型迁移。
Considerations
请考虑以下策略,使用推荐的方法缓解潜在的性能问题。
租户大小差异
如果您有许多租户,但每个租户的文档相对较少,或者由于数据分布不均衡(一些大租户和许多小租户)而遇到性能问题,请考虑以下策略。
对许多小型租户使用平面索引
如果您有许多租户(最多 1 百万),并且每个租户的文档相对较少(每个租户少于 10,000 个文档),请使用 flat索引而不是默认的Hierarchical Navigable Small Worlds索引。要创建平面索引,请在索引定义中将 indexingMethod字段设立为 flat。
当每个租户拥有少量文档时,筛选到特定租户的查询已经进行了详尽搜索。在这些情况下,分层可导航小世界图表没有任何好处,但会增加内存和维护开销。扁平索引消除了这种不必要的开销。
平面索引可为多租户工作负载提供以下优势:
针对选择性过滤器进行了优化:对于每个租户拥有少量文档的高度选择性查询,穷举扫描已经是最快路径。平面索引直接支持这一点,从而改善延迟和召回。
可预测的性能:无论针对哪个租户,查询延迟都保持在一个狭窄的范围内,从而消除了租户之间的嘈杂邻居影响。
资源效率:扁平索引消除了与构建“分层可导航小世界”图表相关的内存和维护开销。
示例,以下索引定义创建带有 tenant_id 过滤器字段的平面索引。如果您希望 MongoDB 向量搜索使用 Voyage AI 模型自动生成嵌入,请选择 autoEmbed 类型;如果您已有嵌入,请选择 vector 类型:
对较大租户的视图使用 HNSW 索引
对于每个拥有超过 10,000 文档的较大租户,请使用MongoDB视图上的“分层可导航小世界”索引将大租户与小租户分开:
大型租户(前 1%):
为每个大型租户创建一个视图。
为每个视图创建一个Hierarchical Navigable Small Worlds索引。
维护大型租户的记录,在查询时进行检查,以便相应地路由查询。
小型租户(剩余租户):
为所有小型租户创建单一视图。
为此视图构建单个平面索引。
使用
tenant_id字段作为预过滤器来相应地路由查询。
例子
以下示例演示了如何使用 mongosh 为大小租户创建视图:
记录您的大型租户及其相应的 tenant_id 值,然后为每个租户创建一个视图:
db.createView( "<viewName>", "<collectionName>", [ { "$match": { "tenant_id": "<largeTenantId>" } } ] )
为小型租户创建视图,过滤掉大型租户:
db.createView( "<viewName>", "<collectionName>", [ { "$match": { "tenant_id": { "$nin": [ "<largeTenantId1>", "<largeTenantId2>", ... ] } } } ] )
创建视图后,为每个视图创建索引。请验证以下内容:
在为索引指定集合名称时,请使用视图名称而非原始集合名称。
对于大型租户视图,请创建Hierarchical Navigable Small Worlds索引(默认)。
有关创建索引的说明,请参阅创建索引页面。
许多大型租户
从“每个租户一个集合”模型迁移
要从每个租户一个集合的模型迁移到单个集合的模型,请处理每个租户的集合,并将文档插入到一个新的集合中。
示例,以下脚本使用 Node.js驾驶员将数据从每个租户的集合模型迁移到单个集合模型。该脚本还根据源集合的名称为每个文档包含一个 tenant_id字段。
import { MongoClient } from 'mongodb'; const uri = "<connectionString>"; const sourceDbName = "<sourceDatabaseName>"; const targetDbName = "<targetDatabaseName>"; const targetCollectionName = "<targetCollectionName>"; async function migrateCollections() { const client = new MongoClient(uri); try { await client.connect(); const sourceDb = client.db(sourceDbName); const targetDb = client.db(targetDbName); const targetCollection = targetDb.collection(targetCollectionName); const collections = await sourceDb.listCollections().toArray(); console.log(`Found ${collections.length} collections.`); const BATCH_SIZE = 1000; // Define a suitable batch size based on your requirements let totalProcessed = 0; for (const collectionInfo of collections) { const collection = sourceDb.collection(collectionInfo.name); let documentsProcessed = 0; let batch = []; const tenantId = collectionInfo.name; // Uses the collection name as the tenant_id const cursor = collection.find({}); for await (const doc of cursor) { doc.tenant_id = tenantId; // Adds a tenant_id field to each document batch.push(doc); if (batch.length >= BATCH_SIZE) { await targetCollection.insertMany(batch); totalProcessed += batch.length; documentsProcessed += batch.length; console.log(`Processed ${documentsProcessed} documents from ${collectionInfo.name}. Total processed: ${totalProcessed}`); batch = []; } } if (batch.length > 0) { await targetCollection.insertMany(batch); totalProcessed += batch.length; documentsProcessed += batch.length; console.log(`Processed ${documentsProcessed} documents from ${collectionInfo.name}. Total processed: ${totalProcessed}`); } } console.log(`Migration completed. Total documents processed: ${totalProcessed}`); } catch (err) { console.error('An error occurred:', err); } finally { await client.close(); } } await migrateCollections().catch(console.error);