方案架構
該文檔主要介紹如何通過召回引擎版實現文本、向量多路召回。
該實踐可用於有大模型演算法的團隊實現對話式搜尋服務,方案架構如下:

以上就是對話式搜尋的簡易架構,召回引擎版在整個架構中類似於向量檢索資料庫,支援使用者通過向量和文本進行多路召回,同時支援豐富的排序函數和運算式,可以滿足不同使用者的不同排序需求,使得最終召回的結果最符合使用者問題的答案。
以上架構使得整個對話式搜尋服務變得更加靈活,使用者可以根據自己的業務定製:切片模型、向量化模型,以及後面format的大模型。(此處召回引擎版僅支援文本向量化和圖片向量化,其餘的模型需要業務方有自己的演算法團隊進行探索)。
基於對話式搜尋服務配置召回引擎執行個體
根據以往使用者的問題,本文中會舉出一些通用的配置方法和排序運算式,使用者可以直接使用。
整個配置流程分3部分:
表結構的設計:此處將介紹對話式搜尋服務需要的必選欄位,以及這些欄位如何在召回引擎版中配置索引
查詢文法:此處將介紹如何通過ha3文法實現在召回引擎版的多路召回(文本、向量)功能。
文檔排序:由於向量和文本是不同維度,多路召回後有文本召回的doc,也有向量召回的doc,其中如何編寫排序運算式,使得召回的結果中top1或者topN的結果為最相關的至關重要
表結構設計
基於對話式搜尋的互動頁面:(以智能問答版為例)

在設計表時需要有以下欄位:
欄位名稱 | 類型 | 說明 | 是否必須 |
pk | STRING/INT64 | 主鍵 | 必須 |
chunk_id | STRING/INT64 | 片段的唯一標識 | 可選 |
doc_id | STRING/INT64 | 原始文檔的唯一標識 | 可選 |
content | TEXT | 切片後的文檔內容 | 必須 |
title | TEXT | 文檔標題 | 可選 |
embedding | 多值float | content向量化後的向量 | 必須 |
url | STRING | 原文連結 | 可選 |
picture | 多值float | 圖片向量化後的向量 | 可選 |
namespace | STRING | 命名空間 | 可選(用於不同類型的資料隔離) |
DUP_content | STRING | 基於content複製出的欄位 | 必須(用於content的展示) |
以上欄位僅供參考,業務有其他需求可以自訂其他欄位。
索引設計:
索引名稱 | 類型 | 包含欄位 | 是否必須 | 說明 |
pk | PRIMARYKEY64 | pk | 必須 | 主鍵索引 |
default | PACK | content | 必須 | 用於文本一路召回 |
vector | CUSTOMIZED | pk,embedding (如果有namespace,可以配置上) | 必須 | 用於向量一路召回 |
title | PACK | title | 可選 | 用於標題召回 |
chunk_id | chunk_id | STRING | 可選 | 通過chunk_id召回片段 |
doc_id | doc_id | STRING | 可選 | 通過doc_id 召回該doc的所有片段 |
除此之外,所有的欄位都需要配置搜尋結果展示,非text類型的欄位都勾選屬性欄位。
另外向量的維度根據演算法產生的出的向量維度而定,向量距離為歐式距離和內積,如果需要餘弦相似性,可以把向量歸一化為[-1,1]然後取內積距離,向量檢索演算法有qc和HNSW,根據自己的演算法而定。
配置截圖如下:
欄位配置:

DUP_content欄位需要在進階配置中配置,表示該欄位的內容同content一致:
{
"copy_from": "content"
}索引配置:

所有PACK類型的索引,必須在進階配置中配置如下內容:(在後面文本算分時會用到)

