当选用 elasticsearch 作为电商的商品搜索存储系统时,用户输入一个 query 时,这个 query 是如何从es 中查询出商品数据的?
首先,用户输入的 query 词会通过query 分析服务产出若干个从不同维度表达用户意图的tokens。比如输入“红富士苹果”,经 query 分析后会产出以下维度的tokens:
比如搜索“红富士苹果”,query分析产出的 tokens有:品牌词:红富士;本质词:苹果、水果;商品的四级类目id;商品的SEO词(正宗、清甜、现摘)
- 本质词
- 品牌词
- 商家名称
- query 所要召回的商品四级类目 id
- 商品的 seo词
...
这里不对 query 分析作详细讨论。总之 query 分析是作用是将用户输入的 query 词变成若干能表达用户搜索意图的“维度”,然后基于这些“维度” 去召回商品。正是 query 分析会产出不同“维度”的tokens,根据这些 tokens 查询 es 获取商品数据的过程,就称为多路召回,每一路召回,我们可以根据它的表征“维度”,构造不同的 es 查询语句。
比如:针对四级类目 id,则可以使用:bool-query-filter 查询;而针对:品牌词,可以使用 bool-query-must-term 查询,而针对本质词,可能会有多个层级:比如苹果、水果,则可以决定更相关的使用 must 查询、低相关的使用 should 查询。此外,如果某一路产出的 token有很多,而且不同的 token 之间权重不一样,也可以考虑采用 constant-score-query 来封装某些 token的查询。
"query": {
"bool": {
"filter": [] // part 1: 过滤条件
"must": [], // part 1: 召回商品必须满足的条件
"should": [] // part 2: 加分,相关性条件
}
}
然而,对于召回来说,保证每次查询结果的稳定性对用户体验、线上 case 的排查都非常重要。但是由于es的分片机制,以及线上增量数据的不断写入,某些 es 查询语句并不具有幂等性。比如就描述了使用 rescore 查询时因es 分片参数不一致导致的查询结果不一致问题。而对于线上核心电商搜索系统而言,应当要尽力避免这种不一致,所以一般在生产系统中,构造出来的查询语句要尽量保证其得分的稳定性,比如使用 constant-score-query 来封装子查询。另外,一般在 es 中并不存储 text 类型的字段并进行打分查询,而是通过 query 分析,将 query 分解成能表达用户意图的 token,基于这些 token 做“过滤”查询。
之所以是这样设计,其实也是跟电商搜索是:“结构化”召回有关。思考这样一个问题:es 里面要存储哪些字段来支持商品的搜索呢?比如对于一个商品 sku而言:
- 商品名称的分词结果,使用不同的分词器产出的分词结果不一样,会在 es 中使用多个字段存储分别存储不同分词器的分词结果。
- 商家名称的分词结果
- 商品的价格
- 商品的销量
- 品牌名称(分词结果)
- 商品的一二三四级类目id
- 商品的本质词(模型特征)
- spu_id、poi_id 等一些正排字段...
这些字段,其实都是结构化了的字段。注意,我们并不是使用 text 类型存储商品名称,而是将商品名称分词之后,在 es 索引构建时,将它们存储到 es 中,这样做的目的是:term 查询比 text 查询性能要好;text 查询的排序结果受es打分影响(分片上 doc 数量的变化),可能会导致从 es 中查出来的排序结果不稳定;在召回之前,肯定是要有query 分析的,query 分析不仅仅是对用户输入的查询语句做分词,还有其它一些 处理,比如同义词、相似词,query 的类目分析等;因此到 es 查询这一步,主要是 term 或者 filter 查询,并没有需要分词的 text 查询。
正因为多路召回的存在,可能导致查询语句非常的复杂,如果将多路召回表示成一个 es 查询语句,很有可能在召回阶段导致的耗时较高,因此将每一路召回(或者相关的某几路)放到一个 es 查询语句中(SearchBuilder),然后以线程池隔离向 es 发起查询,所以一次用户查询,到 es 召回时会被“放大”成多次 es 查询,存在着“查询放大”效应。
多路召回面临的一个问题是:查询结果的合并。对于商品而言,会有主键 spu_id 唯一标识一个商品,因此可以将它作为多路召回结果的合并主键。接下来就是将合并结果送到 rank 服务做排序了。rank 排序这里不做详细介绍。
从工程上来说,多路召回需要考虑的一些因素有:
- 统一各路召回的查询语句,当有新的需求或者新的特征被挖掘出来,需要新增一路召回时,能够方便地新增一路召回,从而方便业务快速迭代。
- 如果在 rank 层排序时,依赖es 的分数,则需要谨慎考虑修改 es 查询语句对 spu_id 的打分的影响。一般地,es 的打分只是作为粗排的一个因子,粗排完之后,rank 的模型打分才是决定商品顺序的关键因素。
- 某一路查询结果失败,并不影响其它路的查询结果,可允许部分失败。
- 能够较好地分析查询结果 spu_id 是由哪一路查询语句召回的,甚至是由哪一个查询条件召回的,这样有利于 case 排查分析。参考:es matched_queries。
- 每一路召回的查询耗时监控、查询结果数量 tp999 监控,支持异步查询 es(可参考 es 提供的 ListenableActionFuture 类),查询语句的限流等。这需要在RestHighLevelClient的基础之上进一步封装 es 客户端,并且与公司内部的基础组件打通(限流组件、监控组件)
- ES 索引构建时,避免doc 在分片上分布不均匀,从而影响 es 查询语句的稳定性。比如 es 索引实时更新时,es分片的doc 数量可能会动态变化,或者说同一个 spu_id 会不会在不同的索引构建日期,分布在不同的分片上。这往往是在索引构建时,指定 doc哈希到固定的分片。具体的索引构建本文不详细介绍。
一般常用的 es query 有如下几种:
- query-bool-must
- query-bool-filter
- query-bool-should
- constant-score-query
- rescore query
视待查的具体字段,以及 query 分析产出结果选择何种查询。
参考:
- https://mp.weixin.qq.com/s/Nisgorg_Qgr-AdVm3c99LQ
- https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html