1、背景作為一家搜索引擎公司,我們會(huì)很倚賴 ES 幫忙處理包括文章召回,數(shù)據(jù)源劃分,實(shí)體、標(biāo)簽管理等任務(wù),而且都收到了不錯(cuò)的結(jié)果。 最近我們需要對(duì)行業(yè)知識(shí)庫(kù)進(jìn)行建模,其中可能會(huì)涉及到實(shí)體匹配、模糊搜索、向量搜索等多種召回和算分方式,最終我們選擇了通過(guò) ES 7.X (最終選擇 7.10)里的新功能,Dense vector 幫忙一起完成這部分的需求。 2、技術(shù)選型2.1 解決方案需求
2.2 使用場(chǎng)景設(shè)計(jì)- 在離線數(shù)據(jù)構(gòu)建完成后,存入該引擎
- 引擎對(duì)數(shù)據(jù)中各字段進(jìn)行索引
- 根據(jù)
query 理解結(jié)果構(gòu)建的 query 語(yǔ)句進(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ì)包含以下一些字段 - 實(shí)體、屬性、取值:用來(lái)描述知識(shí)的具體內(nèi)容
- 分類 flag:知識(shí)主要分類及推薦 category 等
- 向量表示:作為知識(shí)相似性、相關(guān)性召回、打分的依據(jù)
- ref 信息:用來(lái)回溯解析/獲取該知識(shí)的源信息
- 其他屬性:包括生效、刪除、修改時(shí)間等支持性的通用屬性