開發人員模式的schema如下:
{
"file_compress": [
{
"name": "file_compressor",
"type": "zstd"
},
{
"name": "no_compressor",
"type": ""
}
],
"table_name": "main",
"summarys": {
"summary_fields": [
"pk",
"chunk_id",
"doc_id",
"content",
"title",
"embedding",
"url",
"picture",
"namespace",
"DUP_content"
],
"parameter": {
"file_compressor": "zstd"
}
},
"indexs": [
{
"index_name": "pk",
"index_type": "PRIMARYKEY64",
"index_fields": "pk",
"has_primary_key_attribute": true,
"is_primary_key_sorted": false
},
{
"index_name": "default",
"index_type": "PACK",
"index_fields": [
{
"boost": 1,
"field_name": "content"
}
],
"doc_payload_flag": 1,
"has_section_attribute": true,
"position_payload_flag": 1,
"term_frequency_bitmap": 0,
"position_list_flag": 1,
"term_payload_flag": 1,
"term_frequency_flag": 1,
"section_attribute_config": {
"has_field_id": true,
"has_section_weight": true
}
},
{
"index_name": "vector",
"index_type": "CUSTOMIZED",
"index_fields": [
{
"boost": 1,
"field_name": "pk"
},
{
"boost": 1,
"field_name": "embedding"
}
],
"indexer": "aitheta2_indexer",
"parameters": {
"enable_rt_build": "true",
"min_scan_doc_cnt": "20000",
"vector_index_type": "Qc",
"major_order": "col",
"builder_name": "QcBuilder",
"distance_type": "SquaredEuclidean",
"embedding_delimiter": ",",
"enable_recall_report": "true",
"ignore_invalid_doc": "true",
"is_embedding_saved": "false",
"linear_build_threshold": "5000",
"dimension": "128",
"rt_index_params": "{\"proxima.oswg.streamer.segment_size\":2048}",
"search_index_params": "{\"proxima.qc.searcher.scan_ratio\":0.01}",
"searcher_name": "QcSearcher",
"build_index_params": "{\"proxima.qc.builder.quantizer_class\":\"Int8QuantizerConverter\",\"proxima.qc.builder.quantize_by_centroid\":true,\"proxima.qc.builder.optimizer_class\":\"BruteForceBuilder\",\"proxima.qc.builder.thread_count\":10,\"proxima.qc.builder.optimizer_params\":{\"proxima.linear.builder.column_major_order\":true},\"proxima.qc.builder.store_original_features\":false,\"proxima.qc.builder.train_sample_count\":3000000,\"proxima.qc.builder.train_sample_ratio\":0.5}"
}
},
{
"index_name": "title",
"index_type": "PACK",
"index_fields": [
{
"boost": 1,
"field_name": "title"
}
],
"doc_payload_flag": 1,
"has_section_attribute": true,
"position_payload_flag": 1,
"term_frequency_bitmap": 0,
"position_list_flag": 1,
"term_payload_flag": 1,
"term_frequency_flag": 1,
"section_attribute_config": {
"has_field_id": true,
"has_section_weight": true
}
},
{
"index_name": "chunk_id",
"index_type": "STRING",
"index_fields": "chunk_id"
},
{
"index_name": "doc_id",
"index_type": "STRING",
"index_fields": "doc_id"
}
],
"attributes": [
{
"field_name": "pk",
"file_compress": "no_compressor"
},
{
"field_name": "chunk_id",
"file_compress": "no_compressor"
},
{
"field_name": "doc_id",
"file_compress": "no_compressor"
},
{
"field_name": "embedding",
"file_compress": "no_compressor"
},
{
"field_name": "url",
"file_compress": "no_compressor"
},
{
"field_name": "picture",
"file_compress": "no_compressor"
},
{
"field_name": "namespace",
"file_compress": "no_compressor"
},
{
"field_name": "DUP_content",
"file_compress": "no_compressor"
}
],
"fields": [
{
"user_defined_param": {},
"field_name": "pk",
"field_type": "STRING",
"compress_type": "uniq"
},
{
"field_name": "chunk_id",
"field_type": "STRING",
"compress_type": "uniq"
},
{
"field_name": "doc_id",
"field_type": "STRING",
"compress_type": "uniq"
},
{
"user_defined_param": {},
"field_name": "content",
"field_type": "TEXT",
"analyzer": "chn_standard"
},
{
"user_defined_param": {},
"field_name": "title",
"field_type": "TEXT",
"analyzer": "chn_standard"
},
{
"user_defined_param": {},
"field_name": "embedding",
"field_type": "FLOAT",
"compress_type": "uniq",
"multi_value": true
},
{
"field_name": "url",
"field_type": "STRING",
"compress_type": "uniq"
},
{
"user_defined_param": {},
"field_name": "picture",
"field_type": "FLOAT",
"compress_type": "uniq",
"multi_value": true
},
{
"field_name": "namespace",
"field_type": "STRING",
"compress_type": "uniq"
},
{
"user_defined_param": {
"copy_from": "content"
},
"field_name": "DUP_content",
"field_type": "STRING",
"compress_type": "uniq"
}
]
}查詢文法說明

