1. 程式人生 > >ElasticSearch - 輸入即搜尋 edge n-gram

ElasticSearch - 輸入即搜尋 edge n-gram

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

    • 然而在現實情況下,使用者已經漸漸習慣在輸入完查詢內容之前,就能為他們展現搜尋結果,這就是所謂的即時搜尋(instant search),或是可以稱為 輸入即搜尋(search-as-you-type)

    • 雖然ES提供了一系列的字首搜尋match_phrase、prefix、wildcard、regexp,然而這樣的查詢的效能非常差,要知道使用者每多輸入一個新的字母,就意味著要重新進行一次搜尋,在實時的web系統中,100毫秒可能就會是一個難以忍受的延遲

    • 因此為了加快 輸入即搜尋 的查詢效率,可以改使用edge n-gram建立索引,如此可以避免使用字首查詢,在建立索引時就進行優化,使用空間換取時間,讓查詢的速率增快

  • 使用edge n-gram建立索引

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

      • 使用者輸入h、he時會找不到索引(倒排索引中只有hello),因此匹配失敗

    • 而對於輸入即搜尋這種應用場景,可以使用一種特殊的n-gram,稱為 邊界n-grams (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 127.0.0.1:9200/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 127.0.0.1:9200/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"
                          }
                      }
                  }
              }
          }