2.4 解決方案對(duì)比為了能支持上述的使用需求,我們對(duì)比了包括 ES、Faiss 等多種解決方案。其中,Faiss 和 SPTAG 只是核心算法庫(kù),需要進(jìn)行二次開(kāi)發(fā)包裝成服務(wù);Milvus 的 1.x 版本中只能存儲(chǔ) id 和 向量,不能完整的滿足我們的使用需求;基于集群穩(wěn)定性和可維護(hù)性等考慮,相對(duì)于后置插件的部署,我們更傾向于使用 ES 的原生功能,所以選擇 ES 的原生向量搜索功能作為我們的最終選擇。 對(duì)比參考: | 種類 | 實(shí)現(xiàn)語(yǔ)言 | 客戶端支持 | 多條件召回 | 學(xué)習(xí)成本 | 引入成本 | 運(yùn)維成本 | 分布式 | 性能 | 社區(qū) | 備注 |
|---|
| Elasticsearch | Java | Java/Python | yes | 低 | 低 | 中 | yes | 中 | 活躍 | 原生功能 | | Faiss | Python | Python | no | 中 | 高 | 高 | no | 高 | 一般 | 需要二次開(kāi)發(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 | 高 | 一般 | 需要二次開(kāi)發(fā) |
3、數(shù)據(jù)流轉(zhuǎn)流程3.1 離線數(shù)據(jù)處理部分- 從多數(shù)據(jù)源采集數(shù)據(jù)
- 通過(guò)算法引擎將知識(shí)轉(zhuǎn)換為向量
- 將知識(shí)的基礎(chǔ)信息連同向量數(shù)據(jù)存入
ES
3.2 在線數(shù)據(jù)召回部分- 通過(guò)
query 理解模塊進(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)
- 為了支持高性能過(guò)濾,將常用的字段預(yù)先加載在內(nèi)存中
- 對(duì)慢查詢開(kāi)啟日志方便后續(xù)性能問(wèn)題的調(diào)查
- 知識(shí)庫(kù)的重建是離線的,會(huì)在更新時(shí)進(jìn)行大量寫(xiě)入,所以對(duì)
translog 的提交間隔拉長(zhǎng),加快寫(xiě)入速度 - 在實(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í)庫(kù)中的知識(shí)條目會(huì)有一部分進(jìn)行專家/人工審核和維護(hù),所以會(huì)對(duì)不同的條目設(shè)置不同的置信度
- 數(shù)據(jù)預(yù)處理之后會(huì)轉(zhuǎn)成 512 位的向量存在這個(gè)字段中
4.2 數(shù)據(jù)流轉(zhuǎn)- 通過(guò)
模型A 從文章中找到知識(shí)條目 - 通過(guò)
模型B 將知識(shí)條目轉(zhuǎn)化成向量 - 此處
模型A 模型B 為自研模型,運(yùn)用了包括知識(shí)密度計(jì)算等算法以及 bert tersonflow 等框架
- 將原文、知識(shí)條目等核心內(nèi)容插入數(shù)據(jù)庫(kù)
- 將核心知識(shí)內(nèi)容、向量等組裝成檢索單元插入
ES - 專家團(tuán)隊(duì)會(huì)針對(duì)數(shù)據(jù)庫(kù)中的知識(shí)條目進(jìn)行審核、修改和迭代
- 算法團(tuán)隊(duì)會(huì)根據(jù)知識(shí)條目的更新以及其他的標(biāo)注對(duì)數(shù)據(jù)鏈路中的模型進(jìn)行迭代,對(duì)在線知識(shí)庫(kù)進(jìn)行更新
- 前端收到請(qǐng)求之后調(diào)用
query 理解 組件進(jìn)行分析 - 剔除無(wú)效內(nèi)容之后,找出
query 里的分類信息等意圖之后,構(gòu)建用來(lái)召回的向量和相關(guān)的篩選條件 - 通過(guò)組合出來(lái)的
ES 的 query 條件對(duì)知識(shí)庫(kù)進(jìn)行篩選,并配合置信度等對(duì)結(jié)果進(jìn)行調(diào)整 - 對(duì)召回結(jié)果進(jìn)行不同策略的分?jǐn)?shù)調(diào)整和排序,最后輸出給前端
4.3 示例 queryPOST 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、遇到的問(wèn)題5.1 響應(yīng)時(shí)間長(zhǎng)由于需要進(jìn)行向量計(jì)算,ES 需要耗費(fèi)大量時(shí)間、資源做距離計(jì)算,為此我們進(jìn)行了以下一些優(yōu)化: - 為了保證特征的表征,我們并沒(méi)有調(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%)
- 將一部分分?jǐn)?shù)計(jì)算的邏輯外置,盡可能精簡(jiǎn)
ES 需要處理的運(yùn)算邏輯 - 在召回之后增加多種打分策略,通過(guò)配置進(jìn)行應(yīng)用、權(quán)重調(diào)整等操作
- 這樣降低了
ES 的響應(yīng)時(shí)間(約 X%),同時(shí)通過(guò)外置的打分公式調(diào)整,間接的提高了準(zhǔn)確性(約 Y%)
5.2 知識(shí)質(zhì)量參差不齊由于知識(shí)條目是通過(guò)算法進(jìn)行抽取的,而且知識(shí)還會(huì)存在一定的時(shí)效性,可能造成知識(shí)的不準(zhǔn)確等問(wèn)題,為此我們進(jìn)行了以下一些優(yōu)化: - 根據(jù)用戶埋點(diǎn)信息和標(biāo)注信息對(duì)模型進(jìn)行持續(xù)迭代
- 選取更加優(yōu)質(zhì)的知識(shí)抽取結(jié)果對(duì)線上數(shù)據(jù)進(jìn)行全量/增量更新
- 經(jīng)過(guò)
X 批次的迭代,將知識(shí)的正確性從 Y% 提高到了 Z%
- 對(duì)模型輸出的知識(shí)進(jìn)行后置處理
- 將僅存在部分助詞(如
的)差異的知識(shí)條目進(jìn)行過(guò)濾、合并 - 給部分熱門的知識(shí)條目設(shè)置過(guò)期時(shí)間,并通過(guò)部分人工審核的方式干預(yù)知識(shí)條目的生產(chǎn)
- 維護(hù)專家知識(shí)庫(kù)的方式對(duì)可信知識(shí)進(jìn)行標(biāo)記及提權(quán)
- 維護(hù)了
X 類目的 Y 條專家知識(shí),同時(shí)經(jīng)過(guò)人工干預(yù)了大概 Z% 的知識(shí)條目,將知識(shí)的正確性從 W% 提高到了 K%
結(jié)論與展望本文依托我們公司的使用場(chǎng)景,對(duì)圍繞 ES 向量字段(Dense vector)構(gòu)建的一個(gè)系統(tǒng)進(jìn)行了大致描述,同時(shí)對(duì)一些常見(jiàn)問(wèn)題及解決方案進(jìn)行了闡述。 目前該方案支持了我們對(duì)于知識(shí)庫(kù)的相關(guān)搜索功能,相較于之前的純基于實(shí)體識(shí)別和 ngram 匹配的方案整體準(zhǔn)確率和召回率都有將近兩位數(shù)百分比的提升。 未來(lái)我們會(huì)對(duì)整個(gè)系統(tǒng)的響應(yīng)速度、穩(wěn)定性進(jìn)行提升,并對(duì)知識(shí)庫(kù)的構(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 說(shuō)明
上個(gè)月,死磕 Elasticsearch 知識(shí)星球搞了:“群智涌現(xiàn)”杯輸出倒逼輸入——Elastic干貨輸出活動(dòng)。 后續(xù)會(huì)不定期逐步推出系列文章,目的:以文會(huì)友,“輸出倒逼輸入”。 更短時(shí)間更快習(xí)得更多干貨!
已帶領(lǐng)77位球友通過(guò) Elastic 官方認(rèn)證! 比同事搶先一步學(xué)習(xí)進(jìn)階干貨!
|