Elasticsearch 8.X:這個複雜的檢索需求如何實現?

大資料技術前線發表於2023-09-25

來源:銘毅天下Elasticsearch

1、企業級真實問題

問題描述如下:

Elasticsearch 8.X:這個複雜的檢索需求如何實現?

如上圖所示,index中有這樣四個欄位:title  content  question answer。要查詢這四個欄位,支援最多輸入5個關鍵詞模糊查詢,多關鍵詞以空格隔開。

匹配度計算邏輯:

  • 關鍵詞有序排列 ,權重依次降低,即排列在前的關鍵詞權重最高,依此降低;檢索順序和結果順序一致的排在前面。
  • title(question)較content(answer)權重高,比如權重高10倍
  • 詞頻(關鍵詞出現次數)越高,匹配度越高
  • 在匹配度相同的條件下按更新時間倒序排列

就拿上面的截圖來看,doc標題:“小學語文週週學和基礎天天練是否為配套練習?”這個doc應該排在第一位。

提問球友的 DSL 為:

{
"bool": {
  "should": [
    {
      "multi_match": {
        "query""小學  天天 練習",
        "fields": [title  content  question answer],
        "type""best_fields",
        "tie_breaker": 0.3
      }
    },
    {
      "match": {
        "title": {
          "query""小學  天天 練習",
          "boost": 10
        }
      }
    },
    {
      "match": {
        "question": {
          "query""小學  天天 練習",
          "boost": 10
        }
      }
    }
  ]
}
 }

2、需求重新梳理

問題有點長,我們重新梳理一下。

  • 需求 1:檢索順序和結果順序一致的排在前面。
  • 需求 2:title(question)較content(answer)權重高,比如權重高10倍。
  • 需求 3:詞頻(關鍵詞出現次數)越高,匹配度越高。
  • 需求 4:時間倒序排序。

已和提問確認,就是上述四個需求。

3、實現討論

  • 針對需求 2,這個設定權重就可以實現。

  • 針對需求 3,這個 TF-IDF 機制決定的,檢索後結果自然滿足,也就是評分邏輯就是基於這個實現的(後續升級為BM25模型,原理一致),我們們不用動就可以。

  • 針對需求 4,加個時間排序就可以。

針對需求2、3、4,實現參考如下(欄位權重根據實際業務場景自我調整即可):

POST new_index_2023/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query""小學 天天",
            "fields": [
              "title^10",
              "question^10",
              "content",
              "answer"
            ],
            "type""best_fields"
          }
        }
      ]
    }
  },
  "sort": [
    {
      "timestamp": {
        "order""desc"
      }
    }
  ]
}

問題來了,需求 1 :檢索順序和結果順序一致的排在前面咋搞呢?

我第一反應想到的是 Match_phrase 和 slop 結合的方案。

擴充套件說明一下:在 Elasticsearch 中,match_phrase 查詢用於搜尋精確的短語,而 slop 引數定義了詞條之間的允許的最大距離。

slop 的意思是允許搜尋的短語中的詞條有多少的移動量來使其與文件中的短語匹配。

一句話:Match_phrase 和 slop 結合的方案,並不能直接實現需求1。

那怎麼辦?我們單獨分析一下吧。

4、需求 1 實現討論

針對需求1,通常在 Elasticsearch 裡,檢索順序和結果順序一致的功能是相對複雜的,尤其是當查詢涉及多個欄位和多個關鍵詞時。通常這一需求是透過應用層的程式碼進行處理,而不是在 Elasticsearch 中。

可能的解決方案參考如下:

  • 欄位分析和排序
  • 應用層處理
  • 自定義評分查詢(function_score)

如果確實想在 Elasticsearch 裡解決這個問題,那麼指令碼排序可能是唯一可行的內建解決方案,儘管這樣可能會帶來效能和可維護性的問題。

在多欄位和多關鍵詞的情況下,使用 Painless 指令碼可能是最直接的方法來精確控制排序邏輯,但通常會犧牲一些效能。

