电商搜索核心技术解析:从查询理解、构建到业务排序实战 1. 项目概述与核心痛点最近在复盘一个电商项目的检索服务重构正好做到第六章也就是最核心的查询与聚合部分。这章内容太密拆成了上、中、下三篇来写今天这篇“中篇”我们聚焦在从用户输入关键词到最终返回排序结果这个“黑盒”里到底发生了什么。很多团队在做搜索时容易陷入两个极端要么过度依赖ES/OpenSearch等引擎的默认配置结果就是召回率、精准度总差那么点意思要么就是自己从头造轮子把大量精力花在了分词、倒排索引这些底层实现上反而忽略了业务场景的适配。我这次重构的核心思路就是在这两者之间找到一个平衡点用相对可控的复杂度实现业务收益的最大化。简单来说一个电商搜索框背后远不止是“匹配关键词”那么简单。它需要理解用户的模糊意图比如“夏季连衣裙”可能隐含“透气”、“轻薄”的需求、处理复杂的商品属性品牌、型号、规格、SKU、并在一两百毫秒内从百万甚至千万级商品中找到最相关的那几十个并且按“好”的顺序排好。这个“好”字就是业务的核心可能是销量最高、可能是利润最大、也可能是平台最想推的新品。检索服务就是把这个商业目标翻译成搜索引擎能理解的查询语言和排序规则的过程。接下来我会拆解这个翻译过程的几个关键环节查询理解、查询构建、以及相关性排序的初探。2. 查询理解从关键词到用户意图用户输入搜索框的文字我们称之为“查询串”Query String。这串文字通常很短且充满歧义。查询理解Query Understanding的任务就是尽可能准确地解读这串文字背后的真实意图。2.1 查询预处理与归一化这是第一步也是最基础的一步目的是清洗和标准化原始输入。空格处理与特殊字符过滤去除首尾空格将多个连续空格合并为一个。过滤掉大多数无意义的特殊字符如!#$%但保留一些可能有语义的字符如“”、“-”、“()”这些可能在后续的查询语法中用到。对于中文我们通常直接进行分词空格处理相对次要。大小写归一化对于英文或拼音通常统一转为小写lowercase确保“iPhone”和“iphone”能被同等对待。但要注意品牌词等专有名词是否需要在特定场景下保持原样。纠错与拼写检查用户可能会输错字比如“连衣裙”打成“连衣群”。我们可以维护一个常见错别字词典或者使用开源库对于中文可以用PySpellChecker的适配或类似算法进行自动纠正。这一步的挑战在于平衡纠错的准确性和对生僻词、新词如网络流行语、新品名的包容性。同义词扩展这是提升召回率的关键。我们需要建立一个业务相关的同义词库。例如用户搜“手机”应该也能匹配到“智能手机”、“移动电话”搜“NB”应该能联想到“New Balance”这个品牌。同义词库可以是静态的人工维护也可以是动态的通过用户点击、购买日志挖掘。在查询时将原始词条及其同义词一起加入查询条件通常用OR逻辑连接。注意同义词扩展需要谨慎过度扩展会导致召回结果不相关稀释排序效果。通常我们会给原始词更高的权重同义词较低的权重。2.2 查询词权重分析与业务词典注入不是所有分词后的词条都同等重要。我们需要识别出查询中的核心实体和修饰词。词性标注与命名实体识别利用NLP工具如HanLP、LTP或jieba的简单词性标注对分词后的结果进行分析。识别出名词通常是商品类目、品牌、型号、形容词颜色、尺寸、材质等属性、动词如“防水”、“保暖”这类功能需求。例如对于“华为黑色防水智能手机”“华为”品牌、“黑色”颜色、“防水”功能、“智能手机”类目的权重和后续处理策略是不同的。业务词典优先电商搜索有强烈的领域特性。通用分词器可能把“iPhone 15 Pro Max”切分成“iPhone”、“15”、“Pro”、“Max”四个词这不利于精准匹配。我们必须将品牌词库、型号词库、品类词库等业务词典提前加载到分词器中确保“iPhone 15 Pro Max”能作为一个整体词条被识别出来。这能极大提升品牌、型号等精确查询的体验。停用词过滤过滤掉“的”、“了”、“吗”等对搜索无实质贡献的虚词。但在电商场景下要小心有些词如“新款”、“2024年”可能具有时效性筛选意义不应简单过滤。经过查询理解我们把“夏季轻薄连衣裙”这样一个查询转化成了结构化的意图表示核心类目是“连衣裙”属性要求包含“季节:夏季”和“材质风格:轻薄”并且我们可能还附上了“裙子”、“夏裙”等同义词。这个结构化的表示就是下一步构建搜索引擎查询的蓝图。3. 查询构建将意图翻译为引擎指令查询理解产出的结构化意图需要被“翻译”成搜索引擎如Elasticsearch能够执行的查询语句Query DSL。这个过程的核心是多策略查询融合。3.1 基础查询策略Match与Term的权衡搜索引擎提供了多种查询类型我们需要根据词条的性质进行选择。对核心类目、品牌、型号等精确字段使用term查询例如品牌ID、分类ID、确定的型号名称。term查询不做分词完全匹配可以保证结果的绝对精准。例如{term: {brand_id: 1001}}。对商品标题、描述等文本字段使用match查询match查询会对输入文本进行分词然后默认以OR逻辑搜索各分词。为了平衡召回率和精准度我们通常会采用match_phrase短语匹配要求分词顺序一致或给match查询设置operator:and要求所有分词都出现。例如对于“黑色手机”{match: {title: {query: 黑色手机, operator: and}}}会比默认的OR逻辑更精准。多字段查询一个用户的查询意图可能需要在多个字段中寻找。例如“华为手机”既可能在title字段也可能在brand_name字段。我们可以使用multi_match查询同时搜索多个字段并可以指定字段的权重^符号。例如{multi_match: {query: 华为, fields: [brand_name^3, title^2, keywords^1]}}这表示在品牌名字段匹配的权重最高。3.2 构建复合布尔查询Must、Should、Filter、Must_Not真实的电商搜索查询几乎都是复杂的组合。我们使用bool查询来组装。must子句必须满足的条件参与相关性算分。通常用于表达用户明确的核心需求。例如类目为“手机”并且标题中包含“华为”。should子句应该满足的条件满足的越多分数越高。常用于同义词扩展、多字段匹配。例如标题中应该包含“华为”或“HUAWEI”。should子句在bool查询中如果没有must或filter则至少需要满足一条如果存在must或filter则作为加分项。filter子句必须满足的条件但不参与相关性算分。这是性能优化的关键用于那些非文本的、确定性的筛选条件如价格区间、库存状态是否有货、商品状态是否上架、品牌、分类ID等。因为不计算分数且可以利用缓存filter的性能远高于must。务必把能放进filter的条件都放进去。must_not子句必须不满足的条件同样不参与算分。用于排除某些商品如排除已下架商品。一个典型的电商商品搜索的bool查询结构如下{ query: { bool: { filter: [ // 确定性筛选不参与算分性能好 {term: {status: ON_SALE}}, {term: {has_stock: true}}, {range: {price: {gte: 100, lte: 500}}} ], must: [ // 核心文本匹配参与算分 {match: {category_name: 智能手机}} ], should: [ // 加分项提升相关度 {match_phrase: {title: {query: 华为旗舰, slop: 2}}}, // 允许中间间隔2个词 {match: {attributes: 5G}} // 匹配属性字段 ], minimum_should_match: 1 // 在存在must时should至少满足1条才加分 } } }3.3 处理复杂场景Function Score与自定义排序当基础的文本相关性TF-IDF/BM25算法算出的分数无法满足业务排序需求时我们就需要介入修改最终得分。这就是function_score查询的用武之地。 假设我们的业务需求是在文本相关的基础上优先展示销量高、好评率高、且是新上架的商品。{ query: { function_score: { query: {...}, // 上面bool查询的全部内容 functions: [ { filter: {range: {sold_count: {gte: 100}}}, // 仅对销量大于100的商品应用此函数 weight: 2 // 权重因子 }, { field_value_factor: { // 使用字段值影响分数 field: rating, factor: 1.2, modifier: log1p // 使用log(1rating)来平滑影响避免极高评分商品分数爆炸 } }, { exp: { // 指数衰减函数用于时间因素 publish_time: { scale: 30d, // 30天衰减一半 decay: 0.5, offset: 7d // 7天内不衰减 } } } ], score_mode: sum, // 多个函数分如何组合求和 boost_mode: multiply // 函数分如何与原始查询分组合相乘 } } }通过function_score我们可以将销量、评分、上新时间等业务指标巧妙地融合到最终的排序分数中实现复杂的业务排序规则。4. 排序策略初探从相关性到业务目标查询构建解决了“找出来”的问题而排序策略要解决“怎么排”的问题。排序是电商搜索的终极战场直接关系到转化率和GMV。4.1 文本相关性排序BM25算法及其调优Elasticsearch默认使用BM25算法计算文本相关性分数。理解其核心参数对调优至关重要k1控制词频饱和度的参数。值越大词频对分数的影响越大。对于标题这类短文本词频通常不高可以适当调高k1如1.5-2.0让出现关键词的商品分数更高。对于描述这类长文本可以保持默认1.2或调低避免词频过度影响。b控制字段长度归一化的参数。值在0到1之间。设为0则禁用长度归一化长文本字段如商品详情会占便宜设为1则完全启用短文本字段如标题会占便宜。电商标题通常较短可以适当调低b如0.3-0.5削弱长度的影响让标题匹配更精准。 我们可以在索引映射mapping中为特定字段设置BM25参数{ mappings: { properties: { title: { type: text, similarity: my_bm25, fields: {...} } } }, settings: { index: { similarity: { my_bm25: { type: BM25, k1: 1.6, b: 0.4 } } } } }4.2 业务权重排序非文本因素的融合纯文本相关性的排序常常不符合业务预期。我们需要引入业务权重。除了前面提到的function_score还有更直接的方式直接按字段排序对于“按价格从低到高”、“按销量从高到低”这类明确需求可以直接在查询中使用sort参数。但要注意这完全抛弃了相关性只适用于用户明确选择了排序方式的场景。混合排序更常见的是将相关性分数与业务分数进行线性加权。例如最终得分 0.6 * 文本相关性分归一化后 0.3 * 销量分归一化后 0.1 * 新品分。这需要在应用层检索服务内部进行计算因为ES原生的function_score的boost_mode虽然灵活但进行精细的加权求和不如在应用层控制方便。个性化排序根据用户的历史行为点击、购买、浏览时长实时调整排序权重。例如对经常购买高端品牌的用户在其搜索“手机”时提高“价格”字段的权重。这需要实时用户画像和在线计算能力的支持是搜索排序的进阶领域。4.3 排序稳定性与多样性一个好的排序系统不仅要准还要“稳”和“丰富”。稳定性避免搜索结果在短时间内用户无新操作发生剧烈跳动这会让用户感到困惑。可以在计算分数时加入一个微小的随机因子或者对分数非常接近的结果如分差小于0.1保持相对顺序。多样性避免同一店铺或同一相似商品霸屏。可以在排序后处理阶段对结果列表进行重排确保前几页能展示不同品牌、不同款式、不同价位的商品给用户更多选择。这可以通过分组bucket后再在每个组内取Top N来实现但会牺牲一定的全局最优性。实操心得排序策略没有银弹必须进行A/B测试。任何权重调整、新排序因子的加入都必须通过线上小流量实验核心观察指标包括点击率CTR、转化率CVR、平均订单金额AOV、以及更宏观的GMV。切忌凭感觉调整参数。5. 性能优化与查询调试一个设计再精妙的查询如果响应时间超过500ms用户体验也是灾难性的。性能优化贯穿检索服务始终。5.1 索引设计优化查询的性能很大程度上在索引设计阶段就决定了。字段类型选择精确匹配用keyword全文检索用text并配置合适的分析器。数值范围查询用integer、float等。避免用text类型做精确匹配效率极低。索引映射优化禁用不必要的字段对于确定不需要被搜索或聚合的字段设置index: false。规范命名避免使用动态映射dynamic mapping明确指定每个字段的类型和属性防止字段爆炸。使用copy_to如果经常需要跨多个字段进行搜索如同时搜标题和副标题可以使用copy_to将这些字段的内容复制到一个组合字段中然后只对这个组合字段进行搜索减少查询条件数量。分片与副本分片数在索引创建时设定后期修改成本极高。需要根据数据总量和硬件资源预估。单个分片大小建议在20GB-50GB。副本数number_of_replicas提供高可用和读取吞吐可以根据读压力动态调整。5.2 查询DSL优化善用filter上下文如前所述将不参与算分的条件全部放入filter。filter条件会被缓存速度极快。避免深度分页from size方式的分页在深度翻页时如from10000性能损耗巨大因为需要全局排序并跳过大量结果。对于深度翻页应使用search_after参数配合上一页最后一个结果的排序值进行查询。限制返回字段使用_source过滤只返回前端渲染必需的字段减少网络传输和数据序列化开销。避免脚本查询尽可能避免在查询中使用Painless脚本脚本执行非常耗时。尽量通过索引设计如将计算好的值索引为一个字段来避免运行时脚本计算。设置查询超时使用timeout参数避免个别慢查询拖垮整个服务。5.3 调试与分析工具当查询结果不符合预期或性能不佳时需要工具来诊断。使用explainAPI在查询URL后加上?explaintrue可以返回每个文档得分的详细计算过程理解为什么这个文档被召回以及分数如何构成。这是调试相关性问题的利器。使用Profile API在查询体中设置profile: true可以获取查询执行过程中各个组件如Query、Rewrite、Collector的详细耗时精准定位性能瓶颈。使用Kibana Dev Tools提供一个交互式控制台方便地编写、测试和调试查询DSL。慢查询日志在ES集群配置中开启慢查询日志定期分析那些执行时间过长的查询进行针对性优化。6. 容错与降级策略检索服务作为核心链路必须具备高可用性。当依赖的搜索引擎出现故障或性能下降时需要有降级方案。超时与重试客户端调用检索服务以及检索服务调用搜索引擎都必须设置合理的连接超时和读取超时。对于可重试的错误如网络抖动、引擎暂时过载可以实现有间隔的指数退避重试机制。熔断与降级熔断当调用搜索引擎的失败率或慢请求比例超过阈值时熔断器打开后续请求直接失败避免雪崩。经过一段时间后进入半开状态尝试放行部分请求。降级当搜索引擎完全不可用或熔断器打开时触发降级。降级策略可以包括返回缓存结果如果之前对热门查询结果有缓存注意缓存时效性可以返回缓存数据。返回简化结果切换到备用数据源如数据库执行简单的SQL查询只返回最基本的商品信息ID、标题、主图、价格并明确提示用户“当前为简化搜索模式”。返回空结果并友好提示这是最后的选择比返回错误页面或长时间等待要好。限流在服务入口对请求进行限流防止突发流量击垮搜索引擎。可以根据用户ID、IP或查询类型设置不同的限流策略。7. 监控与指标体系建设没有度量就无法优化。必须建立完善的监控体系。核心业务指标查询量QPS反映服务压力。平均响应时间RT、P95/P99响应时间衡量服务性能。错误率请求失败的比例。召回率与精准率需要离线抽样评估通过人工标注或利用点击数据近似计算衡量搜索结果的质量。用户体验指标无结果率返回结果数为0的查询占比。过高可能意味着查询理解或索引数据有问题。首位点击率用户点击第一条结果的比例反映排序效果。搜索退出率用户在搜索结果页未发生任何点击就离开的比例。系统资源指标ES集群健康状态green/yellow/red。节点CPU、内存、磁盘IO。JVM堆内存使用率与GC情况。告警对核心指标如P99 RT 1s 错误率 1% 集群状态非green设置告警确保问题能第一时间被发现。构建一个健壮、高效、智能的电商检索服务是一个持续迭代的过程。从精准的查询理解到高效的查询构建再到融合业务的智能排序每一个环节都需要紧密结合实际业务数据进行打磨和调优。这次重构让我深刻体会到搜索不仅仅是技术更是技术与商业理解的结合。在“中篇”我们搭建了核心的查询与排序框架在接下来的“下篇”我们会探讨更高级的主题聚合分析Facet实现高效的筛选导航、搜索建议Suggest与自动补全、以及基于向量检索的语义搜索和个性化推荐如何与现有系统结合让搜索体验再上一个台阶。