如圖所示,多路召回其中有一部分文檔,是只用向量檢索回來的,有一部分是用文本檢索回來的,而有一部分可以匹配兩路的查詢。那麼在查詢的時候如何進行組合呢?
這裡需要說明不同組合方式的區別:
首先在大的方面,以上述配置的schema為例:
●文本與向量 AND 召回:
query=default:'xxx' AND vector:'xxx'此種方式召回即為向量和文本同時命中的部分,召回邏輯是,比如向量一路召回取100個結果,則先通過向量召回100個相關度最高的結果,再在100個結果裡進行文本匹配,兩路全部匹配的內容作為最終的召回結果。
弊端:此種組合方式經常會出現召回不全的情況,或一些相關度比較高的doc並未召回的情況。
●文本與向量 OR 召回:
query=default:'xxx' OR vector:'xxx'此種方式召回即取文本召回和向量召回的並集。
弊端:會引入一些文本召回的bad caase,比如:搜尋“歌曲黑色毛衣”,有兩個doc,content分別為“周杰倫的歌曲《黑色毛衣》”和“我在下雨天穿著一件黑色的毛衣,嘴裡哼著一首悲傷的歌曲”,很明顯前一個doc更符合預期同時向量召回該doc的相關度也比較高,但是文本一路的召回後一個doc的相關度也比較高。
其次,針對與文本一路,又有2種召回方式:
and方式:常值內容分詞後的term全匹配召回
or 方式:文本分詞後的term匹配上一個即可召回
舉個簡單的例子,搜尋“我在杭州等你”,其中有兩個doc內容分別為“杭州歡迎你”和“我在杭州餘杭,等你”
如果是and的方式只能召回後一個doc,如果是or方式可以將兩個doc都召回。
and的方式的弊端:會因為分詞的bad case導致相關的結果無法召回,比如:“德意澳,三日遊”,分詞可能是“德意|澳,三|日|遊”,如果搜尋“德”就無法把這條doc召回,出現了空結果的情況
or方式的弊端:很顯然or 的方式是為了擴大召回而使用,該種情況會召回大量不相關的doc,幹擾排序結果。
經過多年經驗沉澱,以上組合方式中,召回率較高,同時效果較好的召回方式為:
query=vector:'xxx&n=100&sf=1.100000' OR default:'xxx'其中向量索引中的:
n:表示向量召回的topN
sf:控制向量相似性得分,歐式距離為上限,內積距離為下限
如果不在config裡配置default_operator參數,預設文本召回為and方式召回,詳情可參考config子句
如果向量模型相對優秀的話,也可以僅僅用向量召回即可。
補充:相關文檔參考
文檔排序
該步驟中,在通過文本、向量多路召回後,召回後的doc是沒有順序的,或者說順序是不符合我們預期的,因此需要通過排序運算式去幹預已召迴文檔的排序,使top1或者top5是最相關的答案。
以上根據經驗給出不同方式召回的排序運算式:
文本 OR 向量:
formula:if(query_min_slide_window(title\, true\, title)>0.99\, 1\, 0)+
if(query_min_slide_window(content\, true\, default)>0.99\, 0.5\, 0)
+text_relevance(content)*0.2+normalize(score)*0.1-proxima_score(vector)其中formula表示精排算分運算式,引擎在排序時有2階段排序,先粗排first_formula,然後再精排formula,進入精排的文檔預設分會+10000分,引擎通過config中的rerank_size() 控制進入精排的doc數。
proxima_score()函數,在多路召回中,如果文檔是文本召回的但向量未召回,該函數的得分預設會是10000分,如果需要調整可以加入另一個參數,proxima_score(vector,default_value),其中default_value表示在未通過向量召回時該函數的預設得分。
補充:相關參考文檔
以下給出一個完整的查詢語句,僅供參考:
query=vector:'xxx&n=100&sf=1.100000' OR default:"1948年在城南莊發生了什麼"
OR title:'1948年在城南莊發生了什麼'&&cluster=general&&sort=-RANK
&&config=start:0,hit:3,rerank_size:100,format:json
&&kvpairs=fetch_fields:pk;content,
formula:if(query_min_slide_window(title\, true\, title)>0.99\, 1\, 0)
+if(query_min_slide_window(content\, true\, default)>0.99\, 0.5\, 0)
+text_relevance(content)*0.2+normalize(score)*0.1-proxima_score(vector)召回 & 排序 bad case
短查詢情境下,提升文本分數權重:
使用者輸入的查詢為 "arthas效能分析"。
下面是上述query串得到的排序最靠前的文檔:

可以看到,召回的內容並不相關,通過開啟trace開關,可以發現,該文檔是通過向量一路召回的。文檔的召回無法調節,但是可以分析文檔被排到最前面的原因,通過排序運算式將更相關的文檔排序到前面。
通過對上述query進行分析,可以看到,上述query串會優先將向量召回的結果排到前面。其原因在於,如果文檔是通過文本召回的,proxima_score的分數是10000,整個運算式的分數會變得很小,因此,文本召回的文檔在排序中並不佔有優勢。因此,在這裡,將proxima_score相關的部分修改為
if(proxima_score(vector)<10000\,proxima_score(vector)\,a)通過對參數a的設定,可以調節向量分數和文本分數之間的權重關係。
將a設定為0,則表示優先使用文本召回的文檔

可以看到,針對"arthas效能分析"的查詢,文本召回可以得到更好的結果。
文本召回使用OR 邏輯,增強召回內容相關性
上述查詢串中,文本召回的邏輯預設是AND:查詢串進行分詞之後,索引中的文本需要匹配所有分詞才會召回。這樣的邏輯固然會增加召回結果的相關性,但是也會有文本召回結果為空白的情況。在上述查詢串中,還有向量召回一路,因此,該情況下只會使用向量召回的結果,最後結果的排序也只會使用向量相似性。
如使用上述模板,有如下查詢結果:

可以看到,召回結果第一條明顯是不相關的內容。通過trace可以看到,這條資料是通過向量一路召回的。向量召回的結果,相關性完全取決於向量模型,而如果使用的是通用的模型,往往會有召回不準確的情況。
文本召回一定程度上可以保證召回的文檔是相關的,而讓更多的文檔參與到最後的結果中,可以將上述文本召回的邏輯由AND改為OR。

可以看到,召回的文檔相比上面的結果有提升。