ES dense vector 切换为BES方案
Elasticsearch 从8.0版本开始提供了 Approximate KNN Search,可以对 dense_vector 类型字段存储的向量数据进行近似最近邻搜索。相比于 Exact (brute-force) KNN Search,ANN Search 可以提供更低的检索延迟。Elasticsearch 的 Approximate KNN Search 通过 Lucene 9 实现,而目前Lucene仅提供了基于HNSW的近似最近邻(ANN)实现。
BES提供向量检索插件实现了向量数据的存储和检索,可以支持多种索引类型,并融入ES的数据生命周期和逻辑结构,提供ES风格的DSL进行查询。 因此,ES 8的用户可以非常自然地理解并掌握向量索引插件的使用方法,并可以从ES 8平滑迁移到BES。
下面介绍一些ES8和BES在使用上的区别和迁移方式。
功能对比和迁移
创建索引
快速开始
在ES8创建索引时,增加dense_vector类型字段,指定维度(dims),并开启索引(index:true),即可开始写入向量数据,进行ANN检索了。示例如下。
1// ES8 示例
2PUT /image-index
3{
4 "mappings": {
5 "properties": {
6 "image-vector": {
7 "type": "dense_vector",
8 "dims": 3,
9 "index": true
10 },
11 "title": {
12 "type": "text"
13 },
14 "file-type": {
15 "type": "keyword"
16 }
17 }
18 }
19}
而BES的向量能力通过插件提供,所以创建索引的时候,需要在index settings中开启KNN,然后增加bpack_vector类型字段,指定索引类型(index_type)和维度(dims),即可开始写入向量数据,进行ANN检索了,配置内容和ES8类似,示例如下:
1// BES 示例
2PUT /image-index
3{
4 "settings": {
5 "index": {
6 "knn": true
7 }
8 },
9 "mappings": {
10 "properties": {
11 "image-vector": {
12 "type": "bpack_vector",
13 "index_type":"hnsw",
14 "dims": 3,
15 },
16 "title": {
17 "type": "text"
18 },
19 "file-type": {
20 "type": "keyword"
21 }
22 }
23 }
24}
自定义索引参数
在ES8创建索引的时候,除了必要的维数参数,还可以指定相似度算法(similarity参数,可选值包括 l2_norm/dot_product/cosine 3种)以及HNSW构图的关键参数(index_options参数),示例如下:
1// ES8 示例
2PUT image-index
3{
4 "mappings": {
5 "properties": {
6 "image-vector": {
7 "type": "dense_vector",
8 "dims": 3,
9 "index": true,
10 "similarity": "l2_norm",
11 "index_options": {
12 "type": "hnsw",
13 "m": 32,
14 "ef_construction": 100
15 }
16 },
17 "title": {
18 "type": "text"
19 },
20 "file-type": {
21 "type": "keyword"
22 }
23 }
24 }
25}
而BES除了支持HNSW索引以外,还支持 HNSW_sq8、HNSW_pq、ivf、ivf_pq 等索引类型,具体参数配置请参考文档:创建索引。
而对于HNSW的场景,BES也支持配置相似度算法(space_type参数,可选值包括 l2/dot_prod/cosine 3种,和ES8的命名略有区别)以及HNSW构图的关键参数(parameters参数)。此外,也可以在 index settings 中设置默认值,这样没有显示设置parameters参数的字段会使用默认值构建。示例如下:
1// BES 示例
2PUT /image-index
3{
4 "settings": {
5 "index": {
6 "knn": true,
7 "bpack.knn.hnsw.space": "l2",
8 "bpack.knn.hnsw.m": 32,
9 "bpack.knn.hnsw.ef_construction": 200
10 }
11 },
12 "mappings": {
13 "properties": {
14 "image-vector": {
15 "type": "bpack_vector",
16 "dims": 3,
17 "index_type": "hnsw",
18 "space_type": "l2",
19 "parameters": {
20 "m": 32,
21 "ef_construction": 200
22 }
23 },
24 "title-vector": { // 使用 index settings 的默认 hnsw 参数
25 "type": "bpack_vector",
26 "index_type": "hnsw",
27 "dims": 3
28 },
29 "title": {
30 "type": "text"
31 },
32 "file-type": {
33 "type": "keyword"
34 }
35 }
36 }
37}
ES8除了float类型的向量,还支持byte类型的向量,这部分BES目前没有支持,无法迁移。
向量检索
ANN检索
ES8的Search API增加了knn参数用于定义KNN查询,通过参数filter可以支持对标量数据进行过滤,通过参数query_vector指定查询的目标向量,通过参数num_candidates可以指定在每个shard检索时的候选队列大小,用于平衡查询性能和召回率。
此外,ES8支持通过similarity参数指定向量检索结果的相似度阈值,不会返回相似度低于阈值的向量。示例如下:
1// ES8 示例
2POST /image-index/_search
3{
4 "size": 10,
5 "knn": {
6 "field": "image-vector",
7 "query_vector": [-5, 9, -12],
8 "k": 10,
9 "num_candidates": 200, // 用于调整hnsw的ef_search参数
10 "filter": {
11 "term": {
12 "file-type": "png"
13 }
14 }
15 },
16 "fields": [ "title", "file-type" ]
17}
BES是在Search API的Query DSL里面,增加了knn相关的语法来定义KNN查询,通过参数filter可以支持对标量数据进行过滤,通过参数vector指定查询的目标向量,通过ef参数指定在每个shard检索时的候选队列大小,用于平衡查询性能和召回率,等同于ES8的num_candidates参数。详细语法可以参考文档:向量检索。示例如下:
1// BES 示例
2GET /image-index/_search
3{
4 "size": 10,
5 "query": {
6 "knn": {
7 "image-vector": {
8 "vector": [-5, 9, -12],
9 "k": 10,
10 "ef": 200, // 如果使用hnsw算法,用这个参数调整ef_search参数
11 "linear":false, // 设置为true则进行精确检索(暴力检索),默认false
12 "filter": {
13 "term": {
14 "file-type": "png"
15 }
16 }
17 }
18 }
19 },
20 "fields": [ "title", "file-type" ]
21}
精确检索
ES8支持通过script_score query来遍历所有向量,通过script计算相似度,来获取精确的TopN结果,即Exact (brute-force) KNN Search。script_score 内的query参数可以对标量字段进行过滤,数据过滤越多,需要通过script计算score的数据越少,检索的效率越高,不需要filter的场景可以使用match_all query获取全量数据,示例如下:
1// ES8 示例
2POST /image-index/_search
3{
4 "size": 10,
5 "query": {
6 "script_score": {
7 "query": {
8 "term": {
9 "file-type": "png"
10 }
11 },
12 "script": {
13 "source": "cosineSimilarity(params.queryVector, 'image-vector') + 1.0",
14 "params": {
15 "queryVector": [-5, 9, -12]
16 }
17 }
18 }
19 }
20}
BES可以通过参数linear指定是否进行精确检索,在需要精确检索的时候,将linear参数设置为true即可,查询DSL的结构和ANN Search相同,示例如下:
1// BES 示例
2GET /image-index/_search
3{
4 "size": 10,
5 "query": {
6 "knn": {
7 "image-vector": {
8 "vector": [-5, 9, -12],
9 "k": 10,
10 "ef": 200, // 如果使用hnsw算法,用这个参数调整ef_search参数
11 "linear":true, // 设置为true则进行精确检索(暴力检索),默认false
12 "filter": {
13 "term": {
14 "file-type": "png"
15 }
16 }
17 }
18 }
19 },
20 "fields": [ "title", "file-type" ]
21}
此外,也可以通过ES的script_score
查询通过脚本计算进行精确检索,也是遍历全量数据进行暴力计算。
BES通过自定义script engine实现了简化的script计算逻辑,通过指定"lang":"knn"
和"source":"bpack_knn_script"
可以使用BES预定义的计算逻辑,只需要在params中指定向量字段和距离算法等参数即可。具体的距离计算公式和score计算公式请参考文档:算法介绍。示例如下:
1// BES 示例
2POST /image-index/_search
3{
4 "size": 10,
5 "fields": [ "title", "file-type" ],
6 "query": {
7 "script_score": {
8 "query": { // 填写所需要的filter,不需要filter则使用match_all
9 "match_all": {}
10 },
11 "script": {
12 "source": "bpack_knn_script", //固定参数
13 "lang": "knn", // 固定参数
14 "params": {
15 "space": "cosine", // 距离计算算法,支持 l2/cosine/innerproduct
16 "field": "image-vector", // 向量字段
17 "vector": [-5, 9, -12] // 查询向量
18 }
19 }
20 }
21 }
22}
如果需要更灵活的计算逻辑,也可以直接使用默认的painless脚本语言,在source调用距离计算函数,完成复杂的计算。示例如下:
1// BES 示例
2GET /image-index/_search
3{
4 "size": 10,
5 "fields": [ "title", "file-type" ],
6 "query": {
7 "script_score": {
8 "query": { // 填写所需要的filter,不需要filter则使用match_all
9 "match_all": {}
10 },
11 "script": {
12 "source": "(1.0 + cosineSimilarity(params.query_vector, doc[params.field])) / 2", // 支持的距离计算函数包括,l2Squared/cosineSimilarity/innerProduct
13 "params": {
14 "field": "image-vector", // 向量字段
15 "vector": [-5, 9, -12] // 查询向量
16 }
17 }
18 }
19 }
20}
迁移方案
向量数据从ES8迁移到BES的场景,需要重新构建向量索引,所以可以考虑通过重灌数据或者Logstash的方式进行数据迁移。
- 重灌数据的方式,即手动在BES侧新建索引(BES和ES8的参数差异参考上文),然后通过bulk请求写入数据。
- Logstash的方式,也需要先手动在BES侧新建索引,因为Logstash不会主动创建BES的向量索引。然后可以通过Logstash完成数据的迁移。
下面介绍基于Logstash的数据迁移方式。
- 在BES新建目标索引,以上文的数据为例:
1PUT /image-index
2{
3 "settings": {
4 "index": {
5 "knn": true
6 }
7 },
8 "mappings": {
9 "properties": {
10 "image-vector": {
11 "type": "bpack_vector",
12 "index_type":"hnsw",
13 "dims": 3,
14 },
15 "title": {
16 "type": "text"
17 },
18 "file-type": {
19 "type": "keyword"
20 }
21 }
22 }
23}
- 安装部署Logstash与Java 8 以上的JDK环境,对于7.10.2版本的BES集群,推荐部署logstash-oss的7.10.2版本。
1wget https://artifacts.elastic.co/downloads/logstash/logstash-oss-7.10.2-linux-x86_64.tar.gz
2tar xvf logstash-oss-7.10.2-linux-x86_64.tar.gz
- 配置Logstash任务。简单示例如下,其中中括号的部分是需要替换为实际内容的,另外,可以根据需要添加认证方式,例如 user/password 等,详情可以参考文档:plugins-outputs-elasticsearch
1input {
2 elasticsearch {
3 hosts => ["http://{es8-host}:{es8-port}"]
4 index => "{es8-index}"
5 }
6}
7output {
8 elasticsearch {
9 hosts => ["http://{bes-host}:{bes-port}"]
10 index => "{bes-index}"
11 user => "{bes-user}"
12 password => "{bes-password}"
13 }
14}
- 启动Logstash
1nohup ./bin/logstash -f ~/*.conf 2>&1 >/dev/null &
然后等待Logstash完成数据迁移即可。
注意事项:需要准备Logstash环境并连通ES8和BES集群的网络。