作者:sudop
链接:https://www.jianshu.com/p/9e2c6a8e1b54
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

搜索Suggest需要优化问题:
  • 怎么优化Suggest词库,提升Suggest词准确率
  • 怎么提高响应速度

    suggest词库获取
  • 冷启动可以从内容中提取热词数据来解决,或者人工设置

  • 挖掘搜索日志:
    • 挖掘近1个月搜索日志,按照每天独立IP进行统计频次,即每个IP用户天搜索同一关键词多次只记一次,用IP过滤也有其局限性,伪IP,动态IP,局域网共享同一公网IP,都会影响到基于IP来判断用户的准确性,你也可以使用sessionId或者userId来判断
    • 统计后搜索词频次之后,抽取搜索频次>100(自定阈值)的词,同时对日志数据进行清洗,过滤去除大于10个字(去除太长的长尾词),单字和符号内容
    • 定时更新suggest词库中。
  • 搜索日志里面包含大量 误输入词:

    1. 需要在suggest词库里面去掉误输入词,对于搜索频次高的词,可以挖掘其对应的正确词,通过同义词进行查询改写。
    2. 误输入词同义词挖掘可以通过挖掘搜索session序列,使用word2vec训练来获取误输入词的同义词,通过分词器同义词设置,对误输入词进行查询改写。
      搜索Suggest需实现以下几个功能:
  • 匹配:能够通过用户的输入进行前缀匹配;

  • 排序:根据建议词的优先级或者搜索热度进行排序;
  • 纠错:能够对用户的输入进行拼写纠错(suggest建议优先prefix匹配,不宜过多提示,因此只需提供前缀匹配,中文拼音匹配即可)


image.png
搜索时:如上图所示,可引导用户选择category,提升Suggest准确度

匹配

  1. 通过分词器对Suggest进行单字,全拼,拼音首字母进行索引
    Elasticsearch 对于的字段mapping settings及分词器设置参考

    suggest 字段

  • “preserve_separators”: false, 这个设置为false,将忽略空格之类的分隔符
  • “preserve_position_increments”: true,如果建议词第一个词是停用词,我们使用了过滤停用词的分析器,需要将此设置为false
    提升响应速度
    关于completion FST编码原理
    如:“天上人间” 分析为:“天上人间”、“天上”、“上人”、“人间” 四个词条。 要注意这4个词条还有顺序,也就是position分别为0, 1, 2, 3。FST实际上是前缀编码,这些词被顺序串联在一起进行编码,并记录了每个词条的相对位置,编码后形如:天上人间|天上|上人|人间0 1 2 3
    特别注意,这时候所有的查找都只能从0位置的“天”开始。做completion suggest的时候, 输入的词条经过分析后, 必须有相同的前缀和相对位址。 因为你的搜索用的simple analyzer,当输入”天”的时候, 分析出来的是”天” (0), 在FST里是从起始位置开始可以匹配到。其他输入“天上” “天上人” 都是从位置0开始的前缀,也都可以匹配。
    但是如果你输入“上”, simple analyzer分析出来的是”上” (0), 去FST里查,第一个就不匹配,所以没结果。
    为了帮助理解,针对你的例子,可以试一下如下的搜索:
    1. POST test_suggestion/_search
    2. {
    3. "suggest": {
    4. "term-suggestion": {
    5. "prefix": "天上人间 天上 上",
    6. "completion": {
    7. "field": "keyword_suggestion"
    8. }
    9. }
    10. }
    11. }

    你会发现,上面用空格分隔的3个词,也可以match。 原因在于搜索用的simple analyzer是用空格一类的分隔符分词的,分词结果是: 天上人间|天上|上 0 1 2,顺着FST走下去,可以做到前缀匹配。

总结来说,当使用completion suggester的时候, 不是用于完成 类似于 “关键词“这样的模糊匹配场景,而是用于完成关键词前缀匹配的。 对于汉字的处理,无需使用ik/ HanLP一类的分词器,直接使用keyword analyzer,配合去除一些不需要的stop word即可。

