最近复盘自己电商项目的商品搜索模块,并利用AI模拟了一些深度问题,以下为问题梳理。
(Q:问题;A:标准回答;M:我最开始的答案的问题)
Q1:介绍一下实现的商品搜索模块整体流程,从用户输入搜索关键词开始,到最终返回商品列表,中间经过了哪些步骤?为什么不用 MySQL 的 like 查询,而选择 Elasticsearch?
A:
商品搜索模块主要基于Elasticsearch实现。用户输入关键词后,请求进入搜索服务,ES根据配置的分词器对关键词进行分析,然后利用倒排索引快速定位相关商品文档,同时支持按照价格、销量、新品等条件排序,并返回商品列表和筛选条件。
相比MySQL,MySQL更适合结构化数据查询,虽然有B+树索引优化,但是对于商品名称这种全文搜索场景,需要支持分词、关键词匹配以及相关度排序,而ES基于倒排索引,更适合这种搜索业务。
M:
我之前对这个问题的回答存在两处明显漏洞,理解不够精准,表述也不够专业:
第一处错误:我模糊说“业务层对关键词分词”,这个说法是不准确的。真正的流程里,分词不是业务代码完成的,用户输入关键词后,是ES根据字段配置的IK分词器,在内部搜索分析阶段完成分词、词条匹配,业务层只负责调用ES接口,不参与分词逻辑,之前混淆了业务层和ES的职责边界。
第二处严重缺失:完全没有提及ES的存储数据细节。面试官一定会追问ES索引存储的数据内容,这是搜索模块的核心关键点。我之前的回答只讲了查询流程,没说明ES会预存商品id、名称、分类、品牌、价格、销量等搜索所需字段,搜索命中后再按需查MySQL补全详情,这是MySQL和ES分工的关键。
第三处表述不严谨:我之前错误提及MySQL是“从上到下一条条搜索”,这个说法很容易扣分。MySQL是有B+树索引的,并非全表扫描,正确的逻辑是MySQL不擅长分词匹配、相关度排序这类全文检索场景,而非单纯查询速度慢。
Q2:你的商品数据是如何同步到 Elasticsearch 的?比如商品新增、修改、删除后,ES里面的数据如何保证和 MySQL 保持一致?
A:
商品数据采用MySQL和Elasticsearch双写同步。管理员操作商品时,首先修改MySQL中的商品数据,然后调用搜索服务同步ES。同步过程中会把数据库中的商品详情对象转换成ES专用的搜索实体,只保存搜索需要的字段。新增和修改商品使用save更新ES文档,商品下架或者删除时根据商品id删除ES中的文档。这样保证MySQL作为业务数据源,ES作为搜索数据源。
M:
我之前的回答有一个致命错误,完全不符合项目实战逻辑:
我之前错误认为商品修改后需要清空ES全部数据再重新同步,这是非常低级的认知错误。真实项目中如果商品数据量达到几十万、上百万,每次单商品修改就清空全量ES数据同步,会造成ES写入压力爆炸、服务卡顿、数据延迟极高,完全不可用。
结合我的项目源码,正确的同步逻辑是精准单条数据同步:新增/修改商品时,先更新MySQL,再调用syncGoodsToES()方法,将单条商品数据转为GoodsES实体,通过save方法单条更新ES;删除/下架商品时,根据商品id单条删除ES文档,不会批量清空数据。
另外我之前随口提到“消息数据同步”,这也是不严谨的。我的项目中没有使用MQ消息队列,主动提及会被面试官追问细节,答不上来直接扣分,后续回答必须贴合自身项目,不额外延伸未用到的技术。
Q3:你的商品搜索使用了 Elasticsearch,你为什么要设计 单独的 GoodsES 实体类,而不是直接把数据库里的 Goods 实体类存入 Elasticsearch?
A:
MySQL的Goods实体是面向业务存储设计的,只保存商品主表字段;而ES是面向搜索场景,需要品牌、分类、规格等多表的冗余字段。
所以单独设计GoodsES实体,同步时把多张表的数据组装聚合,转换成搜索需要的结构存入ES,提升检索效率。
M:
我之前的回答核心方向对了,但理由存在明显偏差,解释得很不专业:
我之前错误说“ES无法直接识别前端JSON数据”,这是错误认知。ES本身就是基于JSON格式存储数据,完全可以识别Java序列化后的JSON对象,不存在识别不了的问题。
真正的核心原因是实体设计的场景不同:MySQL的Goods实体是为业务存储设计的,字段精简,只保留核心业务字段;而GoodsES是纯搜索场景设计的实体,需要聚合品牌名称、分类名称、规格参数、搜索标签等多表冗余字段,专门适配全文检索、筛选、聚合的需求,所以必须单独封装实体,适配搜索业务。
Q4:你的ES索引中,商品名称 goodsName 使用了 IK 分词器,而品牌 brand 使用了 keyword 类型,为什么这样设计?为什么不能所有字段都使用分词?
A:
商品名称goodsName是全文检索场景,用户输入关键词,需要IK分词拆分词语,依靠倒排索引做模糊匹配,所以用text+IK分词。
brand是筛选过滤条件,需要完整精确匹配,不能分词拆分,因此用keyword类型。
如果全部字段都分词,像品牌这类筛选字段会被切分,造成误匹配,筛选结果出错。
M:
这道题我的标准答案逻辑是通顺的,结合我的项目索引设计,核心逻辑完全匹配:
我项目中goodsName采用text类型+IK分词器,专门用于商品名称的全文模糊检索;brand采用keyword类型,完整保留品牌字符串,实现精准筛选过滤。所有字段不能统一分词,否则精准筛选字段会被拆分,导致搜索结果错乱,这部分知识点我已掌握,无核心漏洞。
Q5:你的商品搜索支持价格区间查询,比如最低价和最高价。你在ES中是怎么实现价格范围查询的?为什么价格字段不适合使用text类型?
A:
商品价格查询属于范围查询,不是关键词匹配。用户传入最低价和最高价后,我会构造ES的RangeQuery,如果有最低价使用gte条件,如果有最高价使用lte条件,对price字段进行过滤。
价格字段不能使用text,因为text主要用于全文检索,会进行分词,而价格需要支持范围比较和排序,所以应该使用数值类型。
M:
我之前的回答存在表述错误和要点缺失的问题:
第一处错误:我之前说“先检索出最高价和最低价”,完全偏离业务逻辑。实际业务中,是用户主动传入最低价、最高价参数,我的代码直接构造RangeQuery,通过gte、lte匹配价格区间,不是程序先查询价格极值。
第二处缺失要点:没有讲透价格不用text类型的核心原因。text类型会对内容分词,只适用于文本检索,不支持大小比较、区间查询、排序;而价格需要频繁做区间筛选和排序,必须用double等数值类型,这也是我项目中price字段设计为double类型的核心原因。
Q6:做商品搜索时,页面要展示当前搜索结果对应的品牌、分类筛选项。为什么不查MySQL,而是用ES聚合来获取这些筛选维度?
A:
因为筛选条件是当前搜索命中商品的子集维度。
ES支持聚合,直接在搜索结果内部统计出品牌、分类、规格,不需要二次查询数据库,减少数据库IO(输入输出),响应更快。
如果去数据库查,需要拿到ES返回的商品id,再去MySQL做in查询,会增加数据库压力,性能较差。
M:
我之前的回答结论正确,但对底层逻辑的理解不够透彻,没有讲清楚ES聚合的核心优势:
ES聚合的核心是查询时同步统计,无需将数据加载到Java内存处理。比如搜索“手机”,ES在返回商品列表的同时,会自动对命中的文档做分组统计,筛选出当前结果对应的品牌、分类,只统计本次搜索的子集数据,而非全量数据库数据。
如果改用MySQL查询,需要先获取所有命中商品ID,再通过in查询数据库,多一次DB请求,大幅增加数据库IO压力。ES聚合全程在搜索引擎内部完成,性能优势非常明显,这也是项目选用ES聚合做筛选维度的核心原因。
Q7:你的搜索服务对外提供了接口,如果有一天线上发现搜索响应非常慢,接口超时率明显上升,你从何入手排查?请说出你的排查思路和可能的原因,不限于ES本身。
A:
我会先从日志看请求参数是否异常,是不是由于输入了特殊字符,或者是用户输入的字段过长导致的响应变慢;
然后看ES的监控,重点看看当前ES的CPU、内存、查询响应时间。如果 CPU 飙高,说明查询太重,可能是 查询语句写得不合理,或者索引数据量太大,没有做分页限制 的原因;
如果ES正常,再看下游的MySQL和Redis,查 MySQL的慢查询日志、Redis的响应时间(超时);
最后检查网络和中间件。是不是网络抖动,或者Nacos/网关转发有延迟。
按这个顺序逐步缩小范围。
M:
我之前的回答思路是对的,但缺乏工程化排查思维,排查逻辑不够清晰、优先级混乱:
正确的线上排查核心是从最简单、最高效的维度开始,逐层缩小范围:
1. 优先排查请求本身:看日志区分是个别请求慢还是全部请求慢,排查是否存在超长关键词、特殊字符导致解析异常;
2. 排查核心服务ES:查看监控CPU、内存、响应时间,判断是否存在查询语句不合理、无分页、未使用缓存等问题;
3. 排查下游依赖:ES无异常时,检查MySQL慢查询、Redis超时等问题;
4. 最后排查网络、网关、服务GC等底层问题。
我之前只是罗列了排查点,没有明确优先级,真实线上排查必须遵循「先简单后复杂、先业务后底层」的原则,避免盲目排查。
Q8:你的搜索接口支持按销量和价格排序。如果用户要求“按价格从低到高”排序,ES内部是如何实现的?这种排序对性能有什么影响?如果价格字段数据量很大(比如上千万商品),排序会变慢吗?为什么?你怎么优化?
A:
ES 搜索是基于倒排索引实现的,而排序依赖 doc values(正排索引),ES 会把命中的商品(的价格)全部加载到内存里,进行全局排序。
如果对字段数据量非常大的做排序,会非常吃内存和 IO,可能导致查询变慢甚至 OOM。因为ES排序不是免费的,数据量大了会慢,深分页会慢,对text字段排序会慢。
优化手段包括:限制分页深度、用更轻量的排序方式(比如 search_after)、设计索引时避免对 text 字段排序。最笨最贵的优化方式就是加内存。
M:
我之前的回答只记住了结论,但对底层原理理解模糊,存在概念混淆:
第一,我混淆了排序和聚合的内存逻辑,二者都依赖doc values,但排序需要将所有命中文档的排序字段值全部加载到内存,内存消耗远大于聚合,数据量大时极易触发OOM内存溢出;
第二,我对深分页的原理理解不深,项目中用from+size分页,from值越大,ES需要遍历的数据越多,排序性能指数级下降;
第三,没有讲透字段类型的重要性,text字段无法用于排序,只有数值、keyword类型支持排序,这是索引设计的基础规范,也是我之前的知识盲区。
整体来说,我只会调用ES排序API,但不了解底层执行逻辑,后续需要重点吃透ES排序、分页的底层原理,应对深度提问。
Q9:你搜索商品的时候,如果用户输入“手机”关键词,能正常返回结果。但如果输入“手机 华为 5000”这种包含价格和品牌混合的搜索词,你的搜索接口能正确处理吗?如果不能,问题出在哪里?你会怎么设计来支持这种“自然语言混合搜索”?
A:
用户输入的搜索词需要先做意图识别,拆分成不同维度的条件,而不是直接全部交给分词器。我会把输入词按空格拆分,分别判断:如果包含“华为”这种品牌词,走品牌过滤;如果包含“5000”这种数字,走价格范围过滤;剩余部分走商品名称的全文搜索。然后组合成ES的bool查询,把must和filter拼在一起,这样才能做到精准搜索。
M:
我之前的回答给出了解决方案,但没有讲透核心问题根源,回答不够完整:
核心问题是:IK分词器只能处理文本词条,无法识别数值、品牌这类特殊语义。像“手机 华为 5000”这种混合搜索词,如果直接交给分词器,会把价格数字“5000”当成普通文本关键词匹配,而非价格区间条件,导致搜索结果错乱,出现名称含5000但价格超标的无效商品。
同时我只说了拆分、组合查询,没有补充具体实现方式。完善的方案需要通过正则识别数字、自定义品牌词典识别品牌词,拆分出文本、品牌、价格三类条件,再组合bool查询,精准匹配搜索需求,这部分细节是我之前缺失的。