干貨 | Elasticsearch 向量搜索的工程化實(shí)戰(zhàn)
1、背景
作為一家搜索引擎公司,我們會(huì)很倚賴 ES 幫忙處理包括文章召回,數(shù)據(jù)源劃分,實(shí)體、標(biāo)簽管理等任務(wù),而且都收到了不錯(cuò)的結(jié)果。
最近我們需要對(duì)行業(yè)知識(shí)庫進(jìn)行建模,其中可能會(huì)涉及到實(shí)體匹配、模糊搜索、向量搜索等多種召回和算分方式,最終我們選擇了通過 ES 7.X (最終選擇 7.10)里的新功能,Dense vector 幫忙一起完成這部分的需求。
2、技術(shù)選型
2.1 解決方案需求
支持向量搜索 支持多維度篩選、過濾 吞吐速率 學(xué)習(xí)、使用成本 運(yùn)維成本
2.2 使用場(chǎng)景設(shè)計(jì)
離線數(shù)據(jù)準(zhǔn)備 在離線數(shù)據(jù)構(gòu)建完成后,存入該引擎 引擎對(duì)數(shù)據(jù)中各字段進(jìn)行索引 在線數(shù)據(jù)召回 根據(jù) query理解結(jié)果構(gòu)建的query語句進(jìn)行數(shù)據(jù)召回對(duì)結(jié)果進(jìn)行一定的篩選 對(duì)結(jié)果進(jìn)行一定的打分排序
2.3 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
在確定了數(shù)據(jù)的使用場(chǎng)景我們確定了數(shù)據(jù)結(jié)構(gòu)中,大致會(huì)包含以下一些字段
唯一 id:用以做知識(shí)的去重和快速獲取 實(shí)體、屬性、取值:用來描述知識(shí)的具體內(nèi)容 置信度:用來描述知識(shí)的可信度 分類 flag:知識(shí)主要分類及推薦 category 等 向量表示:作為知識(shí)相似性、相關(guān)性召回、打分的依據(jù) ref 信息:用來回溯解析/獲取該知識(shí)的源信息 其他屬性:包括生效、刪除、修改時(shí)間等支持性的通用屬性 
2.4 解決方案對(duì)比
為了能支持上述的使用需求,我們對(duì)比了包括 ES、Faiss 等多種解決方案。其中,Faiss 和 SPTAG 只是核心算法庫,需要進(jìn)行二次開發(fā)包裝成服務(wù);Milvus 的 1.x 版本中只能存儲(chǔ) id 和 向量,不能完整的滿足我們的使用需求;基于集群穩(wěn)定性和可維護(hù)性等考慮,相對(duì)于后置插件的部署,我們更傾向于使用 ES 的原生功能,所以選擇 ES 的原生向量搜索功能作為我們的最終選擇。
對(duì)比參考:
| 種類 | 實(shí)現(xiàn)語言 | 客戶端支持 | 多條件召回 | 學(xué)習(xí)成本 | 引入成本 | 運(yùn)維成本 | 分布式 | 性能 | 社區(qū) | 備注 |
|---|---|---|---|---|---|---|---|---|---|---|
| Elasticsearch | Java | Java/Python | yes | 低 | 低 | 中 | yes | 中 | 活躍 | 原生功能 |
| Faiss | Python | Python | no | 中 | 高 | 高 | no | 高 | 一般 | 需要二次開發(fā) |
| Milvus | Python + GoLang | Python/Java/GoLang | no | 中 | 中 | 中 | no | 高 | 一般 | 1.x 功能不全 |
| OpenDistro Elasticsearch KNN | Java + C++ | Java/Python | yes | 中 | 中 | 中 | yes | 中 | 一般 | 內(nèi)置插件 |
| SPTAG | C++ | Python + C# | no | 高 | 中 | 中 | no | 高 | 一般 | 需要二次開發(fā) |
3、數(shù)據(jù)流轉(zhuǎn)流程
3.1 離線數(shù)據(jù)處理部分
從多數(shù)據(jù)源采集數(shù)據(jù) 數(shù)據(jù)清洗及預(yù)處理 通過算法引擎提取知識(shí) 通過算法引擎將知識(shí)轉(zhuǎn)換為向量 將知識(shí)的基礎(chǔ)信息連同向量數(shù)據(jù)存入 ES
3.2 在線數(shù)據(jù)召回部分
從前端獲取搜索條件 通過 query理解模塊進(jìn)行檢索條件解析從 ES中進(jìn)行搜索對(duì)結(jié)果進(jìn)行分?jǐn)?shù)調(diào)整 返回前端
4、ES 向量搜索的使用示例
4.1 索引設(shè)計(jì)
Settings:
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2,
"index": {
"routing": {
"allocation": {
"require": {
"node_group": "hot" // 1)
}
}
},
"store": {
"preload": [ // 2)
"knowledge",
"category",
"available",
"confidence",
"del",
"kid"
]
},
"search": {
"slowlog": {
"threshold": {
"query": {
"warn": "1s" // 3)
},
"fetch": {
"warn": "1s" // 3)
}
}
}
},
"translog": {
"flush_threshold_size": "512mb", // 4)
"sync_interval": "5m", // 4)
"durability": "async" // 4)
},
"sort": {
"field": [ // 5)
"kid",
"confidence"
],
"order": [ // 5)
"asc",
"desc"
]
}
}
}
}
說明:
由于向量數(shù)據(jù)較大,所以傾向于將整個(gè)索引都放置在硬件性能更好的節(jié)點(diǎn) 為了支持高性能過濾,將常用的字段預(yù)先加載在內(nèi)存中 對(duì)慢查詢開啟日志方便后續(xù)性能問題的調(diào)查 知識(shí)庫的重建是離線的,會(huì)在更新時(shí)進(jìn)行大量寫入,所以對(duì) translog的提交間隔拉長(zhǎng),加快寫入速度在實(shí)際使用中kid是自增id,同時(shí)可能會(huì)對(duì)知識(shí)的置信度做排序等,所以會(huì)使用 sort field存儲(chǔ)這兩個(gè)字段
Mapping:
{
"mappings": {
"properties": {
"kid": {
"type": "keyword"
},
"knowledge": {
"type": "keyword"
},
"knowledge_phrase": { // 1)
"type": "text",
"analyzer": "faraday"
},
"attribue": { // 1)
"type": "keyword",
"fields": {
"phrase": {
"type": "text",
"analyzer": "faraday"
}
}
},
"value": { // 1)
"type": "keyword",
"fields": {
"phrase": {
"type": "text",
"analyzer": "faraday"
}
}
},
"confidence": { // 2)
"type": "double"
},
"category": {
"type": "keyword"
},
"vector": { // 3)
"type": "dense_vector",
"dims": 512
},
"ref": {
"type": "text",
"index": false
},
"available": {
"type": "keyword"
},
"del": {
"type": "keyword"
},
"create_timestamp": {
"type": "date",
"format": [
"strict_date_hour_minute_second",
"yyyy-MM-dd HH:mm:ss"
]
},
"update_timestamp": {
"type": "date",
"format": [
"strict_date_hour_minute_second",
"yyyy-MM-dd HH:mm:ss"
]
}
}
}
}
說明:
除了對(duì)知識(shí)條目的完整搜索之外,還會(huì)需要進(jìn)行模糊檢索,我們使用了自研的 farady分詞器對(duì)知識(shí)條目的各部分進(jìn)行了分詞處理知識(shí)庫中的知識(shí)條目會(huì)有一部分進(jìn)行專家/人工審核和維護(hù),所以會(huì)對(duì)不同的條目設(shè)置不同的置信度 數(shù)據(jù)預(yù)處理之后會(huì)轉(zhuǎn)成 512 位的向量存在這個(gè)字段中
4.2 數(shù)據(jù)流轉(zhuǎn)
離線部分:
數(shù)據(jù)采集及清洗 通過 模型A從文章中找到知識(shí)條目通過 模型B將知識(shí)條目轉(zhuǎn)化成向量此處 模型A模型B為自研模型,運(yùn)用了包括知識(shí)密度計(jì)算等算法以及berttersonflow等框架將原文、知識(shí)條目等核心內(nèi)容插入數(shù)據(jù)庫 將核心知識(shí)內(nèi)容、向量等組裝成檢索單元插入 ES專家團(tuán)隊(duì)會(huì)針對(duì)數(shù)據(jù)庫中的知識(shí)條目進(jìn)行審核、修改和迭代 算法團(tuán)隊(duì)會(huì)根據(jù)知識(shí)條目的更新以及其他的標(biāo)注對(duì)數(shù)據(jù)鏈路中的模型進(jìn)行迭代,對(duì)在線知識(shí)庫進(jìn)行更新
在線部分:
前端收到請(qǐng)求之后調(diào)用 query 理解組件進(jìn)行分析剔除無效內(nèi)容之后,找出 query里的分類信息等意圖之后,構(gòu)建用來召回的向量和相關(guān)的篩選條件通過組合出來的 ES的query條件對(duì)知識(shí)庫進(jìn)行篩選,并配合置信度等對(duì)結(jié)果進(jìn)行調(diào)整對(duì)召回結(jié)果進(jìn)行不同策略的分?jǐn)?shù)調(diào)整和排序,最后輸出給前端
4.3 示例 query
POST knowledge_current_reader/_search
{
"query": {
"script_score": {
"query": {
"bool": {
"filter": [
{
"term": {
"del": 0
}
},
{
"term": {
"available": 1
}
}
],
"must": {
"bool": {
"should": [
{
"term": {
"category": "type_1",
"boost": 10
}
},
{
"term": {
"category": "type_2",
"boost": 5
}
}
]
}
},
"should": [
{
"match_phrase": {
"knowledge_phrase": {
"query": "some_query",
"boost": 10
}
}
},
{
"match": {
"attribute": {
"query": "some_query",
"boost": 5
}
}
},
{
"match": {
"value": {
"query": "some_query",
"boost": 5
}
}
},
{
"term": {
"knowledge": {
"value": "some_query",
"boost": 30
}
}
},
{
"term": {
"attribute": {
"value": "some_query",
"boost": 15
}
}
},
{
"term": {
"value": {
"value": "some_query",
"boost": 10
}
}
}
]
}
},
"script": {
"source": "cosineSimilarity(params.query_vector, 'vector') + sigmoid(1, Math.E, _score) + (1 / Math.log(doc['confidence'].value))",
"params": {
"query_vector": [ ... ]
}
}
}
}
}
說明:
上述 query的條件、參數(shù)僅做示意,屬于實(shí)際線上使用的脫敏、簡(jiǎn)化版計(jì)算公式為迭代中某一版,后續(xù)調(diào)整和升級(jí)并未體現(xiàn) 邊界條件及空值在輔助服務(wù)和 pipeline中進(jìn)行處理,簡(jiǎn)化了其中邊界條件處理和判斷部分邏輯
5、遇到的問題
5.1 響應(yīng)時(shí)間長(zhǎng)
由于需要進(jìn)行向量計(jì)算,ES 需要耗費(fèi)大量時(shí)間、資源做距離計(jì)算,為此我們進(jìn)行了以下一些優(yōu)化:
特征值截取小數(shù)位數(shù): 為了保證特征的表征,我們并沒有調(diào)整由 bert框架輸出的向量位數(shù)在權(quán)衡了存取效率、數(shù)據(jù)精度和計(jì)算速度之后,我們將每一個(gè) label的精度由16位截取為5位小數(shù)這樣雖然損失了部分精度(約 X%),但是大大降低了存取和計(jì)算時(shí)間(約Y%)在進(jìn)行 query之前預(yù)先對(duì)意圖、可能分類進(jìn)行分析為了減少納入計(jì)算排序的數(shù)據(jù),我們會(huì)在 query組裝之前對(duì)原始query內(nèi)容進(jìn)行分析配合用戶行為埋點(diǎn)和專家的先驗(yàn)知識(shí),將知識(shí)進(jìn)行大致分類,并對(duì) query和分類進(jìn)行不同權(quán)重的匹配這樣雖然降低了召回率(約 X%),但增加了準(zhǔn)確性(約Y%),同時(shí)也提高了部分計(jì)算效率(約Z%)精簡(jiǎn)計(jì)算公式 將一部分分?jǐn)?shù)計(jì)算的邏輯外置,盡可能精簡(jiǎn) ES需要處理的運(yùn)算邏輯在召回之后增加多種打分策略,通過配置進(jìn)行應(yīng)用、權(quán)重調(diào)整等操作 這樣降低了 ES的響應(yīng)時(shí)間(約X%),同時(shí)通過外置的打分公式調(diào)整,間接的提高了準(zhǔn)確性(約Y%)
5.2 知識(shí)質(zhì)量參差不齊
由于知識(shí)條目是通過算法進(jìn)行抽取的,而且知識(shí)還會(huì)存在一定的時(shí)效性,可能造成知識(shí)的不準(zhǔn)確等問題,為此我們進(jìn)行了以下一些優(yōu)化:
持續(xù)的算法迭代: 根據(jù)用戶埋點(diǎn)信息和標(biāo)注信息對(duì)模型進(jìn)行持續(xù)迭代 選取更加優(yōu)質(zhì)的知識(shí)抽取結(jié)果對(duì)線上數(shù)據(jù)進(jìn)行全量/增量更新 經(jīng)過 X批次的迭代,將知識(shí)的正確性從Y%提高到了Z%對(duì)模型輸出的知識(shí)進(jìn)行后置處理 將僅存在部分助詞(如 的)差異的知識(shí)條目進(jìn)行過濾、合并給部分熱門的知識(shí)條目設(shè)置過期時(shí)間,并通過部分人工審核的方式干預(yù)知識(shí)條目的生產(chǎn) 維護(hù)專家知識(shí)庫的方式對(duì)可信知識(shí)進(jìn)行標(biāo)記及提權(quán) 維護(hù)了 X類目的Y條專家知識(shí),同時(shí)經(jīng)過人工干預(yù)了大概Z%的知識(shí)條目,將知識(shí)的正確性從W%提高到了K%
結(jié)論與展望
本文依托我們公司的使用場(chǎng)景,對(duì)圍繞 ES 向量字段(Dense vector)構(gòu)建的一個(gè)系統(tǒng)進(jìn)行了大致描述,同時(shí)對(duì)一些常見問題及解決方案進(jìn)行了闡述。
目前該方案支持了我們對(duì)于知識(shí)庫的相關(guān)搜索功能,相較于之前的純基于實(shí)體識(shí)別和 ngram 匹配的方案整體準(zhǔn)確率和召回率都有將近兩位數(shù)百分比的提升。
未來我們會(huì)對(duì)整個(gè)系統(tǒng)的響應(yīng)速度、穩(wěn)定性進(jìn)行提升,并對(duì)知識(shí)庫的構(gòu)建效率以及知識(shí)的準(zhǔn)確性持續(xù)進(jìn)行迭代。
作者介紹
死敵wen,Elastic 認(rèn)證工程師,搜索架構(gòu)師,10年+工作經(jīng)驗(yàn),畢業(yè)于復(fù)旦大學(xué)。
博客:https://blog.csdn.net/weixin_40601534
Github:https://github.com/godlockin
說明

上個(gè)月,死磕 Elasticsearch 知識(shí)星球搞了:“群智涌現(xiàn)”杯輸出倒逼輸入——Elastic干貨輸出活動(dòng)。
后續(xù)會(huì)不定期逐步推出系列文章,目的:以文會(huì)友,“輸出倒逼輸入”。
推薦
更短時(shí)間更快習(xí)得更多干貨!
已帶領(lǐng)77位球友通過 Elastic 官方認(rèn)證!

