Elasticsearch搜索Suggest功能優(yōu)化

搜索Suggest需要優(yōu)化問題:
  • 怎么優(yōu)化Suggest詞庫,提升Suggest詞準(zhǔn)確率
  • 怎么提高響應(yīng)速度
suggest詞庫獲取
  • 冷啟動可以從內(nèi)容中提取熱詞數(shù)據(jù)來解決,或者人工設(shè)置
  • 挖掘搜索日志:
    • 挖掘近1個月搜索日志,按照每天獨(dú)立IP進(jìn)行統(tǒng)計(jì)頻次,即每個IP用戶天搜索同一關(guān)鍵詞多次只記一次,用IP過濾也有其局限性,偽IP,動態(tài)IP,局域網(wǎng)共享同一公網(wǎng)IP,都會影響到基于IP來判斷用戶的準(zhǔn)確性,你也可以使用sessionId或者userId來判斷
    • 統(tǒng)計(jì)后搜索詞頻次之后,抽取搜索頻次>100(自定閾值)的詞,同時對日志數(shù)據(jù)進(jìn)行清洗,過濾去除大于10個字(去除太長的長尾詞),單字和符號內(nèi)容
    • 定時更新suggest詞庫中。
  • 搜索日志里面包含大量 誤輸入詞:
    1. 需要在suggest詞庫里面去掉誤輸入詞,對于搜索頻次高的詞,可以挖掘其對應(yīng)的正確詞,通過同義詞進(jìn)行查詢改寫。
    2. 誤輸入詞同義詞挖掘可以通過挖掘搜索session序列,使用word2vec訓(xùn)練來獲取誤輸入詞的同義詞,通過分詞器同義詞設(shè)置,對誤輸入詞進(jìn)行查詢改寫。
搜索Suggest需實(shí)現(xiàn)以下幾個功能:
  • 匹配:能夠通過用戶的輸入進(jìn)行前綴匹配;
  • 排序:根據(jù)建議詞的優(yōu)先級或者搜索熱度進(jìn)行排序;
  • 糾錯:能夠?qū)τ脩舻妮斎脒M(jìn)行拼寫糾錯(suggest建議優(yōu)先prefix匹配,不宜過多提示,因此只需提供前綴匹配,中文拼音匹配即可)
搜索框

搜索時:如上圖所示,可引導(dǎo)用戶選擇category,提升Suggest準(zhǔn)確度

匹配

  1. 通過分詞器對Suggest進(jìn)行單字,全拼,拼音首字母進(jìn)行索引
    Elasticsearch 對于的字段mapping settings及分詞器設(shè)置參考

suggest 字段

  • "preserve_separators": false, 這個設(shè)置為false,將忽略空格之類的分隔符
  • "preserve_position_increments": true,如果建議詞第一個詞是停用詞,我們使用了過濾停用詞的分析器,需要將此設(shè)置為false
提升響應(yīng)速度
關(guān)于completion FST編碼原理

如:“天上人間” 分析為:“天上人間”、“天上”、“上人”、“人間” 四個詞條。 要注意這4個詞條還有順序,也就是position分別為0, 1, 2, 3。FST實(shí)際上是前綴編碼,這些詞被順序串聯(lián)在一起進(jìn)行編碼,并記錄了每個詞條的相對位置,編碼后形如:天上人間|天上|上人|人間 0 1 2 3

特別注意,這時候所有的查找都只能從0位置的“天”開始。做completion suggest的時候, 輸入的詞條經(jīng)過分析后, 必須有相同的前綴和相對位址。 因?yàn)槟愕乃阉饔玫膕imple analyzer,當(dāng)輸入"天"的時候, 分析出來的是"天" (0), 在FST里是從起始位置開始可以匹配到。其他輸入“天上” “天上人” 都是從位置0開始的前綴,也都可以匹配。
但是如果你輸入“上”, simple analyzer分析出來的是"上" (0), 去FST里查,第一個就不匹配,所以沒結(jié)果。

為了幫助理解,針對你的例子,可以試一下如下的搜索:

    POST test_suggestion/_search
    {
      "suggest": {
        "term-suggestion": {
          "prefix": "天上人間 天上 上",
          "completion": {
            "field": "keyword_suggestion"
          }
        }
      }
    }