举个例子,做火车站站名的自动提示补全,你可能希望用户输入“上海” 或者 “虹桥” 都提示”上海虹桥火车站“ 。 如果想使用completion suggester来做,正确的方法是为”上海虹桥火车站“这个站名准备2个completion词条,分别是:
“上海虹桥火车站”
“虹桥火车站”
这样用户的输入不管是从“上海”开始还是“虹桥”开始,都可以得到”上海虹桥火车站”的提示。

  • 因此想要实现completion suggest 中文拼音混合提示,需要提供三个字段,中文字段,采用standard分词,全拼字段,首字母字段,对汉字都采用standard分词,分词后对单字进行分词,确保FST索引的都是单字对应的拼音,这样应该就可以完成中英文拼音suggest
  • 第一步是先采用汉字前缀匹配的结果,使用全拼匹配也可以返回结果,但是存在同音字时,weight高的同音字会覆盖原来的字,导致suggest不准确
  • 第二部,当汉字匹配数量不够时,启用全拼匹配,可以达到拼音纠错补充效果,索引时只索引全拼拼音
  • 第三步:正常来说首字母拼音一般匹配不到内容,此时可以使用拼音首字母匹配,索引时只索引首字母拼音
  • 第四步:前面匹配的Suggest词不够时,最后也可以采用fuzzy查询进行补全

    使用fuzzy模糊查询

    fuzzy模糊查询是基于编辑距离算法来匹配文档。编辑距离的计算基于我们提供的查询词条和被搜索文档。
    Complete suggest支持fuzzy查询,计算编辑距离对CPU消耗比较大,需要设置以下参数来限制对性能的影响:

  1. prefix_length 不能被 “模糊化” 的初始字符数。 大部分的拼写错误发生在词的结尾,而不是词的开始。 例如通过将 prefix_length 设置为 3 ,你可能够显著降低匹配的词项数量。
  2. min_length 开始进行模糊匹配的最小输入长度
  3. fuzzy查询只在前缀匹配数不够时启用进行补全

排序

从搜索日志挖掘的Suggest词,可以根据搜索词的搜索频次作为热度来设置weight,Suggest会根据weight来排序。

  1. LinkedHashSet<String> returnSet = new LinkedHashSet<>();
  2. Client client = elasticsearchTemplate.getClient();
  3. SuggestRequestBuilder suggestRequestBuilder = client.prepareSuggest(elasticsearchTemplate.getPersistentEntityFor(SuggestEntity.class).getIndexName());
  4. //全拼前缀匹配
  5. CompletionSuggestionBuilder fullPinyinSuggest = new CompletionSuggestionBuilder("full_pinyin_suggest")
  6. .field("full_pinyin").text(input).size(10);
  7. //汉字前缀匹配
  8. CompletionSuggestionBuilder suggestText = new CompletionSuggestionBuilder("suggestText")
  9. .field("suggestText").text(input).size(size);
  10. //拼音搜字母前缀匹配
  11. CompletionSuggestionBuilder prefixPinyinSuggest = new CompletionSuggestionBuilder("prefix_pinyin_text")
  12. .field("prefix_pinyin").text(input).size(size);
  13. suggestRequestBuilder = suggestRequestBuilder.addSuggestion(fullPinyinSuggest).addSuggestion(suggestText).addSuggestion(prefixPinyinSuggest);
  14. SuggestResponse suggestResponse = suggestRequestBuilder.execute().actionGet();
  15. Suggest.Suggestion prefixPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("prefix_pinyin_text");
  16. Suggest.Suggestion fullPinyinSuggestion = suggestResponse.getSuggest().getSuggestion("full_pinyin_suggest");
  17. Suggest.Suggestion suggestTextsuggestion = suggestResponse.getSuggest().getSuggestion("suggestText");
  18. List<Suggest.Suggestion.Entry> entries = suggestTextsuggestion.getEntries();
  19. //汉字前缀匹配
  20. for (Suggest.Suggestion.Entry entry : entries) {
  21. List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
  22. for (Suggest.Suggestion.Entry.Option option : options) {
  23. returnSet.add(option.getText().toString());
  24. }
  25. }
  26. //全拼suggest补充
  27. if (returnSet.size() < 10) {
  28. List<Suggest.Suggestion.Entry> fullPinyinEntries = fullPinyinSuggestion.getEntries();
  29. for (Suggest.Suggestion.Entry entry : fullPinyinEntries) {
  30. List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
  31. for (Suggest.Suggestion.Entry.Option option : options) {
  32. if (returnSet.size() < 10) {
  33. returnSet.add(option.getText().toString());
  34. }
  35. }
  36. }
  37. }
  38. //首字母拼音suggest补充
  39. if (returnSet.size() == 0) {
  40. List<Suggest.Suggestion.Entry> prefixPinyinEntries = prefixPinyinSuggestion.getEntries();
  41. for (Suggest.Suggestion.Entry entry : prefixPinyinEntries) {
  42. List<Suggest.Suggestion.Entry.Option> options = entry.getOptions();
  43. for (Suggest.Suggestion.Entry.Option option : options) {
  44. returnSet.add(option.getText().toString());
  45. }
  46. }
  47. }
  48. return new ArrayList<>(returnSet);

