ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Milvus向量检索实战:稀疏、密集、二进制向量的选型与调参指南

Milvus向量检索实战:稀疏、密集、二进制向量的选型与调参指南 聊到向量检索这两年最常被问到的三个问题几乎绕不开稀疏向量“密集向量”和“二进制向量”。很多人一上来就对着 Milvus 的文档发懵三种向量到底有什么区别我的场景该用哪一种检索时选余弦值还是内积更麻烦的是很多人照着网上的例子把数据怼进 Collection 里结果召回效果稀烂或者查询慢得没法忍。这篇文章我就把自己在 Milvus 里折腾这三种向量的完整过程整理出来。不会只讲概念而是从环境安装到 Schema 设计从向量写入到检索调参尽量把每个环节的关键决策点都拆开说清楚。如果你正在做 RAG、图片去重、推荐召回或者多模态检索这份实践指南可以帮你省掉不少试错的弯路。1. 向量检索必读三种向量类型到底在解决什么问题1.1 从“向量检索”说起向量检索的本质是把图片、文本、音频这类非结构化数据映射成高维空间里的坐标点然后通过计算坐标点之间的距离来度量它们的相似度。这个过程里最重要的是两件事一是用什么模型产出向量二是用什么引擎把向量存下来并快速搜索。Milvus 就是那个引擎角色它本身不负责生成向量只负责把向量索引好、搜索快。在 Milvus 里向量字段的类型直接决定了你能用哪种索引、哪种度量方式、以及大概要花多少内存。很多人一开始不重视这个选择等到数据量上来再改 Schema那代价是非常痛苦的。所以在写代码之前先把三种向量的特性和适用边界搞清楚。1.2 密集向量Dense Vector密集向量是最常见的向量形式几乎所有深度学习的 embedding 输出都是密集向量。它的特点很直白向量绝大多数维度都不为零长度固定比如 BERT 的 768 维、OpenAI 的 1536 维、CLIP 的 512 维等。因为密集向量包含的信息密度高所以在语义检索、相似图片匹配、推荐召回这类任务里效果非常好。Milvus 对密集向量的支持也是最成熟的默认的 AUTOINDEX 可以自动选择合适索引HNSW、IVF 这些经典索引也都能直接用。但密集向量的问题也明显维度一旦高起来内存开销和计算量都会跟着涨。1536 维的向量一亿条数据就是大概 600GB 的裸内存再算上索引的额外开销对硬件是不小的考验。1.3 稀疏向量Sparse Vector稀疏向量和密集向量正好相反绝大部分维度都是零只有少数维度有值。经典的例子是 BM25 这类词频统计特征词的维度就是整个词表的大小可能有好几万甚至几十万维但一篇文章里真正出现过的词就那么几百个所以非零维度非常少。在 Milvus 中稀疏向量用SPARSE_VECTOR类型表示存储和计算时只处理非零维度内存和计算量都大大降低。它对精确关键词匹配、专有名词识别、以及那些语义模型容易翻车的场景特别有用。比如一个医疗术语缩写用 Dense Embedding 检索可能因为语义相近而召回一堆不相关的东西但稀疏向量能精确命中词项。所以现在很多生产级 RAG 系统会把 Dense 和 Sparse 两条路都走最后做融合排序兼顾语义泛化和关键词精确性。1.4 二进制向量Binary Vector二进制向量是另一种极端每个维度只能取 0 或 1存储时按位压缩。比如一个 512 维的二进制向量实际只占 64 字节只有同样维度 Float32 向量的十六分之一。Milvus 里添加二进制向量字段时维度必须填能被 8 整除的数因为底层是按字节存的。二进制向量非常适合对存储极度敏感的场景比如大规模图片哈希去重、视频指纹识别、快速近似去重。你可以用感知哈希算法把图片转成一个 256 位的二进制向量在海量图库里快速找到完全相同或非常接近的图。当然二进制向量牺牲了精度不适合做细粒度语义匹配。它更擅长粗排或者过滤先用二进制向量快速圈定一个候选集再用密集向量做精排这是很常见的工程组合拳。1.5 三种向量的适用场景对比维度密集向量稀疏向量二进制向量表现形式绝大多数维度非零绝大多数维度为零每个维度是 0 或 1典型维度512 / 768 / 1536词表大小几万到几十万64 / 128 / 256 位代表算法BERT、CLIP、OpenAI EmbeddingBM25、SPLADE、Learned Sparse感知哈希、SimHash擅长任务语义相似、推荐召回关键词精确匹配、术语检索去重、指纹、粗筛磁盘与内存高较低最低Milvus 常用索引AUTOINDEX、HNSW、IVFSPARSE_INVERTED_INDEXBIN_IVF_FLAT这张表是一个快速入口。实际项目里不用死守某一类很多高流量场景是三类向量共存的后面我会讲怎么在一个 Collection 里同时管理它们。2. 环境准备Milvus Standalone 模式安装与连接2.1 为什么选 Standalone 模式Milvus 有 Standalone 和 Cluster 两种部署形态。很多人第一次接触就被各种组件名吓到etcd、MinIO、Pulsar、coordinator 等等。其实做学习和中小型项目Standalone 模式足够用了它把所有组件打包在一个进程组里通过 Docker Compose 一条命令就能拉起来。我建议不要一上来就折腾 Cluster。先把 Standalone 玩熟练理解 Collection、Index、Partition 这些核心概念后面真有扩容需求再平滑迁移到 Cluster。Standalone 内部也包含 etcd 和 MinIO只是以依赖组件的形式一起跑数据持久化和元数据管理都没问题。2.2 安装步骤Docker Compose 拉起 Milvus以 v2.4 以上版本为例我使用的是milvusdb/milvus官方镜像。先在项目目录创建docker-compose.yml内容核心是几个服务etcd 负责元数据minio 负责对象存储milvus 是主服务。version: 3.5 services: etcd: image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODErevision - ETCD_AUTO_COMPACTION_RETENTION1000 - ETCD_QUOTA_BACKEND_BYTES4294967296 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd command: etcd -advertise-client-urlshttp://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: image: minio/minio:RELEASE.2023-03-20T20-16-18Z environment: MINIO_ACCESS_KEY: minioadmin MINIO_SECRET_KEY: minioadmin volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data command: minio server /minio_data healthcheck: test: [CMD, curl, -f, http://localhost:9000/minio/health/live] interval: 30s timeout: 20s retries: 3 milvus: image: milvusdb/milvus:v2.4.5 command: [milvus, run, standalone] ports: - 19530:19530 - 9091:9091 volumes: - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus depends_on: - etcd - minio文件准备好后在终端执行docker compose up -d第一次拉镜像会比较久耐心等。启动完成后检查一下进程状态docker compose ps看到 milvus 正常 runningetcd 和 minio 的 health 也都是 healthy说明服务已经是可用的状态。2.3 连接与初始化代码服务起来之后用 PyMilvus 连接。先装依赖pip install pymilvus然后写一段最基础的连接测试from pymilvus import connections connections.connect( aliasdefault, host127.0.0.1, port19530 ) print(Milvus connection established)这里 host 和 port 就是我们映射出来的参数。如果是在服务器上部署记得在你的云安全组里放行 19530 端口否则外部连不上。2.4 资源规划与注意事项Standalone 模式默认配置适合小规模数据但有几个坑我要提前说第一Swap 要控制好。Milvus 部署后如果发现查询很慢先看看是不是物理内存不够导致内存和磁盘交换频繁。建议给 Milvus 容器至少 8GB 内存数据量大时再往上加。第二MinIO 的数据不要随便删。volumes目录里的/milvus是数据持久化目录MinIO 里存的是 segment 和 index 文件。如果你把容器删了重建卷还在那就没问题如果卷也被清掉等于从头再来。第三生产环境不要长期用 root 权限跑容器。我在实际部署时吃过这个亏容器内写出来的文件属主是 root后续想备份目录时权限处理特别麻烦。可以在 docker-compose 里指定 user或者容器起来后统一 chown。3. 实操核心在 Milvus 中为三种向量建 Schema、写数据3.1 创建 Collection 前要想清楚的事Milvus 的设计里Collection 可以类比成关系型数据库的表。在创建之前你需要明确几个问题主键用自增还是自定义需要哪些标量字段做过滤向量字段需要支持什么度量方式是否需要多个向量字段共存一个常见误区是很多人把所有向量都塞进一个字段里。实际上 Milvus 从 2.4 开始支持一个 Collection 里同时存在多个向量字段而且可以是不同类型的向量。这意味着你完全可以在一个 Collection 里同时维护 Dense、Sparse、Binary 三种向量检索时分别算分最后再做融合。3.2 密集向量 Collection 示例我们先创建一个最经典的密集向量 Collection。假设我们做一个商品图片检索向量由 CLIP 模型产出维度是 512。from pymilvus import ( connections, CollectionSchema, FieldSchema, DataType, Collection ) connections.connect(host127.0.0.1, port19530) # 商品ID item_id FieldSchema( nameitem_id, dtypeDataType.INT64, is_primaryTrue, auto_idFalse ) # 商品图片路径作为标量字段保存 image_path FieldSchema( nameimage_path, dtypeDataType.VARCHAR, max_length500 ) # CLIP 产出的 512 维密集向量 dense_embedding FieldSchema( namedense_embedding, dtypeDataType.FLOAT_VECTOR, dim512 ) schema CollectionSchema( fields[item_id, image_path, dense_embedding], descriptionProduct image retrieval collection ) collection Collection( nameproduct_images, schemaschema )创建好 Collection 之后需要创建索引。密集向量用 HNSW 或者 AUTOINDEX 都行。我倾向于先用 HNSW因为它在召回率和查询延迟的平衡上表现最好。index_params { index_type: HNSW, metric_type: COSINE, params: {M: 16, efConstruction: 200} } collection.create_index( field_namedense_embedding, index_paramsindex_params )这里metric_type我选了 COSINE对应的就是大家常说的余弦值。如果你的向量已经做过归一化用内积 IP 和余弦效果等价但 IP 在某些索引上计算更快。3.3 稀疏向量 Collection 示例接下来是稀疏向量。这里我演示一个基于 BM25 风格特征的场景。你可以用文本分词工具先把文本变成稀疏字典格式是{token_id: weight}的映射。from pymilvus import ( FieldSchema, CollectionSchema, DataType, Collection ) doc_id FieldSchema( namedoc_id, dtypeDataType.INT64, is_primaryTrue ) doc_text FieldSchema( namedoc_text, dtypeDataType.VARCHAR, max_length1000 ) sparse_vector FieldSchema( namesparse_embedding, dtypeDataType.SPARSE_FLOAT_VECTOR ) schema CollectionSchema( fields[doc_id, doc_text, sparse_vector], descriptionText retrieval with sparse vectors ) collection Collection( nametext_sparse, schemaschema )稀疏向量字段不需要指定 dim因为它的维度由实际数据的最大 token id 决定。这点和密集向量很不一样很多人刚开始会纠结不填维度会不会报错放心不会。稀疏向量的索引类型是SPARSE_INVERTED_INDEX度量方式通常用IP因为稀疏向量经常是由词频或模型权重组成内积能直接反映共同词项的得分。sparse_index_params { index_type: SPARSE_INVERTED_INDEX, metric_type: IP } collection.create_index( field_namesparse_embedding, index_paramssparse_index_params )写入数据时稀疏向量要传稠密的字典结构而不是列表data [ { doc_id: 1, doc_text: Milvus is a vector database, sparse_embedding: {0: 0.5, 10: 1.2, 35: 0.8} }, { doc_id: 2, doc_text: Vector databases are powerful, sparse_embedding: {10: 0.6, 20: 0.9, 35: 1.1, 40: 0.3} } ] collection.insert(data)3.4 二进制向量 Collection 示例二进制向量的 Schema 设计和密集向量很像只是dtype换成BINARY_VECTOR而且维度必须是 8 的倍数。image_hash FieldSchema( nameimage_hash, dtypeDataType.BINARY_VECTOR, dim256 ) hash_collection Collection( nameimage_dedup, schemaCollectionSchema( fields[ FieldSchema(nameimage_id, dtypeDataType.INT64, is_primaryTrue), image_hash ] ) )写入二进制向量时Milvus 接收的是字节串不是 0/1 列表。我这里用bytes对象来表示 256 位的哈希值# 假设用感知哈希算出 256 位每 8 位转成一个字节 hash_bytes bytes([0b10110110, 0b11001011, ...]) # 总共 32 字节 row { image_id: 1, image_hash: hash_bytes } hash_collection.insert([row])注意当二进制向量数据量大时如果把 0/1 的 int 列表直接传进去性能会非常差。正确做法是先从感知哈希库拿到字符串或 int 列表再按字节打包。3.5 混合向量的一体化设计真实系统里三种向量往往同时存在。我以一套合同文档检索系统为例既要语义召回又要匹配合同编号这类精确词还要快速过滤重复合同。这种情况下我会建一个 Collection里面包含多个向量字段hybrid_schema CollectionSchema( fields[ FieldSchema(namecontract_id, dtypeDataType.INT64, is_primaryTrue), FieldSchema(namecontract_no, dtypeDataType.VARCHAR, max_length100), FieldSchema(namedense_embedding, dtypeDataType.FLOAT_VECTOR, dim768), FieldSchema(namesparse_embedding, dtypeDataType.SPARSE_FLOAT_VECTOR), FieldSchema(namebinary_signature, dtypeDataType.BINARY_VECTOR, dim128) ] )这样一个 Collection 里的每一条记录可以同时拥有语义向量、关键词稀疏向量和去重签名。检索时你可以分别创建索引然后针对不同召回策略做独立查询也可以把多个字段的相似度分数加权合并。多向量字段的代价是内存上升、写入变慢。尤其是 dense 字段768 维已经不小。所以混合设计一定要结合自己的数据量别为了全面把不需要的字段也加上。4. 相似度计算余弦值、内积、H
返回列表