你會發(fā)現(xiàn),上面用空格分隔的3個詞,也可以match。 原因在于搜索用的simple analyzer是用空格一類的分隔符分詞的,分詞結(jié)果是: 天上人間|天上|上 0 1 2,順著FST走下去,可以做到前綴匹配。

總結(jié)來說,當(dāng)使用completion suggester的時候, 不是用于完成 類似于 "關(guān)鍵詞"這樣的模糊匹配場景,而是用于完成關(guān)鍵詞前綴匹配的。 對于漢字的處理,無需使用ik/ HanLP一類的分詞器,直接使用keyword analyzer,配合去除一些不需要的stop word即可。

舉個例子,做火車站站名的自動提示補(bǔ)全,你可能希望用戶輸入“上海” 或者 “虹橋” 都提示"上海虹橋火車站“ 。 如果想使用completion suggester來做,正確的方法是為"上海虹橋火車站“這個站名準(zhǔn)備2個completion詞條,分別是:
"上海虹橋火車站"
"虹橋火車站"
這樣用戶的輸入不管是從“上海”開始還是“虹橋”開始,都可以得到"上海虹橋火車站"的提示。

  • 因此想要實(shí)現(xiàn)completion suggest 中文拼音混合提示,需要提供三個字段,中文字段,采用standard分詞,全拼字段,首字母字段,對漢字都采用standard分詞,分詞后對單字進(jìn)行分詞,確保FST索引的都是單字對應(yīng)的拼音,這樣應(yīng)該就可以完成中英文拼音suggest
  • 第一步是先采用漢字前綴匹配的結(jié)果,使用全拼匹配也可以返回結(jié)果,但是存在同音字時,weight高的同音字會覆蓋原來的字,導(dǎo)致suggest不準(zhǔn)確
  • 第二部,當(dāng)漢字匹配數(shù)量不夠時,啟用全拼匹配,可以達(dá)到拼音糾錯補(bǔ)充效果,索引時只索引全拼拼音
  • 第三步:正常來說首字母拼音一般匹配不到內(nèi)容,此時可以使用拼音首字母匹配,索引時只索引首字母拼音
  • 第四步:前面匹配的Suggest詞不夠時,最后也可以采用fuzzy查詢進(jìn)行補(bǔ)全

使用fuzzy模糊查詢

fuzzy模糊查詢是基于編輯距離算法來匹配文檔。編輯距離的計(jì)算基于我們提供的查詢詞條和被搜索文檔。
Complete suggest支持fuzzy查詢,計(jì)算編輯距離對CPU消耗比較大,需要設(shè)置以下參數(shù)來限制對性能的影響:

  1. prefix_length 不能被 “模糊化” 的初始字符數(shù)。 大部分的拼寫錯誤發(fā)生在詞的結(jié)尾,而不是詞的開始。 例如通過將 prefix_length 設(shè)置為 3 ,你可能夠顯著降低匹配的詞項(xiàng)數(shù)量。
    2.min_length 開始進(jìn)行模糊匹配的最小輸入長度
    3.fuzzy查詢只在前綴匹配數(shù)不夠時啟用進(jìn)行補(bǔ)全

排序

從搜索日志挖掘的Suggest詞,可以根據(jù)搜索詞的搜索頻次作為熱度來設(shè)置weight,Suggest會根據(jù)weight來排序。

java API代碼參考

