作为全球领先的开源向量数据库,Milvus 一直致力于满足不同用户的场景和需求,聆听社区的声音。最近,我们发现,很多用户的数据中常常包含各种不确定类型的数据,也有用户提出希望以 RESTful API 的方式访问 Milvus。
为此,我们引入了 dynamic field 的概念。通过开启 dynamic field,用户可以自由插入各种类型的数据,包括 Milvus 支持的所有数据类型。即使对于同一个键,在不同行中,其对应的值的类型也可以不同。然而,对于 Milvus 来说,动态增加列的方式过于复杂。因此,Milvus 选择用 JSON DataType 来实现 dynamic field。
简单来说,当用户插入 dynamic field 数据时,Milvus 会将这些数据整合到一个 JSON 中,并在表中新增一个名为 meta 的列(请注意,不允许用户的动态列中出现以 meta 为键的数据)。用户的 dynamic data 将会插入到
通过这种方式,Milvus 保证了使用的简便性,并提供了动态字段的灵活性,以满足不同用户的需求。
01.
Dynamic Field 定义
所谓 dynamic field 是指不在用户定义的 schema 中的扩展列。例如用户的 collection 只有 id(int64), vector(float_vector) 列,但用户 enable_dynamic_field = True. 这个时候如果用户插入的数据是 {id: 0, vector:[1.0,2.0,...,100.0], x: "abc"},这行数据里多一个 x 列,这个 x 列就会被当做 dynamic field。
02.
Dynamic Field 和 JSON DataType 在 collection 中的体现(以 python sdk 为例)
- Create Collection
要想使用 dynamic field 的功能,需要在建表时开启。其字段名称为 enable_dynamic_field = True.
代码语言:javascript复制from pymilvus import (
connections,
utility,
FieldSchema, CollectionSchema, DataType,
Collection,
)
connections.connect()
int64_field = FieldSchema(name="int64", dtype=DataType.INT64, is_primary=True)
json_field = FieldSchema(name="json", dtype=DataType.JSON)
float_vector = FieldSchema(name="float_vector", dtype=DataType.FLOAT_VECTOR, dim=128)
schema = CollectionSchema(fields=[int64_field, json_field, float_vector], enable_dynamic_field=True)
hello_milvus = Collection("hello_milvus", schema=schema)
在这个例子中,明确指定了三列 schema,其类型分别为 INT64、 JSON、 FLOAT_VECTOR。但由于 enable_dynamic_field = True, Milvus 会在后台新建一列类型为 JSON 的列来存放动态数据,这个列的列名是 $meta,类型是 JSON,describe_collection 不会看到这一列。
代码语言:javascript复制>>> hello_milvus
<Collection>:
-------------
<name>: hello_milvus
<description>:
<schema>: {'auto_id': False, 'description': '', 'fields': [{'name': 'int64', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': False}, {'name': 'json', 'description': '', 'type': <DataType.JSON: 23>}, {'name': 'float_vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 128}}], 'enable_dynamic_field': True}
- Insert
可以通过行式插入的方式插入动态数据。
代码语言:javascript复制import numpy as np
rng = np.random.default_rng(seed=19530)
rows = []
rows.append({"int64": 0, "float_vector": rng.random((1, 128))[0], "json": {"a": 1, "b": True, "c": "abcd", "d": [1,2,3,4], "e": 100}, "x": "1234", "y": 123, "z": 10.12})
rows.append({"int64": 1, "float_vector": rng.random((1, 128))[0], "json": {"a": 2, "b": True, "c": "abc", "d": [2,3,4,5,6], "f": "hh"}, "x": "abcd", "y": 10})
rows.append({"int64": 2, "float_vector": rng.random((1, 128))[0], "json": {"a": 3, "b": True, "c": "ab", "d": ["a", "b", "c"], "g": "aa"}, "x": "1234", "z": 228})
rows.append({"int64": 3, "float_vector": rng.random((1, 128))[0], "json": {"a": 4, "b": True, "c": "a", "d": ["a", "e", "f"]}, "y": "abcd", "z": "zyx"})
rows.append({"int64": 4, "float_vector": rng.random((1, 128))[0], "json": {"a": 5, "b": True, "c": "aa", "d": [8,7,6,5]}, "$x": "1234", "y": 123})
rows.append({"int64": 5, "float_vector": rng.random((1, 128))[0], "json": {"a": 6, "b": True, "c": "aaa", "d": [1]}, "x": "abcd", "$y": "haha"})
# invalid data, the key of a dynamic field cannot be "$meta".
# rows.append({"int64": 0, "float_vector": rng.random((1, 128))[0], "json": {"a": 1, "b": True, "c": "abcd", "d": [1,2,3,4]}, "x": "abcd", "y": "haha", "$meta": "abc"})
hello_milvus.insert(rows)
在上面的例子中,插入了 6 行数据,其中每一行的数据中都包含 int64, float_vector 和 json 之外,还包含一些其他的动态数据,"x", "y", "z", "x""y" 等。其中 int64, float_vector 和 json 的数据会插入到对应的列中,后面的动态数据每一行会组织成一个 json 格式插入 meta 列中。如第一行 meta 列中插入的数据为{"x": "1234", "y": 123, "z": 10.12}。第二行的数据为 {"x": "abcd", "y": 10},注意在插入的数据中动态数据的 key 不能为 $meta, 否则插入时会报错。
- CreateIndex and Load
index_type = "IVF_FLAT"
index_params = {"nlist": 128}
hello_milvus.create_index("float_vector",
index_params={"index_type": index_type, "params": index_params, "metric_type": "L2"})
hello_milvus.load()
(注意:我们现在还不支持 json 列构建索引。)
- Search or Query
search 或者 query 时都可以对 JSON field 或者 dynamic field 进行表达式过滤,过滤的方式与之前的标量列类似。但需要注意的几点是:
- 不可以直接对 json 列进行过滤,如表达式 json == 1 是非法的。
- 如果要访问 json 列的 key,需要明确写清楚列名 [key], 如 json["a"]
expr = r'json["a"] > 3'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["$meta"])
print(res)
['["id: 3, distance: 17.838302612304688, entity: {'y': 'abcd', 'z': 'zyx'}", "id: 4, distance: 19.294292449951172, entity: {'$x': '1234', 'y': 123}", "id: 5, distance: 22.257793426513672, entity: {'x': 'abcd', '$y': 'haha'}"]']
- 如果访问的是 dynamic field 对应的 key,是可以忽略不写 列名的。如上面的 y,表达式可以是 y > 10 后台会将其 fullback 到 meta 列的,也就是该表达式等价于 $meta["y"] > 10
4. 如果 json 底层的实际数据与 表达式中的常量类型不匹配,算不命中,不会报错。
代码语言:javascript复制expr = r'y > 10'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=3, expr=expr, output_fields=["$meta"])
print(res)
['["id: 4, distance: 21.50458526611328, entity: {'$x': '1234', 'y': 123}", "id: 0, distance: 21.527101516723633, entity: {'x': '1234', 'y': 123, 'z': 10.12}"]']
(如该例子中只返回两条 y 的类型是 int 并且大于 10 的数据。)
5. 如果想访问 json 中的 array 数据,可以通过下标来访问,暂不支持直接拿来与 array 直接比较。
- 表达式 json["d"] == [1,2,3,4]是不正确的。
- 表达式可以是 json["d"][0] > 1
expr = r'json["d"][0] > 1'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["json"])
print(res)
['['id: 4, distance: 0.0, entity: {\'json\': b\'{"a":5,"b":true,"c":"aa","d":[8,7,6,5]}\'}', 'id: 1, distance: 21.50458526611328, entity: {\'json\': b\'{"a":2,"b":true,"c":"abc","d":[2,3,4,5,6],"f":"hh"}\'}']']
6. 从上面的例子中可以看到,outputFields 可以指定 schema 中的列,也可以指定是 $meta, 当然也可以是 dynamic field 的 key。如:
代码语言:javascript复制expr = r'y > 10'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["y"])
print(res)
['["id: 0, distance: 18.593538284301758, entity: {'y': 123}", "id: 4, distance: 19.290794372558594, entity: {'y': 123}"]']
(本例中就只取回了 y 列。)
- 当我们只想查询 json 或者 dynamic field 中包含某个key 的数据时,就可以用 exists 表达式进行过滤。
expr = r'exists x'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["$meta"])
print(res)
['["id: 1, distance: 21.053813934326172, entity: {'x': 'abcd', 'y': 10}", "id: 0, distance: 21.699125289916992, entity: {'x': '1234', 'y': 123, 'z': 10.12}", "id: 5, distance: 22.007081985473633, entity: {'x': 'abcd', '$y': 'haha'}", "id: 2, distance: 25.92767333984375, entity: {'x': '1234', 'z': 228}"]']
expr = r'exists json["g"]'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["json"])
print(res)
['['id: 2, distance: 21.64354705810547, entity: {\'json\': b\'{"a":3,"b":true,"c":"ab","d":["a","b","c"],"g":"aa"}\'}']']
8. 当 dynamic field 的key 中包含特殊字符时(除 identitier 之外的,即「非字母数字下划线组合」),例如 , -, *, /, $ 等等特殊符号,如果想用表达式做过滤,需要明确写 $meta[key],例如现在要想对上面插入的 $x 做过滤,表达式只能是 $meta["$x"] == "1234" 这种类型,不能使用 fullback 的那种方式。
代码语言:javascript复制expr = r'$meta["$x"] == "1234"'
res = hello_milvus.search([rng.random((1, 128))[0]], "float_vector", {"metric_type": "L2"}, limit=6, expr=expr, output_fields=["$meta"])
print(res)
['["id: 4, distance: 21.93911361694336, entity: {'$x': '1234', 'y': 123}"]']