向量数据库 Milvus 的基础实践

63 阅读5分钟

一、概述

Milvus 是一款开源的向量数据库‌,专门用于处理海量向量的相似性搜索,是构建生成式 AI(GenAI)应用的核心基础设施。‌‌它跟传统的关系型数据库不一样,主要处理的是从非结构化数据(如图片、文本、音视频)转换而来的嵌入向量,能支撑十亿甚至万亿级的向量检索,广泛应用于 RAG、AI Agent、大模型记忆、推荐系统和以图搜图等场景。

二、使用流程

设计架构(Schema)→ 建索引(Index) → 根据架构创建集合(Collection) → 存数据(Insert) → 取数据(Search / Query)

概念类比作用
Schema(架构)房间设计预先定义这个房间里有哪些货架、每个货架放什么类型的东西
Collection(集合)仓库里的一个房间、table存一类数据,比如"商品名房间"、"文档切片房间"
Field(字段)房间里的格子/货架定义每种数据放哪,比如"文字放A格"、"向量放B格"
Entity(实体/行)一个箱子、row一条完整的数据,包含所有字段
Index(索引)智能导航图让查找变快,不用翻遍所有箱子

和传统数据库的区别:MySQL 存的是"表格行",Milvus 存的是"向量+业务字段",核心能力是找相似的向量。

三、使用步骤实例

步骤 1:环境配置

配置通过环境变量管理,封装 milvus_config 统一读取:

@dataclass
class MilvusConfig:
    milvus_url: str              # Milvus 服务端地址
    chunks_collection: str       # 切片集合名
    entity_name_collection: str  # 预留集合
    item_name_collection: str    # 商品名集合名

milvus_config = MilvusConfig(
    milvus_url=os.getenv("MILVUS_URL"),
    chunks_collection=os.getenv("CHUNKS_COLLECTION"),
    item_name_collection=os.getenv("ITEM_NAME_COLLECTION")
)

步骤 2:客户端连接(单例模式)

实现 全局单例客户端,避免重复创建连接:

from pymilvus import MilvusClient

_milvus_client = None

def get_milvus_client():
    global _milvus_client
    if _milvus_client is None:
        _milvus_client = MilvusClient(uri=milvus_config.milvus_url)
    return _milvus_client

设计要点:单例模式保证全局只有一个连接实例,减少资源消耗,提升性能。

步骤 3:集合(Collection)初始化

集合不存在时 自动创建,包含 Schema 定义 + 索引构建。以商品名集合为例:

# 1. 创建 Schema:自增主键 + 支持动态字段
schema = client.create_schema(auto_id=True, enable_dynamic_field=True)

# 2. 添加字段
schema.add_field(field_name="pk", datatype=DataType.INT64, is_primary=True, auto_id=True)
schema.add_field(field_name="file_title", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="item_name", datatype=DataType.VARCHAR, max_length=65535)
schema.add_field(field_name="dense_vector", datatype=DataType.FLOAT_VECTOR, dim=1024)
schema.add_field(field_name="sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR)

# 3. 构建索引参数
index_params = client.prepare_index_params()

# 稠密向量索引:HNSW + COSINE
index_params.add_index(
    field_name="dense_vector",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200}
)

# 稀疏向量索引:SPARSE_INVERTED_INDEX + IP
index_params.add_index(
    field_name="sparse_vector",
    index_type="SPARSE_INVERTED_INDEX",
    metric_type="IP",
    params={"inverted_index_algo": "DAAT_MAXSCORE", "normalize": True, "quantization": "none"}
)

# 4. 创建集合
client.create_collection(collection_name=collection_name, schema=schema, index_params=index_params)

关键设计决策:

• auto_id=True:主键自增,无需业务侧生成 • enable_dynamic_field=True:支持动态字段,Schema 更灵活 • HNSW + COSINE:目前性能最好的图索引,适合稠密向量近似最近邻搜索,数据量大精度要求不高可以选择 IVF • SPARSE_INVERTED_INDEX + IP:稀疏向量专用倒排索引,内积度量兼顾语义匹配度和关联强度