LinkedHashSet<String> returnSet = new LinkedHashSet<>();
        Client client = elasticsearchTemplate.getClient();
        SuggestRequestBuilder suggestRequestBuilder = client.prepareSuggest(elasticsearchTemplate.getPersistentEntityFor(SuggestEntity.class).getIndexName());
        //全拼前綴匹配
        CompletionSuggestionBuilder fullPinyinSuggest = new CompletionSuggestionBuilder("full_pinyin_suggest")
                .field("full_pinyin").text(input).size(10);
        //漢字前綴匹配
        CompletionSuggestionBuilder suggestText = new CompletionSuggestionBuilder("suggestText")
                .field("suggestText").text(input).size(size);
        //拼音搜字母前綴匹配
        CompletionSuggestionBuilder prefixPinyinSuggest = new CompletionSuggestionBuilder("prefix_pinyin_text")
                .field("prefix_pinyin").text(input).size(size);
        suggestRequestBuilder = suggestRequestBuilder.addSuggestion(fullPinyinSuggest).addSuggestion(suggestText).addSuggestion(prefixPinyinSuggest);
        SuggestResponse suggestResponse = suggestRequestBuilder.execute().actionGet();
        Suggest.Suggestion prefixPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("prefix_pinyin_text");
        Suggest.Suggestion fullPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("full_pinyin_suggest");
        Suggest.Suggestion suggestTextsuggestion = suggestResponse.getSuggest().getSuggestion("suggestText");
        List<Suggest.Suggestion.Entry> entries = suggestTextsuggestion.getEntries();
        //漢字前綴匹配
        for (Suggest.Suggestion.Entry entry : entries) {
            List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
            for (Suggest.Suggestion.Entry.Option option : options) {
                returnSet.add(option.getText().toString());
            }
        }
        //全拼suggest補(bǔ)充
        if (returnSet.size() < 10) {
            List<Suggest.Suggestion.Entry> fullPinyinEntries = fullPinyinSuggestion.getEntries();
            for (Suggest.Suggestion.Entry entry : fullPinyinEntries) {
                List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                for (Suggest.Suggestion.Entry.Option option : options) {
                    if (returnSet.size() < 10) {
                        returnSet.add(option.getText().toString());
                    }
                }
            }
        }
        //首字母拼音suggest補(bǔ)充
        if (returnSet.size() == 0) {
            List<Suggest.Suggestion.Entry> prefixPinyinEntries = prefixPinyinSuggestion.getEntries();
            for (Suggest.Suggestion.Entry entry : prefixPinyinEntries) {
                List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
                for (Suggest.Suggestion.Entry.Option option : options) {
                    returnSet.add(option.getText().toString());
                }
            }
        }
        return new ArrayList<>(returnSet);

ES setting mapping配置

{
  "settings": {
    "analysis": {
      "analyzer": {
        "prefix_pinyin_analyzer": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "prefix_pinyin"
          ]
        },
        "full_pinyin_analyzer": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "full_pinyin"
          ]
        }
      },
      "filter": {
        "_pattern": {
          "type": "pattern_capture",
          "preserve_original": 1,
          "patterns": [
            "([0-9])",
            "([a-z])"
          ]
        },
        "prefix_pinyin": {
          "type": "pinyin",
          "keep_first_letter": true,
          "keep_full_pinyin": false,
          "none_chinese_pinyin_tokenize": false,
          "keep_original": false
        },
        "full_pinyin": {
          "type": "pinyin",
          "keep_first_letter": false,
          "keep_full_pinyin": true,
          "keep_original": false,
          "keep_none_chinese_in_first_letter": false
        }
      }
    }
  },
  "mappings": {
    "suggest": {
      "properties": {
        "id": {
          "type": "string"
        },
        "suggestText": {
          "type": "completion",
          "analyzer": "standard",
          "payloads": true,
          "preserve_separators": false,
          "preserve_position_increments": true,
          "max_input_length": 50
        },
        "prefix_pinyin": {
          "type": "completion",
          "analyzer": "prefix_pinyin_analyzer",
          "search_analyzer": "standard",
          "preserve_separators": false,
          "payloads": true
        },
        "full_pinyin": {
          "type": "completion",
          "analyzer": "full_pinyin_analyzer",
          "search_analyzer": "full_pinyin_analyzer",
          "preserve_separators": false,
          "payloads": true
        }
      }
    }
  }
}

DSL查詢語句

POST    _suggest

{
  "text": "cy",
  "prefix_pinyin": {
    "completion": {
      "field": "prefix_pinyin",
      "size": 10
    }
  },
  "full_pinyin": {
    "completion": {
      "field": "full_pinyin",
      "size": 10
    }
  },
  "suggestText": {
    "completion": {
      "field": "suggestText",
      "size": 10
    }
  }
}
suggest性能優(yōu)化,從之前平均響應(yīng)時間5.5ms 降低到3.5ms,Suggest詞更加準(zhǔn)確
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容