ES setting mapping配置

  1. {
  2. "settings": {
  3. "analysis": {
  4. "analyzer": {
  5. "prefix_pinyin_analyzer": {
  6. "tokenizer": "standard",
  7. "filter": [
  8. "lowercase",
  9. "prefix_pinyin"
  10. ]
  11. },
  12. "full_pinyin_analyzer": {
  13. "tokenizer": "standard",
  14. "filter": [
  15. "lowercase",
  16. "full_pinyin"
  17. ]
  18. }
  19. },
  20. "filter": {
  21. "_pattern": {
  22. "type": "pattern_capture",
  23. "preserve_original": 1,
  24. "patterns": [
  25. "([0-9])",
  26. "([a-z])"
  27. ]
  28. },
  29. "prefix_pinyin": {
  30. "type": "pinyin",
  31. "keep_first_letter": true,
  32. "keep_full_pinyin": false,
  33. "none_chinese_pinyin_tokenize": false,
  34. "keep_original": false
  35. },
  36. "full_pinyin": {
  37. "type": "pinyin",
  38. "keep_first_letter": false,
  39. "keep_full_pinyin": true,
  40. "keep_original": false,
  41. "keep_none_chinese_in_first_letter": false
  42. }
  43. }
  44. }
  45. },
  46. "mappings": {
  47. "suggest": {
  48. "properties": {
  49. "id": {
  50. "type": "string"
  51. },
  52. "suggestText": {
  53. "type": "completion",
  54. "analyzer": "standard",
  55. "payloads": true,
  56. "preserve_separators": false,
  57. "preserve_position_increments": true,
  58. "max_input_length": 50
  59. },
  60. "prefix_pinyin": {
  61. "type": "completion",
  62. "analyzer": "prefix_pinyin_analyzer",
  63. "search_analyzer": "standard",
  64. "preserve_separators": false,
  65. "payloads": true
  66. },
  67. "full_pinyin": {
  68. "type": "completion",
  69. "analyzer": "full_pinyin_analyzer",
  70. "search_analyzer": "full_pinyin_analyzer",
  71. "preserve_separators": false,
  72. "payloads": true
  73. }
  74. }
  75. }
  76. }
  77. }

DSL查询语句

  1. POST _suggest
  2. {
  3. "text": "cy",
  4. "prefix_pinyin": {
  5. "completion": {
  6. "field": "prefix_pinyin",
  7. "size": 10
  8. }
  9. },
  10. "full_pinyin": {
  11. "completion": {
  12. "field": "full_pinyin",
  13. "size": 10
  14. }
  15. },
  16. "suggestText": {
  17. "completion": {
  18. "field": "suggestText",
  19. "size": 10
  20. }
  21. }
  22. }

suggest性能优化,从之前平均响应时间5.5ms 降低到3.5ms,Suggest词更加准确