您可以使用MongoDB Vector Search实现多租户,以便应用程序的单个实例为多个租户提供服务。本页介绍专门应用于MongoDB Vector Search 的设计建议。这些建议与我们针对Atlas 的 多租户建议不同。
建议
在为MongoDB Vector Search 设计多租户架构时,请参阅以下建议。
重要
本指导假设您可以在单个 VPC中共置租户。否则,您必须为每个租户维护单独的项目,我们不建议对MongoDB Vector Search 这样做。
一个集合适用于所有租户
我们建议将所有租户数据存储在单个集合以及单个数据库和集群中。您可以通过在每个文档中包含tenant_id 字段来区分租户。此字段可以是租户的任何唯一标识符,例如 UUID 或租户名称。您可以将此字段用作MongoDB Vector Search 索引和查询中的 预筛选器。
这种集中式方法具有以下优点:
易于建模和扩展。
简化维护操作。
通过
tenant_id预过滤以实现高效的查询路由。注意
我们保证不会为不符合此筛选条件的租户提供服务。
每个租户一个集合或每个租户一个数据库
由于以下原因,我们不建议将每个租户存储在单独的集合或数据库中:
性能影响:此方法可能会导致变更流负载发生变化,具体取决于集合数量,这可能会对性能和监控功能产生负面影响。
无需额外隔离:Atlas中的隔离性ACID 一致性保证应用于数据库级别。在同一数据库中使用单独的集合不会提供额外的数据隔离性优势。使用单独的数据库会带来操作复杂性,对于大多数使用案例来说,没有任何有意义的安全优势。
相反,对所有租户使用一个集合。有关如何从每个租户一个集合模型迁移到单个集合模型的示例,请参阅从每个租户一个集合模型迁移。
Considerations
请考虑以下策略,使用推荐的方法缓解潜在的性能问题。
租户大小差异
如果您有许多租户,每个租户的向量相对较少,或者由于数据分布不均(一些大租户和许多小租户)而遇到性能问题,请考虑以下策略。
对许多小型租户使用平面索引
如果您有许多租户(最多 1 万),并且每个租户拥有的向量相对较少(每个租户少于 10,000 个向量),请使用 flat索引而不是默认的Hierarchical Navigable Small Worlds索引。要创建平面索引,请在索引定义中将 indexingMethod字段设立为 flat。
当每个租户拥有少量向量时,筛选到特定租户的查询已经进行了穷举搜索。在这些情况下,分层可导航小世界 图表没有任何好处,但会增加内存和维护开销。扁平索引消除了这种不必要的开销。
平面索引可为多租户工作负载提供以下优势:
针对选择性过滤器进行了优化:对于每个租户拥有少量向量的高度选择性查询,穷举扫描已经是最快路径。扁平索引直接支持这一点,从而改善延迟和召回。
可预测的性能:无论针对哪个租户,查询延迟都保持在一个狭窄的范围内,从而消除了租户之间的嘈杂邻居影响。
资源效率:扁平索引消除了与构建“分层可导航小世界”图表相关的内存和维护开销。
例子
以下索引定义创建带有 tenant_id过滤器字段的平面索引:
{ "fields": [ { "type": "vector", "path": "<fieldToIndex>", "numDimensions": <numberOfDimensions>, "similarity": "euclidean | cosine | dotProduct", "indexingMethod": "flat" }, { "type": "filter", "path": "tenant_id" } ] }
对较大租户的视图使用 HNSW 索引
对于每个具有超过10 、000 个向量的较大租户,请使用Hierarchical Navigable Small Worlds 索引在MongoDB Views上将大租户与小租户分开:
大型租户(前 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索引(默认)。
对于小租户视图,使用平面索引方法创建索引,并包含
tenant_id字段作为预筛选器。
有关创建索引的说明,请参阅创建索引页面。
许多大型租户
如果您有许多租户,每个租户都有大量向量,请考虑使用基于分区的系统,将数据分发到分片上。
从“每个租户一个集合”模型迁移
要从每个租户一个集合的模型迁移到单个集合的模型,请处理每个租户的集合,并将文档插入到一个新的集合中。
例如,以下脚本使用 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);