ElasticSearch - 輸入即搜索 edge n-gram

古古

2018/08/19


閱讀本文需要先了解 Elastic Search 的 index 和 analyzer 的相關知識

ElasticSearch - index mapping(5.x以上)

ElasticSearch - 自定義 analysis

  • 在此之前,ES 所有的查詢都是針對整個詞進行操作,也就是說倒排索引存了 hello 這個詞,一定得輸入 hello 才能找到這個詞,輸入 h 或是 he 都找不到倒排索引中的 hello

    • 然而在現實情況下,用戶已經漸漸習慣在輸入完查詢內容之前,就能為他們展現搜索結果,這就是所謂的即時搜索(instant search),或是可以稱為 輸入即搜索(search-as-you-type)
    • 雖然 ES 提供了一系列的前綴搜索 match_phrase、prefix、wildcard、regexp,然而這樣的查詢的性能非常差,要知道用戶每多輸入一個新的字母,就意味著要重新進行一次搜索,在實時的 web 系統中,100 ms 可能就會是一個難以忍受的延遲
    • 因此為了加快 輸入即搜索 的查詢效率,可以改使用 edge n-gram 建立索引,如此可以避免使用前綴查詢,在建立索引時就進行優化,使用空間換取時間,讓查詢的速率增快
  • 使用 edge n-gram 建立索引

    • 假設有一個詞 hello,普通建索引時,就是把這個詞 hello 放入倒排索引

      • 用戶輸入 h、he時會找不到索引(倒排索引中只有 hello),因此匹配失敗
    • 而對於輸入即搜索這種應用場景,可以使用一種特殊的 n-gram,稱爲 edge n-grams

      • 所謂的 edge n-gram,就是指它會固定詞語開始的一邊滑動窗口,他的結果取決於 n 的選擇長度

      • 以單詞 hello 爲例,它的 edge n-gram 的結果如下

        h
        he
        hel
        hell
        hello
        
      • 因此可以發現到,在使用 edge n-gram 建索引時,一個單詞會生成好幾個索引,而這些索引一定是重頭開始

      • 這符合了輸入即搜索的特性,即是用戶打 h、he 能找到倒排中的索引 hhe,而這些索引對應著的數據就是 hello

    • 具體實例

      • 建立索引時使用 edge n-gram 的 token 過濾器,為每個經過這個 token 過濾器的詞條們,都生成從頭開始的字符組合

        • 假設有一個輸入 QUICK! RUN!,分詞器會先將它分詞成兩個詞 quickrun,此時這些詞再一一的通過 edge n-gram token 過濾器,產生了 8 個索引 q、qu、qui、quic、quick、r、ru、run,接著存入倒排索引中
        • 如此,任何詞條像是 quick、run,都能生成他們自己的 n-gram
      • 另外要注意,要額外定義一個 search_analyzer 分析器,供查詢使用

        • 原因是因為我們為了要保證倒排索引中包含各種組合的詞,所以在建索引時才加入了 edge n-gram 過濾器,然而在查詢時,我們只想匹配用戶輸入的完整詞組,像是用戶的輸入 runqu

        • 因此需要定義兩套分析器,一套是建索引的分析器(包含edge n-gram 過濾器),另一套是查詢使用的正常的分析器

          PUT my_index
          {
              "settings": {
                  "number_of_shards": 1,
                  "analysis": {
                      "filter": {
                          //定義一個edge n-gram的token過濾器
                          //並設置任何通過這個過濾器的詞條,都會生成一個最小固定值為1,最大固定值為20的n-gram
                          "my_autocomplete_filter": {
                              "type": "edge_ngram",
                              "min_gram": 1,
                              "max_gram": 20
                          }
                      },
                      "analyzer": {
                          //自定義一個分析器,並使用自定義的edge n-gram過濾器
                          "my_autocomplete_analyzer": {
                              "type": "custom",
                              "tokenizer": "standard",
                              "filter": [
                                  "lowercase",
                                  "my_autocomplete_filter"
                              ]
                          }
                      }
                  }
              },
              "mapping": {
                  "my_type": {
                      "properties": {
                          "name": {
                              "type": "text",
                              "analyzer": "my_autocomplete_analyzer", //在索引時用,生成edge n-gram的每個詞
                              "search_analyzer": "standard"  //查詢用,只搜索用戶輸入的詞
                          }
                      }
                  }
              }
          }
          
  • 讓非 text 字段也能使用 edge n-gram

    • 由於 edge n-gram 是一個 token 過濾器,他包含在 analyzer 分析器裡面,因此只有 text 類型的字段才能使用(其他類型的字段不會被分詞,所以不會使用到 analyzer,因此不能用 edge n-gram)

    • 但是可能會有一種情況是,有些精確值也希望能通過 edge n-gram 生成組合,這時就要搭配使用一個叫做 keyword 的分詞器

      • 注意,此 keyword 分詞器和 keyword 字段類型是不同的東西
      • keyword 分詞器主要的功用是,將輸入的詞條,原封不動的 output 出來,不對其內容做任何改變
      • 因此可以利用這個特性,將精確值的字段類型改成 text,但是分詞器使用 keyword,如此就可以避免分詞的效果,又能使用 edge n-gram
    • 具體實例

      • 將 postcode 這個本來是 keyword 類型的精確值,改成使用 text 類型並搭配 keyword 分詞器

      • 因此假設有一個輸入 ABC EF,先經過 keyword 分詞器分詞成 ABC EF(和輸入一模一樣),接著再經過 edge n-gram 生成 A、AB、ABC、ABC (有一個空格) 、ABC E、ABC EF

      • 如果是使用正常的分詞器,生成的 edge n-gram 會是 A、AB、ABC、E、EF,他們是有差別的

        PUT my_index
        {
            "settings": {
                "analysis": {
                    "filter": {
                        "postcode_filter": {
                            "type": "edge_ngram",
                            "min_gram": 1,
                            "max_gram": 8
                        }
                    },
                    "analyzer": {
                        "postcode_index": {
                            "tokenizer": "keyword",
                            "filter": [
                                "postcode_filter"
                            ]
                        },
                        "postcode_search": {
                            "tokenizer": "keyword"
                        }
                    }
                }
            },
            "mapping": {
                "my_type": {
                    "properties": {
                        "postcode": {
                            "type": "text",
                            "analyzer": "postcode_index",
                            "search_analyzer": "postcode_search"
                        }
                    }
                }
            }
        }