【踩坑分析】Elasticsearch 中文 PhraseQuery 沒法精確匹配問題分析

問題復現

  • 首先,看下下面的例子,猜下兩條查詢能不能召回 doc1
PUT test_phrase
{
  "mappings" : {
      "_doc" : {
        "properties" : {
          "body" : {
            "type" : "text",
            "analyzer" : "ik_max_word",
            "search_analyzer" : "ik_smart"
          }
        }
      }
    }
}

PUT test_phrase/_doc/1
{
  "body":"南京市長"
}

GET test_phrase/_search
{
  "query": {
    "match_phrase": {
      "body": "南京市長"
    }
  }
}

GET test_phrase/_search
{
  "query": {
    "match_phrase": {
      "body": "南京"
    }
  }
}
複製代碼

緣由分析

  • 那麼爲何呢?首先看一下,兩個分詞器結果不一,因此直接懷疑是因爲分詞不一形成的查不到的問題
# GET test_phrase/_analyze
# {
#   "text": ["南京市長"],
#   "analyzer": "ik_max_word"
# }
{
  "tokens" : [
    {
      "token" : "南京",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "南京市",
      "start_offset" : 0,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "市長",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    }
  ]
}

# GET test_phrase/_analyze
# {
#   "text": ["南京市長"],
#   "analyzer": "ik_smart"
# }
{
  "tokens" : [
    {
      "token" : "南京",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "市長",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    }
  ]
}
複製代碼
  • 那麼爲何會因爲分詞不一而形成問題呢?
  • 整理源碼,咱們能夠看到 Phrase 的總體流程以下
  • 具體以下
    1. org.elasticsearch.index.query.MatchPhraseQueryBuilder#doToQuery 收到查詢請求,轉爲 matchQuery 的 phrase 查詢
    2. org.elasticsearch.index.search.MatchQuery#parse 解析請求並轉爲 lucene 請求
    • 其中 org.elasticsearch.index.search.MatchQuery#getAnalyzer 肯定分詞器,若是查詢沒指定 analyzer 的話,那麼 phrase 就用 searchQuoteAnalyzer,若是沒有 searchQuoteAnalyzer 則用 searchAnalyzer
    1. org.apache.lucene.util.QueryBuilder#createFieldQuery 進行分詞並判斷有無 graph 和同義詞,若是沒有就用簡單的 phrase 查詢
    2. org.apache.lucene.util.QueryBuilder#analyzePhrase 構建真實查詢,肯定每一個詞的 postion
    3. org.apache.lucene.search.PhraseWeight#getPhraseMatcher 召回倒排鏈,而後判斷 slop,若是爲0,則轉爲ExactPhraseMatcher
    4. org.apache.lucene.search.ExactPhraseMatcher#nextMatch 比較 position 是否相等

臨時解法

  • 由上面第二步,能夠看到,其實 ES 自己也提供了利用 searchQuoteAnalyzer 的解決方案。所以臨時解法能夠是讓用戶給 text 字段增長 search_quote_analyzer參數,search_quote_analyzer 官方文檔
  • 另外若是是單場景能夠在查詢時指定 analyzer 或者 querystring 裏的 quote_analyzer

解決思路&難點

  • 可是讓用戶修改並不優雅,所以咱們仍是但願能夠尋求在引擎層解決的方案。可是目前還沒有找到一個較好方案,暫時記錄下思路,之後再作補充
  • 由上面分析可知,這個問題的本質是 PhraseQuery 以 postion 位置連續性肯定是否爲短句,而因爲寫入時和查詢時不一樣分詞器,position不一致。
  • 所以,有兩個思路,但都有些難點,也但願拋磚引玉,你們能指導一下

多重 Position

  • 思路: 修改 position 生成邏輯, 使得查詢和寫入時一致。 好比 ik_max_word 模式下有三種切分方式,就分別標記 position,而不是原來的混標,這樣就能夠保證 smart 也是 max 的一個子集
  • 以『南京市長江大橋維修』爲例
    • 目前混標方案:html

      南京 南京市 市長 長江 長江大橋 大橋 維修
      0 1 2 3 4 5 6
    • 獨立標註方案:apache

      南京 南京市 市長 長江 長江大橋 大橋 維修
      0 - 1 2 - 3 4
      0 - 1 - 2 - 3
      - 0 - 1 - 2 3
      - 0 - - 1 - 2
  • 難點:由上例子能夠看出,這種方案雖然能夠保證 position 的一致性,可是一旦有歧義詞,則會形成後繼詞位置全不同,會形成大量數據膨脹。如使用此方法,則須要找到一種快速記錄查找多重 position 的方法

Offset 連續性判別

  • 思路:目前以 position 的連續性判斷是否是短句,不過咱們從_analyzer結果能夠看到除了 position,咱們還有 start_offsetend_offset , 而這兩個比 position 更加準確。所以能夠考慮使用 offset 的diff 一致性來判別。
  • 難點:須要注意的有兩點,一是停用詞形成的 offset 不連續,二是『new york』這種詞中的多個空格,也會形成 offset 的不一致。另外,如要修改此邏輯,則須要修改 lucene,維護成本較大。

參考資料

  • ElastiSearch 6.6 源碼
相關文章
相關標籤/搜索