Milvus向量数据库入门

文章目录

    • 一、Milvus基础概念
      • [1. 什么是 Milvus?](#1. 什么是 Milvus?)
      • [2. Milvus 相关术语](#2. Milvus 相关术语)
      • [3. 非结构化数据](#3. 非结构化数据)
      • [4. Embeddings](#4. Embeddings)
      • [5. Milvus 数据建模能力](#5. Milvus 数据建模能力)
      • [6. Milvus 部署模式](#6. Milvus 部署模式)
      • [7. Milvus 为何如此快速?](#7. Milvus 为何如此快速?)
    • 二、系统架构
      • [1. 核心设计思想](#1. 核心设计思想)
      • [2. Milvus核心架构](#2. Milvus核心架构)
        • [2.1 数据处理层](#2.1 数据处理层)
        • [2.2 元数据管理层](#2.2 元数据管理层)
        • [2.3 存储层](#2.3 存储层)
        • [2.4 架构核心流程](#2.4 架构核心流程)
      • [3. 四层架构](#3. 四层架构)
        • [3.1 访问层(Access Layer)](#3.1 访问层(Access Layer))
        • [3.2 协调器(Coordinator)](#3.2 协调器(Coordinator))
        • [3.3 工作节点(Worker Nodes)](#3.3 工作节点(Worker Nodes))
        • [3.4 存储层(Storage Layer)](#3.4 存储层(Storage Layer))
      • [4. 插入/查询数据流](#4. 插入/查询数据流)
      • [5. 流媒体服务](#5. 流媒体服务)
        • [5.1 信息](#5.1 信息)
        • [5.2 WAL 组件](#5.2 WAL 组件)
        • [5.3 恢复存储](#5.3 恢复存储)
        • [5.4 查询委托器](#5.4 查询委托器)
    • 三、Milvus核心数据模型
      • [1. 数据库(Database)](#1. 数据库(Database))
      • [2. 集合(Collection)](#2. 集合(Collection))
      • [3. 分区(Partition)](#3. 分区(Partition))
      • [4. 字段(Field)](#4. 字段(Field))
      • [5. 数据行(Row)](#5. 数据行(Row))
      • [6. 数据模型层级关系](#6. 数据模型层级关系)
    • 四、Milvus索引体系
      • [1. ANNS技术核心与价值](#1. ANNS技术核心与价值)
      • [2. 索引分类](#2. 索引分类)
      • [3. 浮点数索引](#3. 浮点数索引)
        • [3.1 精确检索索引:FLAT](#3.1 精确检索索引:FLAT)
        • [3.2 基于量化的索引:IVF系列](#3.2 基于量化的索引:IVF系列)
        • [3.3 基于图的索引:HNSW系列](#3.3 基于图的索引:HNSW系列)
        • [3.4 SCANN与DiskANN](#3.4 SCANN与DiskANN)
      • [4. 核心索引参数](#4. 核心索引参数)
      • [5. mmap(内存映射)](#5. mmap(内存映射))
    • 五、距离度量方式
      • [1. 余弦距离(Cosine Distance)](#1. 余弦距离(Cosine Distance))
      • [2. 欧氏距离(L2 Distance)](#2. 欧氏距离(L2 Distance))
      • [3. 曼哈顿距离(L1 Distance)](#3. 曼哈顿距离(L1 Distance))
      • [4. 其他常用度量方式](#4. 其他常用度量方式)
        • [4.1 内积(Dot Product)](#4.1 内积(Dot Product))
        • [4.2 汉明距离(Hamming Distance)](#4.2 汉明距离(Hamming Distance))
      • [5. 距离度量方式选择建议](#5. 距离度量方式选择建议)
      • [6. 归一化](#6. 归一化)
      • [7. 应用场景以及性能差距](#7. 应用场景以及性能差距)
    • 六、Milvus安装
    • 七、Milvus基础操作
      • [1. 连接Milvus](#1. 连接Milvus)
      • [2. Schema](#2. Schema)
      • [3. 创建Collection](#3. 创建Collection)
        • [3.1 **向量距离计算方式**](#3.1 向量距离计算方式)
        • [3.2 **加载Collection**](#3.2 加载Collection)
        • [3.3 **释放Collection**](#3.3 释放Collection)
      • [4. 索引管理](#4. 索引管理)
        • [4.1 索引类型](#4.1 索引类型)
        • [4.2 创建索引](#4.2 创建索引)
        • [4.3 标量字段索引](#4.3 标量字段索引)
        • [4.4 索引管理操作](#4.4 索引管理操作)
        • [4.5 使用 AUTOINDEX](#4.5 使用 AUTOINDEX)
        • [4.5 HNSW 索引案例](#4.5 HNSW 索引案例)
        • [4.6 IVF_FLAT索引案例](#4.6 IVF_FLAT索引案例)
      • [5. 插入数据](#5. 插入数据)
        • [5.1 **批量插入**](#5.1 批量插入)
        • [5.2 **插入到指定分区**](#5.2 插入到指定分区)
      • [6. 向量搜索Search](#6. 向量搜索Search)
        • [6.1 基本向量搜索](#6.1 基本向量搜索)
        • [6.2 向量搜索 + 标量过滤](#6.2 向量搜索 + 标量过滤)
        • [6.3 搜索参数配置](#6.3 搜索参数配置)
        • [6.4 分页搜索](#6.4 分页搜索)
      • [7. 标量查询Query](#7. 标量查询Query)
        • [7.1 基本查询](#7.1 基本查询)
        • [7.2 字符串匹配查询](#7.2 字符串匹配查询)
        • [7.3 数值范围查询](#7.3 数值范围查询)
        • [7.4 统计查询](#7.4 统计查询)
      • [8. 分区管理](#8. 分区管理)
        • [8.1 创建和管理分区](#8.1 创建和管理分区)
        • [8.2 加载和释放分区](#8.2 加载和释放分区)
        • [8.3 使用 Partition Key](#8.3 使用 Partition Key)
      • [9. 数据过期管理(TTL)](#9. 数据过期管理(TTL))
        • [9.1 TTL 概述](#9.1 TTL 概述)
        • [9.2 创建带 TTL 的 Collection](#9.2 创建带 TTL 的 Collection)
        • [9.3 修改已有 Collection 的 TTL](#9.3 修改已有 Collection 的 TTL)
        • [9.4 删除 TTL 设置](#9.4 删除 TTL 设置)
      • [10. Upsert插入或更新](#10. Upsert插入或更新)
      • [9. Delete向量](#9. Delete向量)

参考官网:https://milvus.io/docs/zh/quickstart.md

参考CSDN向量数据库介绍:https://blog.csdn.net/qq_43692950/article/details/133838940

参考easy-vecdb博客:https://easy-vecdb.datawhale.cc/Milvus/chapter3/PyMilvus核心API实战.html

qdrant向量数据库:https://github.com/qdrant/qdrant

一、Milvus基础概念

1. 什么是 Milvus?

Milvus是开源高性能、高扩展性向量数据库

Milvus 采用共享存储架构 ,存储计算完全分离 ,计算节点支持横向扩展。从架构上来看,Milvus 遵循数据流和控制流分离,整体分为了四个层次,分别为接入层(access layer) 、协调服务(coordinator service) 、执行节点(worker node)和存储层(storage)。各个层次相互独立,独立扩展和容灾。

非结构化数据(如文本、图像和音频)格式各异 ,蕴含丰富的潜在语义,因此分析起来极具挑战性。为了处理这种复杂性,Embeddings 被用来将非结构化数据转换成能够捕捉其基本特征的数字向量。然后将这些向量存储在向量数据库中,从而实现快速、可扩展的搜索和分析

Milvus 提供强大的数据建模功能,使您能够将非结构化或多模式数据组织成结构化的 Collections。它支持多种数据类型,适用于不同的属性模型,包括常见的数字和字符类型、各种向量类型、数组、集合和 JSON,为您节省了维护多个数据库系统的精力。

向量数据库作用:

  • 存储向量(Embeddings)
  • 建立索引(如 IVF、HNSW)
  • 快速相似度搜索(ANN)

核心特点:

  • 支持高维向量存储与检索

  • 高性能 ANN 搜索

  • 可扩展(从本地到分布式)

2. Milvus 相关术语

术语:https://milvus.io/docs/zh/glossary.md
Milvus采用多层级嵌套 的数据组织模型,从顶层到底层依次为:数据库(Database)→ 集合(Collection)→ 分区(Partition)→ 段(Segment)→ 字段(Field)→ 数据行(Row)

  • Collection(集合) : 类比为关系数据库中的表(Table) ,用于存储一类向量数据,一个 Collection 包含多个 Entity,需要定义 Schema(字段结构)。例如:用户向量表 user_embedding

  • Entity(实体) : 类比为关系数据库中一行数据,一条完整的数据记录,由多个 Field 组成。例如:{id: 1, name: "Tom", embedding: 0.12, 0.98, ...}

  • Field(字段) :类比关系型数据库中的列(Column),分两类:

    1. 标量字段(Scalar Field) :int / float / string 等结构化数据(对象属性的结构化数据)id → INT
    2. 向量字段(Vector Field) :存储embedding(高维向量),用于相似度搜索(对象特征的向量)
  • Partition(分区) :在 Collection 内部做逻辑划分 (同节点),用于提高查询效率(只查某个分区)、数据隔离(如按时间/类别)。查询时可以指定 Partition,避免全表扫描。每个分区又可以包含多个段

  • Segment(分段) :Milvus 的底层存储单元 。每个 Partition 会被拆成多个 Segment,一个 Segment 可以包含多个 Entity。数据写入是先进入 Segment ,查询时会对所有 Segment 并行搜索,再合并结果 。Segment 是查询并行的核心单位,Segment 越多 → 并行能力越强(但管理成本也增加)。

    • 写入数据流 :写入数据 → Segment(内存)→ Flush(落盘)→ 变成持久化 Segment
    • 查询流程 :Query → 多个 Segment 并行搜索 → 合并 TopK → 返回结果
  • Sharding(分片) :类比分库/分布式拆分,将数据分散到多个节点上 ,提升写入吞吐、查询并行能力。默认情况下,一个 Collection 默认 2 个 Shard

  • Index(索引) : 用于加速向量相似度搜索 。如果没有索引:暴力搜索(Brute Force) → O(N)。有索引后:近似最近邻(ANN) → 快很多。一个向量字段只能建一个索引

常见索引类型:

索引类型 特点
IVF_FLAT 倒排索引 + 桶内暴力破解
IVF_PQ 压缩向量,节省内存
HNSW 图结构,高性能
FLAT 暴力搜索(最准确)
bash 复制代码
Collection
 ├── Partition
 │    ├── Segment
 │    │     ├── Entity
 │    │     │     ├── Field

Milvus 是以 Collection 为逻辑单位,通过 Partition 做数据隔离,Segment 做存储与计算单元,Shard 做分布式扩展,并通过向量 Index 实现高效相似度搜索的向量数据库

3. 非结构化数据

非结构化数据是指没有固定数据模型或表结构的数据 (如文本、图像和音频)信息丰富,但难以直接计算、难以用传统数据库进行高效检索 。核心问题:难以做"语义级搜索"

4. Embeddings

Embeddings(向量化表示):将非结构化数据转换为数值向量(Vector)。

把语义转成数学空间中的点,相似内容----->向量距离更近。

常见应用:

  • 语义搜索
  • 推荐系统
  • 相似性匹配
  • RAG检索增强生成

核心价值:让机器理解语义

5. Milvus 数据建模能力

Milvus 不只是存向量,还支持结构化建模

Collection(集合)相当于 表(Table)

支持的数据类型:

  • 数值类型(int、float)
  • 字符串(string)
  • 向量(vector)
  • 数组(array)
  • JSON

**优势:**可同时管理向量数据、元数据(metadata),不需要多个数据库(如:ES + MySQL)

完整数据流:

bash 复制代码
非结构化数据
   ↓
Embedding 模型(向量化)
   ↓
向量(Vector)
   ↓
Milvus(存储 + 索引)
   ↓
相似度搜索(ANN)
   ↓
返回结果

6. Milvus 部署模式

  • Milvus Lite(轻量版) :Python 库形式,无需复杂部署、可嵌入应用。适用:本地开放。了解更多信息
  • Milvus Standalone(单机部署) :docker一键运行(包含etcd、minio、Milvus)所有组件集成,适用:小型生产环境。了解更多
    • Milvus:负责提供系统的核心功能
    • etcd :元数据引擎,管理 Milvus 内部组件的元数据访问和存储,例如proxy、index node 等
    • MinIO :存储引擎,负责维护 Milvus 的数据持久化
  • Milvus Distributed(分布式版) :基于Kubernetes实现多节点部署 ,各组件(Proxy、Query Node、Data Node等)独立扩容、弹性伸缩。采用计算与存储分离设计 ,通过协调器统一调度任务。适用:大规模AI系统、推荐系统、企业生产环境。了解更多

运行Milvus 有两种模式:

1.独立运行:在一个进程中运行所有组件的 Milvus 单实例,适用于数据集小、工作量小的情况。在 Standalone 模式下,可以选择更简单的 WAL 实现,如 woodpecker 和 rocksmq,以消除对第三方 WAL 存储依赖的要求。(无法从 独立实例 在线升级到 Milvus 集群)

2.集群:分布式部署模式,每个组件独立运行,可进行弹性扩展。这种设置适用于大型数据集和高负荷场景

7. Milvus 为何如此快速?

  • 硬件感知优化:为了让 Milvus 适应各种硬件环境,我们专门针对多种硬件架构和平台优化了其性能,包括 AVX512、SIMD、GPU 和 NVMe SSD。
  • 高级搜索算法 :Milvus 支持多种内存和磁盘索引/搜索算法,包括 IVF(倒排索引)、HNSW(图结构索引)、DiskANN(磁盘优化索引) 等。不找全部,只找最可能的
  • C++ 搜索引擎:向量数据库性能的 80% 以上取决于其搜索引擎。由于 C++ 语言的高性能、底层优化和高效资源管理,Milvus 将 C++ 用于这一关键组件。最重要的是,Milvus 集成了大量硬件感知代码优化,从汇编级向量到多线程并行化和调度,以充分利用硬件能力。
  • 面向列:Milvus 是面向列的向量数据库系统。其主要优势来自数据访问模式。在执行查询时,面向列的数据库只读取查询中涉及的特定字段,而不是整行,这大大减少了访问的数据量。此外,对基于列的数据的操作可以很容易地进行向量化,从而可以一次性在整个列中应用操作,进一步提高性能。

硬件加速 + 高效索引 + C++引擎 + 列存储

二、系统架构

Milvus 是一个:云原生(Cloud-Native)、分布式、面向向量检索(Embedding Search)的高性能向量数据库。底层集成了Faiss、HNSW、DiskANN、ScaNN。核心目标:在海量非结构化数据(文本 / 图片 / 音频)中做相似性搜索

架构图:

架构原则:Milvus 遵循数据平面和控制平面分解 的原则,由四个主要层组成,在可扩展性和灾难恢复方面相互独立。这种共享存储架构具有完全分解的存储层和计算层,可实现计算节点的横向扩展,同时将啄木鸟作为零磁盘 WAL 层实施,以增强弹性并减少操作开销。

1. 核心设计思想

1. 计算与存储分离

  • 计算层:QueryNode / DataNode / StreamingNode
  • 存储层:对象存储 + WAL + 元数据

好处:可水平扩展(Scale Out)、计算无状态(易扩容)、成本更低(冷热分离)

2. 数据平面&控制平面

  • 数据平面:真正处理数据(查询 / 插入) (Worker Nodes)
  • 控制平面:调度、元数据管理(Coordinator)

3.流批一体(Streaming + Batch)

类型 节点
流处理(实时数据) Streaming Node
批处理(历史数据) Query Node / Data Node
  • 老数据 ------>流处理
  • 新数据 ------> 索引查询

4. WAL(先写日志)

常见实现:

  • Apache Kafka
  • Apache Pulsar
  • Woodpecker(Milvus 推荐)

作用:数据可靠性、崩溃恢复、分布式一致性

2. Milvus核心架构

Milvus 2.x采用微服务架构,各组件独立部署、协同工作,核心组件分为数据处理层 、元数据管理层 、存储层三大模块,具体组件及功能如下:

2.1 数据处理层

数据处理层:核心业务组件

  • Proxy:接入层组件,负责接收客户端请求(如插入、查询、删除),进行请求校验、路由分发,是客户端与Milvus的交互入口
  • Query Node:查询节点,负责加载索引并执行相似性检索任务,返回检索结果
  • Data Node:数据节点,负责处理异步数据写入任务 ,将原始数据转化为结构化数据并写入存储层
  • Index Node:索引节点,负责异步构建索引(基于用户创建的索引类型),提升检索性能
2.2 元数据管理层

Meta Service:元数据服务,基于ETCD实现,负责存储Milvus的核心元数据,如数据库/集合信息、用户权限信息、组件节点信息等,保障整个系统的正常协同。

2.3 存储层
  • MinIO/S3:对象存储,用于存储原始向量数据、索引文件等大文件数据
  • Pulsar/Kafka:消息队列,用于存储数据写入的日志信息,保障数据传输的可靠性与异步处理能力
2.4 架构核心流程

客户端通过Proxy发送请求 → Proxy将请求分发至对应组件(Data Node处理写入、Index Node构建索引、Query Node处理查询) → 元数据由Meta Service管理 → 数据最终存储在对象存储与消息队列中,实现读写分离、异步处理 的高效架构模式

3. 四层架构

  1. 接入层(Access Layer):处理客户端请求(SDK, REST API)。
  2. 协调服务(Coordinator Services):集群的"大脑",管理元数据和任务调度。
  3. 工作节点(Worker Nodes):执行具体任务的"肌肉"(查询、写入、索引)。
  4. 存储层(Storage):持久化数据和元数据的"基石"。
3.1 访问层(Access Layer)

访问层由一组无状态代理组成 ,是系统的前端也是用户的终端。代理本身无状态 ,它使用 Nginx、Kubernetes Ingress、NodePort 和 LVS 等负载均衡组件提供统一的服务地址 。采用的是大规模并行处理(MPP)架构 ,代理会对中间结果进行聚合和后处理,然后再将最终结果返回给客户端

特点:无状态、支持负载均衡(Nginx / Ingress / LVS)、提供统一入口

核心职责:

  1. 请求校验
  2. 请求路由
  3. 结果聚合(MPP 架构)
3.2 协调器(Coordinator)

协调器是 Milvus 的大脑,负责维护集群拓扑结构、调度所有任务类型并保证集群级一致性

核心职责:

  • DDL/DCL/TSO 管理 :处理数据定义语言 (DDL ) 和数据控制语言 (DCL ) 请求,如创建或删除 Collections、分区或索引,以及管理时间戳 Oracle (TSO) 和时间刻度签发
  • 流服务管理 :先写日志(WAL)与流节点绑定 ,并为流服务提供服务发现功能
  • 查询管理:管理查询节点的拓扑结构和负载平衡,并提供和管理服务查询视图,以指导查询路由
  • 历史数据管理:将压缩和索引建立等离线任务分配给数据节点,并管理数据段和数据视图的拓扑结构
3.3 工作节点(Worker Nodes)

执行层(干活的 ),工作节点是遵从协调器指令的哑执行器,由于存储和计算分离,工作节点是无状态的

1. Streaming Node(流节点) :碎片级的小型大脑。基于底层 WAL 存储提供碎片级的一致性保证和故障恢复 。同时,流节点还负责增长数据查询 和生成查询计划 。还负责将增长数据转换为封存(历史)数据

2. Query Node(查询节点):从对象存储中加载历史数据,并提供历史数据查询。本质:ANN搜索执行器

作用:加载历史数据(对象存储)、执行向量检索

3. Data Node(数据节点):负责离线处理历史数据,如压缩和建立索引。作用:数据压缩、构建索引(HNSW / IVF 等)

3.4 存储层(Storage Layer)
  1. 元存储(Metadata) :元存储存储元数据快照 。如 Collections Schema 和消息消耗检查点。元数据的存储要求极高的可用性、强一致性和事务支持。使用etcd作为元存储 ,存储schema、segment 状态、checkpoint(存储各种组件的元数据)。要求强一致性、高可用。Milvus 还使用 etcd 进行服务注册和健康检查。
  2. 对象存储(Object Storage) :对象存储用于存储日志快照文件、标量和向量数据的索引文件以及中间查询结果 (负责索引和二进制日志文件等大型文件的数据持久化)。MinIO、AWS S3 / Azure Blob。对象存储存在的问题:延迟高、按请求收费。为了提高性能并降低成本,Milvus 计划在基于内存或固态硬盘的缓存池上实现冷热数据分离。
  3. WAL 存储 :先写日志(WAL)存储 是分布式系统中数据持久性和一致性 的基础。作用数据持久化第一步、数据崩溃恢复。通过提前记录每次写入操作 ,WAL 层保证了可靠的全系统恢复和一致性机制 。在啄木鸟零磁盘模式下,WAL直接使用对象存储和元存储,无需其他部署,减少第三方依赖。

4. 插入/查询数据流

插入流程:

  1. step1:客户端发请求 :Client → Proxy,调用pymilvus.insert()。Proxy是入口网关
  2. **step2:Proxy 校验+找RootCoord **:Proxy → RootCoord。主要做两件事:Proxy 验证请求合法性(集合存在、数据格式合规)、负载均衡器转发给 Root Coordinator分配时间
  3. Step 3:RootCoord 分配 TSO(全局时间) :RootCoord → 分配时间戳。Root Coord 为操作分配全局唯一时间戳 (TSO),确保分布式顺序一致性,并通知Data Coord
  4. Step 4:DataCoord 决定写哪 :RootCoord → DataCoord。DataCoord查询 Metadata Storage (etcd) 获取目标集合存储策略、决定数据存储位置(创建新 Segment 或使用现有写入 Segment)、记录 Segment ID、状态、分配信息到 Metadata Storage。数据调度总管
  5. step 5: 写入 Log Broker :Proxy → Pulsar/Kafka(Log Broker)。所有数据变更必须首先写入 Log Broker (如 Pulsar/Kafka),保证操作的持久性和严格顺序性。为什么这样?日志即数据,好处:不丢数据(持久化)、可重放(恢复)、顺序一致(TSO)
  6. Step 6:数据节点消费与持久化 (Data Node) :DataNode做
    1. Data Node 订阅并消费 Log Broker 中的 Insert/Delete 消息
    2. 写入内存(此时可查):数据 → 内存 buffer(Growing Segment),这时已经可查了
    3. 触发 Flush(落盘):数据量够大、或时间到了。将缓冲数据(向量、标量)作为 Segment 文件 写入 Object Storage (MinIO/S3)、Flush 创建新的 Segment
  7. Step 7:触发索引构建(Index Coord) :DataCoord → IndexCoord
    1. Data Coord 通知 Index Coord 有新 Segment 产生
    2. Index Coord 调度 Index Node 为新 Segment 构建索引。
  8. Step 8:IndexNode 异步建索引 :
    1. Index Node 从 Object Storage 读取新 Segment 的原始数据
    2. 执行索引构建算子(如 IVF 聚类、HNSW 图构建)
    3. 将生成的索引文件 写回 Object Storage 。此过程是异步的,避免阻塞写入
bash 复制代码
Client
 → Proxy
 → RootCoord(分配TSO)
 → DataCoord(分配Segment)
 → Log Broker(Pulsar/Kafka)
 → DataNode(消费 + 内存)
 → Flush(Object Storage)
 → IndexNode(异步建索引)

查询流程:

  1. step1:客户端发请求 :Client → Proxy,调用pymilvus.search()。Proxy是入口网关
  2. step2:协调与分发 (Query Coord):
    1. 获取TSO :Proxy 转发请求给 Query Coord ,Query Coord 从 Root Coord 获取时间戳 (TSO),保证读取一致性(快照隔离)。
    2. 找数据在哪:通过 etcd(Metadata Storage)。路由表查询。查询哪些 Segment 存在、在哪些 QueryNode 上
    3. 拆任务:一个查询 → 拆成多个 Segment 子任务,然后分发给不同 QueryNode(并行)
  3. step3:查询节点执行 (Query Node):
    1. 数据准备:所需 Segment 数据(向量、标量、索引)是否在内存?在内存(热数据)直接查、不在内存(冷数据)从 Object Storage 拉原始数据。索引文件(提前算好的加速结构。如IVF\HNSW\FLAT)
    2. 标量过滤算子 :首先应用属性过滤条件 (如 price>100) 进行剪枝,得到候选向量 ID 集。标量过滤(先剪枝):全部数据 → 候选集合(变小)
    3. 向量搜索(ANN) :在加载的索引文件结构上执行近似最近邻搜索(利用索引加速)。是近似搜索,不是最终结果
    4. 向量距离计算算子:对 ANN 返回的候选向量,精确计算与查询向量的距离 (L2/IP/Cosine)。高度优化点 (多线程/GPU)。
    5. 局部 TopK 算子:基于精确距离,计算并保留当前 Query Node 负责的 Segment 数据中的局部 TopK 结果 (ID + 距离)。候选向量 → 精确算距离→ 排序 → 取 TopK。得到当前 QueryNode 的 TopK
  4. Step 4:QueryCoord 汇总 :
    1. Query Coord 收集所有 Query Node 返回的局部 TopK 结果。
    2. 全局 TopK 合并算子 :使用最小堆等算法合并所有局部结果,得到最终的全局 TopK。
    3. 数据组装 :若查询需要返回实体数据(非仅 ID 和距离),则从 Object Storage 或 Query Node 缓存中获取对应 ID 的完整字段。
  5. Step 5:结果返回:最终结果通过 Proxy 返回给客户端 SDK。

QueryNode = 数据加载 + 过滤 + 向量搜索 + 局部TopK

关键点:

  • 实时数据 + 历史数据一起查
  • 多级 Reduce(类似 MapReduce)

Proxy:入口 + 聚合

Coordinator:调度大脑

Worker Nodes:执行层(流 / 查 / 建索引)

Storage:etcd + MinIO + WAL

图中组件 核心职责
Proxy (代理) 接收应用程序发给milvus的请求,然后获取TSO时间戳,分发给数据
Root Coord (根协调器) 管理所有的元数据,发放TSO
Log Broker (日志代理) 数据即日志,日志即数据,顺序的记录着所有数据
Data Coord (数据协调器) 管理图中的Collection A ,负责Collection中的Segment的创建和管理
Channel 1-5 (通道) Collection根据请求的Hash值划分Shard,每个Shard拥有一个独立的物理流水线(Channel),可以并行处理数据,互不阻塞
Segments (分段) Channel上的数据被打包处理为Segments,记录TSO后存入对象存储区域,等待创建索引

5. 流媒体服务

流服务 是 Milvus 内部流系统模块的一个概念,它围绕前向写日志(WAL)构建 ,以支持各种与流相关的功能。这些功能包括流式数据摄取/订阅 、集群状态故障恢复 、将流式数据转换为历史数据 以及增长数据查询。从架构上讲,流服务由三个主要部分组成:

  • 流协调器 :逻辑组件,使用 Etcd 进行服务发现 ,以找到可用的流节点,并负责将 WAL 与相应的流节点绑定 。还负责注册服务 ,以公开 WAL 分布拓扑,让流客户端知道给定 WAL 的相应流节点
  • 流节点集群:负责所有流处理任务(如 WAL 附加、状态恢复、数据增长查询)的流工作节点集群
  • 流客户端 :封装了服务发现和就绪检查等基本功能。它用于启动消息写入 和订阅等操作
5.1 信息

流服务是一个日志驱动的流系统 ,因此 Milvus 中的所有写操作(如 DML 和 DDL)都被抽象为消息

  • 流服务会为每个消息分配一个时间戳 Oracle(TSO)字段,表示消息在 WAL 中的顺序
  • 每个消息都属于一个特定的 VChannel(虚拟通道) ,在该通道内保持一定的不变属性,以确保操作的一致性。例如,在同一通道上,Insert 操作必须始终发生在 DropCollection 操作之前。
5.2 WAL 组件

为了支持大规模横向扩展,WAL 不是一个单一的日志文件,而是多个日志的复合文件。每个日志都能独立支持多个 V 通道的流功能。在任何时候,WAL 组件都只能在一个流节点上操作,这些限制由底层 WAL 存储的围栏机制和流协调器共同保证。

WAL 是多个日志的组合系统,每个日志可以服务多个VChannel(虚拟通道),WAL 被分布在不同节点(流节点)上,同一时间只允许一个流节点操作 WAL。保证了顺序一致性、写入安全性

  1. 围栏机制(Fencing):防止多个节点同时写同一个 WAL。实现:底层 WAL 存储提供"锁/租约"、配合流协调器(Streaming Coordinator)
  2. 分段生命周期管理(Segment Lifecycle):WAL 数据不是无限堆积,而是按"段"管理。触发策略:内存达到阈值、段大小达到上限、长时间未写入(idle)。生命周期:创建 → 写入 → 封存 → 持久化 → 查询可见。本质:把流数据逐步转成"历史数据"(供查询)
  3. 基本事务支持(VChannel级别) :不支持完整事务ACID,只支持轻量事务。特点:单条消息有大小限制、支持 VChannel 级别原子写入。效果: 一批数据要么全部写入成功,要么全部失败
  4. 高并发远程写入 :Milvus WAL 可以基于外部消息队列(如kafka)。问题: 远程写 WAL 有 RTT(网络延迟)。解决方案:并发写入 + TSO(时间戳排序)、多线程并发写日志、使用 TSO(Timestamp Oracle)保证顺序。写入逻辑:并发写入 → 分配 TSO → 按 TSO 排序读取
  5. 预写缓冲区(Write Buffer):数据写入WAL后,先进入本地缓存,避免每次都去远程读取WAL。减低延迟、支持尾读
  6. 多 WAL 存储支持(可插拔) :常见方案:Woodpecker(Milvus 自研)、Apache Pulsar。特殊点:Woodpecker 零磁盘模式, 不依赖远程存储

WAL 在 Milvus 中的数据路径:

bash 复制代码
客户端写入
   ↓
WAL(日志写入)
   ↓
预写缓冲区(加速读取)
   ↓
Segment(分段)
   ↓
持久化存储
   ↓
查询服务可见
5.3 恢复存储

恢复存储组件始终与 WAL 组件在同一个流节点(Streaming Node)上运行,和 WAL 强绑定(同生命周期、同节点)。主要负责将流数据转化成持久化历史数据,并将其存储在对象存储中,还负责处理流节点上 WAL 组件的内存状态恢复(流节点重启后,从持久化数据中恢复WAL的状态)基于历史数据重建内存状态

5.4 查询委托器

查询委托器运行在每个流节点上,始终与 WAL 组件共存(同节点),负责在单个分片上执行增量查询。它生成查询计划,将其转发给相关的查询节点,并汇总查询结果。

  1. 增量查询执行 :面向单个分片,处理 WAL 中的最新数据(未落盘数据) 。负责查最新写入但还没变成历史数据的那部分数据
  2. 查询计划生成 + 分发:接收到查询请求后,生成查询计划,将请求转发给相关的查询节点。负责调度/分发者
  3. 查询结果合并 :从多个 Query Node 收集结果,做合并。本质:分布式查询结果聚合器
  4. **删除操作广播:**将 Delete 操作同步到所有相关 Query Node

副本机制:

如果Collection开启了多副本

  • 一个流节点:有 1 个主查询委托器

  • 其他流节点:部署 N - 1 个委托器

目的:支持HA、提供查询负载均衡、避免单点瓶颈

组件 作用
WAL 存最新数据
Query Delegator 查最新数据(增量查询)
Recovery Storage 把数据变成历史数据
Query Node 查历史数据

新数据 → Query Delegator 负责查(查最新数据增量查询)

老数据 → Query Node 负责查(查历史数据)

三、Milvus核心数据模型

Milvus采用多层级嵌套 的数据组织模型,从顶层到底层依次为:数据库(Database)→ 集合(Collection)→ 分区(Partition)→ 段(Segment)→ 字段(Field)→ 数据行(Row)

1. 数据库(Database)

数据库是最高层逻辑命名空间,核心作用是实现不同业务场景、多租户的数据隔离与资源管控。不同数据库内的资源(集合、索引等)互不干扰

核心特性与约束:

  • 隔离机制:支持物理级数据隔离与资源组绑定,可通过RBAC权限控制实现租户级访问限制,保障数据安全
  • 默认配置:系统默认提供"default"数据库,用户无需创建即可直接使用;自定义数据库数量默认限制为64个(集群级配置可调整)
  • 适用场景:按业务线划分(如"电商推荐数据库""智能文档检索数据库"),或按租户划分(如"企业A专属数据库""企业B专属数据库")

2. 集合(Collection)

集合是存储向量数据及关联元数据 的基本单元,等价于关系数据库中的表。每个集合必须通过Schema(模式)定义数据结构 ,所有插入的数据需严格遵循Schema规范,确保数据类型与字段约束的一致性

Schema依赖:必须显式定义Schema(数据模式)核心组成包括字段定义、字段约束、系统默认配置三大模块。Schema定义后需严格遵循,所有插入数据将通过Schema进行类型校验与约束检查,不满足条件的数据会被拒绝插入

  • 字段要求:必须有一个向量字段(存储高维特征向量) 、可配置多个标量字段(存储属性元数据)、每个集合有且仅有一个主键字段(标量类型),用于唯一标识数据行
  • 层级关联:一个数据库下可创建多个集合(默认上限65535个),每个集合对应一个具体的业务数据集
  • 物理存储:集合的物理存储单元为段(Segment),数据插入时会先写入内存段,满足阈值后自动持久化为磁盘段,段是索引构建与检索的最小物理单元

3. 分区(Partition)

分区是集合的逻辑子单元 ,用于对集合内的数据按指定规则进行分组管理。通过分区可缩小检索时的数据扫描范围,提升查询效率,同时简化数据生命周期管理(如按分区清理过期数据)

核心特性与约束:

  • 分区方式 :支持按业务属性 (如商品类别:服装、电子产品、食品)、时间范围(如2024年1月、2024年2月)、哈希规则 (按主键哈希均匀分布)划分;也可通过Partition Key实现自动路由,数据插入时按分区键值自动分配至对应分区
  • 隔离与资源:分区支持物理级资源隔离,可绑定独立资源组;单个集合默认最大支持1024个分区,基于Partition Key的动态分区可支持更高租户规模(百万级)
  • 核心价值:检索时可指定分区列表(Partition-aware Search),避免全集合扫描;支持按分区执行批量删除、备份等操作,降低数据管理成本

4. 字段(Field)

字段是数据存储的最小单元,定义了数据的类型、属性约束及存储特性,所有字段均需在集合的Schema中预先定义(动态字段除外)。分为**向量字段与标量字段两类,两类字段协同实现 向量检索+标量过滤**的混合检索能力

1. 向量字段(必填)

用于存储高维向量数据(如文本嵌入向量、图像特征向量)

  • 支持类型:FLOAT_VECTOR(浮点型向量,如float32,主流模型默认输出类型,适用绝大多数AI场景)、BINARY_VECTOR(二进制向量,仅存储0/1值,用于轻量化检索场景,存储成本与计算开销更低)
  • 核心配置:必须指定向量维度(如768维对应BERT模型、1536维对应GPT模型、2048维对应ResNet-50模型),维度大小需与模型输出严格一致 ;FLOAT_VECTOR支持1-65536维,BINARY_VECTOR维度需为8的整数倍(如64维、128维)。此外,可配置向量存储参数,如是否启用压缩存储(适用于大规模FLOAT_VECTOR场景)
  • 索引支持:向量字段是索引构建的核心对象,所有Milvus支持的索引类型(如IVF_FLAT、HNSW等)均针对向量字段创建;一个向量字段可创建多个不同类型的索引,但同一时间仅能有一个生效

2. 标量字段(可选)

用于存储与向量关联的属性元数据

  • 支持类型 :基础标量类型包括Int64(商品ID、用户ID等唯一标识)、Float64(价格、评分、相似度分值等数值型属性)、String/VARCHAR(商品名称、描述、标签等文本属性,VARCHAR需指定最大长度)、Bool(是否上架、是否推荐等布尔属性)、DATE(创建时间、更新时间等时间属性);进阶类型包括Array(数组类型,如商品所属分类列表)、JSON(轻量结构化数据,如用户画像标签)
  • 主键约束:必须指定一个标量字段作为主键
  • 索引支持:主流标量类型(Int64、Float64、String/VARCHAR等)可创建标量索引 ,如BloomFilter(用于快速判空与去重 )、Range Index(用于范围查询 ,如价格>1000)、Trie Index(用于String类型的前缀匹配查询 );标量索引与向量索引协同工作,可**先通过标量索引过滤数据,再对剩余向量执行相似性检索**,大幅提升混合检索效率

5. 数据行(Row)

数据行 = 主键值+向量值+其他标量值。每条数据行会通过分区规则分配至集合的某个分区,最终存储在对应分区的段(Segment)中

核心特性:

  • 数据行的字段值必须匹配Schema定义的类型与约束
  • 默认的两个系统字段:
    • _row_id:系统生成的唯一行标识,用于内部数据定位
    • _timestamp:数据插入时的时间戳,用于数据版本管理与时间范围过滤

6. 数据模型层级关系

从顶层到底层: Database(业务/租户隔离)→ Collection(Schema定义的数据集)→ Partition(数据集分组)→ Segment(物理存储单元)→ Field(数据最小单元)→ Row(单条数据记录)

核心关联逻辑:一个Database包含多个Collection,一个Collection包含多个Partition,一个Partition包含多个Segment,一个Segment包含多个Row,每个Row包含所有Field的具体值

四、Milvus索引体系

未创建索引时,只能暴力检索,需要遍历所有向量计算距离,效率极低(亿级数据检索耗时分钟级)

索引作用 :通过索引结构快速定位候选相似向量 ,避免全量计算。不同索引类型在检索精度(召回率)与检索速度之间存在权衡,可根据业务需求灵活选择。先通过标量字段过滤缩小数据范围,再对剩余向量执行索引检索

向量索引解决的问题:海量数据的快速检索 。近似的足够好结果

这就是ANNS(近似近邻检索)的核心理念:通过在可接受范围内牺牲一定精确度,换取检索效率的显著提升

Milvus 支持 11 种以上索引类型,可分为近似最近邻检索(ANN)索引和精确最近邻检索(NN)索引两大类。其中 ANN 索引通过牺牲少量精度来换取显著的检索效率提升 ,是实际大规模应用的主流选择;而 NN 索引则适用于小规模数据或对精度要求极高的场景

1. ANNS技术核心与价值

ANNS(近似近邻检索):不局限返回最精确的topK结果,而是通过在可接受范围内牺牲一定精确度,换取检索效率的显著提升

索引是建立在数据之上的附加结构,其内部结构取决于所采用的具体ANNS算法。索引的构建和使用并非无代价:它会产生额外的预处理时间、存储空间及RAM开销,同时可能导致召回率的轻微降低(尽管这种影响通常可忽略,但仍需关注)。因此,在实际应用中需权衡索引的预处理成本与搜索收益

2. 索引分类

ANNS向量索引的分类可从多个维度构建,核心包括**实现方法与 向量嵌入类型**两大逻辑维度,不同维度间存在交叉关联,共同构成索引技术的全局认知框架

实现方法角度:ANNS向量索引可分为四大类:基于树结构、基于图结构、基于哈希方法和基于量化技术。

  • 基于树结构的索引通过空间划分构建层级树(如KD树),实现向量的快速检索
  • 基于图结构的索引通过构建近邻图(如HNSW),利用图遍历高效定位相似向量
  • 基于哈希方法的索引将高维向量映射到低维哈希码,通过哈希表实现快速查找
  • 基于量化技术的索引则通过向量编码压缩(如乘积量化PQ、标量量化SQ),在降低存储和计算成本的同时保持检索性能
实现方法分类 核心技术原理 典型代表算法
基于树结构 空间划分构建层级树 KD树
基于图结构 构建近邻图实现高效遍历 HNSW
基于哈希方法 高维向量映射到低维哈希码 局部敏感哈希(LSH)
基于量化技术 向量编码压缩降低存储成本 乘积量化(PQ)、标量量化(SQ)

向量嵌入类型角度:索引可按处理的数据类型 划分为浮点嵌入索引、二进制嵌入索引、稀疏嵌入索引及GPU索引

  • 浮点嵌入索引支持各类浮点精度向量(如FLOAT32、FLOAT16),典型代表包括FLAT、IVF系列(IVF_FLAT、IVF_SQ8、IVF_PQ)、HNSW、SCANN、DiskANN等
  • 二进制嵌入索引针对二值化向量,如BIN_FLAT、BIN_IVF_FLAT
  • 稀疏嵌入索引适用于非零元素占比低的稀疏向量,如SPARSE_INVERTED_INDEX、SPARSE_WAND
  • GPU索引则通过图形处理器加速计算,如GPU_CAGRA、GPU_IVF_FLAT等
嵌入类型 支持数据类型 典型索引示例
浮点嵌入索引 FLOAT32、FLOAT16等精度向量 FLAT、IVF系列、HNSW、SCANN、DiskANN
二进制嵌入索引 二值化向量 BIN_FLAT、BIN_IVF_FLAT
稀疏嵌入索引 非零元素占比低的稀疏向量 SPARSE_INVERTED_INDEX、SPARSE_WAND
GPU索引 各类向量(需GPU加速) GPU_CAGRA、GPU_IVF_FLAT、GPU_IVF_PQ

3. 浮点数索引

3.1 精确检索索引:FLAT

FLAT索引 --- 暴力搜索。这种索引方式不对向量进行压缩,采用暴力搜索(即穷举搜索)策略 。每次查询将目标向量与数据集中的所有向量进行逐一比较 。FLAT索引核心价值:提供了绝对精确的结果 。FLAT索引最适合的是那些数据量不大但对精度要求极高的场景。无需指定距离计算方式(如L2欧氏距离或IP内积)

3.2 基于量化的索引:IVF系列

IVF_FLAT:倒排索引+ 桶内暴力破解

IVF系列索引的核心机制是通过基于中心点的分区策略将向量聚类到多个桶中 ,在搜索过程中仅扫描与查询向量中心点相近的桶内向量。IVF系列索引对数据的聚类,通过不同的聚类算法将数据聚类到不同的桶中,每一聚类都有一个聚类中心,每个聚类中心对应一个向量表示。

当我们提供一个查询向量时,会先计算该向量与所有聚类中心之间的距离,然后选择距离最近的聚类中心,并仅搜索该聚类中心内的向量。

缺点:在进行近似最近邻搜索时,可能会出现一种情况:通过算法找到的"最接近查询嵌入的候选嵌入"实际上并不是真正的最近邻

具体来说,当使用聚类方法(例如IVF_FLAT中的k-means)对数据点进行分组时,每个聚类都有一个中心点(centroid) 。在查询时,算法首先会确定哪个聚类的中心点与查询向量最为接近,然后在该聚类内部查找最近的向量作为候选嵌入。然而,如果实际的最近邻(即距离查询向量最近的数据点)位于另一个聚类中,而这个聚类的中心点与查询向量的距离相对更远,那么根据中心点选择的聚类将不会包含真实的最近邻。这就导致了找到的候选嵌入不是准确的最近嵌入

为了解决这个问题,IVF_FLAT提供了两个超参数供我们调整:

  • nlist:整个数据集将被分成多少个子集(即使用k-means算法创建的聚类中心的数量)。增加这个值可以使得每个分区内的向量更加相似,但同时也会增加计算开销。通常设为数据量的平方根。nlist越大,每个聚类内向量越少,搜索越快,但聚类质量可能下降
  • nprobe:搜索期间会检查的分区数量。增大这个值可以提高召回率(即找到真正最近邻的可能性),因为更多的候选分区被考虑进来,但这同样会增加搜索时间。通常为nlist的数据量的1%-10%。nprobe越大,召回率越高,但搜索时间越长

IVF系列包含IVF_FLAT、IVF_SQ8和IVF_PQ三种主要类型,其技术演进逻辑围绕压缩率提升与精度平衡展开,逐步实现内存占用的降低

索引类型 构建参数 搜索参数 压缩技术 内存占用 召回率
IVF_FLAT nlist(范围1,65536,默认128) nprobe(范围1,nlist,默认8)、max_empty_result_buckets(范围1,65535,默认2) 无压缩 最高(与原始数据相近) 最优
IVF_SQ8 nlist(范围1,65536,默认128) nprobe(范围1,nlist,默认8)、max_empty_result_buckets(范围1,65535,默认2) 标量量化(SQ),将FLOAT(4字节)转换为UINT8(1字节) 中等(减少70-75%内存消耗) 次之
IVF_PQ nlist(范围1,65536,默认128)、m(乘积量化因子数,需满足dim mod m=0)、nbits(范围1,64,默认8) nprobe(范围1,nlist,默认8)、max_empty_result_buckets(范围1,65535,默认2) 乘积量化(PQ),将高维向量分解为低维子向量并量化 最低 最低
  • IVF_FLAT:核心特点是每个桶内存储原始向量,未进行压缩处理 。构建时需指定nlist参数(聚类数量)以确定桶的数量 ;搜索阶段支持普通搜索(通过nprobe参数设置查询桶数 ,范围1,nlist )和范围搜索(通过max_empty_result_buckets参数控制最大空白桶数,范围1,65535,默认2)。由于保留原始向量,IVF_FLAT的索引文件大小与原始数据相近,内存占用最高,但召回率表现最优

  • IVF_SQ8:在IVF_FLAT基础上,IVF_SQ8通过引入**标量量化(SQ)技术实现压缩, 将每个4字节的FLOAT类型向量分量转换为1字节的UINT8类型**,可减少70-75%的内存消耗。其构建参数(nlist)和搜索参数(nprobe、max_empty_result_buckets)与IVF_FLAT完全一致,适用于内存资源有限且可接受召回率轻微下降的场景。相较于IVF_FLAT,IVF_SQ8以精度的小幅损失换取了显著的内存优化

  • IVF_PQ :进一步采用乘积量化(PQ)技术深化压缩 ,其流程为:先通过IVF进行聚类分区,再将每个高维向量分解为多个低维子向量,对各子向量独立量化后存储 。该方法使索引文件体积较IVF_SQ8进一步减小,但精度损失更为明显。构建时除nlist参数外,还需配置m(乘积量化因子数,需满足向量维度dim能被m整除 )和nbits(每个子向量的存储位数,范围1,64,默认8)。搜索参数与IVF_FLAT、IVF_SQ8保持一致

IVF系列三种索引的内存占用呈现IVF_PQ < IVF_SQ8 < IVF_FLAT的关系,而召回率则相反,IVF_FLAT最优,IVF_SQ8次之,IVF_PQ最低

使用建议

  • 选择IVF_FLAT:当内存充足且追求高精度时
  • 选择IVF_SQ8:当内存有限但仍需要较高精度时
  • 选择IVF_PQ:当处理超大规模数据且可接受精度损失时
3.3 基于图的索引:HNSW系列

分层导航小世界(HNSW)算法构建了一个多层图 ,有点像不同缩放级别的地图。底层包含所有数据点,而上层则由从底层采样的数据点子集组成。层提供远距离跳转,以快速接近目标,而下层则进行细粒度搜索,以获得最准确的结果。

工作原理:

  1. 入口点:搜索从顶层的一个固定入口点开始,该入口点是图中的一个预定节点
  2. 贪婪搜索 :算法贪婪地移动到当前层的近邻,直到无法再接近查询向量为止。上层起到导航作用,作为粗过滤器,为下层的精细搜索找到潜在的入口点
  3. 层层下降:一旦当前层达到局部最小值,算法就会利用预先建立的连接跳转到下层,并重复贪婪搜索
  4. 最后细化:这一过程一直持续到最底层,在最底层进行最后的细化步骤,找出最近的邻居

M(连接度数):

  • 作用:控制每个节点的最大连接数(朋友数量)
  • 推荐值:16-48
  • 影响:M越大,图越密集,精度越高,但构建时间和内存占用也越大

efConstruction(构建参数):

  • 作用:构建索引时的搜索范围
  • 推荐值:200-500
  • 影响:值越大,索引质量越好,但构建时间越长

ef(搜索参数):

  • 作用:查询时的搜索范围,控制探索的深度。相当于决定在寻找目标时要问多少个朋友
  • 推荐值:top_k到2000之间
  • 影响:值越大,召回率越高,但查询时间越长

HNSW(Hierarchical Navigable Small World)通过构建多层导航图 实现高效向量检索,其核心结构特点为上层稀疏、下层密集,搜索过程从上层起始逐步向下层精确导航 。HNSW在每层都是一个小世界图 ,上层图可以看做下层图的一个缩影,检索时,从上到下,不断指引检索过程逐步靠近想要探寻的向量空间

为优化内存占用,HNSW系列衍生出多种量化变体,这些变体通常通过牺牲部分召回率和构建速度来换取内存效率的提升。

  • HNSW_SQ结合标量量化(SQ)技术,将浮点数据离散化为有限数值(如SQ6量化为64个值),显著减少内存占用,同时保持较高的QPS性能,但构建时间较基础HNSW有所增加
  • HNSW_PQ则采用乘积量化(PQ),将向量分解为多个子向量并分别量化,可灵活权衡索引大小与准确性,但相比HNSW_SQ,其QPS和召回率更低,构建时间更长
变体名称 核心技术 内存占用 QPS性能 构建时间 召回率 主要特点
HNSW 分层导航小世界图 高 高 基准 高 适用于高速查询、高召回率场景,通过M和ef参数控制性能
HNSW_SQ 标量量化(如SQ6) 低 较高 适度增加 较高 将浮点数据离散化为有限数值(如64个值),显著减少内存占用
HNSW_PQ 乘积量化 中-低 中 更长 中 向量分解为子向量量化,可灵活权衡索引大小与准确性
HNSW_PRQ 乘积残差量化 中-低 中 增加数倍 与PQ相当 对PQ残差再次量化,压缩率控制更灵活,精度保持或提升

HNSW_PRQ作为基于乘积残差量化(PRQ)的变体,通过对PQ量化后的残差进行再次量化 ,进一步优化了索引大小与准确性的权衡灵活性。在相同压缩率下,HNSW_PRQ的QPS和召回率与HNSW_PQ相当,但其残差补偿机制使得在精度保持或提升的同时,能够实现更灵活的压缩控制,不过这也可能导致构建时间增加数倍

HNSW vs IVF 对比

特性 HNSW IVF系列
查询速度 通常更快(对数时间复杂度) 较慢
内存占用 很高(需要存储图结构) 中等到低
精度 高 取决于nprobe设置
构建时间 中等 较短
适用场景 低延迟、高精度需求 内存敏感、大规模数据

使用建议

  • 选择HNSW :
    • 对查询延迟要求严格
    • 内存资源充足
    • 数据量不是特别大(千万级以下)
    • 需要高召回率
  • 不选择HNSW :
    • 内存资源有限
    • 数据量特别大(亿级)
    • 对构建时间有严格要求
3.4 SCANN与DiskANN

SCANN(可扩展近邻)与IVF_PQ(倒排索引量化)在基础架构上具有相似性,但通过引入SIMD(单指令多数据)优化显著提升了计算效率,尤其针对GPU环境进行了深度适配,从而实现了更高的查询吞吐量(QPS)。其核心优势在于极高速查询能力与高召回率,适用于内存资源充足的场景。在参数配置方面,SCANN的构建参数包括nlist(聚类数量)和with_raw_data(是否包含原始数据,默认值为True);搜索阶段则可通过调整nprobe(查询的聚类中心数量)、reorder_k(候选向量数量,默认值为top_k)及max_empty_result_buckets(范围搜索参数,默认值为2)进一步优化性能。

索引类型 核心技术 适用场景 构建参数 搜索参数
SCANN 类IVF_PQ架构,SIMD优化,GPU适配 极高速查询、高召回率、内存资源充足场景 nlist(聚类数量)、with_raw_data(默认True) nprobe、reorder_k(默认top_k)、max_empty_result_buckets(默认2)
DiskANN Vamana图结构,PQ量化,磁盘存储 内存无法容纳的十亿级及以上大规模数据集 - -

DiskANN专注于解决内存资源限制下的大规模数据存储 问题,其技术路径基于Vamana图结构,并结合乘积量化(PQ)技术实现磁盘-内存混合存储 。具体而言,DiskANN将图结构存储于磁盘以突破内存容量限制,同时通过PQ对向量进行压缩,减小数据体积并加速近似距离计算,有效缓解了磁盘IOPS瓶颈。这一设计使其特别适用于内存无法容纳的十亿级及以上规模数据集,在平衡查询延迟与存储容量方面具有不可替代性

4. 核心索引参数

不同类型的ANNS索引具有独特的核心参数,其调整机制直接影响索引的构建效率、查询速度及检索精度

索引类型 参数名称 参数作用 推荐值范围 性能影响
IVF系列 nlist 决定向量空间聚类划分粒度 百万级数据1024 增大可提升搜索速度,但可能降低召回率
IVF系列 nprobe 控制查询时遍历的簇数量 8-100 增大可提高召回率,但会增加查询时间
HNSW M 图中每个节点的连接度数 16-48 增大可提升检索精度,但增加构建时间和内存占用
HNSW efConstruction 索引构建阶段的搜索范围参数 200-500 增大可提升索引质量,但延长构建时间
HNSW ef 查询阶段的搜索范围参数 top_k-2000 增大可提高搜索精度,但降低查询速度
SCANN reorder_k 重排序候选向量数量 top_k-1000 增大可提高召回率,但增加计算量和查询时间
稀疏索引 drop_ratio_search 查询时忽略小值元素的比例 - 调整可在精度与性能之间取得平衡

5. mmap(内存映射)

mmap(内存映射):不把整个文件读进内存,而是让操作系统帮你按需分页读取文件。不是把SSD全部复制到内存,

SSD 文件 ↔ 虚拟内存地址,程序感觉像在访问内存,实际上OS在需要时才读页,不需要时自动淘汰。

全部向量数据加载到内存,内存会OOM

使用mmap,文件仍在 SSD,QueryNode 只映射地址

建议:高频访问索引驻留内存,低频索引或原始字段使用 mmap

mmp是怎么实现的呢?

在使用mmp时,每次加载Collection时,Milvus会调用mmap将用于保障搜索速度的关键的索引加载到内存中,而其他的标量或者向量数据将会继续存放在SSD中,查询时,将通过内存映射的方式访问数据。

注意点: 性能可能波动:如果访问的数据不在内存缓存里(比如第一次访问某个冷数据),需要从硬盘读,速度会比纯内存慢一点(这叫 "缓存未命中")。 索引仍需内存:为了保证搜索速度,索引还是要加载到内存,不能映射到硬盘

mmap 的配置级别(怎么用?) Milvus 的 mmap 可以在 4 个级别配置,优先级从高到低 是:字段 / 索引级别 > 集合级别 > 全局级别(优先级高的会覆盖低的)。

1.全局级别(整个集群默认设置) 是整个 Milvus 集群的基础设置,保存在milvus.yaml文件里,影响所有集合。 我们在配置Milvus的时候,可以修改milvus.yaml中的,storage参数,将mmapEnabled设置为true

yaml 复制代码
# milvus.yaml
storage:
  mmapEnabled: true  # 全局启用 mmap
  mmapDirPath: /opt/milvus/data/mmap_files  # 映射文件存储路径

==2.集合级别(针对单个集合)==可以给某个集合单独设置 mmap ,覆盖全局设置。 创建集合时启用:在创建集合(Collection)时,通过properties={"mmap.enabled": "true"}参数开启,这样集合里的所有字段默认用 mmap。 修改已有集合:先释放集合(release_collection),再用alter_collection_properties修改 mmap 设置,最后重新加载集合(load_collection)生效。

举例:给 大大大数据集 这个集合启用 mmap,就能单独让它的数占内存少一点

python 复制代码
from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection

# 连接 Milvus
connections.connect("default", host="localhost", port="19530")

# 创建集合时启用 mmap
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128),
    FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=1000)
]

schema = CollectionSchema(
    fields,
    description="大大大数据集",
    properties={"mmap.enabled": "true"}  # 关键配置:集合级启用 mmap
)

collection = Collection("Dw_easy_db", schema)

# 修改已有集合的 mmap 配置
coll = Collection("Dw_easy_db")
coll.release()  # 必须先释放集合

# 修改 mmap 属性并重新加载
coll.alter_properties({"mmap.enabled": "false"})  # 关闭 mmap
coll.load()

3.字段级别(针对单个字段) 更灵活 ------ 可以只给某个字段(比如超大的标量字段)启用 mmap,其他字段正常加载到内存 。 创建字段时启用:在add_field时加mmap_enabled=True参数(比如给存储长文本的 "doc_chunk" 字段启用)。 修改已有字段:用alter_collection_field修改字段的mmap.enabled属性,同样需要先释放再加载集合。 适合场景:比如一个集合里,"向量" 字段常用(放内存),"详细描述" 字段很大且不常用(用 mmap)。

python 复制代码
# 创建字段时启用 mmap(需 Milvus v2.3.0+)
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
    FieldSchema(
        name="large_text", 
        dtype=DataType.VARCHAR, 
        max_length=65535,
        properties={"mmap.enabled": "true"}  # 字段级启用 mmap
    )
]

# 修改已有字段的 mmap 属性
coll = Collection("text_collection")
coll.release()

# 修改字段属性
coll.alter_field("large_text", {"mmap.enabled": "false"})  # 关闭该字段 mmap
coll.load()

4.索引级别(针对单个索引) 给某个字段的索引单独设置 mmap ,比如给 "标题" 字段的索引启用 mmap。 创建索引时启用:在add_index的参数里加{"mmap.enabled": "true"}。 修改已有索引:用alter_index_properties调整,同样需要释放再加载集合生效

python 复制代码
# 创建索引时启用 mmap
index_params = {
    "index_type": "IVF_FLAT",
    "metric_type": "L2",
    "params": {"nlist": 1024},
    "properties": {"mmap.enabled": "true"}  # 索引级启用 mmap
}

coll = Collection("vector_collection")
coll.create_index("vector", index_params)

# 修改索引 mmap 属性
coll.release()
coll.alter_index("vector_index", {"mmap.enabled": "false"})  # 关闭索引 mmap
coll.load()

Milvus 官方建议:常用的数据和索引一定要放内存,不常用的再用 mmap。 比如: 高频访问的用户画像向量和其索引 ------ 放内存,保证搜索快。 低频访问的历史日志向量------ 用 mmap,存硬盘省内存。

五、距离度量方式

距离度量方式 是判断两个向量相似程度的核心标准

向量有两个属性:方向、长度

1. 余弦距离(Cosine Distance)

余弦距离:只看方向像不像,不关心长度。例如:1,1和100,100Cosine 认为它们几乎一样。所有特别适合文本语义,句子长短通常不重要,例如我喜欢猫和我非常喜欢猫

  • 原理:计算两个向量之间夹角的余弦值 ,取值范围-1, 1;值越接近1,向量夹角越小,语义/特征相似度越高;值接近-1时,相似度最低。本质是衡量向量的**"方向一致性**",忽略向量的绝对大小(自动归一化)
  • 适用场景:文本向量(如BERT、Sentence-BERT编码)、图像向量(如ResNet、ViT编码)等语义/特征匹配场景;适用于向量绝对值无实际业务意义的场景(如文本的语义相似性)

2. 欧氏距离(L2 Distance)

L2(欧氏距离):看空间里实际距离。L2不仅看方向还看长度差异

当选择欧氏距离作为距离度量时,Milvus 只计算应用平方根之前的值

这意味着Milvus在计算欧氏距离时会跳过开平方根的步骤,只计算平方和的结果。

  • 原理:计算两个向量在空间中的直线距离(L2范数),取值范围[0, +∞);值越小,向量在空间中的位置越近,相似度越高
  • 适用场景:图像识别、目标检测等计算机视觉场景(图像向量的绝对值代表像素特征强度);向量绝对值大小具有实际意义的场景(如用户行为强度向量);大部分深度学习视觉模型的默认推荐度量方式

这样做的好处是:

  • 减少计算量,提高性能
  • 对于相似性比较来说,开不开平方根不影响排序结果,因为平方根是单调递增函数

所以虽然数值上不是真正的欧氏距离,但在向量相似性搜索中,这种优化不会影响搜索结果的准确性。

3. 曼哈顿距离(L1 Distance)

  • 原理:计算两个向量对应维度差值的绝对值之和(L1范数),取值范围[0, +∞);值越小,相似度越高。相较于欧氏距离,对异常值更敏感,且计算复杂度更低
  • 适用场景:低维向量数据(维度<100)、对异常值敏感的场景(如异常检测、欺诈识别)、资源受限的边缘设备检索场景

4. 其他常用度量方式

4.1 内积(Dot Product)

内积:同时看 方向 + 长度。例如 A = 1,1 ,B = 100,100 ,IP会觉得 B 非常强,因为长度也参与计算

两个向量在相同维度空间中的一种运算,结果是一个标量(单个数字)。值越大(正值),表示方向越接近;值为 0 表示正交(垂直);值为负表示方向相反。它也间接包含了向量长度信息(模长)。

  • 计算两个向量的点积,取值范围[-∞, +∞);值越大,相似度越高。适用于已归一化的向量场景,计算速度比余弦距离快(无需额外归一化步骤)
4.2 汉明距离(Hamming Distance)
  • 适用于二进制向量,计算两个向量对应位不同的数量;值越小,相似度越高。适用于轻量化检索场景(如指纹识别、低维特征匹配)

5. 距离度量方式选择建议

  • 优先遵循模型推荐:文本向量(BERT、Sentence-BERT、LLaMA)优先选余弦距离或内积;图像向量(ResNet、ViT、CLIP)优先选欧氏距离;二进制向量必须选汉明距离
  • 结合业务场景判断:关注"语义/特征方向"忽略大小 → 余弦距离;关注"特征强度差异" → 欧氏距离;低维+异常敏感 → 曼哈顿距离;轻量化二进制向量 → 汉明距离
  • 实验验证优化:若不确定最优方式,可通过同一批测试数据,分别计算不同度量方式的检索召回率、准确率,结合业务指标(如推荐点击率、检索准确率)选择最优解

6. 归一化

当向量归一化后:所有向量长度 = 1 ,此时 IP ≈ Cosine ,长度都一样了,IP只剩方向比较

归一化是把向量中的元素缩放到一个固定的范围,比如0到1。

公式: 对于向量 V = [v₁, v₂, ..., vₙ],其 L2 范数为 ||V||₂ = √(v₁² + v₂² + ... + vₙ²)。归一化后的向量 V_norm 为: V_norm = V / ||V||₂ = [v₁/||V||₂, v₂/||V||₂, ..., vₙ/||V||₂]

归一化的核心作用是让向量的长度变为1,但方向不变。这在很多场景下非常重要,因为:

  • 当我们只关心向量的方向相似度时,长度会影响我们对相似度的判断
  • 当所有向量长度都是1时,计算欧氏距离(L2)的大小顺序和计算内积(IP)的大小顺序是完全一致的

例如,假设你有一个向量 V = [3, 4]:

  1. 计算这个向量的长度(L2范数):长度 = ||V||₂ = √(3² + 4²) = 5
  2. 归一化后的向量:V_norm = [3/5, 4/5] = [0.6, 0.8]
  3. 验证新向量的长度:||V_norm||₂ = √(0.6² + 0.8²) = 1

Milvus官方建议:对于归一化向量,欧氏距离(L2)在数学上等同于内积(IP)。如果这些相似性度量返回不同的结果,请检查您的向量是否归一化。

Cosine :你俩方向像不像?

IP:你俩方向像不像,而且谁更强?

L2:你俩空间距离近不近?

7. 应用场景以及性能差距

欧氏距离和内积是两种常用的距离度量方式,选择L2还是IP,主要取决于你的具体任务和对相似性的定义:

  1. 对于欧氏距离L2来说,其核心思想是衡量两个点之间在空间中的绝对距离。
    • 在聚类算法(如K-means)中,L2距离用于计算最小化簇内点与质心之间的距离
    • 如果您更关心特征空间中位置的距离,那么L2距离是一个更好的选择。例如,在图像处理中,L2距离可以用来衡量两个图像之间的相似度
    • 当向量本身不包含重要信息,或者已经被归一化了,L2和IP的排序等价,L2是距离,会更加直观。值越小表示越相似
  2. 对于内积IP来说,其核心思想是衡量两个点之间的方向关系。
    • 在自然语言处理中,词向量和句子向量之间的相似度可以使用余弦相似度来衡量,余弦相似度就是内积的一种形式
    • 如果您更关心特征空间中方向的一致性,那么IP内积是一个更好的选择。例如,在推荐系统中,IP内积可以用来衡量用户和物品之间的兴趣相似度
    • 值越大表示越相关,值可正可负,负值表示负相关/相反方向
  3. 在计算复杂度上,L2和IP的计算复杂度都是O(d),其中d是向量的维度。但L2比IP多做了n次减法和1次开平方根运算,对于单次计算,L2的开销要比IP要高,但这种差异在绝大多数场景下均是可忽略的。
  4. 选择建议:
    • 如果需要物理距离或形状匹配感,或者向量已归一化且长度无意义,优先用L2
    • 如果需要衡量相关性,或者向量长度本身编码了重要信息(如推荐系统中的物品热度),优先用IP (或未归一化的余弦)

六、Milvus安装

docker-compose方式安装

bash 复制代码
apt install -y docker-compose

#下载Milvusa官方配置文件
wget https://github.com/milvus-io/milvus/releases/download/v2.4.0/milvus-standalone-docker-compose.yml -O docker-compose.yml

#启动 Milvus
docker-compose up -d

docker-compose ps

#安装milvus可视化工具
docker run -d   -p 3000:3000   -e MILVUS_URL=192.168.1.130:19530   zilliz/attu
bash 复制代码
Milvus
  ├── etcd(元数据)
  ├── minio(对象存储)
  └── query / index 服务

七、Milvus基础操作

1. 连接Milvus

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient("http://localhost:19530")
#如果有认证的话
client = MilvusClient(
    uri="http://localhost:19530",
    token="root:Milvus",
    db_name="default"
)

# 检查服务器连接状态
print(client.get_server_version())

2. Schema

Schema 定义了 Collection 的数据结构,包括:

  • 字段(Field): 每个字段有名称、数据类型和可选的属性
  • 主键(Primary Key): 唯一标识每条记录的字段,支持 INT64 或 VARCHAR 类型
  • 向量字段 : 用于存储向量数据的特殊字段,需要指定维度
  • 标量字段 : 存储元数据的字段,如字符串、整数、浮点数等

创建 Schema 有两种方式:使用 create_schema() 方法或使用 FieldSchema 和 CollectionSchema 类

1. 使用 create_schema()

python 复制代码
from pymilvus import MilvusClient, DataType

client = MilvusClient(
    uri="http://192.168.1.130:19530"
)

# 创建 schema
schema = MilvusClient.create_schema(
    auto_id=False,               #不自动生成ID(必须手动提供),所有字段生效
    enable_dynamic_field=False,  #是否允许动态字段,Flase:只能插入schema中定义的字段
    							 #True:可以插入schema中未定义的字段,保存在临时字段$meta中
)

#schema添加字段
#1.添加主键字段
schema.add_field(
    field_name="id",             #字段名
    datatype=DataType.INT64,     #类型
    is_primary=True              #是否主键
    #auto_id=True,  			 #字段级别
)
#2.添加向量字段
schema.add_field(
    field_name="vector",               #字段名
    datatype=DataType.FLOAT_VECTOR,    #向量类型
    dim=768                            #向量维度(默认最多 32768 维)
    								   #可配置配置 Proxy.maxDimension 支持更大维度
)
#3.添加标量字段
schema.add_field(
    field_name="name",
    datatype=DataType.VARCHAR,   	#字符串
    max_length=255				    #最大长度
)
schema.verify() #对定义的 Schema 做合法性检查(校验)

#为 Collection 的不同字段配置索引(Index):定义索引 → 绑定字段 → 创建 Collection 时生效
#创建索引参数对象(索引配置容器)一个列表,用来存多个字段的索引配置
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="id",      #id---> 用排序索引(方便查询),如果没有会全表扫描
    index_type="STL_SORT" # 排序索引(标量索引)适用:主键查询、范围查询
)

index_params.add_index(
    field_name="vector",   # 对向量字段建立索引,vector---->用向量索引(加速相似搜索)
    index_type="IVF_FLAT", # 索引类型
    metric_type="L2",      # 距离计算方式
    params={"nlist": 1024} # nlist:聚类中心数量(桶的数量)。nlist ≈ sqrt(数据量)
)
# 创建 collection
client.create_collection(
    collection_name="test1",    #表名
    schema=schema,    			#定义的字段结构
    index_params=index_params   #配置的索引
)

2. 使用 CollectionSchema

python 复制代码
from pymilvus import MilvusClient,FieldSchema, CollectionSchema, DataType

# 定义字段
id_field = FieldSchema(
    name="id",
    dtype=DataType.INT64,
    is_primary=True,
    auto_id=False
)

title_field = FieldSchema(
    name="title",
    dtype=DataType.VARCHAR,
    max_length=256
)

embedding_field = FieldSchema(
    name="embedding",
    dtype=DataType.FLOAT_VECTOR,
    dim=768
)

price_field = FieldSchema(
    name="price",
    dtype=DataType.FLOAT
)

# 创建 Schema
schema = CollectionSchema(
    fields=[id_field, title_field, embedding_field, price_field],
    description="Product collection",
    enable_dynamic_field=True
)

启用动态字段

enable_dynamic_field=False:是否允许 动态字段

  • True:可以插入未定义字段(可以插入schema中未定义字段,数据保存中$meta 的保留字段中)
  • False:不允许插入未定义字段(不能插入schema中未定义的字段)

启用动态字段时:Milvus 会创建一个名为 $meta 的保留字段,未定义的字段会以键值对形式存储在动态字段中。开启动态字段后会未定义的字段被插入会自动保存隐藏在隐藏字段内($meta)。

例如:

bash 复制代码
enable_dynamic_field=True

#定义了这些字段
schema.add_field("id", ...)
schema.add_field("title", ...)
schema.add_field("embedding", ...)

# 但插入的数据是:
{
    "id": 1,
    "title": "iPhone",
    "embedding": [...],

    "brand": "Apple",
    "color": "black"
}

# 实际内部会变成
{
    "id": 1,
    "title": "iPhone",
    "embedding": [...],

    "$meta": {
        "brand": "Apple",
        "color": "black"
    }
}

schema检查函数

schema.verify():检查定义的 Schema 是否合法

  1. 是否有主键(必要):必须存在一个 is_primary=True 的字段,没有会报错
  2. 向量字段是否合法:是否设置了 dim、是否类型正确(FLOAT_VECTOR)
  3. 字段类型是否合理:VARCHAR 必须有 max_length、类型必须是支持的类型
  4. 字段名是否合法:不能重复、不能为空
  5. 主键配置是否正确:如果 auto_id=True → 不允许手动传 ID、如果 auto_id=False → 必须提供 ID

3. 创建Collection

Collection 是 Milvus 中存储和管理数据的基本容器 ,可以类比为关系数据库中的表。每个 Collection 都需要一个 Schema 来定义其结构,包括字段名称、数据类型、主键等

创建 Collection 需要指定集合名称、Schema 和可选的索引参数

python 复制代码
client.create_collection(
    collection_name="test",  #表名(唯一标识)
    dimension=5,            #向量维度:必须满足:插入的向量长度(向量维度[集合元素])==dimension
    primary_field_name="id", #主键字段名称
    id_type="int",			 # 主键的类型
    vector_field_name="vector", # 向量字段的名称
    metric_type="L2",        #向量距离计算方式 L2/COSINE/IP
    auto_id=True             #是否自动生成主键(系统生成ID),False的话需要自己提供ID
)

创建带Schema和Index的Collection :

python 复制代码
# 基本创建方式
client.create_collection(
    collection_name="products",
    schema=schema
)

# 创建带索引的 Collection
index_params = client.prepare_index_params()

index_params.add_index(
    field_name="embedding",  # 建立索引的字段名必须的向量字段
    index_type="IVF_FLAT",  # 索引类型。IVF_FLAT表示倒排文件平铺索引,适合大规模向量的近似搜索;常见还有IVF_SQ8`、`HNSW` 等
    metric_type="COSINE",  # 距离度量方式。COSINE余弦相似度;常用还有L2`(欧氏距离)、`IP`(内积)。使用 `COSINE` 时一般需要对向量做归一化
    params={"nlist": 128} #索引的超参数字典。nlist表示聚类中心数量(分桶数)
)

client.create_collection(
    collection_name="products",
    schema=schema,
    index_params=index_params
)

查询/重命名/删除Collection:

python 复制代码
# 列出所有 Collection
collections = client.list_collections()
print(collections)

#重命名 Collection
client.rename_collection(
    old_name="products",
    new_name="product_catalog"
)

#删除Collection
client.drop_collection(collection_name="product_catalog")
3.1 向量距离计算方式
类型 含义 距离范围 使用场景
L2 欧氏距离(距离越小--->越相似): 通用向量,关注绝对距离 (即关注方向也关注距离) [0, ∞) 通用(图像 / 坐标/embedding)
COSINE 余弦相似度(夹角越小 → 越相似):文本向量,关注方向而非大小 (只看夹角)不关心长度,只关心方向 -1, 1 NLP(最常用)
IP 内积(值越大 → 越相似):归一化向量,与余弦相似度相关 [-∞, ∞) 推荐系统
3.2 加载Collection

为什么需要加载?

Milvus 是内存计算型向量数据库 ,向量检索非常依赖内存访问速度、索引结构、CPU Cache,如果每次搜索都从磁盘读取会很慢,因此搜索前必须先把数据放进内存。把 Collection 数据(索引文件、所有字段)加载到 QueryNode 内存中,加载后才能search()、query()

  1. 对于小规模数据,可以加载整个 Collection
  2. 选择性加载字段(优化内存使用),对于大规模数据,可以只加载搜索和查询所需的字段,以减少内存占用并提升性能
python 复制代码
# 加载整个 Collection
client.load_collection(
    collection_name="products"
)
==========================================================
# 只加载指定字段
client.load_collection(
    collection_name="products",
    load_fields=["id", "embedding"], # load_fields: 可选,指定需预加载的字段列表。只加载这些字段,其它字段不加载以省内存。向量检索至少应包含向量字段(如 embedding),通常也加载主键(如 id)。若查询/输出引用未加载字段会失败或无法返回该列。默认 None 表示加载全部字段。
    skip_load_dynamic_field=True  # 加载 Collection 到内存时,跳过 $meta(动态字段),节省内存,但无法查询这些动态字段
)

注意事项:

  • load_fields 中**必须包含主键字段和至少一个向量字段**
  • 只有 load_fields 中指定的字段可用于过滤条件和输出字段
  • skip_load_dynamic_field=True 会跳过加载动态字段($meta),使其无法用于过滤和输出
3.3 释放Collection

搜索和查询是内存密集型操作。使用完毕后应释放 Collection 以节省成本:

python 复制代码
# 释放 Collection
client.release_collection(collection_name="products")

# 检查加载状态
status = client.get_load_state(collection_name="products")
print(status)  # 输出: {'state': <LoadState: Loaded>} 或 {'state': <LoadState: NotLoad>}

4. 索引管理

索引是提升向量搜索性能的关键。Milvus 支持多种索引类型,不同的索引适用于不同的场景

4.1 索引类型

index_type 类型:(重点类型)

类型 核心思想 优点 缺点 适用场景
FLAT 暴力全量搜索 100% 精确 极慢 小数据 / 基准测试
IVF_FLAT(常用) 聚类 + 桶内精确(减少搜索范围) 平衡 需要调参 中等规模
IVF_PQ 聚类 + 向量压缩(压缩换空间) 超省内存 有精度损失 超大规模
HNSW 图搜索(最快推荐) 极快 + 高精度 内存大 实时搜索
  1. FLAT(暴力搜索):逐个计算距离。精确搜索(慢)Query → 和所有向量计算距离 → 排序 → TopK。不用索引就是硬算,精度100%、但数据量大会崩
  2. IVF_FLAT(倒排索引+桶内暴力):Inverted File + Flat。例如:全量数据(100万条)--> 聚类(nlist = 1024)-->变成1024个桶 --> 查询时只查几个最相关的桶(而不是100万条)。两阶段搜索:
    1. 聚类(粗筛) :把所有向量分成 nlist 个簇(桶)。类似 K-Means
    2. 精确搜索(细查) :只在部分簇中做暴力搜索(FLAT)。找nprobe个桶 → 桶内暴力计算
  3. IVF_PQ(倒排索引 + 产品量化):在IVF_FLAT基础上对向量做压缩 。例如:原始向量(128维)----> 切成多个子向量----> 每个子向量用"编号"表示(量化)---->用短码代替原向量(压缩)。用"索引编号"代替真实数据 。优点 :内存占用降低、查询快,缺点 :精度下降(有损)、参数复杂(m / nbits)。适用:亿级 / 十亿级向量
  4. HNSW(分层可导航小世界):Hierarchical Navigable Small World主流的高性能索引。核心思想:所有向量 → 构建"邻居图",查询时从入口点开始→ 一步步跳到更接近目标的点→ 快速逼近最近邻。多层图(上层快速跳跃、下层精细搜索)优点 :速度极快、精度高(接近FLAT)、不需要 nlist/nprobe。缺点:内存占用大、构建慢、更新成本高。适用:实时搜索 / 高QPS系统
  5. IVF_SQ8(倒排索引 + 标量量化):本质简单压缩向量(逐维压缩)。float(4字节) → int8(1字节)。优点:内存降低、比 IVF_PQ 精度更好,缺点:仍然有精度损失
  6. SQ8(标量量化): 不做 IVF,仅做压缩:向量 → 压缩 → 再搜索。优点:实现简单,缺点:没有分桶加速有限。适用:轻量压缩需求
  7. ANNOY(树结构索引):Approximate Nearest Neighbors Oh Yeah(近似最近邻 哦耶)。本质:用"空间切分树"快速定位相似向量。通过随机投影 → 构建多棵树 → 查询时遍历树。优点:构建简单、查询快、内存适中,缺点:精度不如 HNSW、不适合频繁更新。适用:静态数据、快速部署场景
  8. DiskANN: 把大部分索引放磁盘。优点:超大规模、内存小。缺点:依赖 SSD

索引类型:

索引类型 核心原理 精度(召回率) 查询速度 内存占用 适用场景 关键特点
FLAT 暴力遍历(全量距离计算) 很高(100%精确) 低 高 小数据集、测试、需要100%召回率 精确搜索,无压缩,直接计算所有向量距离
IVF_FLAT 聚类(IVF) + 簇内暴力搜索 较高 较高 较高 中等规模数据、通用场景 先聚类再搜索,高速查询,尽可能高召回率,精度与性能平衡
IVF_SQ8 聚类 + 标量量化(Scalar Quantization) 中 很高 较低 内存受限场景 float→int 压缩,非常高速查询,可接受轻微召回率损失
IVF_PQ 聚类 + 乘积量化(Product Quantization) 较低 高 低 大规模数据 高压缩率,内存占用低,适合海量向量
IVF_RABITQ 聚类 + RaBitQ 量化 较高 高 很低 大规模数据且要求高召回率 高精度 1-bit 量化,兼顾召回率与资源利用
SQ8 标量量化 中 较高 较低 仅压缩需求 简单压缩,仅降低内存,不依赖 IVF
HNSW 图结构(多层近邻图) 很高 很高 高 高性能检索、高召回需求 非常高速查询,尽可能高召回率,精度与速度均衡
HNSW_SQ HNSW + 标量量化 较高 很高 中 高性能且内存受限 在 HNSW 基础上减少内存占用,可接受轻微召回率损失
HNSW_PQ HNSW + 乘积量化 中 中高 很低 内存极度受限 使用 PQ 大幅压缩内存,速度与精度有所下降
HNSW_PRQ HNSW + Product Residual Quantization 中 中 很低 内存极度受限 残差量化进一步压缩,适合超大规模场景
ANNOY 随机投影树(Random Projection Trees) 较高 较高 中 高召回场景 构建和查询较快,适合只读或低更新场景
SCANN Google 混合 ANN 算法(聚类 + 量化 + 重排序) 很高 很高 高 高性能需求 Google 提出的高性能 ANN,速度与召回率优秀
DISKANN 磁盘图索引(SSD优化) 很高 快 很低 超大规模数据 基于 SSD 的 ANN,内存占用极低,适合十亿级数据
AUTOINDEX 自动索引优化 自动优化 自动优化 自动优化 快速启动、通用场景 Milvus 自动选择最优索引,适用于所有字段类型
4.2 创建索引

创建向量索引

python 复制代码
# 准备索引参数
index_params = client.prepare_index_params()

# 添加向量字段索引
index_params.add_index(
    field_name="embedding", #给embedding 向量字段建立索引
    index_type="HNSW",     # HNSW图索引
    metric_type="COSINE",  # COSINE 余弦相似度
    params={
        "M": 16,		  #每个向量最大邻居数
        "efConstruction": 256 #建图时搜索候选邻居数量。构建索引时每插入一个节点,会先搜索 efConstruction 个候选邻居,再选择最佳邻居
    }
)

# 在创建 Collection 时指定索引
client.create_collection(
    collection_name="products1",
    schema=schema,
    index_params=index_params
)

为已存在的 Collection 创建索引

python 复制代码
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="IVF_FLAT",
    metric_type="COSINE",
    params={"nlist": 128}
)
# 创建索引
client.create_index(
    collection_name="products",
    index_params=index_params
)
4.3 标量字段索引

对于经常用于过滤的标量字段,可以创建标量索引以提升查询性能

python 复制代码
# 为数值字段创建索引
index_params.add_index(
    field_name="price",   #为标量字段price创建标量索引
    index_type="INVERTED" #类型是倒排索引(值 -> 对应记录ID列表)
)

client.create_index(
    collection_name="products",
    index_params=index_params
)

标量字段支持的索引类型:

  • INVERTED : 倒排索引,适用于除JSON外的所有标量字段
  • STL_SORT: 排序索引,适用于数值类型字段
  • TRIE : 字典树索引,仅适用于VarChar字段
  • BITMAP: 位图索引,适用于Bool、Int8、Int16、Int32、Int64、VarChar和Array字段(元素类型为上述之一)
  • AUTOINDEX: 自动索引,根据数据类型自动选择最佳索引
索引类型 核心原理 适用字段类型 适用场景 关键特点
INVERTED 倒排索引(值 → ID列表) 除 JSON 外的大多数标量字段 通用过滤查询 最常用,适合等值、范围过滤
STL_SORT 排序索引(对数值字段提前排序,然后用二分查找快速定位范围) 数值类型(INT/FLOAT) 范围查询、时间查询、数值排序 对范围过滤效率高
TRIE 字典树(前缀树)。按字符前缀构建树 VARCHAR 字符串前缀匹配、自动补全、字符串过滤 适合文本前缀搜索、字符串友好
BITMAP 位图索引。用0/1位数组表示数据是否满足条件 Bool、整数、VARCHAR、Array 低基数字段(值种类少) 查询极快,内存效率高。位运算是CPU级操作
AUTOINDEX 自动选择最佳索引 自动适配 快速开发 Milvus 自动优化
4.4 索引管理操作
python 复制代码
# 查看索引信息
indexes = client.list_indexes(collection_name="products")
print(indexes)

# 描述索引详情
index_info = client.describe_index(
    collection_name="products",
    index_name="embedding_vector_idx"
)
print(index_info)

# 删除索引
client.drop_index(
    collection_name="products",
    index_name="embedding_vector_idx"
)
4.5 使用 AUTOINDEX

AUTOINDEX 是 Milvus 提供的智能索引选项,会根据数据类型自动选择最合适的索引类型

python 复制代码
index_params = client.prepare_index_params()

index_params.add_index(
    field_name="embedding",
    index_type="AUTOINDEX",
    metric_type="COSINE"
)

client.create_collection(
    collection_name="products2",
    schema=schema,
    index_params=index_params
)

AUTOINDEX 自动选择规则:

数据类型 自动选择索引 核心原因
VARCHAR INVERTED(倒排索引) 支持等值 / 前缀 / 过滤查询
INT8 / INT16 / INT32 / INT64 INVERTED(倒排索引) 高效 value → rowID 映射
FLOAT / DOUBLE INVERTED(倒排索引) 支持数值过滤(range / equality)
4.5 HNSW 索引案例
python 复制代码
##################创建 HNSW 索引###########################
from pymilvus import MilvusClient, DataType

# 连接 Milvus
client = MilvusClient(uri="http://192.168.1.130:19530")

# 1. 定义 Schema
schema = MilvusClient.create_schema(
    auto_id=False,
    enable_dynamic_field=False
)

# 主键字段
schema.add_field(
    field_name="id",
    datatype=DataType.INT64,
    is_primary=True
)

# 向量字段(128维)
schema.add_field(
    field_name="vector",
    datatype=DataType.FLOAT_VECTOR,
    dim=128  #定义字段128维,插入字段时也必须128维
)
schema.verify()

# 2. 定义 HNSW 索引
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="vector",
    index_type="HNSW",          # 使用 HNSW
    metric_type="L2",           # 距离类型(也可以 COSINE)
    params={
        "M": 16,                # 每个节点邻居数,每个节点认识 16 个朋友
        "efConstruction": 200   # 建图复杂度,建索引时搜索200个候选邻居
    }
)
# 3. 创建 Collection
client.create_collection(
    collection_name="hnsw_demo",
    schema=schema,
    index_params=index_params
)
print("Collection + HNSW 索引创建完成")

################## 插入数据 ###########################
import random

# 构造 1000 条随机向量数据
data = []
for i in range(1000):
    data.append({
        "id": i,
        "vector": [random.random() for _ in range(128)]
    })
res = client.insert(
    collection_name="hnsw_demo",
    data=data
)
print("插入数据:", res)

################## HNSW 查询(efSearch)###########################
# 构造一个查询向量
query_vector = [[random.random() for _ in range(128)]]

res = client.search(
    collection_name="hnsw_demo",
    data=query_vector,
    limit=5,
    search_params={
        "metric_type": "L2",
        "params": {
            "ef": 64   # 对应 efSearch(查询复杂度),搜索时探索64个候选节点
        }
    }
)

for item in res[0]:
    print(item)

HNSW关键参数:

参数 作用
M(常用8~32,默认16平衡) 每个向量节点最多连接多少个邻居 。本质:控制图的密度
efConstruction(一般:100~400,常见默认:200) 建索引时,每次插入节点考虑的候选邻居数量 (小--建图快,但图质量差。大----建图慢,但图更优)。总结:影响图结构好不好 。(建索引:一次性把所有点插入到HNSW图中 )建索引时每插入一个点 会发生(每个点:对已有数据集合中的每一个点,逐个插入到图结构中 ),1.找候选邻居(efConstruction 控制搜索范围),2.从候选中选最优 M 个邻居,3.建立图连接
efSearch = ef (一般:32128,高精度:1282556) 查询时,最多探索多少个候选节点。搜索时"愿意多花多少时间找更优解"。例如**efSearch = 64 → 查询时最多探索64个候选点** (找人时,你愿意问多少个人来找目标 )。ef 越大 → 找得更全面 → 更准。ef 越小 → 搜索浅 → 可能错过最优解。总结:影响查得准不准
  • M → 决定图结构(基础质量)
  • efConstruction → 决定图构建质量
  • efSearch → 决定查询精度
4.6 IVF_FLAT索引案例

IVF : 先聚类分桶,再只搜索部分桶,而不是全量扫描

python 复制代码
# 配置索引
index_params = client.prepare_index_params()

#添加向量索引
index_params.add_index(
    field_name="vector",     #指定向量字段,必须是 Vector 类型字段
    index_type="IVF_FLAT",   #用 IVF 索引:IVF先聚类分桶,再只搜索部分桶,而不是全量扫描。
    metric_type="L2",        #距离计算方式,内积
    params={"nlist": 1024}   #IVF 参数:nlist:聚类中心数量(桶的数量)
    						 #数据被分成1024个桶,每个桶一部分向量,搜索时只查部分桶
)
python 复制代码
from pymilvus import MilvusClient, DataType
import random

# 连接 Milvus
client = MilvusClient(uri="http://192.168.1.130:19530")

schema = client.create_schema()
# 主键ID
schema.add_field(
    field_name="id",
    datatype=DataType.INT64,
    is_primary=True,
    auto_id=False
)
# 向量字段
schema.add_field(
    field_name="vector",
    datatype=DataType.FLOAT_VECTOR,
    dim=128
)
# 创建 collection
collection_name = "ivf_demo"
client.create_collection(
    collection_name=collection_name,
    schema=schema
)

# 2. 配置 IVF_FLAT 索引
# 创建索引参数对象
index_params = client.prepare_index_params()
# 添加向量索引
index_params.add_index(
    field_name="vector",      # 指定向量字段
    index_type="IVF_FLAT",    # IVF索引
    metric_type="L2",         # 距离计算方式
    params={
        "nlist": 1024         # 聚类中心数量(桶数量)
    }
)
# 创建索引
client.create_index(
    collection_name=collection_name,
    index_params=index_params
)
print("IVF_FLAT 索引创建成功")

# 3. 插入数据
data = []
for i in range(10000):
    # 构造128维随机向量
    vector = [random.random() for _ in range(128)]
    data.append({
        "id": i,
        "vector": vector
    })

# 插入数据
client.insert(
    collection_name=collection_name,
    data=data
)
print("数据插入完成")

# 4. 加载 Collection
client.load_collection(
    collection_name=collection_name
)
print("Collection 加载完成")

# 5. 向量搜索
query_vector = [random.random() for _ in range(128)]
# 搜索参数
search_params = {
    "metric_type": "L2",
    "params": {   # 查询参数
        "nprobe": 10
    }
}

# 执行搜索
results = client.search(
    collection_name=collection_name,
    data=[query_vector],   # 查询向量(支持批量)
    limit=5, # 返回TopK
    search_params=search_params, # 搜索参数
    output_fields=["id"]  # 返回字段
)

print("\n搜索结果:")
for hits in results:
    for hit in hits:
        print(
            f"id={hit['entity']['id']}, "
            f"distance={hit['distance']}"
        )
  • nlist(分桶数量) :整个向量空间被聚成多少个簇(cluster / bucket)。分多少个桶。数据 → KMeans聚类 → nlist个中心点。每个向量会被分配到最近的桶中。总数据量 / nlist = 每桶数据量。
  • nprobe(查桶数量) :查询时要搜索多少个桶。查多少个桶。查询不会扫描全部桶。查询向量---->找到最近的 nprobe 个桶----->仅在这些桶内部搜索------>返回 TopK
nlist 大小 影响
小(如 128) 每个桶数据多 → 搜索慢
大(如 4096) 每个桶数据少 → 搜索快
太大 KMeans训练慢 + 内存开销增大

建议:nlist ≈ √N (桶的数量=根号数据量)

python 复制代码
search_params = {
    "params": {"nprobe": 10}  #nprobe:查询时要搜索多少个桶
}

流程:

  • 建索引 :数据 → KMeans聚类 → 生成 nlist 个聚类中心 → 每个向量归属到最近桶
  • 查询 :向量 → 计算与所有聚类中心距离 → 选最近 nprobe 个桶 → 在桶内做 L2/Cosine/IP 计算 → 返回 TopK

5. 插入数据

Milvus 接受的数据格式是**字典列表,每个字典代表一条记录。在插入数据之前,需要准备好 向量数据和元数据**。向量数据通常由嵌入模型生成

python 复制代码
from pymilvus import MilvusClient, DataType
import random

client = MilvusClient(
    uri="http://192.168.1.130:19530"
)

# 写入一条
res1 = client.insert(
    collection_name="test1",  
    data={
        "id": 0,  # 主键ID
        "vector": [  # 向量
            0.6186516144460161,
            0.5927442462488592,
            0.848608119657156,
            0.9287046808231654,
            -0.42215796530168403
        ],
        "name": "测试1"  # 其他字段
    }
)
print(res1)

# 生成模拟向量数据 (向量字段的维度必须与 schema 中定义的维度一致)
def generate_embeddings(num_vectors, dim=768):
    return [[random.random() for _ in range(dim)] for _ in range(num_vectors)]

# 准备数据
data = [
    {
        "id": 1,
        "title": "Apple iPhone 15 Pro",
        "embedding": generate_embeddings(1)[0],
        "price": 999.99,
        "brand": "Apple",
        "category": "smartphone"
    },
    {
        "id": 2,
        "title": "Samsung Galaxy S24",
        "embedding": generate_embeddings(1)[0],
        "price": 899.99,
        "brand": "Samsung",
        "category": "smartphone"
    },
    {
        "id": 3,
        "title": "Sony WH-1000XM5",
        "embedding": generate_embeddings(1)[0],
        "price": 349.99,
        "brand": "Sony",
        "category": "headphones"
    }
]

#插入数据
insert_result = client.insert(
    collection_name="products",
    data=data
)
5.1 批量插入

对于大规模数据,使用分批插入 以提高性能和稳定性(比如一万条数据,每次插入一千条)

large_data(1w条)→ 按 batch_size 切分 → 分批 insert 到Milvus

python 复制代码
import time

def safe_insert(client, collection_name, batch, retry=3):
    """带重试机制的插入"""
    for attempt in range(retry):
        try:
            return client.insert(
                collection_name=collection_name,
                data=batch
            )
        except Exception as e:
            print(f"[WARN] 插入失败,第 {attempt + 1} 次重试: {e}")
            time.sleep(1)
    raise RuntimeError("插入失败,已超过最大重试次数")


def generate_embeddings_batch(texts):
    """
    批量生成 embedding(需要替换成自己的模型调用)
    示例假设:generate_embeddings(list[str]) -> list[list[float]]
    """
    return generate_embeddings(texts)


def build_data(num_records):
    """构建数据(批量生成 embedding)"""
    texts = [f"Product {i}" for i in range(num_records)]
    embeddings = generate_embeddings_batch(texts)
    data = [
        {
            "id": i,
            "embedding": embeddings[i],
            "title": texts[i],
            "price": 100.0 + i
        }
        for i in range(num_records)
    ]
    return data


def batch_insert(client, collection_name, data, batch_size=1000, flush=False):
    """分批插入数据(优化版)"""
    total = len(data)
    for i in range(0, total, batch_size):
        batch = data[i:i + batch_size]
        safe_insert(client, collection_name, batch)
        progress = min(i + batch_size, total)
        print(f"[INFO] 进度: {progress}/{total} ({progress / total:.2%})")
    # 是否立即刷新(让数据可见)
    if flush:
        client.flush(collection_name)
    print("[DONE] 所有数据插入完成")


# 使用示例
if __name__ == "__main__":
    large_data = build_data(10000)
    batch_insert(
        client=client,
        collection_name="products",
        data=large_data,
        batch_size=500,
        flush=True
    )

推荐:批量 > 单条(性能更好)

5.2 插入到指定分区

如果 Collection 中有多个分区,可以指定插入到特定分区。分区可以提前创建,也可以依赖默认partition _default。数据插入到指定分区后,查询必须指定partition,否则会scan所有 partition → 性能下降

partition :同一个 Collection 内的逻辑分桶(logical shard)

bash 复制代码
Collection: products
 ├── partition: electronics
 ├── partition: clothes
 └── partition: books
python 复制代码
# 创建分区
client.create_partition(
    collection_name="products",
    partition_name="electronics"
)
#数据插入到指定分区
client.insert(
    collection_name="products",
    data=data,
    partition_name="electronics"
)
client.flush("products") 

注意事项:

  • 插入数据前,Collection 可以处于加载或未加载状态
  • 插入操作是异步的,数据可能不会立即可搜索 ,可以调用 flush() 确保数据持久化

flush() 强制把 buffer 中的数据刷到 segment(持久化),触发可查询状态更新。不要频繁 flush,会导致性能灾难

6. 向量搜索Search

向量搜索用于找出与查询向量最相似的数据记录

6.1 基本向量搜索

单向量搜索

python 复制代码
# 加载 Collection
client.load_collection(
    collection_name="products"
)

# 检查加载状态
status = client.get_load_state(collection_name="products")
print(status)

# 生成模拟向量数据 (向量字段的维度必须与 schema 中定义的维度一致)
def generate_embeddings(num_vectors, dim=768):
    return [[random.random() for _ in range(dim)] for _ in range(num_vectors)]

query_vector = generate_embeddings(1)[0]

results = client.search(
    collection_name="products",
    data=[query_vector],  # 查询向量列表
    limit=5,              # 返回前5个最相似的结果
    output_fields=["title", "price", "brand"]  # 返回的字段
)

# 处理搜索结果
for i, hits in enumerate(results):
    print(f"查询 {i+1} 的结果:")
    for hit in hits:
        print(f"  ID: {hit['id']}")
        print(f"  距离: {hit['distance']:.4f}")
        print(f"  标题: {hit['title']}")
        print(f"  价格: ${hit['price']}")
        print()

批量向量搜索

python 复制代码
query_vectors = generate_embeddings(3)  # 3个查询向量

results = client.search(
    collection_name="products",
    data=query_vectors,
    limit=3,
    output_fields=["title", "price"]
)
print(results)

# results[0] 对应第一个查询向量的结果
# results[1] 对应第二个查询向量的结果
# 以此类推
6.2 向量搜索 + 标量过滤
python 复制代码
# 只搜索电子产品类别
results = client.search(
    collection_name="products",
    data=[query_vector],
    filter='category == "smartphone"',
    limit=5,
    output_fields=["title", "category", "price"]
)

print(results)

# 复杂过滤条件
results = client.search(
    collection_name="products",
    data=[query_vector],
    filter='category == "smartphone" and price < 1000',
    limit=5,
    output_fields=["title", "price"]
)

print(results)

# 使用 IN 操作符
results = client.search(
    collection_name="products",
    data=[query_vector],
    filter='brand in ["Apple", "Samsung", "Google"]',
    limit=5,
    output_fields=["title", "brand"]
)

print(results)

支持的过滤表达式

Milvus 支持丰富的布尔表达式用于标量过滤:

操作符 说明 示例
==,!= 等于、不等于 price == 999
>, >=,<, <= 比较运算符 price > 500
in,not in 包含、不包含 brand in ["Apple", "Samsung"]
and,or,not 逻辑运算符 price > 500 and price < 1000
like 模糊匹配 title like "%Pro%"
6.3 搜索参数配置

搜索行为可以通过 search_params 参数进行精细控制

python 复制代码
results = client.search(
    collection_name="products",
    data=[query_vector],
    limit=10,
    search_params={
        "metric_type": "COSINE",    # 距离度量类型
        "params": {"nprobe": 10}     # 搜索参数
    }
)

print(results)

不同索引类型的搜索参数:

  • IVF 系列索引 (IVF_FLAT, IVF_SQ8,IVF_PQ):核心思想:先聚类分桶,再只搜索部分桶
    • nprobe:要查询的聚类单元数量(要查询的桶数量),范围 1,nlist,默认值 8
  • HNSW 索引 :图索引 核心思想:向量之间建立邻居图,查询时在图中跳跃搜索
    • ef(搜索宽度):控制查询时间/准确度权衡的参数,更高的 ef 会带来更准确但更慢的搜索,范围 top_k,int_max
  • SCANN 索引 :粗筛后再精排多少候选
    • nprobe:要查询的聚类单元数量(搜索多少个聚类桶)
    • reorder_k:粗筛后保留多少个候选,最终精排(reorder)的候选数量 。1. 粗筛(近似搜索)---->扫描 nprobe 个聚类桶----->得到 reorder_k 个候选向量----->2.精排----->对 reorder_k 个候选重新计算真实距离---->返回最终 TopK
6.4 分页搜索

使用 offset 参数实现分页

python 复制代码
# 第一页:前10条结果
page1 = client.search(
    collection_name="products",
    data=[query_vector],
    limit=10,
    offset=0
)

print(page1)

# 第二页:跳过前10条,返回第11-20条
page2 = client.search(
    collection_name="products",
    data=[query_vector],
    limit=10,
    offset=10
)

print(page2)
  • offset 与 limit 的总和应小于 16,384
  • 对于大规模分页,建议使用 search_iterator() 方法(类似数据库的Cursor游标)以获得更好的性能
方式 是否重新 ANN Search
offset 分页 每页都重新搜索
search_iterator 只搜索一次

search_iterator案例:(一次搜索,多次读取。适合深分页。iterator缓存了搜索上下文)

python 复制代码
from pymilvus import MilvusClient
import random

client = MilvusClient(uri="http://192.168.1.130:19530")

collection_name = "products"
#查看表结构
desc = client.describe_collection(
    collection_name=collection_name
)

#自动获取 embedding 字段维度
vector_dim = None
for field in desc["fields"]:
    if field["name"] == "embedding":
        vector_dim = int(
            field["params"]["dim"]
        )
print("向量维度:", vector_dim)

# 构造查询向量:生成一个768维随机向量用于向量搜索,==我要查和这个向量最相似的数据
query_vector = [
    random.random()  
    for _ in range(vector_dim)
]

# 搜索参数
search_params = {
    "metric_type": "COSINE",  #使用余弦相似度计算向量距离
    "params": {
        "nprobe": 10  #搜索10个聚类桶
    }
}

# 创建 iterator
iterator = client.search_iterator(
    collection_name=collection_name,
    data=[query_vector],
    batch_size=5,  #分批返回,每次返回5条,即每一页5条
    limit=20,      #总共最多返回20条,总共4页
    search_params=search_params,
    output_fields=[
        "id",
        "title",
        "price"
    ]
)
print("\n开始分页搜索...\n")


# 分页读取
page_no = 1
while True:
    result = iterator.next() #获取下一批搜索结果
    # 无结果结束
    if not result:
        break
    print(f"========== 第 {page_no} 页 ==========")

    # 注意:这里直接遍历 result
    for hit in result:
        print(f"id={hit['id']}")
        print(f"title={hit['entity']['title']}")
        print(f"price={hit['entity']['price']}")
        print(f"distance={hit['distance']}")
        print("-" * 50)
    print()
    page_no += 1

# 关闭 iterator
iterator.close()

print("搜索结束")

7. 标量查询Query

标量查询:指 不进行向量相似度计算 ,而是基于标量字段 的条件表达式进行数据过滤与检索。在向量检索之外提供的结构化过滤能力,用来在"语义搜索之前或之后"缩小候选集合。

标量查询就体现了数据库特有优势,Miluvs相比faiss来说会更像数据库一些,保留非向量查询的一些基础功能

7.1 基本查询
python 复制代码
# 查询所有数据(限制返回数量)
results = client.query(
    collection_name="products",
    filter="",  # 空过滤条件表示查询所有
    output_fields=["id", "title", "price"],
    limit=100
)

print(results)

# 根据ID查询
results = client.query(
    collection_name="products",
    filter='id in [1, 2, 3]',
    output_fields=["id", "title", "price"]
)

print(results)

# 条件查询
results = client.query(
    collection_name="products",
    filter='category == "smartphone" and price > 500',
    output_fields=["id", "title", "brand", "price"]
)

print(results)

重要说明:

  • 当 filter 设置为空字符串时,必须设置 limit 来限制返回的实体数量
  • offset 和 limit 的总和应小于 16384
7.2 字符串匹配查询
python 复制代码
# 精确匹配
results = client.query(
    collection_name="products",
    filter='brand == "Apple"',
    output_fields=["id", "title"]
)

print(results)

# 模糊匹配
results = client.query(
    collection_name="products",
    filter='title like "%Pro%"',  # 标题包含 "Pro"
    output_fields=["id", "title"]
)

print(results)
7.3 数值范围查询
python 复制代码
# 价格区间查询
results = client.query(
    collection_name="products",
    filter='price >= 100 and price <= 1000',
    output_fields=["id", "title", "price"]
)

print(results)

# 多条件组合
results = client.query(
    collection_name="products",
    filter='(category == "smartphone" and price > 800) or (category == "headphones" and price < 200)',
    output_fields=["id", "category", "title", "price"]
)

print(results)
7.4 统计查询
python 复制代码
# 统计总数
count_result = client.query(
    collection_name="products",
    filter="",
    output_fields=["count(*)"]
)
total_count = count_result[0]["count(*)"]
print(f"总记录数: {total_count}")

# 按类别统计
categories = ["smartphone", "headphones", "tablet"]
for category in categories:
    result = client.query(
        collection_name="products",
        filter=f'category == "{category}"',
        output_fields=["count(*)"]
    )
    count = result[0]["count(*)"]
    print(f"{category}: {count} 条记录")

查询优化建议:

  • 对于大规模数据集的查询,建议使用 query_iterator() 方法进行迭代查询。query():一次性返回满足过滤条件的所有结果(可配合limit、output_fields) 。适合结果集较小的场景。 query_iterator():返回一个迭代器,按批次分页拉取结果,内存友好,适合结果集较大的场景或需要流式处理。
  • 查询前确保 Collection 已加载到内存中
  • 可以通过 consistency_level 参数调整一致性级别以平衡性能和准确性

举个query()和query_iterator()的对比例子:

python 复制代码
# 一次性查询:直接拿到所有匹配记录
all_results = client.query(
    collection_name="products",
    filter='category == "smartphone" and price < 1000',
    output_fields=["id", "title", "price", "category"],
    limit=1000  # 可选,控制最大返回条数
)
print(f"总结果数: {len(all_results)}")
for row in all_results:
    print(row["id"], row["title"], row["price"])

#########################################################
# 迭代式查询:分批获取,适合海量数据
it = client.query_iterator(
    collection_name="products",
    filter='category == "smartphone" and price < 1000',
    output_fields=["id", "title", "price", "category"],
    batch_size=200
)

total = 0
while True:
    batch = it.next()
    if not batch:
        break
    for row in batch:
        total += 1
        # 处理每条记录
        print(row["id"], row["title"], row["price"])

print(f"累计结果数: {total}")

8. 分区管理

分区是 Collection 的逻辑子集 ,每个分区与父Collection 共享相同 Schema ,拥有独立的数据子集,用于实现数据隔离与查询范围缩小。合理使用分区可以提升查询性能并简化数据管理。查询时指定分区才能提高性能

Partition 本质:Collection 内部的数据逻辑分桶

bash 复制代码
Collection: products
 ├── electronics
 ├── clothes
 ├── books
 └── _default

不同 partition:

  • schema 完全一致
  • 只是数据不同
8.1 创建和管理分区

创建集合时 ,Milvus 会自动创建一个名为 _default 的分区 。如果不添加或指定其他分区,所有插入的实体都会进入默认分区,一个集合最多可以有 1024 个分区

创建分区

python 复制代码
# 创建分区
client.create_partition(
    collection_name="products",
    partition_name="partitionA"
)

#数据插入到指定分区
client.insert(
    collection_name="products",
    partition_name="partitionA",
    data=data
)

# 只检索指定分区
client.search(
    collection_name="products",
    partition_names=["partitionA"],
    data=[query_vector],
    limit=10
)

查看分区

python 复制代码
# 列出所有分区
res = client.list_partitions(
    collection_name="products"
)
print(res)

检查分区是否存在

python 复制代码
res = client.has_partition(
    collection_name="products",
    partition_name="partitionA"
)
print(res)
# 输出: True

删除分区 :删除分区前需要确保分区已释放

python 复制代码
# 释放分区
client.release_partitions(
    collection_name="products",
    partition_names=["partitionA"]
)

# 删除分区
client.drop_partition(
    collection_name="products",
    partition_name="partitionA"
)

# 验证删除
res = client.list_partitions(
    collection_name="products"
)
print(res)
8.2 加载和释放分区

加载分区

您可以单独加载集合中的特定分区。如果集合中有未加载的分区,集合的加载状态将保持为未加载

python 复制代码
# 创建分区
client.create_partition(
    collection_name="products",
    partition_name="partitionA"
)

client.load_partitions(
    collection_name="products",
    partition_names=["partitionA"]
)

res = client.get_load_state(
    collection_name="products",
    partition_name="partitionA"
)
print(res)
# 输出: {"state": "<LoadState: Loaded>"}

释放分区

python 复制代码
client.release_partitions(
    collection_name="products",
    partition_names=["partitionA"]
)

res = client.get_load_state(
    collection_name="products",
    partition_name="partitionA"
)
print(res)
# 输出: {"state": "<LoadState: NotLoaded>"}
8.3 使用 Partition Key

Partition Key :是一种基于**标量字段的自动分区预过滤机制**。核心目标:在 ANN 搜索之前,先缩小搜索范围

Partition Key 通过指定特定的标量字段作为 Partition Key,并在搜索时基于 Partition Key 指定过滤条件,可以将搜索范围缩小到若干分区,从而提升搜索效率

Partition Key(分区键)核心思想:先按业务字段缩小搜索范围,再做向量 ANN 搜索

普通模式 :query ----> 搜索全部 Partition ----> ANN Search

Partition Key : query----> 先定位 partition ----> 只搜索部分数据 ----> ANN Search

设置 Partition Key(分区键):按字段自动分区

在添加标量字段时,将其 is_partition_key 属性设置为 True:

python 复制代码
#分类字段
schema.add_field(
    field_name="category",   #按这个字段进行分区
    datatype=DataType.VARCHAR, 
    max_length=512,
    is_partition_key=True,  #开启了分区键,会自动按 category 分区
)							#phone、book、food等会hash 到不同 partition

设置分区数量

当指定标量字段作为 Partition Key 时,Milvus 会自动在集合中创建 16 个分区。也可以自定义分区数量

python 复制代码
client.create_collection(
    collection_name="my_collection",
    schema=schema,
    num_partitions=128   ## 自动创建128个Partition
)

基于 Partition Key 的过滤

在启用 Partition Key功能的集合中进行 ANN 搜索时,需要在搜索请求中包含涉及 Partition Key 的过滤表达式

python 复制代码
# 基于单个 partition key 值过滤:只搜索 A 对应分区
filter='partition_key == "x" && <other conditions>'

# 基于多个 partition key 值过滤
filter='partition_key in ["x", "y", "z"] && <other conditions>'

启用 Partition Key Isolation(分区键隔离)

Partition Key Isolation 功能可以进一步提升搜索性能。启用后,Milvus 会基于 Partition Key 值对实体进行分组,并为每个组创建单独的索引 (每个 partition 建立独立 HNSW 图。例如phone一个HNSW图,book一个HNSW图)

python 复制代码
client.create_collection(
    collection_name="my_collection1",
    schema=schema,
    properties={"partitionkey.isolation": True} #每个 partition 建立独立 HNSW 图
)

为什么更快? 搜索复杂度和图大小相关,如果每个分类单独图,则图更小搜索更快。只能category == "phone",不能in "phone", "book",因为Isolation 模式每个值对应独立索引,只能精准命中一个 Partition

启用 Partition Key Isolation 后,基于 Partition Key 的过滤器应仅包含一个特定的 Partition Key 值。目前,Partition Key Isolation 功能仅适用于索引类型设置为 HNSW 的搜索

  • 普通 Filter:filter='price > 100' 只是搜索后过滤或者搜索时动态过滤,ANN 范围没明显减少
  • Partition Key :搜索前先缩小分区,真正减少 ANN 搜索范围。本质:基于业务字段预先分桶。搜索时先定位分区,再做ANN Search

案例

python 复制代码
from pymilvus import (
    MilvusClient,
    DataType
)
import random

# 1. 连接 Milvus
client = MilvusClient(
    uri="http://192.168.1.130:19530"
)

collection_name = "products_partition_key"
# 如果存在则删除
if client.has_collection(collection_name):
    client.drop_collection(collection_name)

# 2. 创建 Schema
schema = client.create_schema()

# 主键
schema.add_field(
    field_name="id",
    datatype=DataType.INT64,
    is_primary=True,
    auto_id=False
)
# 商品分类(Partition Key)
schema.add_field(
    field_name="category",
    datatype=DataType.VARCHAR,
    max_length=100,
    is_partition_key=True # 启用 Partition Key
)
# 商品标题
schema.add_field(
    field_name="title",
    datatype=DataType.VARCHAR,
    max_length=255
)
# 向量字段
schema.add_field(
    field_name="embedding",
    datatype=DataType.FLOAT_VECTOR,
    dim=128
)

# 3. 创建 Collection
client.create_collection(
    collection_name=collection_name,
    schema=schema,  
    num_partitions=32,  # 自动创建分区数量,名称 _default_0-31
    
    # 开启 Partition Key Isolation:Isolation 目前只支持 HNSW
    properties={
        "partitionkey.isolation": True
    }
)
print("Collection 创建成功")

#创建 HNSW 索引
index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={
        "M": 16,
        "efConstruction": 200
    }
)
client.create_index(
    collection_name=collection_name,
    index_params=index_params
)
print("HNSW 索引创建成功")

#插入数据
categories = [
    "phone",
    "book",
    "food",
    "computer"
]
data = []
for i in range(10000):
    category = random.choice(categories)
    data.append({
        "id": i,
        "category": category,
        "title": f"{category}_product_{i}",
        "embedding": [
            random.random()
            for _ in range(128)
        ]
    })
client.insert(
    collection_name=collection_name,
    data=data
)
print("数据插入完成")

#加载Collection
client.load_collection(
    collection_name=collection_name
)
print("Collection 加载完成")

#基于 Partition Key 搜索
#搜索 phone 分类
query_vector = [
    random.random()
    for _ in range(128)
]

results = client.search(
    collection_name=collection_name,
    data=[query_vector],
    limit=5,
    search_params={
        "metric_type": "COSINE",
        "params": {
            "ef": 100
        }
    },
    # Partition Key Filter:Isolation模式 只能单个值过滤
    filter='category == "phone"',
    output_fields=[
        "id",
        "category",
        "title"
    ]
)

print("\n搜索结果:")
for hits in results:
    for hit in hits:
        print(f"id={hit['entity']['id']}")
        print(f"category={hit['entity']['category']}")
        print(f"title={hit['entity']['title']}")
        print(f"distance={hit['distance']}")
        print("-" * 50)

9. 数据过期管理(TTL)

对于某些应用场景,如临时数据存储或会话数据,可以设置 Collection 的 TTL(Time To Live),让数据在指定时间后自动过期

9.1 TTL 概述

**TTL(Time-to-Live)**本质:数据自动过期机制(数据超过指定时间后,自动对查询不可见) ,用于在插入或修改数据后的特定时间段内保持数据有效或可访问 。例如,如果您每天导入数据但只需要保留 14 天的记录,可以通过将集合的 TTL 设置为 14 × 24 × 3600 = 1209600 秒来配置 Milvus 自动删除超过该时间的数据

TTL 工作原理:

  • 过期数据不会被搜索到
  • 数据不会立即物理删除(逻辑删除),TTL到期后只是逻辑不可见 ,真正删除依赖 compaction(压缩)。过期数据可能会保留在存储中,直到下一次数据压缩(默认24小时后清理),DataCoord Compaction会异步清理过期数据
  • 可以通过设置 Milvus 配置文件中的 dataCoord.compaction.expiry.tolerance 配置项来控制何时触发数据压缩。该配置项默认为 -1,表示使用现有的数据压缩间隔 。当更改为正整数(如 12)时,数据压缩将在任何实体过期后的指定小时数后触发

Compaction 本质:Segment 重写 + 清理无效数据

  • 合并 segment
  • 清理 deleted entities
  • 清理 expired entities(TTL)
  • 减少碎片

TTL 适用场景:

  • 聊天会话:最近7天聊天记录
  • 临时向量缓存:RAG Query Cache
  • 推荐系统实时数据: 只保留最近30天用户行为
  • 日志系统:自动删除旧日志向量
9.2 创建带 TTL 的 Collection
python 复制代码
from pymilvus import MilvusClient

# 创建 Collection 时设置 TTL
client.create_collection(
    collection_name="my_collection2",
    schema=schema,
    properties={
        "collection.ttl.seconds": 1209600  # 14天
    }
)
client.flush(collection_name)#需要flush下,否则无法立即生效
9.3 修改已有 Collection 的 TTL
python 复制代码
client.alter_collection_properties(
    collection_name="my_collection2",
    properties={"collection.ttl.seconds": 1209600}
)
client.flush(collection_name)#需要flush下,否则无法立即生效
9.4 删除 TTL 设置

永久保留数据,删除TTL

python 复制代码
client.drop_collection_properties(
    collection_name="my_collection2",
    property_keys=["collection.ttl.seconds"]
)

10. Upsert插入或更新

Upsert 本质:存在则更新(根据主键判断),不存在则插入 (UPDATE + INSERT)

更新:旧数据标记删除 + 插入新版本(类似MVCC多版本)

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient(
    uri="http://192.168.1.130:19530"
)

## 查询 id = 2 的数据
res = client.query(
    collection_name="test1",
    filter="id == 2",
    output_fields=["*"]
)
# 判空
if not res:
    print("未找到 id=2 的数据")
else:
    row = res[0]
    print("原数据:", row)
    # 修改字段
    row["name"] = "张三"
    # Upsert 保存
    client.upsert(
        collection_name="test1",
        data=[row]
    )
    print("更新成功")
==========================================
client.upsert(
    collection_name="test1",
    data=[{
        "id": 2,
        "name": "张三",
        "vector": [0.6186516284942627,0.5927442312240601]
    }]
)
==========================================================
update_data = [
    {
        "id": 1,  # 已存在的记录,会被更新
        "title": "Apple iPhone 15 Pro Max",
        "price": 1099.99,
        "embedding": generate_embeddings(1)[0],
        "brand": "Apple",
        "category": "smartphone"
    },
    {
        "id": 999,  # 不存在的记录,会被插入
        "title": "Google Pixel 8",
        "price": 699.99,
        "embedding": generate_embeddings(1)[0],
        "brand": "Google",
        "category": "smartphone"
    }
]

upsert_result = client.upsert(
    collection_name="products",
    data=update_data
)
print(f"Upsert 处理的记录数: {upsert_result['upsert_count']}")

Upsert本质 :存在 → 更新 、不存在 → 插入

Upsert 到指定分区

python 复制代码
client.upsert(
    collection_name="products",
    data=update_data,
    partition_name="electronics"
)

与 insert 一样,upsert 也是异步操作

9. Delete向量

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient(
    uri="http://192.168.1.130:19530"
)

client.delete(
    collection_name="test1",
    ids=[1, 2]
)

查询数据:

python 复制代码
from pymilvus import MilvusClient

client = MilvusClient(uri="http://192.168.1.130:19530")

res = client.query(
    collection_name="test1",
    filter="",
    output_fields=["*"],
    limit=1000
)
for row in res:
    print(row)

整个流程本质:

插入 :数据 → 向量 → 建索引

查询:查询 → 向量 → 找最近向量

相关推荐
cspttty1 小时前
2027校招准备:数学与应用数学专业如何补齐管理咨询的业务、分析与表达能力
大数据·数据库
曹牧1 小时前
C#:字符串占位符与值数量不一致
开发语言·数据库·c#
小马不起床1 小时前
Django 后端实战指南_14:门店、省份、用户门店关系查询接口
数据库·django·sqlite
我星期八休息1 小时前
Redis 的原理和使用(2)
前端·数据库·redis·python·mysql·缓存·bootstrap
betazhou1 小时前
Apache Seatunnel 抽取SQL server实时同步到Oracle(四)
数据库·oracle·apache·sql server·seatunnel
l1t1 小时前
DeepSeek总结的PostgreSQL 的 MVCC 很糟。其他所有的也一样。第1部分
数据库·postgresql
川石课堂软件测试2 小时前
JMeter异步接口测试实战
数据库·python·jmeter·mysql·kubernetes·测试用例·prometheus
下页、再停留2 小时前
【C#桌面客户端系列学习-14】SQLite数据库之增删改查完整示例
数据库·sqlite
她的男孩2 小时前
JAR 全对仍 checksum mismatch:Forge Admin 把 Flyway 从 filesystem 改成 classpath
java·数据库·spring boot