Elasticsearch percolator 用于电商搜索治理:将模糊查询转换为可控的检索策略
作者来自 Elastic Alexander Marquardt, Honza Král 及 Taylor Roy学习如何使用 Elasticsearch percolator 来实现搜索治理。在这篇博客中我们概述了在生产环境中构建受控策略引擎所需的模式并创建一个可控的检索策略。更多阅读Elasticsearch理解 Elasticsearch 中的 Percolator 数据类型及 Percolate 查询ElasticsearchElasticsearch percolate 查询刚接触 Elasticsearch可以参加我们的 Elasticsearch 入门网络研讨会。你也可以开始免费的云试用或者现在就在本地机器上尝试 Elastic。这篇文章是对第 3 部分中描述的控制平面架构的技术深入解析展示如何使用 Elasticsearch percolator 来构建它。文章概述了在生产环境中实现一个确定性的、受治理的策略引擎所需的模式。从架构到实现第 3 部分描述了控制平面架构将反向匹配作为一种查询机制将匹配与动作分离的策略文档以及将多个策略组合为单一执行计划的级联转换。这篇文章将深入使用 Elasticsearch 的 percolator 查询来实现这一策略查找功能。percolator 非常适合用于治理场景因为它以与控制平面需求完全一致的方式反转了搜索方向。本文将逐步讲解其实现从 percolator 的作用及其重要性开始然后深入到索引设计、策略存储、查询时评估以及多策略组合。普通搜索是如何工作的在电商系统中你可能有几十万甚至上百万个产品文档这些文档包含 title、category 和 price 等字段。当用户发起搜索时本质上是在让 Elasticsearch 将用户的搜索字符串与这些产品文档中的一个或多个存储字段进行匹配。Elasticsearch 的默认分析器standard analyzer会将文本转为小写并拆分为 token。例如搜索 “oranges” 会匹配 “Oranges”因为进行了小写归一化。如果使用包含词干提取的语言分析器它还可以匹配 “orange”因为两者会被归一化为相同的词干。例如下面这个 match 查询会返回 title 字段中包含 “orange” 或 “oranges” 的文档。POST products/_search { query: { match: { title: oranges } } }因此对于上述查询Elasticsearch 会返回 title 字段中匹配 “oranges” 的产品文档例如 “Orange Fruit Spread”、“Orange Juice”、“Juicy oranges”、“Orange Marmalade” 等等。需要记住的关键点是Elasticsearch 通常用于将搜索字符串与文档进行比较并返回与该搜索字符串匹配的文档。治理问题在搜索产品之前找到相关策略正如第 1 到第 3 部分所建立的那样一个受治理的搜索系统不会直接将用户的搜索字符串发送到产品目录。相反它首先会检查是否有任何策略适用于该搜索字符串。一位商品运营人员决定当有人精确搜索 “oranges” 时结果应限制在 Oranges 类别从而排除橙汁、橙子果酱和橙味汽水等内容。这个业务决策被存储为一条策略。当用户输入 “oranges” 时控制平面需要找到该策略读取其指令并相应地调整对产品目录的搜索。为了做到这一点控制平面需要判断哪些已存储的策略与该搜索字符串相关。在企业级部署中可能存在数百甚至数千条这样的策略。通过 if/else 逻辑逐一检查是第 2 部分中描述的应用层反模式。我们需要一种方式将所有策略存储在一个索引中并能够即时找到与给定搜索字符串匹配的策略。这正是 percolator 发挥作用的地方。反转方向Percolator我们之前提到在普通搜索中Elasticsearch 通常用于将搜索字符串与文档进行比较并返回包含该搜索字符串的文档。Percolator 则反转了这一过程。在 percolator 中你有一个索引其中每个文档存储的是一个查询模式然后将传入的搜索字符串与这些已存储的查询进行匹配以确定哪些查询模式被触发。对于治理而言“已存储的查询模式”就是策略。每条策略都包含一个模式用来描述它应该匹配哪类搜索字符串。例如搜索字符串是否精确匹配 “oranges”或者是否包含 “olive oil”。传入的字符串是用户的搜索文本在查询时到达需要与所有已存储的策略模式进行匹配。这一点在相关 PRISM 视频的 4:09 处有说明。逐步说明一次 “oranges” 搜索如何找到对应策略策略一位商品运营人员创建了一条策略当用户搜索内容完全等于 “oranges” 且没有其他词时进行匹配。一旦 percolator 命中该文档的其余部分会包含控制平面用于构建产品查询的规则在这个例子中其中一条规则是将结果限制过滤在 Fruits 类别中。{ percolator: { match_phrase: { query: START oranges END } }, rule_type: filter, rule_args: { filters: [ { field: categories, values: [Fruits], mode: hard_filter, on_conflict: soft_boost, on_conflict_boost_weight: 1.0 } ] }, priority: 0, enabled: true }percolator 字段包含定义该策略何时触发的匹配模式。在这个例子中它匹配短语 “START oranges END”。rule_type 和 rule_args 字段定义了当策略被触发时应该执行的操作。START 和 END 标记是边界标记我们稍后会进行解释。你可以在相关 PRISM 视频 2:52 处的 PRISM Studio UI 中看到策略是如何编写的。用户发起搜索一位购物者在搜索框中输入 “oranges”。控制平面检查匹配策略在搜索产品目录之前控制平面会拦截用户的搜索字符串为其添加边界标记并将其发送给 percolatorPOST policies/_search { query: { percolate: { field: percolator, document: { query: START oranges END } } } }字符串 “START oranges END” 会与所有已存储的策略模式进行匹配。在内部Elasticsearch 会运行这些已存储的策略模式来检查该字符串并返回所有匹配的策略。这就是 percolator 的工作方式。用户的搜索字符串会与所有已存储的策略模式进行比对并返回所有匹配项。没有 if/else 链式判断也没有顺序遍历执行索引负责完成匹配。控制平面应用策略控制平面读取已匹配策略的动作。上述策略指示控制平面将结果限制在 Fruits 类别。随后控制平面基于产品目录构建最终的 Elasticsearch 查询如下POST products/_search { query: { bool: { must: [ { match: { title: oranges } } ], filter: [ { terms: { categories: [Fruits] } } ] } } }用户搜索了 “oranges”。产品目录接收到一个被限制在 Fruits 类别中的 “oranges” 查询。由于这个约束orange juice、orange marmalade 和 orange soda 都被排除。为什么 “orange marmalade” 不会触发 “oranges” 策略假设另一个用户搜索 “orange marmalade”。控制平面会对该字符串进行封装并执行 percolation“START orange marmalade END”。而 “oranges” 策略的模式是 match_phrase“START oranges END”。该策略不会匹配因此不会被应用结果也不会被限制在 Fruits 类别。这正是 START 和 END 边界标记的作用。如果没有这些标记一个匹配 “oranges” 的策略可能会错误触发在 “orange marmalade” 这样的查询上。通过将用户搜索字符串包裹在 START 和 END 中并在策略模式中包含这些标记我们确保该策略只在 “oranges” 是完整搜索字符串没有其他词时才触发。这同时符合购物者和商品运营人员的意图。第二个策略“olive oil” 在词干化字段上的匹配并不是所有策略都需要精确字符串匹配。“olive oil” 策略是在词干化字段上匹配因此即使存在轻微的词形变化它也会触发{ percolator: { bool: { should: [ { match_phrase: { query.stemmed: START olive oil END } } ] } }, rule_type: filter, rule_args: { filters: [ { field: categories, values: [Olive oils], mode: hard_filter, on_conflict: soft_boost, on_conflict_boost_weight: 1.0 } ] }, priority: 300, enabled: true }该策略的模式不是在 query 字段上匹配而是在 query.stemmed 字段上匹配。当用户的搜索字符串到达时它会同时存储在两个字段中query 字段原始文本以及 query.stemmed 字段使用词干分析器处理后的文本该分析器会将词语还原为词干例如 “olives” 和 “olive” 都会被还原为同一个词干“oils” 和 “oil” 也是如此。该策略的模式会在词干化后的字符串上进行匹配因此即使存在轻微的词形变化也会触发。START 和 END 边界标记同样作用于 stemmed 字段从而确保该策略只在 “olive oil” 是完整搜索字符串时触发而不会在它出现在更长查询的一部分时触发。本文其余部分将介绍使其具备生产可用性的实现细节支持两种匹配模式的索引 mapping、如何通过高亮驱动短语移除与已消费短语追踪以及多个冲突策略如何组合成单一执行计划。策略索引 mapping策略索引需要一个 percolator 字段来存储已定义的查询模式以及一个文本字段用于镜像 percolator 将要匹配的输入搜索字符串结构。下面的 mapping 是简化版本以便理解。在生产环境中会更加复杂例如使用自定义分析器来处理边界标记、可变模式匹配例如识别 “under $4” 中的货币值以及其他类型的文本分析。PUT policies { mappings: { properties: { percolator: { type: percolator }, query: { type: text, fields: { stemmed: { type: text, analyzer: stemming } } }, rule_type: { type: keyword }, rule_args: { type: object, enabled: false }, priority: { type: integer }, enabled: { type: boolean } } } }该索引被命名为 policies因为每个文档都代表一个完整的受治理策略正如第 2 部分所定义的那样。这包括匹配条件、动作、优先级以及元数据。rule_type 和 rule_args 字段包含策略的动作部分即控制平面在构建针对产品目录执行查询时所使用的指令。query 字段是 percolator 用来匹配的字符串它有两个版本精确版本和词干化版本。当用户的搜索字符串到达时它会被放入这个字段对应的临时内存索引中。在 query 上匹配的策略会看到原始精确字符串在 query.stemmed 上匹配的策略则会看到词干化后的版本。带高亮、过滤和排序的 percolation上述简单示例展示的是最小化的 percolation 请求。在实际应用中控制平面会加入高亮功能、过滤掉已禁用的策略并按照优先级进行排序POST policies/_search { query: { bool: { must: [ { percolate: { field: percolator, document: { query: START olive oil END } } }, { term: { enabled: true } } ] } }, highlight: { fields: { query: { matched_fields: [query.stemmed] } } }, sort: [ { priority: { order: desc } } ] }高亮配置使用 “query” 作为字段键并在 matched_fields 中使用 “query.stemmed”。这告诉 Elasticsearch 的统一高亮器unified highlighter在返回高亮结果时基于父字段 query但同时在判断哪些 token 需要高亮时也考虑来自 query.stemmed 子字段的匹配结果。这使得即使某个策略是在 stemmed 字段上匹配也能在原始文本上生成准确的高亮范围而控制平面正需要这些信息来进行短语移除和已消费短语追踪。enabled: true 过滤条件确保已禁用的策略会被跳过。按 priority 排序则保证高优先级策略优先返回使控制平面能够按正确顺序执行级联转换。highlight 字段是最关键的补充它可以精确指出用户搜索字符串中哪些词触发了每个匹配。对于 “olive oil” 搜索其响应可能如下所示{ hits: { hits: [ { _id: en_2c3021c8, _source: { rule_type: filter, rule_args: { filters: [ { field: categories, values: [Olive oils], mode: hard_filter, on_conflict: soft_boost, on_conflict_boost_weight: 1.0 } ] }, priority: 300 }, highlight: { query: [emSTART olive oil END/em] } } ] } }为什么高亮很重要注意响应中的高亮START olive oil END。Elasticsearch 在告诉我们正是用户搜索字符串中的哪些词触发了该策略的匹配。这不仅仅是展示效果高亮元数据驱动了两个关键的下游行为短语移除。某些策略需要在构建产品目录查询之前将匹配到的文本从搜索字符串中移除。例如一个匹配 “cheap” 的策略会移除该词并将其转换为价格过滤条件。高亮可以精确标识该策略匹配的是搜索字符串中的哪一段因此系统知道应该移除哪一部分。已消费短语追踪。正如第 3 部分所描述当多个策略匹配同一个搜索字符串时高优先级策略可能会移除低优先级策略也匹配到的词。通过将每个策略的高亮结果与当前不断变化的搜索字符串进行对比系统可以识别某个短语已被“消费”并跳过低优先级策略。这避免了重复处理并确保行为的确定性。你可以在这篇文章中了解更多关于高亮机制的内容。从 percolation 到执行计划percolator 返回的是一组匹配的策略。但正如第 3 部分所描述的查找只是故事的一半另一半是将这些匹配组合成一个一致的执行计划。下面是一个具体查询的示例。完整示例“Cheap chocolate” 在圣诞活动期间假设系统中有两条激活策略“Cheap chocolate” 策略优先级 210和 “Christmas chocolates” 策略优先级 300两者在第 3 部分中都有详细描述。1步骤 1Percolate。用户搜索 “cheap chocolate”。控制平面将其封装为 “START cheap chocolate END”并发送给 percolator。两个策略匹配“Cheap chocolate” 策略通过短语 “cheap chocolate” 匹配“Christmas chocolates” 策略通过 stemmed 字段匹配 “chocolate”。2步骤 2按优先级排序。percolator 返回两个策略并按优先级降序排序。首先处理 “Christmas chocolates”300然后是 “Cheap chocolate”210。2步骤 3应用级联转换。这对应第 3 部分中的初始状态 → [Policy A] → state → [Policy B] → state → 执行计划模型。优先级 300 的 “Christmas chocolates” 策略首先应用添加类别硬过滤Christmas foods and drinks、Christmas sweets添加价格过滤低于 $7添加类别软提升Advent calendars3x接着优先级 210 的 “Cheap chocolate” 策略在修改后的状态上应用尝试添加类别硬过滤Chocolates、Milk chocolates但由于 Christmas 策略已设置该字段且 on_conflict: override因此被覆盖并丢弃尝试添加价格过滤$2但 Christmas 策略设置 on_conflict: restrict且 $2 比 $7 更严格因此 $2 生效移除搜索词 “cheap”3步骤 4构建 Elasticsearch 查询。控制平面将执行计划组装为一个针对产品目录的 Elasticsearch 查询POST products/_search { query: { function_score: { query: { bool: { must: [ { match: { title: chocolate } } ], filter: [ { terms: { categories: [Christmas foods and drinks, Christmas sweets] } }, { range: { price: { lt: 2 } } } ] } }, functions: [ { weight: 1 }, { filter: { terms: { categories: [Advent calendars] } }, weight: 3 } ], score_mode: sum, boost_mode: multiply } } }原始搜索字符串是 “cheap chocolate”。最终到达产品目录的查询是一个经过治理的、具备意图感知的检索计划词语 “cheap” 已被消耗并转换为价格约束结果被限制在圣诞季节性类别中Advent calendar 产品获得排名提升并且价格上限采用了来自低优先级策略中更严格的值。每一次转换都是确定性的、可追踪的、可解释的。关于这些乘数如何与基础 BM25 分数交互的快速说明可以参考相关 PRISM 视频 8:45 处其中简要讨论了乘法式提升multiplicative boosts。为什么这种方式具备可扩展性percolator 在这个用例中之所以高效是因为存在结构上的不对称性一个企业级电商系统可能有数百万个产品但只有数百或数千条治理策略。percolator 只是在将单个传入的搜索字符串与这组已存储的策略模式进行匹配而不是扫描整个产品目录。其成本与策略数量成正比并且 Elasticsearch 通过内部优化例如对已存储查询模式进行索引、对布尔逻辑进行短路处理来保持匹配效率。新增一条策略只需要索引一条新文档禁用策略只需要更新一个字段无需代码修改、无需部署、无需重启。从查找到受治理的检索percolator 提供了快速的反向匹配原语使第 3 部分中的控制平面架构能够在规模上落地。策略作为数据被存储并建立索引并被高效地与传入的搜索字符串进行匹配。控制平面通过第 3 部分中描述的级联转换和按字段冲突解决机制将匹配到的策略组合成一个受治理的执行计划。随后检索引擎在产品目录上执行该执行计划。最终结果是一个系统商品运营人员可以在不接触应用代码的情况下编写新策略在代表性查询上进行测试将其发布到生产环境并立即看到效果。percolator 使策略查找变得快速控制平面使策略组合变得确定性而受治理的工作流则让整个过程变得安全。本系列下一篇本系列的下一篇文章将把受治理的控制平面扩展到新的领域。它将介绍一个多层级搜索架构解释如何在保持稳定分页和 facets 的同时协调严格检索、宽松检索和语义检索。将受治理的电商搜索付诸实践本文中描述的基于 percolator 的控制平面 —— 从索引 mapping、边界标记到基于高亮的短语追踪再到级联策略组合 —— 由 Elastic Services Engineering 构建作为可复用的电商搜索加速方案的一部分。文中的每一个查询示例和策略结构都来自经过企业级产品目录验证的真实系统。如果你想在 Elasticsearch 上实现一个受治理的、基于策略的控制平面Elastic Services 可以帮助你更快实现。联系 Elastic 专业服务团队。加入讨论对搜索治理、检索策略或电商搜索架构有疑问欢迎加入更广泛的 Elastic 社区讨论。原文https://www.elastic.co/search-labs/blog/elasticsearch-percolator-search-governance