步骤 4:数据插入

幂等性设计:插入前先删除同 item_name 的旧数据,避免重复:

# 1. 加载集合(必须加载后才能删除/查询)
client.load_collection(collection_name=collection_name)

# 2. 删除同名商品历史数据
safe_item_name = escape_milvus_string(item_name)
filter_expr = f'item_name=="{safe_item_name}"'
client.delete(collection_name=collection_name, filter=filter_expr)

# 3. 构造数据并插入
data = {
    "file_title": file_title,
    "item_name": item_name,
    "dense_vector": dense_vector,
    "sparse_vector": normalize_sparse_vector(sparse_vector)
}
client.insert(collection_name=collection_name, data=[data])

# 4. 再次加载,确保数据立即可查
client.load_collection(collection_name=collection_name)

注意:Milvus 的 delete 和 insert 操作后需要 load_collection() 才能立即生效。

步骤 5:混合检索(Hybrid Search)

封装混合搜索能力:

from pymilvus import AnnSearchRequest, WeightedRanker

query_text = "white headphones, quiet and comfortable"
query_dense_vector = generate_dense_vector(768)

# 1. 构建双向量搜索请求
# 稠密向量,匹配语义
search_param_1 = {
    "data": [query_dense_vector],         # 向量/文本
    "anns_field": "text_dense",           # 字段
    "param": {"metric_type": "cosine"},   # 配置项
    "limit": 2                            # 查询的条数
}
request_1 = AnnSearchRequest(**search_param_1)

# 稀疏向量,匹配关键字
search_param_2 = {
    "data": [query_text],
    "anns_field": "text_sparse",
    "param": {"metric_type": "IP"},
    "limit": 2
}
request_2 = AnnSearchRequest(**search_param_2)

reqs = [request_1, request_2, request_3]

# 2. 加权排名
rerank = WeightedRanker(0.5, 0.5, norm_score=True)  # 开启归一化

res = client.hybrid_search(
    collection_name=collection_name,
    reqs=reqs,
    ranker=rerank,                      # 负责合并和重排序
    limit=2,                            # 最终要合并的条数,通常跟搜索条数保持一致
    output_fields=["item_name"]
)

返回结果:

# 基本搜索返回的是双层 list,对应多个结果,混合检索返回一个结果。
["['id: 1, distance: 0.006047376897186041, entity: {}', 'id: 2, distance: 0.006422005593776703, entity: {}']"]

# "distance": 相似度的分数,0-1,越大越相似
# "entity": 匹配的实体其他的列

常用算法

WeightedRanker:加权排名器,给不同搜索方式的"打分"分配权重,加权求和后重新排名。

RRF Ranker 重排序算法:倒数(1 ÷ 这个数)排名融合,对同一个结果在不同搜索方式中的排名取倒数,然后相加重新排序。

特性RRFWeightedRanker
输入只看排名(第几名)看原始分数(distance 值)
好处不考虑不同搜索的分数量级差异能保留原始相似度信息
问题丢掉了"这个产品到底多匹配"的信息不同搜索的分数量级可能差异很大

四、核心设计点

设计点说明
单例客户端全局复用 MilvusClient,避免重复连接开销
自动建表集合不存在时自动创建 Schema + 索引,零运维
幂等写入按 item_name 先删后插,保证多次执行结果一致
双向量存储稠密向量(语义)+ 稀疏向量(关键词),兼顾泛化与精确
混合检索WeightedRanker 融合双路召回结果,提升检索准确性
主键回填自增 chunk_id 回填到业务数据,供下游关联使用
异常隔离所有 Milvus 操作包裹 try-except,失败不中断主流程

五、可视化

通过部署 Attu 对 Milvus 进行可视化操作。如果 Milvus 用 Docker 跑的,Attu 可以一起配。

services:
  milvus:
    image: milvusdb/milvus:latest
    ports:
      - "19530:19530"
  
  attu:
    image: zilliz/attu:latest
    ports:
      - "8000:3000"
    environment:
      MILVUS_URL: milvus
    depends_on:
      - milvus

然后 docker-compose up -d 一键启动。