最近在研究 AI 应用开发时,我发现向量数据库是一个绕不开的话题。作为神经网络模型的输出格式,向量能够高效地对信息进行编码,在知识库、语义搜索、RAG(检索增强生成)等 AI 应用中扮演着核心角色。

做了一轮调研后,我选了 Milvus。它能力够用,另外还有轻量级的 Milvus Lite 版本,很适合快速上手和做原型。

环境准备与安装

开始之前,确保你的 Python 环境是 3.8+。安装一条命令就够:

$ pip install -U pymilvus

这个包包含 Python 客户端库,也内置了 Milvus Lite,可以在本地快速搭起向量数据库环境。

创建本地向量数据库

创建一个本地 Milvus 向量数据库简单得让人惊喜,只需要实例化一个MilvusClient,并指定数据文件名:

from pymilvus import MilvusClient

client = MilvusClient("milvus_demo.db")

就这样,向量数据库就创建好了。所有数据都会存储在 milvus_demo.db 这个文件中。

Collections的概念与创建

在 Milvus 中,Collections 是存储向量及其相关元数据的容器,你可以把它理解为传统 SQL 数据库中的表。创建 Collections 时,需要定义 Schema 和索引参数来配置向量规格,比如维度、索引类型和距离度量方式。

Milvus 有不少高级配置可以优化搜索性能,但入门时先抓基础功能就行。最基本的,只需要设置 Collections 名称和向量字段维度:

if client.has_collection(collection_name="demo_collection"):
    client.drop_collection(collection_name="demo_collection")
client.create_collection(
    collection_name="demo_collection",
    dimension=768,  # 使用768维的向量
)

在这个配置中:

  • 主键和向量字段使用默认名称(“id"和"vector”)
  • 距离度量方式设置为默认的COSINE(余弦相似度)
  • 主键字段接受整数,不使用自动递增功能

数据准备:从文本到向量

这次实践里,我想实现文本语义搜索。前提是把文本转换为向量,这个过程叫做“嵌入”(Embedding)。

安装模型库

先安装模型库,它包含 PyTorch 等机器学习工具:

$ pip install "pymilvus[model]"

注意:如果你的环境之前没有安装过PyTorch,这个下载过程可能需要一些时间。

生成文本向量

使用 Milvus 提供的默认嵌入模型生成向量。数据需要准备成字典列表,每个字典代表一条数据记录(在 Milvus 中称为“实体”):

from pymilvus import model

# 如果连接 https://huggingface.co/ 失败,可以取消下面两行的注释
# import os
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'

# 这会下载一个小型嵌入模型 "paraphrase-albert-small-v2"(约 50MB)
embedding_fn = model.DefaultEmbeddingFunction()

# 准备要搜索的文本数据
docs = [
    "人工智能作为一门学科成立于1956年。",
    "艾伦·图灵是第一个在AI领域进行实质性研究的人。",
    "图灵出生于伦敦的梅达维尔,在英格兰南部长大。",
]

vectors = embedding_fn.encode_documents(docs)
# 输出向量有768个维度,与刚创建的collection匹配
print("维度:", embedding_fn.dim, vectors[0].shape)  # 维度: 768 (768,)

# 每个实体包含id、向量表示、原始文本,以及一个用于演示元数据过滤的主题标签
data = [
    {"id": i, "vector": vectors[i], "text": docs[i], "subject": "history"}
    for i in range(len(vectors))
]

print("数据包含", len(data), "个实体,每个实体的字段: ", data[0].keys())
print("向量维度:", len(data[0]["vector"]))

输出结果:

维度: 768 (768,)
数据包含 3 个实体,每个实体的字段:  dict_keys(['id', 'vector', 'text', 'subject'])
向量维度: 768

数据插入与存储

准备好数据后,就可以将其插入到 Collections 中:

res = client.insert(collection_name="demo_collection", data=data)

print(res)

输出结果:

{'insert_count': 3, 'ids': [0, 1, 2], 'cost': 0}

