关于elasticsearch的query_string通用搜索方案改造

在实际使用elasticsearch做搜索引擎的业务场景中,我们经常会被很多组合条件弄得晕头转向。如果在业务中使用JSON来做搜索条件的处理,你会发现调用客户端api的同事,需要跟你一样精通搜索语法,带来了额外的沟通和学习成本。

elasticsearch官方支持了query_string这种直观简洁的搜索语法,从而成为了我们做搜索业务的首选。

话不多说,我们来看一下query_string相关的搜索语法示例:

{
    "query": {
        "query_string": {
             "query": "province: ('31' OR '32') AND goods:'小馒头包子'",
             "default_operator": "AND",
             "allow_leading_wildcard": false,
             "analyze_wildcard": false
        }
    }
}

从上面的语法可以看出,相对于JSON来说,其协议十分简洁直观,值得细品。


而要想达成上面的效果,其实我们还有很多事情要做。接下来,小编带大家一起来踩坑啦!

1,索引settings,如何配置?mapping如何设计?

2,排序TOPN,如何设计?

上面两个难题,相信做过搜索的人都经历过。

开始前,我们先来认识一下es常用的数据类型。

Numeric: int, long, short, float, double等等数字类型

Keyword: 关键字类型

Text: 文本类型

es中没有数组类型,但是以上常见的数据类型,都可以直接传数组(参考:官方对Arrays的解释

以上三种类型都支持范围查询语法。但一般来说,最好用Numeric类型的字段来做范围查询比较合理。

Keyword类型是一种特殊类型,所有定义为keyword类型的字段,都只能全量匹配。同时keyword与numeric有着一定的联系,下面是官方的解释:


简而言之,就是说需要用到范围查询的,尽量定义为numeric类型,不需要范围查询,只是匹配的,完全可以用keyword更加高效。

text文本类型是用来做分词的,不需要分词的就直接用keyword就行。

从这里可以看出,keyword实际上就是一个不可分割的term,term query场景下非常合适。


有了以上概念之后,我们开始进入今天的正题。

我们举一个简单的搜索场景示例:搜索淘宝商品功能。

我们先定一个商品索引,字段有:省份,商品名称,店铺名称,价格

索引配置如下:

{
    "mappings": {
        "dynamic": "strict",
        "properties": {
            "province": {
                "type": "keyword",
                "normalizer": "keyword"
            },
            "goods": {
                "type": "text",
                "analyzer": "chn_standard",
                "copy_to": ["search"]
            },
            "name": {
                "type": "text",
                "analyzer": "chn_standard",
                "copy_to": ["search"]
            },
            "price": {
                "type": "double"
            },
            "search": {
                "type": "text",
                "analyzer": "chn_standard"
            }
        }
    },
    "settings": {
      "index": {
        "mapping": {
          "coerce": "false"
        },
        "analysis": {
          "filter": {
            "graph_synonyms": {
              "type": "synonym_graph",
              "synonyms_path": "analysis/synonym.txt",
              "lenient": "true"
            },
            "english_stop": {
              "type": "stop",
              "stopwords": "_english_"
            }
          },
          "char_filter": {
            "punctuation": {
              "type": "mapping",
              "mappings": [
                "'=>",
                "\"=>"
              ]
            }
          },
          "normalizer": {
            "keyword": {
              "filter": [
                "lowercase",
                "asciifolding"
              ],
              "type": "custom",
              "char_filter": [
                "punctuation"
              ]
            }
          },
          "analyzer": {
            "chn_standard": {
              "filter": [
                "graph_synonyms",
                "lowercase"
              ],
              "char_filter": [
                "punctuation"
              ],
              "type": "custom",
              "tokenizer": "hanlp_smart"
            }
          },
          "tokenizer": {
            "hanlp_smart": {
              "enable_stop_dictionary": "true",
              "enable_custom_config": "true",
              "enable_place_recognize": "true",
              "type": "hanlp_standard",
              "enable_offset": "true"
            }
          }
        },
        "max_rescore_window": "1000000"
      }
    }
}

小编这里使用的是hanlp分词库,来做中文分词。hanlp插件 需要安装。

看上面的索引配置,mapping中定义的字段及其类型都按照文章开头的描述定义好了。

其中有个search字段是(goods, name)组成的组合字段,组合字段的好处就是,search: '小馒头' 就相当于 goods: '小馒头' OR name:'小馒头'

一个查询可以匹配多个字段。

对于keyword类型字段,例如:province,录入的时候最好使用数字替代中文,至于原因自行体会(上海,上海市)。

我们重点来关注一下analysis里面的配置项。

filter里面定义了同义词映射,英文停用词,

charfilter里面定义了punctuation特定标点替换,为的就是能够支持带引号的搜索,es里面如果直接搜索province: '31'是不能匹配到province为31的文档的,因为它去匹配了'31'。需要去掉引号搜索,char_filter就提供了这种功能。

keyword类型没有analyzer配置项,官方提供的解决方案是配置normalizer项,详情见上面的配置。字段定义的时候再配置 "normalizer": "keyword",即可支持带引号的匹配。

analyzer中我们定义了chn_standard,就是标准的hanlp中文分词。

在我们搜索goods: '小馒头包子' 的时候,es会根据search_analyzer指定的分词器对'小馒头包子'分词,文章开头有一个搜索示例,其中增加了一个参数default_operator: "AND", 其作用就是匹配goods字段中既包含‘小馒头’ ,又包含‘包子'的文档。default_operator参数默认是“OR”

,不指定就会导致搜索结果不精确。

至此,我们就解决了索引设计相关的常见问题,希望对大家有所帮助。


下面,我们重点来说一说TOPN排序场景应该如何设计。

大部分的排序场景就是,从海量的文档中选取我们最感兴趣的前N条文档返回结果。

排序需要分阶段进行,不知大家有没有注意到上面的配置,有个参数:max_rescore_window

这个就是用来限制召回文档粗略排序的量级。我们所要做的事情就是对搜索引擎匹配到的这100万个相关的文档进行TOPN排序。

但问题来了,如此大批量的文档排序会有很大的性能损失。最好的解决方案,就是利用数值类型字段进行粗略的排序,数值计算和排序不会消耗太多服务器资源。然后对粗排后的结果,再进行TOPN精确排序,这样就能很大程度上保证,前N条文档的搜索准确性。

搜索请求body如下:

我们使用function_score的script_score来做粗排,rescore来做精确排序。注意rescore的window_size必须要跟索引主分片总量关联起来。window_size表示的是单个索引分片参与排序文档数。例如:主分片个数为15,则参与rescore的文档总数为15*100。track_total_hits参数用来限制统计匹配文档总数的量级。当数量超过track_total_hits,es只会返回gte: track_total_hits,而不是实际的总数。所以尽量不要用es来做总量统计,出于性能方面的考虑,它只能给你一个估算值。

{
  "size": "5",
  "from": "0",
  "query": {
    "function_score": {
      "query": {
        "query_string": {
          "query": "search:'小馒头包子' AND province:'31'",
          "allow_leading_wildcard": false,
          "analyze_wildcard": false,
          "default_operator": "AND"
        }
      },
      "script_score": {
        "script": {
          "lang": "painless",
          "source": "_score*500+price*10"
        }
      }
    }
  },
  "rescore": [
    {
      "window_size": 100,
      "external": {
        "factor": 1,
        "keyword": "小馒头包子",
        "formula": "text_relevance(goods)*10000+text_relevance(name)*10000",
        "explain": false
      }
    }
  ],
  "highlight": {
    "order": "score",
    "number_of_fragments": 1,
    "type": "fvh",
    "require_field_match": false,
    "fragment_size": 120,
    "pre_tags": [
      "<em>"
    ],
    "post_tags": [
      "</em>"
    ],
    "fields": {
      "goods": {
        "fragment_size": "256"
      },
      "name": {
        "fragment_size": "256"
      }
    }
  },
  
  "_source": [
    "name",
    "goods"
  ],
  "sort": [
    {
      "_score": {
        "order": "desc"
      }
    }
  ],
  "track_total_hits": 10000000
}


以上就是小编在es搜索使用方面的经验总结,觉得有帮助的帮忙点波赞哦!!!

发布于 2020-08-18 16:36