如果说 embedding 技术可以被看作文本表示学习,那么向量存储技术则涉及向量的索引、保存和查询等过程。Embeddings 负责通过编码外部信息,形成并传递“神经介质”,将输入信息转换成可以理解和处理的向量表示。而向量数据库则像海马体一样,在复杂的语义空间中存储和组织向量化信息,并在需要时实现高效的记忆检索。
通过 embedding 技术,一篇文章会被转换成一串数字;通过向量存储技术,我们可以从数十亿个向量中快速找到最相关的那一个。这项技术关注的正是如何优化这一过程,以实现更快、更准确、资源消耗更低的结果。
图 4.1:embedding 方法概览,包括常见 embedding 模型、稀疏和稠密 embedding 表示,以及用于特定领域检索的微调和 ColBERT 等专用 embedding 方法。
向量数据库是向量存储技术的典型应用。传统数据库以表格形式存储数据,并通过为数据点赋值来建立数据索引。当收到查询请求时,传统数据库会返回与查询精确匹配的结果。相比之下,向量数据库以 embeddings 的形式存储向量,并支持基于相似度指标的向量搜索功能,而不是精确匹配。虽然二者都被称为数据库,但它们适用于不同场景,并没有谁天然优于谁。
图 4.2:数据库模式图,展示了 sales 和 production 表之间的关系及关键字段。
图 4.3:流程图展示非结构化数据经过 embedding 模型处理后进入向量数据库。
随着 AI 技术的发展,向量数据库在现代应用中变得越来越重要。它们的技术栈不仅限于“存储向量”,还包括针对向量索引和搜索机制的优化。接下来,我们将详细讨论这些主题。
向量是如何存储的
即使不安装商业向量数据库,仅仅使用 LlamaIndex,你也可以快速建立一种基于内存或本地磁盘的向量存储机制。
从 LlamaIndex 设计视角看一个简单向量索引
麻雀虽小,五脏俱全——使用 LlamaIndex 实现向量存储机制。Lewis 将其称为一个简单向量索引。接下来我们看一个简单示例。
首先,读取目录中的所有文档。然后生成索引,也就是存储向量。完整代码可参考 github.com/PacktPublis…
from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("/data").load_data() # 加载文档
len(documents)
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents) # 创建索引
vars(index)
输出如下:
{'_use_async': False,
'_store_nodes_override': False,
'_embed_model': OpenAIEmbedding(model_name='text-embedding-ada-002', embed_batch_size=100, callback_manager=<llama_index.core.callbacks.base.CallbackManager object at 0x0000022E8FEB1730>, num_workers=None, additional_kwargs={}, api_key='sk-8j2Vjv......', api_base='https://api.openai.com/v1', api_version='', max_retries=10, timeout=60.0, default_headers=None, reuse_client=True, dimensions=None),
'_insert_batch_size': 2048,
'_storage_context': StorageContext(docstore=<llama_index.core.storage.docstore.simple_docstore.SimpleDocumentStore object at 0x0000022ECC6291C0>,
index_store=<llama_index.core.storage.index_store.simple_index_store.SimpleIndexStore object at 0x0000022E90DE6570>,
vector_stores={'default': SimpleVectorStore(stores_text=False, is_embedding_query=True, data=SimpleVectorStoreData(embedding_dict={'03fa0674-d5cc-4e2b-b65d-45a36da94cb2': [-0.008005363866686821, 0.012379131279885769, 0.005627532955259085, 0.01884971372783184, -0.02227955311536789, -0.01039039995521...]})
上面输出中的 “index” 信息非常丰富,其中包含用于数据管理和组织的核心组件,例如课程存储 SimpleDocumentStore、索引存储 SimpleIndexStore 和向量存储 SimpleVectorStore。这些组件各自承担不同职责,并在整个数据处理工作流中相互协作。
课程存储:用于保存和读取课程,课程会被表示为 node 对象。Nodes 是原始课程分块后的结果,包含文本内容和相关 metadata。
索引存储:包含轻量级索引 metadata,也就是构建索引时创建的附加状态信息。它用于存储索引结构和相关 metadata,以支持快速检索和查询操作。
向量存储:一个内存存储系统,以字典形式存储 embeddings,将 node ID 映射到对应的 embeddings。
在这个向量索引中,llama_index 会自动将导入的课程内容拆分成一个个 nodes。LlamaIndex 中的 nodes 是向量存储的基本单位。
nodes 之间的关系,以及 nodes 和 embeddings 之间的连接,都会被完整记录在 index 中。
nodes = index.index_struct.nodes_dict
for node in nodes:
print(node)
输出如下:
4a2aedf6-6c9e-4359-9125-786119be2b50
77f2ab3f-4ac0-433d-9b5a-f3fa442587a5
07388425-9662-4690-b0b4-6ede6943561d
当然,你也可以手动切分 nodes。完整代码可参考 github.com/PacktPublis…
from llama_index.core.node_parser import SentenceSplitter
text_splitter = SentenceSplitter(chunk_size=512, chunk_overlap=10)
nodes = text_splitter.get_nodes_from_documents(documents) # 将课程切分为 nodes,也就是文本块
index = VectorStoreIndex(nodes) # 从 nodes 生成索引
nodes = index.index_struct.nodes_dict
for node in nodes:
print(node)
接下来,使用 storage_context 将索引保存到磁盘,并查看索引文件的结构。
index.storage_context.persist(persist_dir="saved_index") # 保存索引
在生成的 saved_index 目录中,可以看到以下内容:
图 4.4:文件资源管理器窗口中显示 savedindex 文件夹内的五个 JSON 文件。
该目录包含多个 JSON 文件,用于存储索引及相关数据。
default_vector_store.json:存储默认向量存储配置或数据。用于存储嵌入后的默认向量数据,例如用户上传文本、文档或其他资源的向量化表示。在检索过程中,系统会将它作为主要向量索引库使用。
docstore.json:存储文档存储的 metadata 或实际内容。它保存详细的文档信息,例如文档原始内容、标题和 path。在检索过程中,向量索引返回的结果可以通过 docstore 获取具体文档内容。
graph_store.json:存储知识图谱或关系图数据。如果系统支持知识图谱增强检索,该文件会记录文档或向量之间的关系。在需要基于 node 关系进行推理时使用。
image_vector_store.json:存储与图像相关的向量数据。如果系统支持多模态检索,例如混合文本和图像检索,该文件可能包含嵌入后的图像数据,也就是向量化表示。借助该文件,系统可以基于图像内容进行相似度检索。
index_store.json:存储索引本身的结构或元信息。它保存所有已创建索引的 metadata,例如每个索引覆盖的文档范围、embedding 模型、参数配置等。它用于管理整个索引系统,并决定如何调度不同索引。
图 4.5:Jupyter Notebook 界面展示 indexstorejson 的 JSON 树状视图,其中 data 字段已展开。
图 4.6:Jupyter Notebook 中展示一个 JSON 结构,其中包含来自 PDF 文档的 metadata 和文本。
docstore.json 中的数据结构信息尤其丰富。例如,展开 relationships 后,可以看到存储的 nodes 之间的关系。
图 4.7:一个软件界面展示了两个关系,每个关系都包含 node ID、类型和 metadata 细节。
其中,1 和 3 是关系类型,用于表示不同 nodes 之间的关联。例如,当前文档 node、前一个 node 和后一个 node 都通过 node_id 连接。node_type 用于区分不同类型的 node:4 表示 document node,1 表示普通 vector node,也就是 text chunk。
default__vector_store.json 的内容如下。该文件存储的是文本 chunks 的 embedding 向量。
这里,embedding_dict 包含每个文本 chunk 的 embedding 向量,每个向量都是一个 1536 维的 OpenAI embedding 向量。
图 4.8:Jupyter 文件浏览器展示一个 JSON 文件,其中包含 embeddings 和 metadata 的嵌套文件夹。
图 4.9:树状视图展示一个字典,其中 key 是数字,value 是浮点数。
这些数字是 embedding 模型蒸馏出的信息,被称为 dense embeddings。向量存储或索引,只是高效组织这些数字的方式。
Alex:Lewis,我这里有个问题。embedding_dict 中的内容对应的是单个文本 chunk,还是单个 token?
Lewis:它们是文本 chunks,也就是 LlamaIndex 中的 nodes,或者 LangChain 中的 chunks。key,例如 6e95bafe-5646-4ae5-90b2-3174a4ae2792,是文本 chunk 的唯一 ID。每个文本 chunk,可以是一句话、一段文字,甚至是一块更大的文本,都会被转换成一个 1536 维向量,而不是为每个 token 生成一个向量。
embedding 模型的输入是文本,而不是单个 token。当你输入一段文本,例如一句话或一段话时,模型首先会将文本拆分成 tokens。这只是内部计算过程,并不影响最终输出。
虽然每个 token 在中间处理阶段都会被表示为向量,但经过 Transformer 计算后,模型最终会生成一个固定长度向量,例如 1536 维向量,用来表示整个输入文本的语义。模型不会为每个 token 单独计算 embedding;相反,它会为整段输入文本计算一个 embedding 向量。因此,一个 1536 维 embedding 表示的是整个文本 chunk 的语义信息,而不是某个单独 token 的独立信息。
这里详细介绍 LlamaIndex 的索引构建和存储细节,是为了帮助你理解向量存储,也就是一个小型向量数据库中的信息组织过程。
向量数据库的组成部分
Alex:Lewis,既然使用 LlamaIndex 已经可以实现本地向量存储和搜索,为什么我们还需要 Milvus 或 Weaviate 这样的开源或商业向量数据库?
Lewis:这些向量数据库提供了更全面的向量数据管理方案。要在实践中实现一个 RAG 系统,向量数据库需要具备可扩展性,允许数据动态变化,支持 metadata 存储和过滤,保持高可用和容错,并提供监控、安全、访问控制、多用户支持和数据隔离等功能。
接下来,我们以 Milvus 为例说明。
图 4.10:图示展示一篇关于巨龟的文章,以及映射到数据库存储的数据字段。
Milvus 提供三种部署模式,适用于从简单应用到管理数百亿向量的大规模 Kubernetes 集群等不同数据规模。
Milvus Lite 作为 Milvus 的轻量版本,易于集成到应用中。本课程将以它作为示例介绍。
Milvus Standalone 是 Milvus 的单机版本,所有组件都被打包到一个 Docker 镜像中,方便部署。
Milvus Distributed 可以部署在 Kubernetes 集群上,在云原生架构中支持数十亿向量甚至更大规模的场景。
在实际应用中,仅仅存储向量是不够的,因为业务逻辑通常需要额外的 metadata,例如文本、时间、标签和类别。Milvus 将非结构化或多模态数据组织成结构化 collections,并支持多种数据类型,包括常见数值类型和字符类型、多种向量类型、数组、集合以及 JSON,从而减少维护多个数据库系统的需求。
更重要的是,向量数据库会使用算法为 vector embeddings 创建索引并执行查询。这些算法利用哈希、量化或基于图的搜索方法,实现 ANN,也就是 Approximate Nearest Neighbor,近似最近邻搜索。ANN 搜索的目标是找到离查询最近的向量。虽然它的准确性略低于 k-NN 搜索,也就是精确最近邻搜索,但它所需计算量更少,更适合高效处理大规模、高维数据集。
在性能方面,Milvus 采用硬件感知优化,例如 AVX512、SIMD 和 GPU 支持,高级搜索算法,例如 IVF、HNSW 和 DiskANN,以及用 C++ 开发的高性能搜索引擎,使其速度非常快。列式存储进一步提升了查询效率。
在可扩展性方面,Milvus 采用云原生架构,支持处理数百亿规模的向量数据。其模块化设计使搜索、数据插入和索引构建等核心功能可以独立扩展。
在功能方面,Milvus 支持多种搜索方法,例如 ANN search、filtered search、range search 等,提供多语言 SDK,也就是 Software Development Kits,支持多种数据类型,并具备分区管理、多租户支持和安全授权等企业级功能。此外,Milvus 还提供丰富的工具生态,例如可视化管理界面、监控工具,以及与主流 AI 工具的集成支持。
| 功能 | 描述 |
|---|---|
| 性能与容错 | 分片,将数据分布到多个节点;复制,在不同节点上创建多个数据副本。发生故障时可以实现容错,确保性能稳定。 |
| 监控 | 监控资源使用、查询性能和系统运行状态,并持续优化性能和容错能力。 |
| 访问控制 | 确保数据安全,提供合规、问责和审计能力;保护数据不被未授权访问,并记录用户活动。 |
| 可扩展性与可调节性 | 支持横向扩展,以适应不同插入速率、查询速率和硬件差异。 |
| 多用户和数据隔离 | 支持多用户或多租户;实现数据隔离,确保用户活动,例如插入、删除、查询,不影响其他用户的私有数据。 |
| 备份 | 定期创建数据备份;在数据丢失或损坏时,支持将系统恢复到先前状态,减少停机时间。 |
| APIs 和 SDKs | 提供易用 API;封装多个 API,帮助开发者将向量数据库用于特定用例,例如语义搜索、推荐系统等,而不必关注底层结构。 |
表 4.1:向量数据库的核心企业能力及其运维收益。
向量数据库中的索引
在向量数据库能够高效返回相关结果之前,它必须以支持快速相似度搜索的方式组织已存储向量。这正是索引变得必不可少的地方。索引不是让查询向量与数据库中的每个向量逐一比较,而是提供一条结构化检索路径,从而减少搜索空间并提升查询性能。在本节中,我们将研究向量数据库如何构建和使用索引,然后介绍几种常见索引方法,包括 FLAT、IVF、基于量化的索引、基于图的索引和哈希技术。
向量数据库的典型工作流包括以下步骤:
数据存储:将高维向量数据存入向量数据库。
索引构建:使用哈希、量化或图结构等技术,为 vector embeddings 构建索引,以加速查询过程。
查询检索:收到查询请求后,向量数据库会将查询向量与索引中的向量进行比较,使用相似度度量方法,例如余弦相似度、欧氏距离或点积,确定最近的向量。
检索后处理:对找到的最近邻结果进行进一步过滤或排序,以优化查询输出。
在大规模数据集中执行向量搜索是一项计算密集型任务,尤其是在高维空间中。为了提升搜索效率,可以引入各种索引结构,减少需要比较的向量数量,从而降低计算成本。
索引是向量数据库以及更广义的 RAG 系统中的重要概念。如前所述,RAG 系统中的索引目标是通过为文本 chunks 构建高效存储机制来优化检索。这包括构建文档 embeddings 和 metadata 的过程与方法。
这个定义指的是应用层面的索引。相比之下,这里讨论的向量数据库内部索引更偏技术层面。它们的主要目标是优化高维向量数据的存储和检索性能,从而提升相似度搜索效率。这涉及多种数据结构和算法,例如 FLAT、IVF 和 HNSW,它们被用于组织和加速向量上的 ANN 搜索。
这类索引属于数据库内部实现的一部分,更关注底层数据处理和搜索优化。
向量索引是加速向量数据库检索过程的关键技术。它决定了向量如何被组织,以及使用何种检索策略。下面几节将详细介绍几种常见向量索引方法。
我们将从最简单的索引方法 FLAT 开始,然后介绍更优化的方法,例如 IVF、基于量化的索引、图索引和哈希。
Flat index
Flat index,简称 FLAT,是最基础的索引方法。它的主要特点是逐一比较数据库中的每一个向量,以确保检索结果绝对准确。由于它不使用近似或压缩,FLAT 可以返回精确最近邻结果,并在相似度指标配置正确的情况下实现 100% recall。
执行查询时,FLAT 会通过计算查询向量与数据库中每个向量之间的相似度或距离来进行比较。不过,由于它需要遍历所有向量,当数据规模很大时,检索速度会显著下降。
可以想象一个二维空间,所有向量都分布在这个空间中。查询向量需要计算它与每个数据点之间的距离。
图 4.11:白色背景上随机散布着一簇紫色点,其中一个红点靠近中心。
FLAT 适合相对较小的数据集,或者精确检索准确性比查询速度更重要的离线评估场景。当应用场景对检索准确性要求极高,并且可以容忍更长查询时间时,FLAT 是理想选择。
IVF index
Inverted File Index,也就是 IVF,通过对向量数据进行聚类,减少查询时需要比较的向量数量,从而显著加快检索速度。IVF 最基础的形式是 IVF_FLAT。它也有 IVF_SQ8 和 IVF_PQ 等变体。
IVF_FLAT 首先将向量数据聚成若干 clusters,每个 cluster 表示数据空间中的一个特定区域。执行查询时,它会先计算查询向量与每个 cluster center 之间的距离,然后只选择与查询向量最相似的前 n 个 clusters。接下来,在这些被选中的 clusters 内,对所有向量执行精确匹配。在示例中,向量被分到不同 clusters 中。查询向量只需要与这些被选中 clusters 中的向量进行比较。
图 4.12:四个面板展示紫色点构成的 clusters,其中一个面板中包含一个大的粉色点和一个红点。
通过只搜索部分 clusters,IVF_FLAT 大大减少了需要计算的向量数量,提升了查询效率。同时,由于在被选中的 clusters 内执行精确比较,检索结果的准确性也得到了保证。
Alex:为什么叫 “inverted”?
Lewis:在文本检索场景中,“倒排索引”指的是为某个词或关键词维护一个文档列表,列出所有包含该词的文档。这样,当搜索某个词时,只需要从这个倒排列表中查找相关文档,而不需要遍历整个文档集合。
IVF 借用了这个思路,将所有向量聚类成多个 clusters,每个 cluster 都记录自己的向量列表,就像文本检索中的倒排列表。执行查询时,会根据查询向量与各个 cluster center 的相似度,只选择最相似的 clusters 进行检索,而不是遍历整个数据集。
量化索引
量化技术通过压缩向量数据来减少存储空间和计算量。主要量化方法包括 Scalar Quantization,简称 SQ;Product Quantization,简称 PQ;以及 Optimized Product Quantization,简称 OPQ。
图 4.13:流程图对比三种向量量化方法,并展示逐步转换过程。
索引方法决定了向量如何组织和检索,而量化技术则关注向量数据的压缩和优化。在实际应用中,这两类技术常常结合使用,以获得最佳效果。
Scalar quantization 是最简单的量化方法;它直接将每个浮点数转换为低位整数,例如 8-bit 或 4-bit 整数。这种方法容易实现,量化和反量化过程都很快,可以显著降低存储需求。不过,它可能产生较大的量化误差,从而影响检索准确性。
Product quantization 会将高维向量拆分成几个低维部分,并对每个部分独立执行量化。每个子空间中的值会被映射到预先生成的 codebook 所表示的离散 codewords,这个 codebook 捕捉了最接近查询向量的数据。由于只需要存储 codeword indices,product quantization 可以大幅减少存储需求并提升查询效率,使得距离计算能够在压缩空间中完成。
Optimized product quantization 是对 product quantization 的改进。它会先通过线性变换,例如旋转,优化向量分布,然后再执行 product quantization。这种方法有助于降低量化误差并提升检索准确性。在相同压缩率下,optimized product quantization 可以比 product quantization 提供更高准确率,并更好适应向量数据的实际分布。
IVF_SQ8 是一种在 IVF_FLAT 基础上应用 scalar quantization 压缩向量的方法。在这种方法中,向量会被转换为低位表示,也就是将浮点数转换为 8-bit 整数,在聚类基础上进一步降低存储需求,并大幅降低内存和存储要求,因此适合资源受限环境。虽然存在一定量化误差,但在多数应用场景中,这种影响是可以接受的。
IVF_PQ 则结合了 IVF 和 product quantization:在聚类基础上,将高维向量拆分为多个低维子空间,并分别进行量化,从而提升查询效率并减少存储需求。不过,这种方法会引入一定检索准确性损失,因此需要在准确率和效率之间取得平衡。
图索引
Hierarchical Navigable Small World,简称 HNSW 图,是一种高效的基于图的索引结构,专门用于加速向量的 ANN,也就是 Approximate Nearest Neighbor 搜索。它利用小世界网络的特性,通过构建多层图结构,使大规模向量数据集中的相似向量能够被快速检索。
在 HNSW 中,图结构被组织为多层,每一层具有不同密度。最顶层图非常稀疏,节点之间连接较少,这有助于查询时进行粗定位和快速“跳跃”。越接近底层,图越密集,能够进行更细致的搜索。查询过程从顶层开始,先快速缩小搜索范围,大致定位查询向量。然后逐层深入,每一层都会基于上一层结果细化搜索,直到在最后一层找到最相似的向量。HNSW 可以在保持高召回率的同时提供极快查询速度。
图 4.14:三层网络结构,包含彩色节点和边,并通过虚线连接。
HNSW 的优势在于,在不牺牲太多准确性的情况下显著提升查询效率,非常适合同时要求快速查询和高准确率的应用场景。不过,使用 HNSW 要求系统具备足够内存资源来存储其图结构,以确保查询效率和结果质量。
哈希技术
Locality Sensitive Hashing,简称 LSH,通过构造一组哈希函数,将相似的高维向量映射到同一个 hash bucket 中,从而加速 ANN 搜索。执行查询时,只需要搜索与查询向量位于同一 hash bucket 中的向量,这大大减少了计算开销。
图 4.15:图示展示数据点如何从原始向量空间被哈希到不同 hash buckets 中。
LSH 的优势是检索速度快、算法简单且易于实现。不过,它的缺点是准确率相对较低,只适合近似检索场景。此外,根据数据分布不同,LSH 的性能可能会有较大差异。
因此,LSH 特别适合需要快速返回近似结果的应用场景,例如推荐系统和实时查询,也适合那些数据分布稀疏且不要求严格准确性的应用。
向量检索:相似度度量
在向量数据库中,检索或搜索的本质是在高维空间中找到与查询向量最相似的向量,因此也可以称为相似度度量。为了在向量数据库中实现快速高效的检索,理解其底层原理至关重要。
在向量数据库中,索引结构和相似度度量之间存在紧密关系。相似度度量决定如何量化向量之间的距离或相似性,而索引结构则利用这种度量方法来组织和加速向量数据检索。选择合适的相似度度量和索引方法,对于提升检索效率和准确性至关重要。
Anna:你在第 3 节提到过,度量方法包括欧氏距离、曼哈顿距离、余弦相似度和内积。
Lewis:是的。曼哈顿距离在向量相似度度量中较少使用。这里,我会给出几种常用度量方法的计算公式,也会介绍偶尔使用的 Hamming distance 和 Jaccard similarity。
Euclidean distance,L2:衡量空间中两个向量之间的直线距离,适合连续数值数据。计算公式如下:
图:一个包含数字和字母的数学公式。
Cosine similarity:衡量两个向量之间夹角的余弦值,反映向量方向上的相似性。计算公式如下:
图:展示两个数之间余弦相似度的公式。
Inner product,IP:衡量两个向量之间的投影关系,反映向量的方向和大小。计算公式如下:
图:白色背景上的一个数学公式。
Hamming distance:衡量二进制向量之间不同位的数量,适合二进制数据。计算公式如下:
图:一个包含数字的数学公式。
Jaccard similarity:通过计算交集与并集的比例来衡量两个集合之间的相似性,适合集合或稀疏数据。计算公式如下:
图:白色背景上的一个公式。
Alex:Lewis,这些索引方法和向量相似度度量显然非常重要。你在这里总结得很清楚,让我对这部分知识有了系统理解。不过我感觉在实际应用中,操作起来还是很难。
Lewis:确实如此。这些都是向量数据库的核心概念。要真正掌握它们,需要亲自动手练习并尝试。在第 4.5 节,我会带你用这些索引方法写一些向量检索示例,这样你就能更清楚地理解在不同情况下应该选择哪些索引和度量方法。
主流向量数据库
随着大语言模型和向量检索技术的广泛采用,各种开源和商业向量数据库如雨后春笋般涌现。这些数据库在 AI 和大模型应用开发过程中发挥着重要作用。
这些数据库在可扩展性、性能优化、部署方式和生态系统方面各有特点。下面简要介绍几个常用向量数据库。
Milvus:Lewis 日常工作中最常使用的数据库是 Milvus。它是一个开源、高性能向量数据库,支持 PB 级数据,并具备优秀的可扩展性和高吞吐能力。
Milvus 尤其注重开发者体验,提供了完整文档,方便学习者快速上手。此外,它支持 Docker 部署,因此特别适合采用云原生架构的应用。对于小团队和个人开发者,Milvus 的本地安装版本更容易使用。下面的示例中,我们也会使用 Milvus 本地版本进行演示。
Milvus 因适用场景广泛而受到青睐,在需要处理大规模向量数据的云原生应用中表现尤其出色。需要注意的是,Milvus 配置优化需要一定经验,因此学习曲线相对陡峭。
Zilliz Cloud:Zilliz Cloud 是基于 Milvus 的全托管 SaaS 企业版本,专注于向量数据库商业化,为企业用户提供托管服务和高级功能,例如自动扩展和优化。使用 Milvus 完成原型项目开发后,如果需要迁移到更大规模的生产应用场景,从 Milvus 转向 Zilliz Cloud 就成为自然选择。因此,无论是从小项目到大规模应用,还是从个人开发者到企业用户,Milvus 及其企业版本 Zilliz Cloud 都提供了一站式解决方案。
Weaviate:Weaviate 的关键特点在于其开源属性,以及支持存储和检索多模态数据,例如文本、图像和视频。它易于快速上手,非常适合概念验证和中小型项目。Weaviate 支持 embedded mode,方便在本地或小型设备上运行,并提供数据持久化支持,可与多种后端存储选项集成,例如 S3、GCP。
Weaviate 适合快速构建多模态应用,例如基于图像和文本的智能问答系统。不过,它在大规模可扩展性和性能优化方面仍有提升空间。
Qdrant:Qdrant 以优秀性能著称,专门针对高性能场景进行了优化。它可以扩展到包含超过 5000 万条记录的大规模向量集合,并且在近实时检索和动态写入方面表现突出。不过,在高并发写入负载下,它可能面临一些挑战。如果你需要实现动态索引更新、实时推荐或个性化内容分发,可以考虑使用 Qdrant。
Qdrant 提供开源版本、Cloud 版本和 Enterprise 版本。Cloud 版本提供 1GB 免费空间,适合 demo 级项目。
Faiss:Faiss 由 Facebook AI Research 团队开发,是一个专门用于高效相似度搜索和稠密向量聚类的库,主要用于处理大规模向量数据。该库使用 C++ 编写,并提供 Python 包。
Faiss 分为 CPU 和 GPU 版本:CPU 版本适合小规模数据集,可以在普通服务器上运行;GPU 版本利用 CUDA 加速,适合大规模数据集,并能够显著提升搜索性能。需要注意的是,Faiss 的 GPU 版本只支持特定 CUDA 版本,如果你的 CUDA 版本不受支持,安装时会报错。
Faiss 提供向量存储所需的核心功能,包括最近邻搜索,也就是 k-NN / ANN,索引结构优化,支持 HNSW、IVF、PQ 等算法,以及向量聚类。
虽然 Faiss 具有轻量、易上手等优势,但功能相对有限。索引只能存储在内存中,并且不提供数据库级存储管理,例如事务、数据持久化。与 Milvus 和 Weaviate 不同,它不原生支持分布式存储和查询。因此,Faiss 更适合处理百万级向量数据集,其处理十亿级向量数据集的能力有限。它是初学者学习的理想选择。
Pinecone:Pinecone 是知名商业向量存储方案,专注于托管服务,目标是降低部署和维护成本。它的优势包括易用性和可扩展性,提供一站式解决方案,并支持自动扩展和高可用。不过,在某些定制化性能需求方面,Pinecone 可能不如开源替代方案灵活,而且成本通常更高。因此,对于稳定性和可用性要求高、但缺少额外运维能力的组织来说,Pinecone 是一个不错选择。
Pinecone 的文档也非常完整,其中包含许多有价值的 RAG 方案示例,值得学习和参考。
Chroma:与 Faiss 类似,开源库 Chroma 也提供检索所需的所有基础功能:存储 embeddings 及其 metadata、向量搜索、全文搜索、文件存储、metadata 过滤和多模态检索。
默认情况下,Chroma 使用 SQLite 存储,并支持持久化到磁盘,这意味着它不需要额外数据库管理系统,非常适合快速搭建项目。不过,这也意味着 Chroma 不适合大规模生产环境,在支持分布式系统、复杂查询和企业级应用方面存在限制。
和 Faiss 一样,Chroma 的优势在于易用性。Chroma 文档也提供了大量与向量存储相关的知识,帮助用户更好地理解和使用它。
Elasticsearch:作为成熟的分布式搜索引擎,Elasticsearch 凭借近实时检索能力、横向扩展能力和丰富查询语法,已经成为企业级搜索场景中的标杆工具。近年来,Elasticsearch 引入了向量搜索功能,例如 dense_vector 字段类型和 k-NN 搜索接口,使其能够同时支持传统关键词检索、数值范围过滤,以及高维向量相似度计算。这特别适合需要混合多模态查询的场景,例如电商搜索中将文本语义向量和商品属性过滤结合起来。
对于已经深度集成 Elasticsearch 技术栈的团队,可以直接复用现有集群和运维系统,从而降低引入向量能力的边际成本。Elasticsearch 的分布式架构可以轻松处理百亿级数据集,并支持多租户和细粒度权限控制等企业级功能。不过,在纯向量检索场景中,Elasticsearch 的性能可能不如专用向量数据库,需要通过分片策略或近似算法优化来提升性能。
PGVector:PGVector 是 PostgreSQL 的一个扩展,通过为 PostgreSQL 提供向量数据类型和 ANN 检索,例如 HNSW、IVF_FLAT,将向量计算能力无缝集成到关系数据库中。这种设计特别适合同时需要处理结构化数据关联和向量相似度分析的场景,例如在用户画像系统中将用户行为向量与 SQL 表中的标签数据关联,或者在地理信息系统中执行结合空间坐标和特征向量的多条件查询。
在 PGVector 中,开发者可以直接使用标准 SQL 语法执行向量和结构化数据 JOIN 的复杂查询,避免跨数据库同步的复杂性。虽然 PGVector 通过索引优化提升了性能,但在处理超大规模向量数据集,例如千万级或更大规模,或者高并发场景时,其吞吐和延迟仍可能落后于专用向量数据库。PGVector 的优势在于它原生支持事务一致性和复杂 SQL 操作,例如窗口函数和存储过程,适合中等规模数据集,以及需要强事务保障的应用,例如金融风控中的实时特征比对。
向量数据库的选择和评估
Alex:问题来了。现在有这么多向量数据库,而且表面看起来都差不多,应该怎么选?
Lewis:这确实是一个很难回答的问题。RAG 和向量数据库的概念都比较新,目前真正对多个向量数据库做过生产级项目深度评估的人并不多。这方面的实践经验也有限。
向量数据库选型
尽管如此,我们仍然可以考虑以下关键因素,并结合具体需求和场景做出选择。
开源 vs. 付费托管服务:如果你的团队具备强 DevOps 能力,可以选择开源数据库,例如 Milvus、Weaviate、Qdrant 等,以获得灵活性。如果你希望降低运维成本,可以选择商业托管服务,例如 Pinecone、AWS Kendra 等。
性能,也就是查询速度:对于需要大规模、高并发查询的场景,Milvus 和 Pinecone 表现良好。对于延迟敏感应用,例如推荐系统,应优先选择 Milvus 等低延迟数据库。
可扩展性:虽然各类向量数据库的可扩展性普遍不错,但在海量数据集方面,Milvus 和 Pinecone 表现突出。
开发者体验:尽量选择文档完整、社区支持良好的数据库,例如 Milvus 和 Weaviate。如果你的开发团队更熟悉 Python,可以选择 API 简单的数据库,例如 Chroma。
功能支持:如果需要复杂混合搜索,也就是结合结构化数据和向量数据,Weaviate、Qdrant 和 Milvus 都是不错选择。对于动态数据和索引更新,Milvus 和 Qdrant 提供更强支持。
成本和预算:使用开源工具,例如 PGVector、Chroma,可以降低初始投入。虽然 Pinecone 等商业方案更昂贵,但更适合追求稳定性和服务支持的企业。
技术栈兼容性:如果现有技术栈使用 Elasticsearch,可以扩展其向量搜索能力,以降低学习成本。使用 PostgreSQL 的团队可以考虑 PGVector,方便整合结构化数据和向量数据。
对于云原生应用,Vald 和 Milvus 提供更好的 Kubernetes 支持。在对 GPU 加速需求较高的场景中,Faiss 是一个优秀选择。
基于这些维度,你可以快速匹配最适合的向量数据库。例如,开发实时推荐系统时,可以选择 Milvus 或 Qdrant;做概念验证或小项目开发时,可以选择 Chroma 或 Weaviate;开发企业级搜索时,可以选择 Pinecone 或 Elasticsearch;如果需要与关系数据库集成,PGVector 是不错选择。
需要注意的是,随着时间推移,每个向量数据库的功能和特性都可能发生变化。
通过明确你的项目需求和预算,并参考下面的对比,就可以完成向量数据库选型过程。
LangChain 等框架为几乎所有向量数据库提供了接口,因此开发者可以自由切换不同向量数据库,体验性能差异。
表 4.2:部分向量数据库对比,包括部署模式、搜索能力和治理功能。
| 数据库 | QPS | 延迟 ms | 免费计划 | 50k 向量价格 USD | 20M 向量价格 USD |
|---|---|---|---|---|---|
| Pinecone | 150+ scaled | 1 | Yes | 70 | 227,HP: 2074 |
| Weaviate | 791 | 2 | Yes | From 25 | 1536 |
| Milvus | 2406 | 1 | Yes | From 65 | 309,HP: 2291 |
| Qdrant | 326 | 4 | Yes | From 9 | 281,HP: 820 |
| Chroma | Unknown | Unknown | Yes | Self-hosted free | Self-hosted free |
| Elastic-search | 700~100 | 5~10 | Self-hosted free | Varies | 1225 |
| PGVector | 141 | 8 | Self-hosted free | Varies | Varies |
表 4.3:部分向量数据库性能和估算托管成本对比。
常见的向量数据库评估工具包括 ANN Benchmark 和 VectorDBBench。
ANN Benchmark 是一个外部工具,用于在真实数据集上评估向量索引算法的性能。由于向量索引是向量数据库中资源密集型的核心组件,其性能会显著影响整体数据库性能。ANN Benchmark 擅长衡量向量索引算法性能,并为选择和比较不同向量搜索库提供可靠参考。不过,它的局限在于无法评估复杂且成熟的向量数据库系统,也不覆盖向量搜索与条件过滤结合等场景。
相比之下,VectorDBBench 是一个专门用于评估向量数据库的开源工具。它适用于开源向量数据库,例如 Milvus 和 Weaviate,也适用于全托管服务,例如 Zilliz Cloud 和 Pinecone。它支持评估数据库的关键指标,例如每秒查询数和召回率,并关注资源消耗、数据加载能力和系统稳定性。与 ANN Benchmark 不同,VectorDBBench 能够模拟更接近真实生产环境的测试场景,因此更适合全面评估向量数据库的实际性能。
Yifan Cai 在文章《Open-Source Vector Database Performance Comparison: Milvus, Chroma, Qdrant》中使用 VectorDBBench 工具评估了三个常用开源向量数据库。
图 4.16:网页展示 Vector Database Benchmark 图表,包括搜索性能和召回分数。
该文章详细介绍了使用 VectorDBBench 评估各种向量数据库的具体步骤和细节。这里我们只展示最终结论:根据测试结果,Milvus 适合处理大规模数据集和高性能要求应用,提供最佳性能,也就是查询速度;Qdrant 延迟较低,适合规模较小且对延迟要求较高的应用;Chroma 更适合小规模、低负载应用。
向量数据库中的索引和搜索设置
使用向量数据库时,索引和搜索指标的选择是最关键部分。本节以 Milvus 为例,详细解释这两个参数及其适用场景。
Milvus 向量操作示例
操作 Milvus 的整体代码工作流如下。该代码完全独立于 LangChain 或 LlamaIndex 等框架,目的是清晰展示如何在 Milvus 中创建 collection、插入向量和执行检索。
图 4.17:中文流程图展示 Milvus 操作、数据准备、embedding 和搜索流程。
在 Milvus 中,数据组织和管理主要通过 collections,也就是 Collection 实现。Collections 支持 insert、delete、index creation 和 search 等多种操作。它是 Milvus 的核心数据结构,代表一组数据。创建 collection 时,需要指定 collection name 和 schema。
这里介绍两个与 collection 相关的核心概念:FieldSchema 和 CollectionSchema。
FieldSchema:定义 collection 中每个字段的属性。每个字段表示数据的某个具体属性。它的主要属性包括字段名和数据类型,例如整数 INT64、浮点数 FLOAT、字符串 VARCHAR,或者向量类型 FLOAT_VECTOR。
CollectionSchema:定义整个 collection 的结构,包括 collection 包含哪些字段及其描述。它的主要属性包括字段列表和描述信息。通过定义 collection schema,用户可以清晰指定 collection 的数据结构和组织方式,从而更容易管理和操作数据。
接下来,我们安装 Milvus Lite,也就是 Milvus Python 包的本地版本。对于初学者来说,Milvus Lite 已经足够。如果需要构建更大规模系统,则应安装更强大的 Milvus 版本。
pip install pymilvus
pip install pymilvus[model]
下面的代码示例演示如何从零开始使用自定义数据创建一个 Milvus 向量 collection,并执行向量搜索操作。
准备示例数据集
在这个示例中,我们创建一个包含怪物信息的小型数据集。每条记录包含怪物 ID、名称、位置、难度等级、同义词和描述等字段。之后,这些字段会作为 metadata 与 vector embeddings 一起存储。完整代码可参考 github.com/PacktPublis…
### 准备示例数据集
import pandas as pd
data_records = [
{
"monster_id": "BM001",
"monster_name": "Tiger Vanguard",
"location": "Bamboo Forest Pass",
"difficulty": "High",
"synonyms": "Fierce Tiger Demon, Tiger Demon",
"description": "A tiger-shaped monster appearing in the Bamboo Forest checkpoint, extremely powerful."
},
{
"monster_id": "BM002",
"monster_name": "Fire Ape",
"location": "Volcano Cave",
"difficulty": "Low",
"synonyms": "Flame Ape, Blazing Ape",
"description": "A primate monster living in volcanic caves, just a minor character."
},
]
df = pd.DataFrame(data_records)
创建 Milvus collection 和 index
准备好数据集之后,我们连接到 Milvus Lite,并创建一个名为 Wukong_Monsters 的 collection。schema 同时定义结构化 metadata 字段和一个向量字段。向量维度通过使用所选 embedding 模型生成一个 sample embedding 来确定。
该 collection 还使用自动生成的主键。auto_id=True 设置意味着 Milvus 会自动创建主键值,因此用户在插入时不需要提供主键。
### 创建或连接到 Milvus
from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema
from pymilvus import model
db_path = "./wukong.db"
client = MilvusClient(db_path)
collection_name = "Wukong_Monsters"
### 获取 embedding 模型的向量维度
from pymilvus.model.dense import SentenceTransformerEmbeddingFunction
embedding_function = SentenceTransformerEmbeddingFunction(model_name='BAAI/bge-large-zh')
sample_embedding = embedding_function(["Sample text"])[0]
vector_dim = len(sample_embedding)
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=vector_dim),
FieldSchema(name="monster_id", dtype=DataType.VARCHAR, max_length=50),
FieldSchema(name="monster_name", dtype=DataType.VARCHAR, max_length=100),
FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=100),
FieldSchema(name="difficulty", dtype=DataType.VARCHAR, max_length=20),
FieldSchema(name="synonyms", dtype=DataType.VARCHAR, max_length=200),
FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=500),
]
schema = CollectionSchema(fields, description=" Wukong Monsters", enable_dynamic_field=True)
if not client.has_collection(collection_name):
client.create_collection(collection_name=collection_name, schema=schema)
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="AUTOINDEX",
metric_type="L2",
params={"nlist": 1024}
)
client.create_index(
collection_name=collection_name,
index_params=index_params
)
from tqdm import tqdm
for start_idx in tqdm(range(0, len(df)), desc="Inserting data"):
row = df.iloc[start_idx]
doc_parts = [str(row['monster_name'])]
if row['synonyms']:
doc_parts.append(f"(Alias: {row['synonyms']})")
if row['location']:
doc_parts.append(f"Scene: {row['location']}")
if row['description']:
doc_parts.append(f"Description: {row['description']}")
doc_text = ";".join(doc_parts)
embedding = embedding_function([doc_text])[0]
索引是在 vector 字段上创建的。在这个示例中,使用的是 AUTOINDEX,它允许 Milvus 根据数据和配置自动选择合适的索引类型。相似度指标设置为 L2,也就是使用欧氏距离进行向量比较。
示例:插入怪物数据并使用向量搜索查询
创建 collection 和 index 后,我们可以将向量化后的怪物数据插入 Milvus。每条记录会通过组合怪物名称、同义词、位置和描述等字段转换成文本表示。然后,该文本会被转换为 embedding 向量,并连同 metadata 一起插入 collection。
data_to_insert = [{
"vector": embedding,
"monster_id": str(row["monster_id"]),
"monster_name": str(row["monster_name"]),
"location": str(row["location"]),
"difficulty": str(row["difficulty"]),
"synonyms": str(row["synonyms"]),
"description": str(row["description"])
}]
client.insert(collection_name=collection_name, data=data_to_insert)
## 测试搜索
search_query = "High difficulty monster"
search_embedding = embedding_function([search_query])[0]
search_result = client.search(
collection_name=collection_name,
data=[search_embedding.tolist()],
limit=3,
output_fields=["monster_name", "location", "difficulty", "synonyms"]
)
print(f"Search results for '{search_query}':", search_result)
## 测试条件查询
query_result = client.query(
collection_name=collection_name,
filter="difficulty == 'Low'",
output_fields=["monster_name", "location", "difficulty", "synonyms"]
)
print(f"Monsters with difficulty Low:", query_result)
输出如下:
Inserting data: 100%|████████████████████| 2/2 [00:01<00:00, 1.68it/s]
Search result for 'high difficulty monster': data: ["[{'id': 456823454216224768, 'distance': 1.0104241371154785, 'entity': {'difficulty': 'High', 'location': 'Bamboo Forest Pass', 'monster_name': 'Tiger Vanguard', 'synonyms': 'Ferocious Tiger Demon, Tiger Demon'}}]"]
Monster with difficulty Low: data: ["{'id': 456823565118078978, 'difficulty': 'Low', 'location': 'Volcano Cave', 'monster_name': 'Fire Ape', 'synonyms': 'Blazing Ape, Flame Ape'}"]
搜索示例会检索与查询 “High difficulty monster” 语义相似的怪物。相比之下,条件查询会基于精确 metadata 条件检索记录,例如 difficulty == 'Low'。
输出显示,Milvus 既可以返回基于向量的搜索结果,也可以返回 metadata 过滤后的查询结果。例如,向量搜索识别出 Tiger Vanguard 是一个高难度怪物,而条件查询返回 Fire Ape 作为低难度怪物。
这段代码创建并连接到本地 Milvus 数据库 wukong.db,同时创建了一个名为 Wukong_Monsters 的 collection,用于存储向量数据。在 collection 的字段 schema 中,is_primary=True 表示 id 字段是主键,用于唯一标识一条记录。auto_id=True 表示主键由 Milvus 自动生成,因此你不需要也不能手动指定它。所以,在插入数据时,我们没有提供 id 字段,而只提供 Schema 中定义的其他字段。此外,enable_dynamic_field=True 表示定义 collection Schema 时启用了动态字段功能,允许插入数据时包含 Schema 中未定义的字段。这些额外字段不会被丢弃,而是会被自动存储到一个名为 $meta 的内置字段中。
现在,我们将重点关注代码中的 index_type,也就是索引类型,以及 metric_type,也就是相似度指标,并讨论它们的可选值和适用场景。
选择合适的索引类型
索引是组织数据的过程,使相似度搜索能够高效执行。它在让相似度搜索具备实用性方面发挥着关键作用,因为它可以显著加速大规模数据集上的耗时查询。Milvus 使用专门结构组织数据,并将 metadata 存储在索引文件中,以便在搜索或查询操作期间快速检索所需信息。
Milvus 支持的大多数向量索引类型都使用 ANN,也就是 Approximate Nearest Neighbor 搜索。与通常非常耗时的精确搜索相比,ANN 搜索的核心思想不是返回最精确结果,而是只搜索目标的邻居。这样,ANN 搜索通过在可接受范围内牺牲一定准确性,大幅提升搜索效率。
按照实现方式,ANN 向量索引可以分为 flat indexes、graph-based indexes、tree-based indexes、hash-based indexes 和 quantization-based indexes。某个给定向量数据库不一定支持所有索引类型。
索引类型与 vector embedding 类型密切相关。Float embeddings,也称为 float vectors 或 dense vectors;binary embeddings,也称为 binary vectors;以及 sparse embeddings,也称为 sparse vectors,各自都有适合的索引类型。为了增强查询性能,可以为每个向量字段指定索引类型。目前,每个向量字段只能分配一种索引类型;切换索引类型时,Milvus 会自动移除旧索引。在代码中,add_index 方法的 index_type 参数用于指定 Milvus 中向量数据使用的索引类型。表 4 总结了 Milvus 的主要索引类型、适用场景及其支持的 embedding 格式。
| Index type | Category | Applicable scenarios | Embedding type |
|---|---|---|---|
| FLAT | Flat index | 相对较小的数据集,需要 100% recall | Float embedding |
| IVF_FLAT | Tree-based index | 需要较高查询速度,同时需要尽可能高的 recall | Float embedding |
| IVF_SQ8 | Quantization-based index | 查询速度要求极快,内存资源有限,可以接受 recall 轻微妥协 | Float embedding |
| IVF_PQ | Quantization-based index | 查询速度要求快,内存资源有限,可以接受 recall 轻微妥协 | Float embedding |
| Index name | Index type | Application scenarios | Supported embeddings |
|---|---|---|---|
| ScaNN | Quantization-based index | 查询速度非常快;要求高 recall;内存资源充足 | Floating-point embeddings |
| HNSW | Graph-based index | 查询速度非常快;要求高 recall;内存资源相对充足 | Floating-point embeddings |
| HNSW_SQ | Graph-based index | 查询速度非常快;内存资源有限;可接受 recall 轻微妥协 | Floating-point embeddings |
| HNSW_PQ | Graph-based index | 查询速度中等;内存资源非常有限;可接受 recall 轻微妥协 | Floating-point embeddings |
| HNSW_PRQ | Graph-based index | 查询速度中等;内存资源非常有限;可接受 recall 轻微妥协 | Floating-point embeddings |
| BIN_FLAT | Standard index | 数据集较小;需要精确搜索结果;不需要压缩 | Binary embeddings |
| BIN_IVF_FLAT | Tree-based index | 要求高查询速度;要求高 recall;数据集较大 | Binary embeddings |
| SPARSE_INVERTED_INDEX | Standard index | 数据集较小;需要 100% recall;适用于稀疏向量搜索 | Sparse embeddings |
表 4.4:Milvus 向量索引类型、适用场景和支持的 embedding 格式。
接下来,我们将详细介绍上面列出的索引类型,以及磁盘索引和 GPU 索引。
FLAT:精确暴力搜索
FLAT 是最简单的索引类型。它不构建任何索引结构,而是遍历所有数据并执行精确暴力搜索。这种索引方法适合相对较小的数据集,或者精确检索准确性比查询速度更重要的评估场景。
FLAT 不压缩向量,可以确保搜索结果完全准确,并达到 100% recall。这使它成为衡量其他索引性能的理想基准。
在向量搜索中,recall 是一个重要指标,用于衡量搜索结果是否覆盖所有正确的最近邻向量。公式如下:
Recall = 检索到的正确向量数量 / 所有可能正确向量的总数
这里,分子表示算法 top-k 结果中返回的真正相关向量数量,分母表示暴力搜索返回的正确最近邻向量数量。当 recall rate 为 1.0,也就是 100% 时,表示所有正确最近邻都被检索到了,没有遗漏。当 recall rate 小于 1.0 时,表示有些正确最近邻没有被找到,说明搜索遗漏了一些相关结果。
FLAT 搜索方法是穷举式的;也就是说,对于每个查询向量,它都会与数据集中的所有向量进行比较。这种索引方法不需要任何参数配置或训练数据;数据插入后即可直接查询。不过,随着数据量增加,查询速度会显著下降;在非常大的数据集上,查询速度会变慢。
Alex:Lewis,你提供的代码中,index_type 是 AUTOINDEX。这是怎么回事?
Lewis:当设置为 AUTOINDEX 时,Milvus 会根据数据类型自动选择最合适的索引类型和参数。对于我们的简单应用来说,没有必要手动控制具体索引参数。设置 AUTOINDEX 实际上会为每个向量字段创建索引。
IVF_FLAT:倒排文件索引 + 精确搜索
IVF_FLAT 通过将向量数据划分为 nlist 个 clusters 来加速查询,并在搜索时只与若干个最相似的 clusters 进行比较。具体来说,它会先计算查询向量与每个 cluster center 的距离,然后根据 nprobe 参数选择最相似的 clusters,最后只在这些 clusters 内搜索目标向量。
通过调整 nprobe 的值,可以在准确率和查询速度之间取得平衡。nprobe 值越大,准确率越高,但查询速度越慢;反之,较小的 nprobe 会带来更快查询速度,但准确率可能较低。
IVF_FLAT 不压缩向量,因此 index 文件大小基本与原始数据大小相同。如果数据量非常大,加载索引可能会消耗大量内存。IVF_FLAT 的主要索引构建参数如下。
| 参数 | 描述 | 取值范围 | 默认值 |
|---|---|---|---|
| nlist | clusters 数量 | [1, 65536] | 128 |
下面的代码示例演示如何使用 IVF_FLAT 创建索引。
### 创建 IVF_FLAT 索引
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 100} # 设置 100 个 clusters
}
collection.create_index("vector", index_params)
### 向 collection 插入向量数据
import numpy as np
num_vectors = 100000 # 向量数量
dim = 128 # 向量维度
vectors = np.random.random((num_vectors, dim)).astype("float32")
collection.insert([vectors.tolist()]) # 插入向量
IVF_FLAT 的搜索参数如下。
| 类型 | 参数 | 描述 | 取值范围 | 默认值 |
|---|---|---|---|---|
| General search | nprobe | 要搜索的 clusters 数量 | [1, nlist] | 8 |
| Range search | max_empty_result_buckets | 范围搜索过程中,如果连续“空结果 clusters”的数量达到该值,搜索会提前终止。增大该值可以提升 recall,但会增加搜索时间 | [1, 65535] | 2 |
表 4.5:IVF_FLAT 用于普通向量检索和范围检索的搜索参数。
下面的代码示例演示如何使用 IVF_FLAT 执行搜索。
## 创建 IVF_FLAT 搜索
collection.load()
query_vectors = np.random.random((5, dim)).astype("float32") # 5 个查询向量
search_params = {"metric_type": "L2", "params": {"nprobe": 10}} # 搜索 10 个 clusters
results = collection.search(query_vectors.tolist(), "vector", search_params, limit=5)
IVF_FLAT 适合包含数万到数百万条数据的中等规模数据集,并且可以在查询速度和准确率之间取得较好平衡。IVF_FLAT 的查询速度较快,优于 FLAT,索引构建时间也相对较短。不过,使用 IVF_FLAT 时,需要仔细调优 nlist 和 nprobe 参数,以获得最佳结果。
IVF_SQ8:倒排文件索引 + 8-bit 量化
IVF_SQ8 也基于 IVF 原理构建索引。在 IVF_FLAT 基础上,它对向量应用 8-bit SQ 量化压缩,从而降低内存使用。
SQ 量化可以将每个浮点值,也就是 4 字节,转换成 1 字节的 UINT8,将存储需求降低 70% 到 75%。在相同 IVF 策略下,相比 IVF_FLAT,IVF_SQ8 生成的索引文件小得多,特别适合磁盘、CPU 或 GPU 资源有限的环境。例如,在处理 SIFT1B 数据集时,应用 IVF_SQ8 后,索引文件大小可以从约 470GB 降到 140GB。
IVF_SQ8 的索引构建参数和搜索参数与 IVF_FLAT 相同。
这种索引方法适合包含数百万到数千万向量的大规模数据集,尤其适合希望降低内存使用且可以接受轻微准确性损失的场景。使用这种方法,内存消耗会显著降低,查询速度也会提升,但准确率会略有下降,因此需要根据具体需求进行权衡。
IVF_PQ:倒排文件索引 + Product quantization
在执行 IVF 聚类之前,IVF_PQ 会先对向量应用 Product Quantization,也就是 PQ 子块划分。PQ 可以将高维向量均匀分解为 m 个低维子空间,并对每个子空间进行量化。每个子向量会使用 k-means 识别 2 nbits 个质心,然后对于每个子向量,只存储它到最近质心的距离,从而用 m nbits bits 编码整个向量。搜索时,不需要计算查询向量与所有 cluster centers 之间的距离;相反,只计算它与每个子空间中心点之间的距离,显著降低时间和空间复杂度。
相比 IVF_SQ8,IVF_PQ 实现了更高压缩率的量化,因此索引文件更小,但这也会引入一定准确性损失,导致搜索准确率下降。
IVF_PQ 的索引构建参数如下。它的搜索参数与 IVF_FLAT 相同。
| 参数 | 描述 | 取值范围 |
|---|---|---|
| nlist | clusters 数量 | [1, 65536] |
| m | PQ 分解因子数量,向量会被划分为 m 个子向量 | 必须满足 dim mod m == 0 |
| nbits | 可选,每个低维向量的量化 bit 数,默认 8 bits | [1, 64],默认是 8 |
表 4.6:IVF_PQ 索引构建参数和配置约束。
IVF_PQ 适合超大规模数据集,尤其是包含数千万到数亿向量的数据集,以及需要尽量降低内存使用的场景。虽然这种索引方法的主要优势是低内存消耗,非常适合处理极大数据集,但它的缺点是准确性损失明显,并且索引构建时间较长。
ScaNN
ScaNN,也就是 Scalable Nearest Neighbor,与 IVF_PQ 类似,也采用向量聚类和 PQ,也就是 Product Quantization 方法,并通过 SIMD,也就是 Single-Instruction/Multi-data 优化实现高效计算。不过,其内部 PQ 实现细节和优化技术与 IVF_PQ 不同。ScaNN 为 m 和 nbits 等参数提供优化后的默认设置,使其在大多数应用场景中能够获得更好性能。
ScaNN 的索引构建参数如下。
| 参数 | 描述 | 取值范围 |
|---|---|---|
| nlist | clusters 数量 | [1, 65536] |
| with_raw_data | 是否在索引文件中存储原始数据 | True 或 False,默认 True |
表 4.7:ScaNN 索引构建参数和配置选项。
对于 m 和 nbits 等参数,ScaNN 采用默认值以确保最佳性能,因此不需要显式配置。ScaNN 的搜索参数如下。
| 搜索类型 | 参数 | 描述 | 取值范围 | 默认值 |
|---|---|---|---|---|
| Standard search | nprobe | 搜索时要探测的 clusters 数量 | [1, nlist] | 8 |
| Standard search | reorder_k | 查询重排序时考虑的候选结果数量 | [top_k, ∞] | top_k |
| Range search | max_empty_result_buckets | 范围搜索过程中,如果连续这么多个 clusters 没有返回结果,搜索会提前停止。增加该值可以提升 recall,但可能增加搜索时间 | [1, 65535] | 2 |
表 4.8:ScaNN 用于标准向量检索和范围向量检索的搜索参数。
HNSW
Milvus 也支持 HNSW 索引。如前所述,这类索引基于一定规则,为数据构建一个多层导航图结构。图的上层更稀疏,节点之间距离较大;下层更密集,节点之间更接近。搜索过程从图的最顶层开始,寻找离目标向量最近的节点,然后进入下一层重复搜索。经过多次迭代后,它可以快速逼近包含最相似向量的区域。
为了增强性能,HNSW 会限制每个节点在每一层的最大连接数,也就是 M。此外,可以使用 efConstruction 参数指定索引构建时的搜索范围,使用 ef 参数指定执行搜索时的搜索范围。
HNSW 的索引构建参数如下。
| 参数 | 描述 | 取值范围 | 默认值 |
|---|---|---|---|
| M | 每一层中每个节点的最大连接数。较大值可以提升 recall,但在相同 ef 或 efConstruction 设置下也会增加计算成本 | [2, 2048] | None |
| efConstruction | 控制索引构建期间的搜索范围。增大该值会提升索引质量,但会降低索引构建速度 | [1, int_max] | None |
表 4.9:HNSW 索引构建参数及其对 recall、计算成本和构建速度的影响。
HNSW 的搜索参数如下。
| 参数 | 描述 | 取值范围 | 默认值 |
|---|---|---|---|
| ef | 定义查询执行期间的搜索范围。增大该值会提升 recall,但会降低查询速度 | [top_k, int_max] | None |
表 4.10:HNSW 查询时搜索参数及其对 recall 和查询速度的影响。
HNSW 适合包含数十万到数千万向量的中大型数据集,也适合要求快速查询和高准确率的场景。它的优势是在快速查询和高准确率检索之间取得良好平衡,但缺点是索引构建时间较长、内存消耗较高。
HNSW_SQ、HNSW_PQ 和 HNSW_PRQ
HNSW 索引家族包括 HNSW_SQ、HNSW_PQ 和 HNSW_PRQ 等派生类型。介绍如下。
HNSW_SQ:基于 HNSW 图索引,结合 SQ 对数据进行离散压缩。例如,SQ6 将浮点数转换为 64 个离散值,并使用 6 bits 编码;而 SQ8 将其量化为 256 个离散值,并使用 8 bits 编码。这种方法可以显著降低内存使用,同时更好保留数据结构。与标准 HNSW 相比,HNSW_SQ 的索引构建开销略高,但它在索引大小和查询速度之间取得了更好平衡。
HNSW_PQ:基于 HNSW 图索引,使用 PQ 进行压缩。相比 HNSW_SQ,在相同压缩率下,HNSW_PQ 的每秒查询数可能更低,但 recall 更高。同时,HNSW_PQ 的索引构建时间也会比 HNSW_SQ 略长。
HNSW_PRQ:基于 HNSW 图索引,结合 PRQ,也就是 Product Residual Quantization,提供更高准确率和更灵活的压缩率选项,不过索引构建时间也相应更长。PRQ 与 PQ 类似,二者都会将向量划分为 m 组,并使用 nbits bits 对每组进行量化。不同之处在于,PRQ 会先执行 PQ,然后计算原始向量和量化向量之间的 residual vector,再对这个 residual 应用 PQ,并重复该过程 nrq 次。因此,一个维度为 dim 的向量最终会被编码为若干 bits。
这些派生类型的索引构建参数和搜索参数这里不再详述。具体参数配置信息请参考 Milvus 官方文档。
SPARSE_INVERTED_INDEX
对于稀疏向量,Milvus 提供 SPARSE_INVERTED_INDEX 索引类型。该索引会为每个维度维护一个列表,记录所有在对应维度具有非零值的向量。执行查询时,系统会遍历查询向量的每个非零维度,并计算那些在这些维度上同样具有非零值的向量得分。
SPARSE_INVERTED_INDEX 的索引构建参数如下。
| 参数 | 描述 | 取值 | 默认值 |
|---|---|---|---|
| inverted_index_algo | 索引构建和搜索期间使用的相关算法 | DAAT_MAXSCORE、DAAT_WAND、TAAT_NAIVE | DAAT_MAXSCORE |
表 4.11:SPARSE_INVERTED_INDEX 用于稀疏向量索引和搜索的算法选项。
SPARSE_INVERTED_INDEX 的搜索参数如下。
| 参数 | 描述 | 取值范围 |
|---|---|---|
| drop_ratio_search | 搜索过程中被排除的小向量值比例 | [0, 1] |
表 4.12:SPARSE_INVERTED_INDEX 用于控制稀疏向量剪枝的搜索参数。
需要注意的是,稀疏 embeddings 的索引只支持 IP 和 BM25 指标,其中 BM25 用于全文搜索。
BIN_FLAT 和 BIN_IVF_FLAT
Milvus 为 binary embedding vectors 提供两种索引:BIN_FLAT 和 BIN_IVF_FLAT。这两种索引类型可以分别类比数值向量的 FLAT 和 IVF_FLAT 索引类型。它们的设计旨在满足不同应用场景需求。
BIN_FLAT:类似于 FLAT,BIN_FLAT 会对所有二进制向量执行穷举搜索,以确保 100% recall。该方法适合需要精确检索的小规模数据集。
BIN_IVF_FLAT:类似于 IVF_FLAT,BIN_IVF_FLAT 会先将二进制向量数据划分为多个 clusters,每个 cluster 表示一组相似向量。执行查询时,只搜索与查询向量最相似的 clusters,以加快检索过程。该方法适合需要高效查询的大规模二进制向量数据集。
对于这两种索引类型,Milvus 支持 Jaccard similarity 和 Hamming distance 作为距离指标。
磁盘索引和 GPU 索引
Alex:讲完了吗?Lewis,索引类型太多了,我听得脑子都快炸了。
Lewis:因为索引构建对于向量存储非常关键,所以我在这里列得比较完整。不过,这还不是全部——上面提到的类型都是内存索引类型。在 Milvus 中,这些内存索引用于将索引数据加载到内存中,从而实现快速查询。
但是,随着数据集规模增长,如果内存资源不足以容纳所有索引数据,就需要引入磁盘索引,例如 DiskANN,并将部分索引数据存储到磁盘上,例如 NVMe SSD,以便在内存有限时处理大规模数据集。
此外,也可以使用 GPU 索引,将向量数据加载到 GPU 显存中,并利用 GPU 的并行计算能力进行高效计算。这种方法适合需要高吞吐、低延迟和高 recall 的场景,尤其适合高并发查询。但受限于 GPU 显存容量,它通常无法处理超大规模数据集,也就是说,你的数据规模需要能够放入 GPU 显存容量中。
Alex:Lewis,我还看到下面这段输出日志。
Database: milvus
Index Mode: flat
Total Vectors: 194
Index Size: 194
Processing Time: 0.730807s
Collection Name: ...
向量数量等于索引数量,这正常吗?
Lewis:是的,有可能。在 Milvus 中,向量数量可以等于索引数量。在 FLAT 模式下,Milvus 不会压缩或分段向量数据——索引数量与向量数量完全相同。每个向量都会直接存储在索引中,所以这里 Total Vectors 和 Index Size 相等。如果使用其他索引模式,例如 IVF、HNSW,就可能不是这种情况,因为分段或压缩可能导致索引数量与向量数量不同。
如前所述,FLAT 是精确匹配模式。它会逐一比较每个向量,适合小数据集上的高精度搜索,但在大数据集上效率较低。如果未来你的向量数量增加,可以考虑 IVF_FLAT、HNSW 等模式来提升搜索效率,但你需要接受一定准确性损失。
选择合适的 metric
在 Milvus 中,相似度指标的选择对搜索结果质量至关重要。不同指标适用于不同类型的数据分布和检索需求。选择正确指标可以显著提升搜索结果的准确性和效率。
表 13 总结了 Milvus 支持的相似度指标,并解释了如何理解它们的距离或相似度值。
| Metric 类型 | 相似度距离值特点 | 相似度距离取值范围 |
|---|---|---|
| Euclidean distance,L2 | 值越小表示越相似 | [0, ∞) |
| Inner product,IP | 值越大表示越相似 | 无固定范围,取决于向量大小和方向 |
| Cosine similarity,COSINE | 值越大表示越相似 | [-1, 1] |
| Jaccard similarity | 值越小表示越相似 | [0, 1] |
| Hamming distance | 值越小表示越相似 | [0, dim,也就是向量维度] |
| BM25 | 基于词频、逆文档频率和文档归一化对相关性评分 | [0, ∞) |
表 4.13:Milvus 支持的相似度指标及距离值解释。
对于某些指标,值越小表示越相似,例如欧氏距离、Jaccard similarity、Hamming distance;而对于另一些指标,值越大表示越相似,例如 inner product、cosine similarity、BM25。
表 14 将 Milvus 向量字段类型映射到其支持和默认的 metric 类型。
| 向量字段类型 | 维度范围 | 支持的 metric 类型 | 默认 metric 类型 |
|---|---|---|---|
FLOAT_VECTOR | 2~32,768 | Cosine similarity、Euclidean distance、inner product | Cosine similarity |
FLOAT16_VECTOR | 2~32,768 | Cosine similarity、Euclidean distance、inner product | Cosine similarity |
BFLOAT16_VECTOR | 2~32,768 | Cosine similarity、Euclidean distance、inner product | Cosine similarity |
| Vector type | Dimensions or requirements | Supported metrics | Default metric |
|---|---|---|---|
| SPARSE_FLOAT_VECTOR | 不需要指定维度 | Inner Product、BM25,仅用于全文搜索 | Inner product |
| BINARY_VECTOR | 8~32,768×8,必须是 8 的倍数 | Hamming distance、Jaccard Similarity | Hamming distance |
表 4.14:不同 Milvus 向量字段类型支持的 metric 类型。
选择 metric 之前,应考虑以下问题:
向量类型:你的数据由浮点向量还是二进制向量组成?
数据特征:是否需要考虑向量大小?数据是否已经归一化?
检索场景:应用场景是文本搜索、图像向量搜索,还是 hash code 匹配等?
资源和性能要求:是否可以接受额外归一化开销?对准确率和速度有什么要求?
稠密浮点向量常用指标
接下来,我们来看稠密浮点向量的三个常用指标:欧氏距离、内积和余弦相似度。
欧氏距离:计算向量之间的欧氏距离,也就是两个点之间的直线距离。适用场景包括连续实数特征向量,例如图像、音频、视频等。优点是常用且直观;缺点是对特征值尺度敏感,可能需要归一化。
内积:计算向量之间的内积,用于衡量向量之间的相似性或相关性。适用场景包括推荐系统中计算用户向量和物品向量之间的相关性,或者文本 embeddings 的语义比较。使用内积时,要注意向量归一化,以确保结果合理。
余弦相似度:通过计算向量夹角的余弦值来衡量向量方向相似性,对向量大小不敏感。适用场景包括文本分析,例如使用 TF-IDF 或 embedding 向量表示文本,以及在文本 embeddings 中可以忽略向量长度,也就是向量的“模”或“范数”的场景,例如由 BERT、GPT、BGE 等大模型生成的 embedding 向量。
Alex:内积和余弦相似度好像都可以比较文本向量,但我不太理解它们的区别。
Lewis:主要区别在于是否关心向量大小。内积指标的取值范围没有固定边界,取决于向量的大小和方向。向量范数越大,内积值可能越大;因此,如果不做归一化,范数较大的向量可能总是产生较大结果。相比之下,余弦相似度的取值范围固定在 [-1, 1],并且与范数无关。计算时,归一化会去除向量大小的影响,只关注向量方向的相似性。
下面用几个例子说明这一点。如果你正在构建推荐系统,并通过协同过滤算法中的用户-物品矩阵分解来计算用户和物品向量的大小,也就是 magnitude,这可以反映用户偏好强度或物品重要性,并进一步用于计算你对某个物品的偏好分数。在这种情况下,建议使用内积,因为推荐系统通常需要综合考虑用户兴趣和物品重要性,内积结果可以直接用于推荐排序。
不过,如果你只关心用户和物品特征方向是否一致,而不考虑向量大小,例如在推荐系统冷启动阶段,用户数据较少时,可以使用余弦相似度。这是因为当向量大小差异较大且大小本身没有意义时,余弦相似度更适合。如果使用内积,可能导致某些范数较大的物品总是出现在推荐列表前面,而余弦相似度可以消除大小带来的影响。
另一个例子:对于文本 embeddings 和语义搜索,当使用大模型,例如 BERT、OpenAI embedding models、BGE 等,生成 embedding 向量时,你通常希望在计算文档相似度时忽略文本长度或其他与语义无关的因素,以判断两句话语义是否接近。余弦相似度只考虑方向,可以避免文本长度差异造成的偏差,这是标准做法。不过,对于已经归一化的文本向量,选择内积也是可行的,因为它计算更快,并且归一化后本质上等价于余弦相似度。
类似地,对于图 embeddings 和节点相似度,如果你的 embedding 向量大小反映了节点影响力或重要性,例如在社交网络分析中推荐朋友或社区成员,使用内积可以自然反映影响力的累积效果。反过来,如果你只想关注图中节点之间的关系方向,而忽略节点本身权重,例如在社交网络分析中寻找兴趣相似的用户群体,那么归一化后的余弦相似度可以更好突出节点之间关系的相似性。
Alex:哦,我现在明白了。
Lewis:我来考考你。如果我想比较两个由 OpenAI embedding 模型生成的文本向量,应该选择哪个 metric?
Alex:这种情况下,两者都可行。因为 OpenAI embedding 模型返回的结果向量已经归一化。这意味着归一化之后,内积和余弦相似度的数值完全相同。如果选择余弦相似度,不会出错,因为它被广泛认为是衡量语义相似度的标准方法。但如果考虑效率,直接使用内积也是不错选择,尤其是它计算速度更快,有性能优势。
Lewis:是的,在这种场景下选择内积确实更好。OpenAI 官方文档说明,他们的 embedding 模型已经归一化,因此使用内积计算相似度不仅准确,而且高效。当然,这并不意味着使用余弦相似度是错的,因为它们本质上等价。这里我给一个参考示例。
## 创建索引参数
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW", # 选择 HNSW 或其他合适索引类型
metric_type="IP", # 设置为内积
params={"M": 16, "efConstruction": 200} # HNSW 索引参数
)
## 创建索引
client.create_index(
collection_name=collection_name,
index_params=index_params
)
## 加载 collection
client.load_collection(collection_name=collection_name)
## 设置搜索参数
search_params = {
"metric_type": "IP", # 必须与 index 的 metric_type 保持一致
"params": {"ef": 64} # HNSW 查询参数,ef 值可按需调整
}
## 执行搜索
search_result = client.search(
collection_name=collection_name,
data=[query_embeddings[0]],
anns_field="vector",
param=search_params,
limit=5,
output_fields=["concept_name", "synonyms", "concept_class_id", "full_name"]
)
logging.info(f"Search result for '{query}': {search_result}")
Lewis:好了,Alex,我继续考你。假设你正在处理一个包含数百万条由 BERT 模型生成的文本 embeddings 的数据集,并希望执行高性能、高准确率的相似度搜索。
Alex:这种情况下,应该选择 HNSW 索引类型,因为它在查询速度和准确率之间提供了良好平衡,非常适合中大型数据集。metric 方面,选择余弦相似度更安全,因为在不确定这些向量是否已经归一化时,关注向量方向相似性更可靠。
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="COSINE", # 使用余弦相似度
params={"M": 16, "efConstruction": 200}
)
选择 metric 后,重要的是始终保持一致。在 Milvus 中,搜索时使用的 metric 必须与创建索引时使用的 metric 一致。
搜索时使用的 metric 必须与索引匹配
Alex:索引已经创建,向量已经存储,metric 类型也已经确定。接下来就该执行 search 了,对吧?也可以称为 retrieval。搜索时可以指定索引类型吗?
Lewis:当然不可以。索引类型是在创建索引时设置的,不能在搜索时更改。不过,你可以指定某些搜索参数,例如 metric_type 和 params。
Alex:Lewis,你看我的搜索代码,为什么报错?
from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema
## 建立 Milvus 连接
db_path = "./test.db"
client = MilvusClient(db_path)
## 定义 Collection Schema
schema = CollectionSchema(
fields=[
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=128)
]
)
## 创建 Collection
collection_name = "test_collection"
client.create_collection(
collection_name=collection_name,
schema=schema
)
## 创建索引
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="IVF_FLAT",
metric_type="IP", # 明确指定 metric 类型为 Inner Product
params={"nlist": 128}
)
client.create_index(
collection_name=collection_name,
index_params=index_params
)
## 创建查询向量
import numpy as np
query_vector = np.random.random(128).tolist()
## 执行搜索
results = client.search(
collection_name="my_collection",
data=[query_vector],
anns_field="vector",
search_params={
"metric_type": "COSINE", # 报错,因为指定了与索引不同的 metric type
"params": {"nprobe": 10}
},
limit=10,
output_fields=["id"]
)
print(results)
错误如下:
pymilvus.exceptions.MilvusException: <MilvusException: (code=1100, message=fail to search: metric type not match: invalid [expected=IP][actual=COSINE]: invalid parameter)>
Lewis:哦,你搜索时指定的 metric 和创建索引时指定的 metric 发生了冲突。当搜索参数中的 metric_type 与索引中的不匹配时,Milvus 可能会忽略搜索时指定的 metric_type,继续使用索引中设置的 metric;也可能直接抛出距离 metric 类型不匹配的错误。为了确保相似度计算正确,搜索时使用的 metric_type 应该与创建索引时使用的一致。在上面的错误示例中,索引创建时使用的是 IP,但搜索使用的是 COSINE,导致 metric 不匹配。
注意,如果想更改 metric type,必须重新创建索引。首先,删除旧索引。
### 删除旧索引,如果存在
client.drop_index(
collection_name=collection_name,
index_name="vector_index"
)
然后创建新索引。
### 创建新索引
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="IVF_FLAT",
metric_type="COSINE", # 使用余弦相似度
params={"nlist": 128}
)
client.create_index(
collection_name=collection_name,
index_params=index_params
)
## 重新加载 Collection,更新索引后必须重新加载
collection.release()
collection.load()
接下来,执行搜索时使用匹配的 metric_type。
### 设置搜索参数
search_params = {
"metric_type": "COSINE", # 与新索引中的 metric_type 一致
"params": {"nprobe": 10}
}
### 执行搜索
results = collection.search(
data=[query_vector],
anns_field="vector",
param=search_params,
limit=10,
expr=None,
output_fields=["id"]
)
接下来,我们简单介绍搜索参数调优。对于 IVF_FLAT,nlist 控制 cluster centers 数量,可以根据数据规模调整到最合适的值。例如,在 HNSW 中,efConstruction 表示查询时的搜索深度;值越大,查询准确率越高,但搜索速度可能变慢。它通常设置为预期返回结果数量的数倍。
### 创建索引参数
index_params = client.prepare_index_params()
index_params.add_index(
field_name="vector",
index_type="HNSW",
metric_type="IP",
params={"M": 16, "efConstruction": 200} # M 和 efConstruction 的值可以按需调整
)
### 创建索引
client.create_index(
collection_name=collection_name,
index_params=index_params
)
Search 和 Query:两种检索方式
配置好索引和 metric 后,Milvus 提供两种主要数据检索方式:Search 和 Query。虽然二者都会从 collection 返回 records,但它们用途不同。
Alex:Lewis,我注意到在 Milvus 中,我们主要使用两种方式检索数据:除了 Search,还有 Query。
Lewis:这两种搜索方法有不同应用场景和特点。Search 用于向量相似度搜索,基于向量距离查找相似实体;而 Query 用于精确匹配检索,基于普通字段或主键进行过滤。因此,Search 要求你提供一个查询向量,而 Query 使用过滤表达式或 ID 列表。Search 结果按相似度排序,而 Query 结果通常不排序,或者按 ID 排序。由于涉及向量计算,Search 的性能开销更高;相比之下,Query 主要涉及简单过滤操作,通常更快。
下面的代码示例展示如何使用 Search 和 Query。
from pymilvus import MilvusClient
client = MilvusClient("./wukong_new.db")
from pymilvus.model.dense import SentenceTransformerEmbeddingFunction
embedding_function = SentenceTransformerEmbeddingFunction(model_name='BAAI/bge-large-zh')
### 使用 Search 进行向量相似度搜索
search_results = client.search(
collection_name="Wukong_Monsters",
data=embedding_function(["powerful monster"]), # 查询向量
filter="difficulty == 'High'", # 可以配合过滤条件使用
limit=2, # 返回结果数量
output_fields=["monster_name", "location", "difficulty", "synonyms"]
)
### 使用 Query 进行精确匹配示例
query_results = client.query(
collection_name="Wukong_Monsters",
filter="location == 'Volcanic Cave'", # 必须指定查询条件
output_fields=["monster_name", "location", "difficulty", "synonyms"]
)
### 按主键查询
id_query_results = client.query(
collection_name="Wukong_Monsters",
ids=[1, 2], # 主键,必须匹配实际主键值才能获得查询结果
output_fields=["vector", "monster_name", "location", "difficulty", "synonyms"]
)
Alex:看来 Search 和 Query 可以一起使用。
Lewis:是的。例如,当我们想找与某篇医学论文相似的文章,但只搜索特定年份发表的文章时,可以使用带过滤条件的 Search。这正是 RAG 系统中基于 metadata 过滤的条件检索。类似地,在电商推荐系统中,我们可以先使用 Query 过滤掉库存为零的商品,然后使用 Search 查找与用户兴趣相似的商品。
使用 Milvus 进行混合搜索
Alex:Lewis,我已经理解了索引和搜索的基本模式,但还有一个问题。前面的示例中,大多数向量都是 Float Vectors,也就是以浮点格式存储的稠密向量。那么在 Milvus 中,应该如何理解 Sparse Float Vector 和 Binary Vector?
Lewis:这正是我接下来要介绍的内容——与 hybrid search 相关的知识。在某些场景中,单一检索方法不足以满足复杂查询需求。这时,hybrid search 可以通过结合多种检索方法的优势,显著改善检索结果。
浮点向量、稀疏浮点向量和二进制向量
在 Milvus 中,有三种类型的向量:floating point vectors、sparse floating point vectors 和 binary vectors。这些向量类型支持不同检索需求:语义匹配、基于关键词的匹配和快速近似匹配,对应以下数据类型:
FLOAT_VECTOR,稠密浮点向量:最常见的向量类型,通常由深度学习模型生成。它的维度固定,每个维度都有对应值。适用于语义搜索和图像特征提取等场景。特点是存储占用大、表达能力强。示例如下:
[0.2, -0.5, 0.8, ..., 0.3] # 512 维
SPARSE_FLOAT_VECTOR,稀疏浮点向量:大多数维度的值为 0,只存储非零值及其索引。虽然维度可以非常高,但实际只存储非零元素。传统 TF-IDF 或 BM25 表示就是典型稀疏向量,适用于关键词检索等场景。存储格式通常是 {index: value} 形式的 key-value pair。例如,某个向量有 10000 维,但只有 3 个非零值。
如果完整表示,它会是一个 10000 维向量,但实际只存储以下 3 个非零值:
{
15: 0.5, # 第 15 维的值为 0.5
128: 0.3, # 第 128 维的值为 0.3
945: 0.8 # 第 945 维的值为 0.8
}
BINARY_VECTOR,二进制向量:每个维度只有两种可能值:0 或 1。这是一种简单的稀疏向量表示形式,常用于快速近似匹配。它适合图像哈希、快速相似度搜索和二值化特征表示等场景。其特点包括存储需求小、计算效率高,但精度可能较低。示例:
[1, 0, 1, 1, 0, 0, 1, 0] # 500 维
这些向量类型在表达能力、存储成本和计算复杂度方面有所不同。FLOAT_VECTOR 提供最强语义表示,但存储和计算消耗最大。SPARSE_FLOAT_VECTOR 在大多数维度为零时更高效,适合基于关键词的检索。BINARY_VECTOR 最紧凑、比较速度最快,但通常精度较低。
选择使用哪种向量存储类型,取决于具体用例和速度要求。对于需要精确语义理解的场景,可以考虑使用 FLOAT_VECTOR。对于关键词匹配场景,可以考虑使用 SPARSE_FLOAT_VECTOR。对于需要快速粗排的场景,可以考虑使用 BINARY_VECTOR。
不仅 Milvus,许多现代向量数据库,例如 Astra DB、Elasticsearch、Neo4j、AzureSearch、Qdrant 等,也都支持混合向量存储,从而带来多种检索方式。
下面的简化示例展示了一个 Milvus collection 如何同时包含稠密、稀疏和二进制向量字段。这支持一个多阶段检索 pipeline,其中不同向量类型用于不同检索目的。
### 定义一个包含三种向量的 Collection
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
### 语义向量
FieldSchema(name="dense_vec", dtype=DataType.FLOAT_VECTOR, dim=768),
### 关键词向量
FieldSchema(name="sparse_vec", dtype=DataType.SPARSE_FLOAT_VECTOR),
### 快速匹配向量
FieldSchema(name="binary_vec", dtype=DataType.BINARY_VECTOR, dim=256)
]
### 多阶段检索策略
def multi_stage_search(query, collection):
### 使用 BINARY_VECTOR 进行快速粗排
stage1_results = collection.search(
binary_vectors,
"binary_vec",
limit=1000
)
### 使用 SPARSE_VECTOR 进行关键词过滤
stage2_results = collection.search(
sparse_vectors,
"sparse_vec",
expr=f"id in {[r.id for r in stage1_results]}",
limit=100
)
### 使用 DENSE_VECTOR 进行精确语义匹配
final_results = collection.search(
dense_vectors,
"dense_vec",
expr=f"id in {[r.id for r in stage2_results]}",
limit=10
)
混合检索策略实现
混合检索结合多种检索方法,以同时提升效率和相关性。一种常见模式是先使用更快的方法缩小候选集,再应用更精确的语义匹配对结果重排序。在现代信息检索系统中,混合检索策略非常常见。例如,在合规文档检索中,预过滤层使用高效关键词匹配进行初步过滤;在精确匹配层,使用 BM25 等方法进行相关性排序;在语义理解层,使用向量相似度进行更深层语义匹配。这种策略也称为 cascaded fusion 或 multi-level retrieval,也就是一个检索方法的结果作为另一个方法的输入。这样既保留了检索效率,也提升了结果相关性和准确性。
Alex:我总是听你提到 BM25。它到底和稀疏向量有什么关系?我有点忘了。
Lewis:BM25 通过计算文档中每个词的重要性来生成向量,它结合了词频和逆文档频率,其中每个维度对应词表中的一个词。由于大多数词不会出现在某个单一文档中,这个向量天然是稀疏的。例如,如果词表大小是 10000,而某个文档只用了 50 个词,那么 BM25 向量中只有这 50 个位置有非零值,这些值按照 BM25 公式计算,其余 9950 个位置全是 0。这就形成了一个稀疏向量。因此,BM25 本质上是一种特殊的稀疏向量表示。当我们说“使用 BM25”时,实际上是指使用特定公式计算稀疏向量中非零值的权重。
Milvus 官方文档提供了一个示例,展示在不同向量类型和检索方法下,同一个问题可能得到不同答案。
图 4.18:Milvus hybrid search demo,搜索结果分为 dense、sparse 和 hybrid 三列。
稠密检索主要关注语义相似度,而稀疏检索更强调关键词是否匹配。混合检索则是在二者之间寻求平衡。
使用 Milvus 构建混合检索系统
本节将演示如何为《Chronicles of the Godslayer: Housun》游戏内容构建一个混合检索系统,结合语义搜索和关键词匹配,以实现更精准的内容检索。在这个系统中,我们会使用第 3 节提到的 BGE-M3 模型,它能够生成多种格式的向量。
示例遵循四个主要步骤:准备数据集、生成稠密和稀疏 embeddings、创建 Milvus collection 和 indexes,以及执行加权混合搜索。
首先,安装必要依赖。
pip install --upgrade pymilvus "pymilvus[model]" pandas numpy pillow
接下来,加载我们准备好的《Chronicles of the Godslayer: Housun》游戏数据集。完整代码可参考 github.com/PacktPublis…
import json
from typing import Optional, Dict
with open("data/Chronicles_of_the_Godslayer/Battle_Scenes.json", 'r', encoding='utf-8') as f:
dataset = json.load(f)
docs = []
metadata = []
for item in dataset['data']:
text_parts = [item['title'], item['description']]
if 'combat_details' in item:
text_parts.extend(item['combat_details'].get('combat_style', []))
text_parts.extend(item['combat_details'].get('abilities_used', []))
if 'scene_info' in item:
text_parts.extend([
item['scene_info'].get('location', ''),
item['scene_info'].get('environment', ''),
item['scene_info'].get('time_of_day', '')
])
docs.append(' '.join(filter(None, text_parts)))
metadata.append(item)
接下来,使用 BGE-M3 模型生成稠密向量和稀疏向量。
from milvus_model.hybrid import BGEM3EmbeddingFunction
ef = BGEM3EmbeddingFunction(use_fp16=False, device="cpu")
docs_embeddings = ef(docs)
输出如下:
start to install package: datasets
successfully installed package: datasets
start to install package: peft
successfully installed package: peft
start to install package: FlagEmbedding>=1.3.3
successfully installed package: FlagEmbedding>=1.3.3
sentencepiece.bpe.model: 100%|██████| 5.07M/5.07M [00:00<00:00, 113MB/s]
tokenizer.json: 100%|█████| 17.1M/17.1M [00:00<00:00, 168MB/s]
colbert_linear.pt: 100%|█████| 2.10M/2.10M [00:00<00:00, 50.5MB/s]
bm25.jpg: 100%|█████| 132k/132k [00:00<00:00, 30.1MB/s]
sentence_bert_config.json: 100%|█████| 54.0/54.0 [00:00<00:00, 218kB/s]
pytorch_model.bin: 100%|██████| 2.27G/2.27G [00:15<00:00, 149MB/s]
Starting to generate vector embeddings......99%|█████| 2.25G/2.27G [00:14<00:00, 236MB/s]
You're using a XLMRobertaTokenizerFast tokenizer. Please note that with a fast tokenizer, using the `__call__` method is faster than using a method to encode the text followed by a call to the `pad` method to get a padded encoding.
Vector generation completed, dense vector dimension: 1024
首次运行时,系统会提示你安装一系列与 BGE embedding 模型相关的包,并下载各种关联模型。
接下来,连接 Milvus,创建 Milvus collection 以及对应 indexes。
## 导入 Milvus 并连接服务
from pymilvus import (
connections,
utility,
FieldSchema,
CollectionSchema,
DataType,
Collection
)
collection_name = "wukong_hybrid"
connections.connect(uri="./wukong.db")
## 创建 Milvus Collection 和 Indexes
fields = [
FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, auto_id=True, max_length=100),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=2048),
FieldSchema(name="id", dtype=DataType.VARCHAR, max_length=100),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=128),
FieldSchema(name="environment", dtype=DataType.VARCHAR, max_length=64),
FieldSchema(name="sparse_vector", dtype=DataType.SPARSE_FLOAT_VECTOR),
FieldSchema(name="dense_vector", dtype=DataType.FLOAT_VECTOR, dim=ef.dim["dense"])
]
schema = CollectionSchema(fields)
if utility.has_collection(collection_name):
utility.drop_collection(collection_name)
collection = Collection(name=collection_name, schema=schema, consistency_level="Strong")
collection.create_index("sparse_vector", {"index_type": "SPARSE_INVERTED_INDEX", "metric_type": "IP"})
collection.create_index("dense_vector", {"index_type": "AUTOINDEX", "metric_type": "IP"})
collection.load()
之后,将文档和向量插入 Milvus collection。
batch_size = 50
for i in range(0, len(docs), batch_size):
end_idx = min(i + batch_size, len(docs))
batch_data = []
for j in range(i, end_idx):
item = metadata[j]
batch_data.append({
"text": docs[j],
"id": item["id"],
"title": item["title"],
"category": item["category"],
"location": item.get("scene_info", {}).get("location", ""),
"environment": item.get("scene_info", {}).get("environment", ""),
"sparse_vector": docs_embeddings["sparse"]._getrow(j),
"dense_vector": docs_embeddings["dense"][j]
})
collection.insert(batch_data)
接下来,执行混合搜索并展示搜索结果。
### 定义并执行混合搜索
from pymilvus import AnnSearchRequest, WeightedRanker
query = "battle scene in the snowfield"
category = "combat"
environment = "snowfield"
limit = 5
search_type = "hybrid"
weights = {"sparse": 0.7, "dense": 1.0}
query_embeddings = ef([query])
### 构建过滤表达式
expr = None
conditions = []
if category:
conditions.append(f'category == "{category}"')
if environment:
conditions.append(f'environment == "{environment}"')
if conditions:
expr = " && ".join(conditions)
search_params = {
"metric_type": "IP",
"params": {}
}
if expr:
search_params["expr"] = expr
if search_type == "hybrid":
dense_req = AnnSearchRequest(
data=[query_embeddings["dense"][0]],
anns_field="dense_vector",
param=search_params,
limit=limit
)
sparse_req = AnnSearchRequest(
data=[query_embeddings["sparse"]._getrow(0)],
anns_field="sparse_vector",
param=search_params,
limit=limit
)
)
rerank = WeightedRanker(weights["sparse"], weights["dense"])
results = collection.hybrid_search(
reqs=[dense_req, sparse_req],
rerank=rerank,
limit=limit,
output_fields=["text", "id", "title", "category", "location", "environment"]
)[0]
else:
field = "dense_vector" if search_type == "dense" else "sparse_vector"
vec = query_embeddings["dense"][0] if search_type == "dense" else query_embeddings["sparse"]._getrow(0)
results = collection.search(
data=[vec],
anns_field=field,
param=search_params,
limit=limit,
output_fields=["text", "id", "title", "category", "location", "environment"]
)[0]
## 展示搜索结果
print(f"\nQuery: {query}")
print("\nSearch Results:")
for i, hit in enumerate(results):
print(f"\n{i+1}. {hit.entity.title}")
print(f"ID: {hit.entity.id}")
print(f"Category: {hit.entity.category}")
print(f"Location: {hit.entity.location}")
print(f"Environment: {hit.entity.environment}")
print(f"Similarity Score: {hit.distance:.4f}")
print(f"Text: {hit.entity.text[:200]}...")
输出如下:
Query: Combat scene in the snow
Search results:
Battle with the White Bone Spirit on the Snowy Mountain
ID: 455509357906362369
Category: combat
Location: Snowy mountain summit
Environment: Snow field
Similarity score: 1.0097
Text: Battle with the White Bone Spirit on the snowy mountain summit. On the snow-covered peak, Wukong and the White Bone Spirit engage in an intense showdown. Amidst the howling cold wind......
Monkey form combat demonstration......
Fiery Eyes Golden Gaze......
上面的代码使用 BGE-M3 模型将用户查询 “combat scene in the snow” 转换为稠密和稀疏向量表示,然后构造 Milvus 布尔过滤表达式,过滤 category 为 "combat" 且 environment 为 "snowfield" 的结果。接着,当 search type 设置为 hybrid 时,它会同时创建稠密向量和稀疏向量搜索请求,并使用 WeightedRanker 设置权重,例如 sparse vector 权重为 0.7,dense vector 权重为 1.0,进行加权融合排序。最后,调用 collection.hybrid_search 方法在 Milvus 中执行检索,并返回最相关的前 5 个结果。在检索过程中,也集成了结构化字段过滤,实现了典型的“语义搜索 + 规则约束”的混合 RAG 检索方式。
向量数据库和多模态检索
文本、图像、音频等不同模态的数据都可以被表示为向量,以支持更通用的相似度搜索。这使向量数据库能够支持多模态检索,即通过比较文本、图像或组合图文 embeddings 来查找结果。
在这个示例中,系统会同时检索文本向量和图像向量,并使用 RRF,也就是 reciprocal rank fusion,等结果融合策略为两个路径的检索结果分配权重,最终生成排序后的结果。
图 4.19:流程图展示文本和图像查询过程,最终进入 reranking 并输出 response。
本节介绍两种多模态和基于图像的检索方法。第一种方法使用 Visualized BGE 模型进行图文 embedding 和匹配。第二种方法使用 ResNet-34 提取图像特征,用于视觉相似度检索。
使用 visualized BGE 模型进行多模态检索
本节将演示如何使用 Visualized BGE 模型和 Milvus 构建一个多模态检索系统。该系统支持基于图像和文本的组合查询,使学习者能够搜索游戏中的相似场景、特定战斗时刻等。
这里使用一个包含《黑神话:悟空》相关图像的示例数据集。该数据集也可以在 Lewis 的 GitHub 仓库中找到。
示例遵循五个主要步骤:加载多模态 embedding 模型、准备图像数据集、生成图像 embeddings、将向量存入 Milvus,以及执行图文相似度搜索。
加载 visualized BGE 模型
首先,加载 Visualized BGE 模型来生成图文 embeddings。
图 4.20:名为 wukongdataset 的文件夹,其中包含十个 JPG 图片文件和一个 metadatajson 文件。
import torch
from visual_bge.modeling import Visualized_BGE
from dataclasses import dataclass
from typing import List, Optional
import json
from tqdm import tqdm
import numpy as np
import cv2
from PIL import Image
from pymilvus import MilvusClient
class WukongEncoder:
"""多模态编码器:将图像和文本编码为向量"""
def __init__(self, model_name: str, model_path: str):
self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path)
self.model.eval()
def encode_query(self, image_path: str, text: str) -> list[float]:
"""编码组合图像和文本查询"""
with torch.no_grad():
query_emb = self.model.encode(image=image_path, text=text)
return query_emb.tolist()[0]
def encode_image(self, image_path: str) -> list[float]:
"""仅编码图像"""
with torch.no_grad():
query_emb = self.model.encode(image=image_path)
return query_emb.tolist()[0]
### 初始化编码器
model_name = "BAAI/bge-base-en-v1.5"
model_path = "./Visualized_base_en_v1.5.pth"
encoder = WukongEncoder(model_name, model_path)
创建图像数据集
加载模型后,定义一个数据集类来管理图像 metadata。每条图像记录都包含图像路径、标题、类别、描述、标签、游戏章节、位置、角色、能力、环境和时间等信息。
@dataclass
class WukongImage:
"""图像 metadata 结构"""
image_id: str
file_path: str
title: str
category: str
description: str
tags: List[str]
game_chapter: str
location: str
characters: List[str]
abilities_shown: List[str]
environment: str
time_of_day: str
class WukongDataset:
"""图像数据集管理类"""
def __init__(self, data_dir: str, metadata_path: str):
self.data_dir = data_dir
self.metadata_path = metadata_path
self.images: List[WukongImage] = []
self._load_metadata()
def _load_metadata(self):
"""加载图像 metadata"""
with open(self.metadata_path, 'r', encoding='utf-8') as f:
data = json.load(f)
for img_data in data['images']:
self.images.append(WukongImage(**img_data))
### 初始化数据集
dataset = WukongDataset("data/multimodal", "data/multimodal/metadata.json")
这个数据集类可以让每张图像及其 metadata 在插入 Milvus 前保持绑定。
生成图像 embeddings
metadata 加载完成后,为数据集中的所有图像生成 image embeddings。每张图像都会被转换成一个向量表示,之后可以存储并在 Milvus 中搜索。
### 为所有图像生成 embedding 向量
image_dict = {}
for image in tqdm(dataset.images, desc="Generating image embeddings"):
try:
image_dict[image.file_path] = encoder.encode_image(image.file_path)
except Exception as e:
print(f"Failed to process image {image.file_path}: {str(e)}")
continue
print(f"Successfully encoded {len(image_dict)} images")
输出如下:
Generating image embeddings: 100%|██████████| 10/10 [00:00<00:00, 29.35it/s]
Successfully encoded 10 images
此时,每张图像都已经被转换为向量,并临时存储在 image_dict 中。
将图像向量存储到 Milvus
生成 embeddings 后,创建一个 Milvus collection 来存储图像向量及其 metadata。这样,每个图像向量都可以和标题、类别、位置、角色、环境等字段一起被搜索。
## 连接或创建 Milvus
collection_name = "wukong"
milvus_client = MilvusClient(uri="./wukong_images.db")
## 创建向量 Collection
dim = len(list(image_dict.values())[0])
milvus_client.create_collection(
collection_name=collection_name,
dimension=dim,
auto_id=True,
enable_dynamic_field=True
)
## 向 Milvus 插入数据
insert_data = []
for image in dataset.images:
if image.file_path in image_dict:
insert_data.append({
"image_path": image.file_path,
"vector": image_dict[image.file_path],
"title": image.title,
"category": image.category,
"description": image.description,
"tags": ",".join(image.tags),
"game_chapter": image.game_chapter,
"location": image.location,
"characters": ",".join(image.characters),
"abilities": ",".join(image.abilities_shown),
"environment": image.environment,
"time_of_day": image.time_of_day
})
result = milvus_client.insert(
collection_name=collection_name,
data=insert_data
)
print(f"Index building completed, {result['insert_count']} records inserted in total")
输出如下:
Index construction complete, 10 records inserted in total
接下来,现在图像向量及其 metadata 已经存储在 Milvus 中,可以用于相似度搜索。
创建多模态搜索函数
向量存储到 Milvus 后,定义一个多模态查询搜索函数。该函数接收查询图像和查询文本,将它们转换为查询向量,应用可选 metadata filters,并返回最相似图像。
def search_similar_images(
query_image: str,
query_text: str,
limit: int = 9,
filters: Optional[dict] = None
) -> List[dict]:
# 生成查询向量
query_vec = encoder.encode_query(query_image, query_text)
# 构建搜索参数
search_params = {
"metric_type": "COSINE",
"params": {"nprobe": 10}
}
# 添加过滤条件
if filters:
filter_conditions = []
for key, value in filters.items():
if isinstance(value, list):
filter_conditions.append(f"{key} in {value}")
else:
filter_conditions.append(f"{key} == '{value}'")
search_params["expr"] = " and ".join(filter_conditions)
# 执行搜索
results = milvus_client.search(
collection_name=collection_name,
data=[query_vec],
output_fields=[
"image_path", "title", "category", "description",
"tags", "game_chapter", "location", "characters",
"abilities", "environment", "time_of_day"
],
limit=limit,
search_params=search_params
)[0]
return results
这个函数执行核心多模态检索操作:它将图像和文本查询组合成一个向量,然后在 Milvus 中搜索相似向量。
执行一个图像搜索查询示例
现在,使用一张图像和文本 prompt “Find similar snowy combat scenes.” 执行示例查询。在这个例子中,搜索被过滤为只返回 environment 为 snow 且 category 为 combat 的结果。
## 执行查询
query_image = "data/multimodal/query_image.jpg"
query_text = "Find similar snowy combat scenes"
filters = {
"environment": "snow",
"category": "combat"
}
results = search_similar_images(
query_image=query_image,
query_text=query_text,
limit=9,
filters=filters
)
## 输出详细信息
print("\nSearch results:")
for idx, result in enumerate(results):
print(f"\nResult {idx}:")
print(f"Image: {result['entity']['image_path']}")
print(f"Title: {result['entity']['title']}")
print(f"Description: {result['entity']['description']}")
print(f"Similarity score: {result['distance']:.4f}")
输出如下:
Results have been saved to search_results.jpg
Search results:
Result 0:
Image: ./wukong_dataset/01.jpg
Title: Battle on the Snow Mountain
Description: Wukong on the snow mountain, gazing into the distance, attempting to unleash the ultimate skill
Similarity score: 0.9454
Result 1:
Image: ./wukong_dataset/09.jpg
Title: Mountain Temple
Description: Ancient temple deep in the mountains, covered in snow, appearing to be an exceptionally tranquil place
Similarity score: 0.8567
...
返回结果展示了基于组合图文查询得到的最相似图像。例如,最高结果是一个包含悟空的雪地战斗场景,与查询意图匹配。
可视化搜索结果
为了更方便检查检索结果,创建一个可视化函数,将查询图像与 top 检索图像并排展示。
import numpy as np
import cv2
from PIL import Image
from typing import List, Dict
def visualize_results(query_image: str, results: List[dict], output_path: str):
# 设置图像大小和网格参数
img_size = (300, 300)
grid_size = (3, 3)
# 创建画布
canvas_height = img_size[0] * (grid_size[0] + 1)
canvas_width = img_size[1] * (grid_size[1] + 1)
canvas = np.full((canvas_height, canvas_width, 3), 255, dtype=np.uint8)
# 添加查询图像
query_img = Image.open(query_image).convert("RGB")
query_array = np.array(query_img)
query_resized = cv2.resize(query_array, (img_size[0] - 20, img_size[1] - 20))
bordered_query = cv2.copyMakeBorder(
query_resized, 10, 10, 10, 10,
cv2.BORDER_CONSTANT,
value=(255, 0, 0)
)
canvas[:img_size[0], :img_size[1]] = bordered_query
# 添加结果图像
for idx, result in enumerate(results[:grid_size[0] * grid_size[1]]):
row = (idx // grid_size[1]) + 1
col = idx % grid_size[1]
img = Image.open(result["entity"]["image_path"]).convert("RGB")
img_array = np.array(img)
resized = cv2.resize(img_array, (img_size[0] - 4, img_size[1] - 4))
y_start = row * img_size[0]
x_start = col * img_size[1]
canvas[y_start:y_start + img_size[0], x_start:x_start + img_size[1]] = resized
## 添加相似度分数
score_text = f"Score: {result['score']:.2f}"
cv2.putText(
canvas,
score_text,
(x_start + 10, y_start + img_size[0] - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.imwrite(output_path, canvas)
## 可视化结果
visualize_results(query_image, results, "search_results.jpg")
可视化后,风格和内容上与查询图像最相似的图像会排在前面。第一个结果可能就是查询图像本身,如果查询图像也属于已索引数据集,这是预期现象。完整代码可参考 github.com/PacktPublis…
图 4.21:一组奇幻战士场景拼图,包含雪地景观、戏剧性战斗和神话野兽。
使用 ResNet-34 提取图像特征并执行检索
你也可以使用传统深度学习模型提取图像特征,而不是依赖多模态 embedding 模型。在这种方法中,ResNet-34 被用于生成仅图像特征向量,然后存入 Milvus 进行相似度搜索。
与 Visualized BGE 方法不同,ResNet-34 只提取图像特征。这意味着它可以检索视觉相似的图像,但不支持图文组合查询。
使用 ResNet-34 提取图像特征
首先,安装所需依赖,并定义一个基于 ResNet-34 的特征提取器。该模型会处理每张图像,并将其转换为归一化特征向量。
pip install pymilvus>=2.4.2 timm torch numpy sklearn pillow
import torch
from PIL import Image
import timm
from sklearn.preprocessing import normalize
from timm.data import resolve_data_config
from timm.data.transforms_factory import create_transform
class WukongFeatureExtractor:
def __init__(self):
# 加载 ResNet-34
self.model = timm.create_model('resnet34', pretrained=True, num_classes=0)
self.model.eval()
# 配置图像预处理
config = resolve_data_config({}, model='resnet34')
self.preprocess = create_transform(**config)
def extract_features(self, image_path):
# 处理游戏场景图像
image = Image.open(image_path).convert("RGB")
input_tensor = self.preprocess(image).unsqueeze(0)
# 提取特征
with torch.no_grad():
features = self.model(input_tensor)
return normalize(features.squeeze().numpy().reshape(1, -1)).flatten()
这个特征提取器会将每张图像转换为固定长度视觉特征向量。
将场景特征存储到 Milvus
接下来,创建一个 Milvus collection,用于存储提取出的场景特征。每条记录都会存储特征向量,以及文件名、场景类型和位置等 metadata。
from pymilvus import MilvusClient
### 连接或创建 Milvus
client = MilvusClient(uri="wukong_images.db")
### 创建游戏场景 Collection
client.create_collection(
collection_name="wukong_scenes",
dimension=512,
auto_id=True,
enable_dynamic_field=True,
metric_type="COSINE"
)
创建 collection 后,处理数据集中的每张图像,并将其特征向量插入 Milvus。
import os
extractor = WukongFeatureExtractor()
scenes_dir = "data/multimodal"
### 索引每个场景
for scene in os.listdir(scenes_dir):
if scene.endswith(('.jpg', '.png')):
path = os.path.join(scenes_dir, scene)
features = extractor.extract_features(path)
### 添加场景 metadata
client.insert(
"wukong_scenes",
{
"vector": features,
"filename": path,
"scene_type": "combat" if "battle" in scene else "environment",
"location": scene.split('_')[0] # 假设文件名格式为 location_scenetype_id
}
)
现在,collection 中已经包含图像特征向量,可以使用视觉相似度进行搜索。
搜索相似场景
最后,定义一个搜索函数,从查询图像提取特征,并从 Milvus 中检索视觉上相似的场景。可以使用可选过滤条件,例如 scene_type,限制搜索结果。
def find_similar_scenes(query_image, top_k=5, scene_type=None):
### 从查询图像中提取特征
query_features = extractor.extract_features(query_image)
### 准备搜索参数
search_params = {"metric_type": "COSINE"}
### 如果指定了场景类型,则添加过滤条件
if scene_type:
search_params["expr"] = f"scene_type == '{scene_type}'"
### 搜索相似场景
results = client.search(
"wukong_scenes",
data=[query_features],
output_fields=["filename", "scene_type", "location"],
search_params=search_params,
limit=top_k
)
return results[0] # 返回第一个查询的结果
现在运行一个示例搜索,查找相似战斗场景。
from IPython.display import display
## 搜索相似战斗场景
combat_scene = "data/multimodal/query_image.jpg"
results = find_similar_scenes(combat_scene, scene_type="combat")
## 展示结果
print("Query scene:")
display(Image.open(combat_scene).resize((200, 200)))
print("\nSimilar combat scenes:")
for hit in results:
scene_path = hit["entity"]["filename"]
similarity = hit["distance"]
print(f"\nSimilarity: {similarity:.2f}")
print(f"Scene location: {hit['entity']['location']}")
display(Image.open(scene_path).resize((200, 200)))
输出示例:
Similar combat scene: 09.jpg (snowy mountain)
Similarity: 1.00
通过这种基于图像特征的方法,系统可以检索相同或视觉上相似的图像内容。不过,由于 ResNet-34 只提取视觉特征,这种方法仅限于图像相似度检索,并不是完整的多模态检索方案。
RAG 系统中的数据维护和向量存储 CRUD 操作
数据被向量化并存储后,工作并没有结束。在 RAG 系统中,知识库必须随着时间持续维护,以确保源数据变化时,embeddings、metadata 和 indexes 仍保持一致。本节解释关键数据维护原则,并演示 Milvus 中常见的插入、更新、删除和 collection 管理操作。
RAG 系统中的数据流维护与管理
无论是传统数据还是向量数据,数据流维护的核心原则如下。
一致性:数据的 vector embeddings、metadata 和 indexes 应始终保持同步。每次 CRUD 操作后,都要确保数据库和检索系统中的数据状态一致。
实时能力:对于动态场景,例如问答系统或推荐系统,需要实时新增或更新数据,以降低延迟。可以使用异步任务或批处理提升性能。
数据审计和可追溯性:记录每次 CRUD 操作,确保数据变化可追踪,使错误排查更容易。
自动化:使用自动化脚本或服务检测并清理或更新过期数据,避免人工操作低效。
周期性任务调度是数据流维护的基础。通过 cron 或 Airflow 等工具,可以定期检查系统的数据流状态,例如清理过期数据或重建索引。这种方式不仅可以确保系统长期稳定运行,也能降低人工维护复杂度。
对于实时更新需求,可以使用 Kafka 或 RabbitMQ 等消息队列技术监听数据 CRUD 事件。一旦检测到变化,系统会自动触发相关操作,实现知识库实时更新。
为了进一步优化更新流程,可以采用增量索引更新技术,只更新受影响的数据部分,而不是重建整个索引。这种方法显著提升效率,并节省系统资源。
性能优化可以从多个层面入手。例如,批量处理新增或删除操作,不仅可以减少频繁访问数据库,也能显著提升操作效率并降低系统负载。根据业务需求调整索引参数,例如 nlist 和 nprobe,可以在检索速度和准确率之间取得平衡,从而改善用户体验。对于高频访问数据,启用缓存机制,将其存储在内存或高速缓存中,可以降低数据库查询负载并加快系统响应。这些措施有助于确保系统在规模扩大时仍能持续高效稳定运行。
RAG 系统数据管理需要分层策略,将数据分为两类:核心数据和动态数据。核心数据通常稳定且长期有效,不需要频繁修改;而动态数据则需要根据业务需求频繁更新。通过分层管理数据,并将增删改操作集中在动态数据上,可以提升整体效率。
为了增强系统可靠性,可以为数据更新引入版本控制机制。每次更新都保留历史版本,以便发生错误时快速回滚。定期执行多副本备份同样重要。通过定期备份知识库数据,可以有效防止硬件故障或人为错误造成的数据丢失,确保系统安全性和可用性。
Milvus 中的向量插入、删除和更新操作
当 RAG 系统中的原始数据源发生变化时,需要确保 embeddings、metadata 和 retrieval indexes 同步更新。Insert,也就是新增或插入数据操作,可用于扩展系统知识库、纠正错误、改进已有数据,或者根据新需求调整内容。此外,它也有助于清理过期、无关或错误数据。
插入向量实体
下面的代码示例演示如何向现有向量数据库 collection 插入新的 embedding vector data。
from pymilvus import MilvusClient
import numpy as np
## 使用本地 SQLite 模式连接现有 Milvus
db_path = "./test_vector.db"
client = MilvusClient(db_path)
## Collection 名称
collection_name = "my_collection"
data_insert = [
{"id": 100001, "vector": np.random.random(128).astype("float32").tolist()},
{"id": 100002, "vector": np.random.random(128).astype("float32").tolist()}
]
res = client.insert(collection_name=collection_name, data=data_insert)
Upserting 向量实体
在 Milvus 中,可以使用 Upsert 操作,也就是 insert or update,表示“如果不存在则插入,如果存在则更新”,将更新和插入数据的功能结合起来。
图 4.22:中文流程图展示 AutoID 过程,根据是否启用 AutoID 分支。
Milvus 会通过检查主键是否存在来判断执行 update 还是 insert 操作。使用该操作时,必须确保 Upsert 请求中的向量实体,也就是 Milvus 中的 Entity,包含主键,否则会发生错误。收到 Upsert 请求后,Milvus 会执行以下流程。
检查 collection 的 primary field 是否启用了 AutoId。
如果启用,Milvus 会用自动生成的主键替换 entity 的主键,并插入数据。
如果未启用,Milvus 会使用 entity 提供的主键插入数据。
基于 Upsert 请求中包含的 entity 主键值执行删除操作。
data_upsert = [
{"id": 100001, "vector": np.random.random(128).astype("float32").tolist()}, # 更新已有 ID
{"id": 100010, "vector": np.random.random(128).astype("float32").tolist()} # 插入新 ID
]
res = client.upsert(collection_name=collection_name, data=data_upsert)
删除向量实体
如果需要删除数据,可以通过主键或过滤条件指定不再需要的 entities。
## 通过主键删除 Entities
res = client.delete(
collection_name=collection_name,
ids=[100002, 100010]
)
## 通过过滤条件删除 Entities
res = client.delete(
collection_name=collection_name,
filter="difficulty == 'high'"
)
完整程序请访问 Lewis 的 GitHub 仓库。
向量数据库中的 Collection 操作
在向量数据库中,collections 是数据存储和管理的基本单位,类似传统数据库中的表。Collection 操作包括创建、删除、查看、清理和索引管理,贯穿向量数据生命周期的所有阶段。
查看 collection 信息
创建 collection 后,可以列出所有 collections,并检查某个 collection 的结构。
## 列出当前所有 Collections
collections = client.list_collections()
print("Current collection list:", collections)
下面的示例查看详细 collection 信息,包括字段结构和 metadata。
info = client.describe_collection(collection_name)
print("\nBasic collection information:")
print("Collection name:", info["collection_name"])
print("Description:", info["description"])
print("Field information:")
for field in info["fields"]:
print(f" - {field['name']} ({field['type']})")
示例输出:
Basic collection information:
Collection name: my_collection
Description:
Field information:
- id (5)
- vector (101)
将 collection 加载到内存
执行向量搜索之前,需要先将 collection 加载到内存中。
client.load_collection(collection_name)
print(f"Collection '{collection_name}' has been loaded into memory")
释放内存资源
当不再需要使用 collection 时,可以释放它占用的内存以节省资源。
client.release_collection(collection_name)
print(f"Collection '{collection_name}' has been released from memory")
删除 collection
删除 collection 会永久移除该 collection 及其所有数据。请谨慎操作。
client.drop_collection(collection_name)
print(f"Collection '{collection_name}' has been permanently deleted")
完整程序请访问 Lewis 的 GitHub 仓库。完整代码可参考 github.com/PacktPublis…
总结
Vector stores 是为高效存储和检索高维向量而优化的。与传统基于标量的数据库不同,vector stores 被设计用于管理复杂 vector embeddings,并支持快速相似度搜索,从而实现快速查询响应并提升数据检索性能。
在本章中,我们首先探索了 LlamaIndex 中默认的向量存储机制,并研究了 RAG 系统中向量索引是如何构建的。随后,我们介绍了向量数据库的主要组成部分,对比了几种主流向量数据库方案,并以 Milvus 作为实践示例,解释了索引类型、相似度指标和搜索配置。
我们还讨论了向量存储如何从稠密向量扩展到稀疏向量和二进制向量。这些不同向量格式为混合检索提供了基础,使语义搜索和基于关键词的检索能够结合起来,从而提升搜索相关性。此外,我们通过嵌入图像探索了多模态检索,并演示了如何将 Milvus 与 BGE、ResNet-34 等模型结合,支持基于图像和图文的检索场景。
最后,我们介绍了 RAG 系统中的数据维护和 CRUD 操作,包括如何在 Milvus 中插入、更新、删除、检查、加载、释放和管理向量 collections。在下一章中,我们将进入检索技术,重点关注 RAG 系统中的检索前处理、检索优化和检索后方法。