簡而言之,Elasticsearch 本身可能不是最適合解決這一具體需求的工具。更合適的方式可能是結合應用層的邏輯來實現這一需求。

一般遇到類似問題,就得有理有據的和產品經理討論清楚需求,不要任憑產品經理“瞎指揮、瞎忽悠”

那麼藉助指令碼如何實現呢?構造資料及拆解實現討論如下:

4.1 步驟1:建立索引及匯入資料

PUT /new_index_2023
{
  "mappings": {
    "properties": {
      "title": {
        "type""text",
        "fields": {
          "keyword": {
            "type""keyword"
          }
        }
      },
      "content": {
        "type""text"
      },
      "question": {
        "type""text"
      },
      "answer": {
        "type""text"
      }
    }
  }
}


POST /new_index_2023/_bulk
{"index": {}}
{"title""基礎天天練有沒有單元練習?"}
{"index": {}}
{"title""計算天天練是小學數學週週學的配套練習嗎?"}
{"index": {}}
{"title""小學語文週週學和基礎天天練是否為配套練習?"}
{"index": {}}
{"title""小學語文周天是否為配套練習"}
{"index": {}}
{"title""計算天天練是週週學的配套練習嗎?"}
{"index": {}}
{"title""小學數學周天是否為配套練習"}
{"index": {}}
{"title""基礎天天練和53的區別是什麼?"}
{"index": {}}
{"title""計算天天練有影片講解嗎?"}
{"index": {}}
{"title""基礎天天練每個學期幾本?"}

4.2 步驟2:指令碼排序實現

如下實現僅針對需求1,指令碼僅供參考。

POST new_index_2023/_search
{
  "query": {
    "match": {
      "title""小學 天天"
    }
  },
  "sort": [
    {
      "_script": {
        "type""number",
        "script": {
          "source""""
            def title = doc['title.keyword'].value;
            def keywordToFind = params.keywordToFind;
            def schoolKeyword = params.schoolKeyword;
            def indexSchool = title.indexOf(schoolKeyword);
            def indexKeyword = title.indexOf(keywordToFind);
            if (indexSchool < indexKeyword) {
              return 1;
            } else if (indexSchool > indexKeyword) {
              return -1;
            } else {
              return 0;
            }
          """
,
          "lang""painless",
          "params": {
            "keywordToFind""天天",      
            "schoolKeyword""小學"     
          }
        },
        "order""desc"
      }
    }
  ]
}

指令碼目的:為了對搜尋結果進行排序,確保"title"欄位中"小學"出現在"天天"之前的文件排在前面。

指令碼實現邏輯解讀

步驟描述
1透過doc['title.keyword'].value獲取當前文件的"title"欄位值並儲存在title變數中。
2使用Java的indexOf方法,找到"小學"在"title"中的位置,並將這個位置儲存在indexSchool變數中。
3使用同樣的方法,找到"天天"在"title"中的位置,並將這個位置儲存在indexKeyword變數中。
4判斷兩個關鍵字的位置:如果"小學"在"天天"之前,返回1。
5如果"小學"在"天天"之後,返回-1。
6如果"小學"和"天天"在相同位置(實際上可能不會發生),返回0。

透過上述指令碼,Elasticsearch 會優先返回那些"title"欄位中"小學"出現在"天天"之前的文件。

讀到這裡,讀者可能會問,這換個詞咋辦?的確這不是普適的解決方案,而是定製的解決方案。

如果要“普適”,得我們們業務層面自己把控實現,這是大前提!

5、小結

如上看似複雜需求,是藉助拆解需求實現的任務分解。

請注意,這是一個非常簡化和特定的例子。更復雜的需求(例如,處理多個欄位或更多的關鍵詞)可能需要更復雜的指令碼。

但切記:如果排序邏輯變得太複雜或影響效能,可能需要考慮在應用層進行後處理,而不是依賴 Elasticsearch 的內部排序。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027827/viewspace-2985643/,如需轉載,請註明出處,否則將追究法律責任。

相關文章