全部產品
Search
文件中心

OpenSearch:多路召回實戰

更新時間:Sep 03, 2024

方案架構

該文檔主要介紹如何通過召回引擎版實現文本、向量多路召回。

該實踐可用於有大模型演算法的團隊實現對話式搜尋服務,方案架構如下:

image.png

以上就是對話式搜尋的簡易架構,召回引擎版在整個架構中類似於向量檢索資料庫,支援使用者通過向量和文本進行多路召回,同時支援豐富的排序函數和運算式,可以滿足不同使用者的不同排序需求,使得最終召回的結果最符合使用者問題的答案。

以上架構使得整個對話式搜尋服務變得更加靈活,使用者可以根據自己的業務定製:切片模型、向量化模型,以及後面format的大模型。(此處召回引擎版僅支援文本向量化和圖片向量化,其餘的模型需要業務方有自己的演算法團隊進行探索)。

基於對話式搜尋服務配置召回引擎執行個體

根據以往使用者的問題,本文中會舉出一些通用的配置方法和排序運算式,使用者可以直接使用。

整個配置流程分3部分:

  1. 表結構的設計:此處將介紹對話式搜尋服務需要的必選欄位,以及這些欄位如何在召回引擎版中配置索引

  2. 查詢文法:此處將介紹如何通過ha3文法實現在召回引擎版的多路召回(文本、向量)功能。

  3. 文檔排序:由於向量和文本是不同維度,多路召回後有文本召回的doc,也有向量召回的doc,其中如何編寫排序運算式,使得召回的結果中top1或者topN的結果為最相關的至關重要

表結構設計

基於對話式搜尋的互動頁面:(以智能問答版為例)

image.png

在設計表時需要有以下欄位:

欄位名稱

類型

說明

是否必須

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,根據自己的演算法而定。

配置截圖如下

  • 欄位配置:

image.png

DUP_content欄位需要在進階配置中配置,表示該欄位的內容同content一致:

{
  "copy_from": "content"
}
  • 索引配置:

image.png

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

image.png

開發人員模式的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"
      }
    ]
  }

查詢文法說明

image.png

如圖所示,多路召回其中有一部分文檔,是只用向量檢索回來的,有一部分是用文本檢索回來的,而有一部分可以匹配兩路的查詢。那麼在查詢的時候如何進行組合呢?

這裡需要說明不同組合方式的區別:

首先在大的方面,以上述配置的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

  1. 短查詢情境下,提升文本分數權重:

使用者輸入的查詢為 "arthas效能分析"。

下面是上述query串得到的排序最靠前的文檔:

image.png

可以看到,召回的內容並不相關,通過開啟trace開關,可以發現,該文檔是通過向量一路召回的。文檔的召回無法調節,但是可以分析文檔被排到最前面的原因,通過排序運算式將更相關的文檔排序到前面。

通過對上述query進行分析,可以看到,上述query串會優先將向量召回的結果排到前面。其原因在於,如果文檔是通過文本召回的,proxima_score的分數是10000,整個運算式的分數會變得很小,因此,文本召回的文檔在排序中並不佔有優勢。因此,在這裡,將proxima_score相關的部分修改為

if(proxima_score(vector)<10000\,proxima_score(vector)\,a)

通過對參數a的設定,可以調節向量分數和文本分數之間的權重關係。

將a設定為0,則表示優先使用文本召回的文檔

image.png

可以看到,針對"arthas效能分析"的查詢,文本召回可以得到更好的結果。

  1. 文本召回使用OR 邏輯,增強召回內容相關性

上述查詢串中,文本召回的邏輯預設是AND:查詢串進行分詞之後,索引中的文本需要匹配所有分詞才會召回。這樣的邏輯固然會增加召回結果的相關性,但是也會有文本召回結果為空白的情況。在上述查詢串中,還有向量召回一路,因此,該情況下只會使用向量召回的結果,最後結果的排序也只會使用向量相似性。

如使用上述模板,有如下查詢結果:

image.png

可以看到,召回結果第一條明顯是不相關的內容。通過trace可以看到,這條資料是通過向量一路召回的。向量召回的結果,相關性完全取決於向量模型,而如果使用的是通用的模型,往往會有召回不準確的情況。

文本召回一定程度上可以保證召回的文檔是相關的,而讓更多的文檔參與到最後的結果中,可以將上述文本召回的邏輯由AND改為OR。

image.png

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