向量数据库入门:Milvus与语义搜索

最近在做一个知识库检索的需求,传统的关键词搜索效果很差——用户搜"怎么退货",但文档里写的是"退款流程",关键词匹配不上。调研了一圈,最后用向量数据库+语义搜索来解决。这篇记录下 Milvus 的入门使用。

为什么需要向量数据库

传统数据库做搜索靠的是精确匹配或者 LIKE 模糊查询,本质上还是字符串匹配。但自然语言里同一个意思有无数种表达方式,关键词搜索根本搞不定。

向量数据库的思路不同:先用模型把文本转成向量(Embedding),语义相近的文本在向量空间里距离也近。搜索时把查询文本也转成向量,然后找距离最近的几个向量,就是语义最相关的结果。

Embedding 原理简述

Embedding 就是把文本映射到一个高维向量空间。比如用 OpenAI 的 text-embedding-ada-002 模型,每段文本会被转成一个 1536 维的向量。

关键特性是:语义相似的文本,向量之间的余弦相似度高。"退货流程"和"怎么退款"的向量距离会很近,而和"今天天气不错"的向量距离会很远。

Milvus 安装

Milvus 是开源的向量数据库,性能不错,社区也活跃。用 Docker 最方便:

# 下载 docker-compose 文件
wget https://github.com/milvus-io/milvus/neleases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml

# 启动
docker-compose up -d

# 检查状态
docker-compose ps

默认端口是 19530。

Python pymilvus 基本操作

安装客户端库:

pip install pymilvus

连接与创建集合

from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection

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

# 定义 Schema
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=2048),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536),
]
schema = CollectionSchema(fields, description="知识库文档集合")

# 创建集合
collection = Collection("knowledge_base", schema)

插入向量

import openai
import numpy as np

def get_embedding(text):
    # 调用 OpenAI 获取文本 embedding
    resp = openai.Embedding.create(input=text, model="text-embedding-ada-002")
    return resp["data"][0]["embedding"]

# 准备数据
texts = [
    "退款流程:用户提交退款申请后,客服审核通过即可原路退回",
    "商品上架需要填写标题、描述、价格、库存等基本信息",
    "配送费满99元包邮,偏远地区另计",
]
embeddings = [get_embedding(t) for t in texts]

# 插入
collection.insert([texts, embeddings])
collection.flush()
print(f"当前文档数: {collection.num_entities}")

创建索引与搜索

# 创建 IVF_FLAT 索引
index_params = {
    "metric_type": "IP",  # 内积,也可以用 L2(欧氏距离)或 COSINE
    "index_type": "IVF_FLAT",
    "params": {"nlist": 128}
}
collection.create_index("embedding", index_params)

# 加载到内存
collection.load()

# 搜索
query = "怎么退货"
query_embedding = get_embedding(query)

results = collection.search(
    data=[query_embedding],
    anns_field="embedding",
    param={"metric_type": "IP", "params": {"nprobe": 10}},
    limit=3,
    output_fields=["text"]
)

for hits in results:
    for hit in hits:
        print(f"距离: {hit.distance:.4f} | 内容: {hit.entity.get('text')}")

输出类似:

距离: 0.9231 | 内容: 退款流程:用户提交退款申请后,客服审核通过即可原路退回
距离: 0.7845 | 内容: 商品上架需要填写标题、描述、价格、库存等基本信息
距离: 0.7102 | 内容: 配送费满99元包邮,偏远地区另计

"怎么退货"和"退款流程"的相似度最高,说明语义搜索是有效的。

与传统数据库的对比

特性 MySQL/PostgreSQL Milvus
搜索方式 关键词匹配、全文索引 向量相似度
语义理解 依赖 Embedding 模型
数据类型 结构化数据 向量 + 标量
适用场景 CRUD、事务 相似搜索、推荐
索引类型 B-Tree、Hash IVF、HNSW、DiskANN

实际使用中,向量数据库不是用来替代传统数据库的,而是互补。我的做法是:结构化数据存 MySQL,文档的 Embedding 向量存 Milvus,搜索时先走 Milvus 找到相关文档 ID,再从 MySQL 取完整信息。

几个注意点

  1. Embedding 模型的选择很关键:不同模型生成的向量维度不同、效果也不同。中文场景可以试试 m3e-basebge-large-zh,不一定要用 OpenAI。
  2. 索引类型影响精度和速度:IVF_FLAT 精度高但慢,HNSW 速度快但占内存多。数据量小的时候差别不大,几百万条以上就需要根据场景选了。
  3. 向量维度不能混用:一个集合里所有向量必须是同一维度。如果换了 Embedding 模型,需要重新建集合并重新生成所有向量。

向量数据库配合 LLM 做 RAG(检索增强生成)是目前很火的方案,后面有机会再单独写一篇。