这个结果表示成功插入了 3 条记录,分配的 ID 分别是 0、1、2,操作耗时为 0。

语义搜索

语义搜索的思路很直接:先把查询文本转换为向量,再在 Milvus 中做向量相似性搜索。

基础向量搜索

Milvus 支持同时处理一个或多个向量搜索请求。这里试试搜索“谁是艾伦·图灵?”:

query_vectors = embedding_fn.encode_queries(["谁是艾伦·图灵?"])
# 如果你没有嵌入函数,可以使用随机向量来完成演示:
# query_vectors = [ [ random.uniform(-1, 1) for _ in range(768) ] ]

res = client.search(
    collection_name="demo_collection",  # 目标collection
    data=query_vectors,  # 查询向量
    limit=2,  # 返回的实体数量
    output_fields=["text", "subject"],  # 指定要返回的字段
)

print(res)

输出结果:

data: ["[{'id': 2, 'distance': 0.5859944820404053, 'entity': {'text': '图灵出生于伦敦的梅达维尔,在英格兰南部长大。', 'subject': 'history'}}, {'id': 1, 'distance': 0.5118255615234375, 'entity': {'text': '艾伦·图灵是第一个在AI领域进行实质性研究的人。', 'subject': 'history'}}]"] , extra_info: {'cost': 0}

这个结果挺有意思:虽然查询是“谁是艾伦·图灵?”,系统返回了与图灵相关的两条记录,并按相似度排序。这里距离值越小,表示相似度越高。

带元数据过滤的高级搜索

Milvus 可以在向量搜索时同时考虑元数据(在 Milvus 中称为“标量”字段),做法是指定过滤表达式。

先添加一些生物学相关的数据:

# 插入另一个主题的更多文档
docs = [
    "机器学习已被用于药物设计。",
    "AI算法的计算合成预测分子特性。",
    "DDR1参与癌症和纤维化。",
]
vectors = embedding_fn.encode_documents(docs)
data = [
    {"id": 3 + i, "vector": vectors[i], "text": docs[i], "subject": "biology"}
    for i in range(len(vectors))
]

client.insert(collection_name="demo_collection", data=data)

# 这将排除"history"主题中的任何文本,即使它们与查询向量很接近
res = client.search(
    collection_name="demo_collection",
    data=embedding_fn.encode_queries(["告诉我AI相关的信息"]),
    filter="subject == 'biology'",
    limit=2,
    output_fields=["text", "subject"],
)

print(res)

输出结果:

data: ["[{'id': 4, 'distance': 0.27030569314956665, 'entity': {'text': 'AI算法的计算合成预测分子特性。', 'subject': 'biology'}}, {'id': 3, 'distance': 0.16425910592079163, 'entity': {'text': '机器学习已被用于药物设计。', 'subject': 'biology'}}]"] , extra_info: {'cost': 0}

这个例子展示了元数据过滤的作用:即使历史主题中有 AI 相关内容,因为设置了filter="subject == 'biology'",系统只返回生物学主题下的 AI 相关内容。

性能优化建议

默认情况下,标量字段不会建立索引。如果你需要在大型数据集中频繁做元数据过滤搜索,建议使用固定 Schema 并开启索引来提高搜索性能。

Schema的深入理解

