0X00举户、前言
Elasticsearch是一個(gè)基于Lucene庫(kù)的搜索引擎尤仍,它提供了一個(gè)分布式、支持多租戶的全文搜索引擎蔼紧。隨著業(yè)務(wù)的飛速發(fā)展婆硬,對(duì)于搜索的需求也會(huì)增加,比如:搜索圖片奸例、相似向量等彬犯。我們可以利用 ElasticSearch 良好的插件規(guī)范、豐富的查詢函數(shù)哩至、分布式可擴(kuò)展的能力開發(fā)一個(gè)腳本插件使其支持向量檢索躏嚎。本教程主要參考StaySense的開源項(xiàng)目(見參考1)蜜自。
本教程演示環(huán)境配置:
- Python: 3.6.4
- Java: 1.8
- Maven
- Docker&Elaseticsearch: 6.7.0
通過(guò)Docker部署Elasticsearch:6.7.0參考:Elasticsearch安裝使用
0X01菩貌、插件開發(fā)
項(xiàng)目地址:https://github.com/DebugWorld-1024/ImageSimilarityPlugin
項(xiàng)目整體目錄:
1、pom.xml
主要配置一些項(xiàng)目環(huán)境重荠、添加依賴箭阶、打包方式等,完整配置查看項(xiàng)目文件戈鲁。
2仇参、plugin.xml
由于 Elasticsearch 要求自定義插件需要打包成 zip 文件,我們可以配置 Maven Assembly 插件使其自動(dòng)生成婆殿,完整配置查看項(xiàng)目文件诈乒。
3、plugin-descriptor.properties
根據(jù) Elasticsearch 要求婆芦,所有的插件必須包含一個(gè)名為 plugin-descriptor.properties 的插件描述文件怕磨,對(duì)其內(nèi)容有要求且必須放置在 elasticsearch 目錄下。我們?cè)?src/main/resources 目錄下創(chuàng)建 plugin-descriptor.properties 并添加內(nèi)容如下:
name=${elasticsearch.plugin.name}
description=${elasticsearch.plugin.description}
version=${project.version}
classname=${elasticsearch.plugin.classname}
java.version=${maven.compiler.target}
elasticsearch.version=${elasticsearch.version
# extended.plugins=${extendedPlugins}
# has.native.controller=${hasNativeController}
除了 extended.plugins和has.native.controller 都是必須參數(shù)消约,其中 classname 一定是插件運(yùn)行的入口文件肠鲫。具體含義參考官方說(shuō)明:Help for plugin authors
4、代碼
通過(guò)查看官網(wǎng)文檔或粮,腳本插件必須繼承Plugin類导饲,通過(guò)"ScriptEngine"來(lái)實(shí)現(xiàn)的,為了開發(fā)一個(gè)自定義的插件氯材,我們需要實(shí)現(xiàn)"ScriptEngine"接口渣锦,并通過(guò)getScriptEngine()這個(gè)方法來(lái)加載我們的插件,該插件以base64類型讀取ES數(shù)據(jù)氢哮,特征向量的相似算法采用歐式距離泡挺,具體代碼查看項(xiàng)目文件。
5命浴、打包
運(yùn)行打包命令:
mvn clean package
在target目錄下會(huì)生成zip
0X02娄猫、插件安裝
通過(guò)dcoker cp 命令把zip文件復(fù)制到docker 容器中 /usr/share/elasticsearch
進(jìn)入Elasticsearch:6.7.0的docker容器中
docker exec -it es /bin/bash
安裝自定義插件
elasticsearch-plugin install file:///usr/share/elasticsearch/ImageSimilarity-plugin.zip
安裝成功后贱除,會(huì)在plugin目錄下發(fā)現(xiàn)安裝文件,每次安裝更新插件都需要重啟ES
docker restart es
docker 啟動(dòng) elasticsearch的話媳溺,日志默認(rèn)沒有輸出到文檔月幌,默認(rèn)被終端接收,可以使用 docker logs -f es 查看悬蔽。
0X03扯躺、插件使用
通過(guò)Python程序向ES集群寫入100w條數(shù)據(jù),要注意索引的mappings設(shè)置蝎困,feature是ES存儲(chǔ)特性向量數(shù)據(jù)的字段录语,以base64形式存儲(chǔ)。不同編程語(yǔ)言List & base64轉(zhuǎn)換程序見參考2禾乘。
import random
import base64
import numpy as np
from elasticsearch import Elasticsearch, helpers
dbig = np.dtype('>f8')
es = Elasticsearch()
body = {
"mappings": {
"image_search": {
"properties": {
"id": {
"type": "keyword"
},
"feature": {
"type": "binary",
"doc_values": True
}
}
}
}
}
index = 'test'
es.indices.delete(index=index, ignore=404)
es.indices.create(index=index, ignore=400, body=body)
def decode_float_list(base64_string):
"""
base64 轉(zhuǎn) list
:param base64_string:
:return:
"""
bytes_ = base64.b64decode(base64_string)
return np.frombuffer(bytes_, dtype=dbig).tolist()
def encode_array(arr):
"""
List 轉(zhuǎn) base64
:param arr:
:return:
"""
base64_str = base64.b64encode(np.array(arr).astype(dbig)).decode("utf-8")
return base64_str
def generator():
i = 0
while True:
yield {
'id': i,
'feature': encode_array([random.random(), random.random()])
}
i += 1
if i >= 1000000:
break
# 批量插入100w數(shù)據(jù)到es
helpers.bulk(es, generator(), index=index, doc_type='image_search')
查詢程序澎埠,注意source和lang要和插件里一致。
import time
import json
import base64
import numpy as np
from elasticsearch import Elasticsearch, helpers
dbig = np.dtype('>f8')
es = Elasticsearch()
body = {
"from": 0,
"size": 5,
"_source": {
"excludes": ""
},
"sort": {
"_score": {
"order": "asc"
}
},
"query": {
"function_score": {
"query": {
"match_all": {}
},
"functions": [
{
"script_score": {
"script": {
"source": "DebugWorld",
"lang": "ImageSimilarity",
"params": {
"field": "feature",
"feature": [0.01, 0.03]
}
}
}
}
]
}
}
}
def decode_float_list(base64_string):
"""
base64 轉(zhuǎn) list
:param base64_string:
:return:
"""
bytes_ = base64.b64decode(base64_string)
return np.frombuffer(bytes_, dtype=dbig).tolist()
time_list = list()
for i in range(1):
start_time = time.time()
result = es.search(index='test', doc_type='image_search', body=body)
for hit in result['hits']['hits']:
hit['_source']['feature'] = decode_float_list(hit['_source']['feature'])
time_list.append(time.time() - start_time)
print(json.dumps(result, indent=4))
print(sum(time_list)/len(time_list), max(time_list), min(time_list))
0X04始藕、插件性能
測(cè)試服務(wù)器配置:2核8G蒲稳,20G磁盤
數(shù)據(jù): 100w數(shù)據(jù)量,160維的特征向量
1000次請(qǐng)求響應(yīng)情況:
平均時(shí)間:0.348s
最慢時(shí)間:1.085s
最快時(shí)間:0.215s
呃伍派。江耀。。百萬(wàn)數(shù)據(jù)量之內(nèi)還是坑得住的诉植,但是阿里云開發(fā)了一個(gè)有點(diǎn)吊的插件aliyun-knn祥国,但是未開源,有興趣的可以看看文檔晾腔。
0X05舌稀、注意事項(xiàng)
- Elasticsearch的score不能為負(fù)數(shù)
- 瀏覽器插件Elasticsearch Head 列表頁(yè)不支持展示數(shù)組類型數(shù)據(jù)
- Elasticsearch版本更新快,不兼容情況嚴(yán)重建车,實(shí)踐請(qǐng)注意ES版本號(hào)
- 從7.2版本開始扩借,Elasticsearch提供了實(shí)驗(yàn)性的向量檢索功能
0X06、參考
- Elasticsearch: Elasticsearch Plugins and Integrations [6.7]
- Github:StaySense/fast-cosine-similarity
- Github: elastic/elasticsearch
- 公眾號(hào):螞蟻金服 ZSearch 在向量檢索上的探索