跳转到内容

5.3 动态字段与 JSON 过滤

动态字段让 Schema 更灵活——但灵活的代价是过滤性能


这一节在讲什么?

在第 2 章我们简单提到了动态字段——启用 enable_dynamic_field=True 后,你可以插入 Schema 之外的字段。这一节我们要深入动态字段的工作原理、JSON 过滤的语法、JSON 索引的用法,以及动态字段 vs 静态标量字段的性能对比。


动态字段的工作原理

当你在创建 Collection 时启用动态字段,Milvus 会在内部创建一个隐藏的 $meta 字段(JSON 类型),用来存储所有 Schema 之外的字段:

python
from pymilvus import FieldSchema, CollectionSchema, DataType, MilvusClient

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

# Schema 只定义了核心字段
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
]

# 启用动态字段
schema = CollectionSchema(fields=fields, enable_dynamic_field=True)
client.create_collection(collection_name="documents", schema=schema)

# 插入时传入 Schema 之外的字段——自动存入 $meta
client.insert(
    collection_name="documents",
    data=[
        {
            "content": "AI breakthrough",
            "embedding": [0.1] * 768,
            # 以下字段不在 Schema 中
            "author": "Alice",
            "year": 2024,
            "tags": ["AI", "ML"],
            "is_published": True,
        }
    ]
)

在 Milvus 内部,这条数据的存储结构是:

id: 1 (自动生成)
content: "AI breakthrough"
embedding: [0.1, 0.1, ..., 0.1]
$meta: {"author": "Alice", "year": 2024, "tags": ["AI", "ML"], "is_published": True}

JSON 过滤表达式

动态字段的过滤语法跟静态标量字段一样——直接用字段名:

python
# 动态字段的等值过滤
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='author == "Alice"'
)

# 动态字段的范围过滤
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='year >= 2024'
)

# 动态字段的布尔过滤
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='is_published == true'
)

# 动态字段的 contains 过滤(数组类型)
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='tags contains "AI"'
)

# 组合过滤
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='author == "Alice" and year >= 2024 and tags contains "AI"'
)

静态 JSON 字段的过滤

如果你在 Schema 中显式定义了 JSON 字段(而不是动态字段),过滤时需要用 [] 语法:

python
# Schema 中定义了 metadata 字段(JSON 类型)
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="metadata", dtype=DataType.JSON),  # 静态 JSON 字段
]

# 过滤时用 [] 语法
results = client.search(
    collection_name="documents",
    data=[[0.1] * 768],
    limit=5,
    filter='metadata["author"] == "Alice" and metadata["year"] >= 2024'
)

注意动态字段和静态 JSON 字段的过滤语法差异——动态字段直接用字段名,静态 JSON 字段需要用 [] 路径。这是因为动态字段的每个键在逻辑上是独立的字段,而静态 JSON 字段是一个整体,需要用路径访问其中的键。


JSON 索引(Milvus 2.5+)

默认情况下,JSON 字段的过滤是全扫描的——Milvus 需要逐条解析 JSON 数据才能判断是否满足条件。Milvus 2.5+ 支持为 JSON 字段创建 INVERTED 索引,可以显著提升过滤性能:

python
# 为 JSON 字段创建索引
index_params = client.prepare_index_params()

# 向量索引
index_params.add_index(
    field_name="embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 256}
)

# JSON 字段的 INVERTED 索引
index_params.add_index(
    field_name="metadata",
    index_type="INVERTED",
    index_name="idx_metadata"
)

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

创建 JSON 索引后,Milvus 会自动为 JSON 字段中的所有键建立倒排索引——metadata["author"]metadata["year"] 等都可以走索引加速。


动态字段 vs 静态标量字段

维度静态标量字段动态字段($meta)
Schema必须预定义不需要预定义
类型安全严格(INT64 不能存字符串)灵活(任何类型都行)
过滤性能快(有独立索引)慢(需要解析 JSON)
JSON 索引不需要Milvus 2.5+ 支持 INVERTED
适用场景高频过滤字段低频、不确定的 metadata

比如,下面的对比展示了两种方式在相同场景下的性能差异,由于静态标量字段有独立的存储和索引,过滤时不需要解析 JSON,所以性能更好:

python
# 方案1:高频过滤字段用静态标量字段(推荐)
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),  # 高频过滤
    FieldSchema(name="year", dtype=DataType.INT32),                       # 高频过滤
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="metadata", dtype=DataType.JSON),                    # 低频字段
]
# 过滤:filter='category == "tech" and year >= 2024'  → 快!

# 方案2:所有字段都放 JSON(不推荐)
fields = [
    FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
    FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=768),
    FieldSchema(name="metadata", dtype=DataType.JSON),  # category/year 也在这里
]
# 过滤:filter='metadata["category"] == "tech" and metadata["year"] >= 2024'  → 慢!

常见误区:把所有字段都设成动态字段

有些同学觉得动态字段很方便——不需要预定义 Schema,想加什么字段就加什么。但动态字段的过滤性能显著低于静态标量字段——因为动态字段存储在 JSON 中,过滤时需要解析 JSON,而静态标量字段可以直接走索引。如果你的 Collection 有 5 个以上需要频繁过滤的字段,应该把它们定义为静态标量字段,而不是全部依赖动态字段。


小结

这一节我们深入了动态字段和 JSON 过滤:动态字段存储在隐藏的 $meta JSON 字段中,过滤语法跟静态字段一样;静态 JSON 字段需要用 [] 路径语法;Milvus 2.5+ 支持为 JSON 字段创建 INVERTED 索引加速过滤。核心原则是"高频过滤字段用静态标量字段,低频灵活字段用 JSON 或动态字段"。下一节我们聊数据一致性与持久化。

基于 MIT 许可发布