在 Milvus 中,Schema 定义数据结构,包括:

  1. 字段(Fields):数据包含哪些属性。在我们的例子中,idvectortextsubject都是字段。

  2. 数据类型(Data Types):每个字段存储什么类型的数据:

    • id:通常是整数(int64
    • vector:向量(float_vector
    • text:字符串(varchar
    • subject:字符串(varchar
  3. 主键(Primary Key):用于唯一标识每一条记录的字段(通常是 id)。

  4. 是否是向量字段(Is Vector Field):标记哪个字段是用于向量搜索的。

  5. 是否开启索引(Enable Indexing):对于标量字段(非向量字段),你可以选择是否为它们创建索引。创建索引可以显著提高过滤查询(例如 subject == 'biology')的性能,尤其是在数据量很大时。

除了向量搜索,还可以执行其他类型的搜索:

查询

query() 是一种操作符,用于检索与某个条件(如过滤表达式或与某些 id 匹配)相匹配的所有实体。

例如,检索标量字段具有特定值的所有实体:

res = client.query(
    collection_name="demo_collection",
    filter="subject == 'history'",
    output_fields=["text", "subject"],
)

也可以通过主键直接检索实体:

res = client.query(
    collection_name="demo_collection",
    ids=[0, 2],
    output_fields=["vector", "text", "subject"],
)

删除实体

如果想清除数据,可以删除指定主键的实体,或删除与特定过滤表达式匹配的所有实体。

# Delete entities by primary key
res = client.delete(collection_name="demo_collection", ids=[0, 2])

print(res)

# Delete entities by a filter expression
res = client.delete(
    collection_name="demo_collection",
    filter="subject == 'biology'",
)

print(res)
[]
[]

加载现有数据

Milvus Lite 的所有数据都存储在本地文件中。程序终止后,你可以用已有文件创建MilvusClient,把数据重新加载到内存中。下面这行会恢复 milvus_demo.db 文件中的 Collections,并继续向其中写入数据。

from pymilvus import MilvusClient

client = MilvusClient("milvus_demo.db")

删除 Collections

如果想删除 Collections 中的所有数据,可以直接删除 Collections:

# Drop collection
client.drop_collection(collection_name="demo_collection")

搜索数据模型设计

信息检索系统(也就是搜索引擎)支撑着很多 AI 应用,比如检索增强生成(RAG)、可视化搜索和产品推荐。它的核心是数据模型:怎么组织、索引和检索信息。

Milvus 允许你通过 Collection Schema 指定搜索数据模型,组织非结构化数据、对应的密集或稀疏向量表示,以及结构化元数据。Data Model Anatomy

数据模型

搜索系统的数据模型设计,通常从分析业务需求开始,再把信息抽象成 Schema 能表达的数据模型。Schema 定义清楚了,数据模型才更容易贴合业务目标,也更容易保证数据一致性和服务质量。

分析业务需求

要满足业务需求,先看用户会发起哪些查询,再确定合适的搜索方法。

  • **用户查询:**确定用户预期执行的查询类型。这有助于确保 Schema 覆盖真实用例,并优化搜索性能。这些查询可能包括:
    • 检索与自然语言查询相匹配的文档
    • 查找与参考图片相似或匹配文本描述的图片
    • 根据名称、类别或品牌等属性搜索产品
    • 根据结构化元数据(如出版日期、标签、评级)过滤项目
    • 在混合查询中结合多种标准(例如,在视觉搜索中,同时考虑图像及其说明的语义相似性)
  • **搜索方法:**根据查询类型选择合适的搜索技术。不同方法解决的问题不同,也经常组合使用:
    • 语义搜索:使用密集向量相似性来查找具有相似含义的项目,适合文本或图像等非结构化数据。
    • 全文搜索:用关键字匹配补充语义搜索。 全文搜索可利用词法分析,避免将长词分解成零散的标记,在检索过程中抓住特殊术语。
    • 元数据过滤:在向量搜索的基础上,应用日期范围、类别或标签等约束条件。

将业务需求转化为搜索数据模型

下一步是把业务需求落到具体的数据模型上:确定信息的核心组件,以及每类信息适合怎么搜索。

  • 定义需要存储的数据,如原始内容(文本、图像、音频)、相关元数据(标题、标签、作者)和上下文属性(时间戳、用户行为等)。
  • 为每个元素确定适当的数据类型和格式。例如
    • 文本描述 → 字符串
    • 图像或文档 Embeddings → 密集或稀疏向量
    • 类别、标签或标志 → 字符串、数组和 bool
    • 价格或评级等数字属性 → 整数或浮点数
    • 结构化信息,如作者详细信息 -> json

这些元素定义清楚后,数据一致性、搜索结果准确性,以及和下游应用逻辑的集成都更容易控制。

Schema 设计

在 Milvus 中,数据模型通过 Collections Schema 表达。Collections Schema 里的字段设计,会直接影响检索效果。每个字段都定义了 Collections 中存储的数据类型,并在搜索过程中承担不同角色。整体看,Milvus 支持两类主要字段:向量字段标量字段

接着把数据模型映射到字段 Schema 中,包括向量字段和辅助标量字段。每个字段都要对应数据模型中的属性,尤其要注意向量类型(密集型或稀疏型)及其维度。

向量字段

向量字段存储文本、图像和音频等非结构化数据的嵌入。这些嵌入可能是密集型、稀疏型或二进制型,取决于数据类型和检索方法。通常,密集向量用于语义搜索,稀疏向量更适合全文或词项匹配。存储和计算资源有限时,二进制向量会更有优势。一个 Collections 可以包含多个向量字段,用来实现多模态或混合检索策略。更多细节可以参考多向量混合检索

下面看密集型、稀疏型和二进制向量的例子及使用场景。

1. 密集型向量 (Dense Vectors)

定义: 密集型向量是大多数元素都非零的向量。它们通常由深度学习模型(如神经网络)生成,用于捕捉数据的语义信息。向量中的每个维度都代表了原始数据在某个抽象特征空间中的一个连续值。

例子: 假设我们有一个句子 “The cat sat on the mat.” 经过预训练的语言模型(如 BERT, Word2Vec, Sentence-BERT)编码后,可能会生成一个固定长度的浮点数向量,例如: [0.123, -0.456, 0.789, 0.010, ..., 0.999] (假设是一个 768 维的向量)

这个向量的每个值都是一个浮点数,且通常大部分值都不是零。

使用场景:

  • 语义搜索 (Semantic Search): 这是最常见的应用。当你想根据内容的“含义”而不是精确的关键词来查找信息时,密集向量很有用。
    • 文本: 搜索与查询语义相似的文档,即使它们没有共享完全相同的关键词。
    • 图像: 搜索与查询图像在视觉上相似的图像(例如,用一张猫的图片搜索其他猫的图片)。
    • 音频: 搜索与查询音频在声音特征上相似的音频片段。
  • 推荐系统: 根据用户过去喜欢的内容的嵌入向量,推荐语义相似的新内容。
  • 聚类与分类: 将语义相似的数据点在向量空间中聚集在一起,或进行分类。
  • 多模态搜索: 将不同模态(如文本、图像)的数据映射到同一个向量空间,实现跨模态搜索。

2. 稀疏型向量 (Sparse Vectors)

定义: 稀疏型向量是大多数元素都为零的向量。它们通常用于表示离散的特征,例如词袋模型 (Bag-of-Words) 或 TF-IDF 特征。稀疏向量通常以 (索引, 值) 对的形式存储,只存储非零元素,以节省空间。

例子: 仍然是句子 “The cat sat on the mat.”

  • 词袋模型 (Bag-of-Words, BoW): 假设我们的词汇表是 {"the": 0, "cat": 1, "sat": 2, "on": 3, "mat": 4}。 句子 “The cat sat on the mat.” 的 BoW 向量可能是: [2, 1, 1, 1, 1, 0, 0, 0, ...] (假设词汇表很大,大部分词都没有出现) 这里,the 出现了 2 次,其他词出现 1 次。大部分位置都是 0。

  • TF-IDF (Term Frequency-Inverse Document Frequency): TF-IDF 比 BoW 更复杂,它会给词语赋予权重,表示其在文档中的重要性。 句子 “The cat sat on the mat.” 的 TF-IDF 向量可能看起来像: [0.1, 0.8, 0.7, 0.9, 0.6, 0.0, 0.0, ...] 这里虽然是非零值,但如果词汇表非常大,绝大多数词在特定文档中是不会出现的,所以大部分维度仍然是零。

使用场景:

  • 全文搜索 (Full-Text Search): 当你想基于精确的关键词匹配或词频来搜索文档时,稀疏向量很有效。
    • 倒排索引 (Inverted Index): 稀疏向量可以很好地与倒排索引结合,快速查找包含特定关键词的文档。
    • BM25 排名: 传统的搜索引擎排名算法,如 BM25,就是基于稀疏向量(通常是词频)进行计算的。
  • 词项匹配 (Lexical Matching): 查找包含特定单词或短语的文档。
  • 混合检索 (Hybrid Search): 将稀疏向量与密集向量结合,以同时利用关键词匹配的精确性和语义匹配的召回率。例如,RAG (Retrieval Augmented Generation) 系统中,通常会同时使用稀疏和密集检索来获取更相关的上下文。

3. 二进制向量 (Binary Vectors)

定义: 二进制向量是一种特殊的稀疏向量,其元素只有两个可能的值:0 或 1。它们通常用于表示二元特征(例如,某个属性是否存在)或通过哈希函数将高维数据压缩为二进制码。

例子:

  • 特征存在性: 假设我们有一些商品的特征:{"防水": 0, "智能": 1, "轻薄": 2, "防震": 3}。 一个商品如果只有 “防水” 和 “轻薄” 特性,它的二进制向量可能是: [1, 0, 1, 0]

  • 局部敏感哈希 (Locality Sensitive Hashing, LSH): 通过 LSH 算法,可以将一个高维的浮点向量映射成一个短的二进制向量。例如,一个 768 维的密集向量经过 LSH 转换后,可能变成一个 64 位的二进制向量: [0, 1, 1, 0, 1, 0, 0, 1, ..., 1]

使用场景:

  • 高效相似度搜索: 当存储和计算资源有限时,二进制向量很有用。它们可以使用位运算(如汉明距离)进行快速的相似度计算。
    • 图像检索: 在大规模图像数据集中进行快速近似相似度搜索。
    • 指纹识别: 生物识别系统中,指纹特征可以编码为二进制向量进行匹配。
  • 特征表示: 表示事物的二元属性。
  • 近似近邻搜索 (Approximate Nearest Neighbor, ANN): 作为 ANN 算法的一种预过滤或加速手段,尤其是在内存受限的环境中。
  • 数据去重: 快速识别重复或近乎重复的数据项。

对比表:

向量类型特点典型生成方式/表示优点缺点典型应用场景
密集型大多数元素非零,浮点数深度学习模型嵌入捕捉语义信息,高召回率存储和计算成本高语义搜索、推荐系统、聚类、多模态搜索
稀疏型大多数元素为零,通常为整数或浮点数词袋、TF-IDF节省存储,适合精确关键词匹配,计算快难以捕捉语义,召回率可能受限于关键词全文搜索、词项匹配、混合检索 (RAG)
二进制元素只有 0 或 1特征存在、LSH极度节省存储,计算速度极快(位运算)损失部分信息,精度可能低于密集向量资源受限环境下的快速近似搜索、指纹识别、数据去重

Milvus 支持这些向量数据类型:FLOAT_VECTOR 表示密集向量,SPARSE_FLOAT_VECTOR 表示稀疏向量BINARY_VECTOR 表示二进制向量

标量字段

标量字段存储原始的结构化值,通常称为元数据,如数字、字符串或日期。这些值可以与向量搜索结果一起返回,对于筛选和排序至关重要。它们允许你根据特定属性缩小搜索结果的范围,比如将文档限制在特定类别或定义的时间范围内。

Milvus 支持BOOLINT8/16/32/64FLOATDOUBLEVARCHARJSONARRAY 等标量类型,用于存储和过滤非向量数据。这些类型可以提高搜索操作的精度和可定制性。

在模式设计中利用高级功能

设计 Schema 时,只把数据映射到字段还不够,还要理解字段之间的关系以及可用的配置策略。设计阶段把关键能力考虑进去,Schema 才能同时满足当前的数据处理要求,并留出扩展空间。下面是创建 Collections Schema 时常见的几个功能:

主键

主键字段是 Schema 的基本组成部分,用来唯一标识 Collections 中的每个实体。主键必须定义,类型必须是整数或字符串标量字段,并标记为is_primary=True。也可以为主键启用auto_id,由系统自动分配递增整数。

有关详细信息,请参阅主字段和自动 ID

分区

为了加快搜索速度,可以开启分区。通过为分区指定一个特定的标量字段,并在搜索过程中按该字段设置过滤条件,可以把搜索范围限制在相关分区内。搜索域缩小后,检索效率通常会更好。

更多详情,请参阅使用 Partition Key

分析器

分析器用于处理和转换文本数据。它会把原始文本转换为 token,并进行结构化处理,方便后续索引和检索。常见步骤包括分词、去停用词,以及词干化。

更多详情,请参阅分析器概述

Analyzer Process Workflow

功能

Milvus 允许把内置函数定义为 Schema 的一部分,用来自动推导某些字段。例如,可以添加内置 BM25 函数,从VARCHAR字段生成稀疏向量,以支持全文搜索。这些函数派生字段可以简化预处理,让 Collections 本身就具备查询所需的数据。

更多详情,请参阅全文检索

真实世界示例

这一节看一个多媒体文档搜索应用的 Schema 设计和代码示例。该 Schema 用于管理文章数据集,数据映射到以下字段:

字段数据源搜索方法使用主键分区键分析器函数输入/输出
article_id (INT64)启用后自动生成auto_id使用 “获取 “进行查询YNNN
标题 (VARCHAR)文章标题文本匹配NNYN
时间戳 (INT32)发布日期按分区密钥过滤NYNN
文本 (VARCHAR)文章原始文本多向量混合搜索NNY输入
文本密集向量 (FLOAT_VECTOR)由文本 Embeddings 模型生成的密集向量基本向量搜索NNNN
文本稀疏向量 (SPARSE_FLOAT_VECTOR)由内置 BM25 函数自动生成的稀疏向量全文搜索NNN输出

有关Schema 的更多信息以及添加各类字段的详细指导,请参阅Schema Explained

初始化模式

先创建一个空 Schema,为后续定义数据模型打底。

from pymilvus import MilvusClient
schema = MilvusClient.create_schema()

添加字段

创建 Schema 后,指定构成数据的字段。每个字段都对应自己的数据类型和属性。

from pymilvus import DataType

schema.add_field(field_name="article_id", datatype=DataType.INT64, is_primary=True, auto_id=True, description="article id")
schema.add_field(field_name="title", datatype=DataType.VARCHAR, enable_analyzer=True, enable_match=True, max_length=200, description="article title")
schema.add_field(field_name="timestamp", datatype=DataType.INT32, description="publish date")
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=2000, enable_analyzer=True, description="article text content")
schema.add_field(field_name="text_dense_vector", datatype=DataType.FLOAT_VECTOR, dim=768, description="text dense vector")
schema.add_field(field_name="text_sparse_vector", datatype=DataType.SPARSE_FLOAT_VECTOR, description="text sparse vector")

在本例中,为字段指定了以下属性:

  • 主键:article_id 用作主键,可自动为输入实体分配主键。
  • Partition Key:timestamp 被指定为分区键,允许通过分区进行过滤。
  • 文本分析器:文本分析器应用于 2 个字符串字段titletext,分别支持文本匹配和全文搜索。

添加功能

为了增强查询能力,可以在 Schema 中加入函数。例如,创建一个函数来处理特定字段相关的数据。

from pymilvus import Function, FunctionType

bm25_function = Function(
    name="text_bm25",
    input_field_names=["text"],
    output_field_names=["text_sparse_vector"],
    function_type=FunctionType.BM25,
)
schema.add_function(bm25_function)

本例在 Schema 中添加了一个内置 BM25 函数,以text字段作为输入,并将生成的稀疏向量存储到text_sparse_vector字段中。