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