如何使用Elasticsearch实现电商平台商品精准搜索
你遇到的匹配错误和multi_match语法本身没关系,核心是当前用的普通object类型处理数组时,会把所有子对象的同名字段打平成多值字段,丢失字段和所属对象的绑定关系。
比如示例里的第二个文档,写入ES后availability字段实际会被处理成下面这种扁平化结构:
availability.city: ["dublin", "london", "dublin"] availability.category: ["book", "book", "dvd"] availability.types: ["one book", "compilation", "one book", "compilation", "disk"]
ES根本不知道dvd这个分类属于哪个城市的可售条目,只要文档里同时出现过dublin和dvd两个词就会被判定匹配,甚至会出现「伦敦地区的DVD+都柏林地区的图书」被错误判定为「都柏林可售DVD」的问题。之前搜索时第一个无DVD的文档相关性更高,只是BM25算法对短字段的权重倾斜导致的分数偏差,本质是数据结构设计不符合关联匹配的要求。
你提到可以任意调整索引结构,优先选实现简单、查询性能高的方案,不建议一开始就上复杂的嵌套查询增加维护成本。
方案1:按城市可售SKU粒度打平文档(优先推荐)
既然搜索时目标城市是已知的传入参数,不需要从搜索词里解析,完全没必要把所有城市的可售信息塞进同一个商品文档。直接把每个城市下的每个可售选项拆成独立的索引文档即可,参考结构:
[ { "product_id": 1001, "product_name": "lord of rings", "seller": "Home Production", "city": "dublin", "category": "book", "type": "one book" }, { "product_id": 1001, "product_name": "lord of rings", "seller": "Home Production", "city": "dublin", "category": "book", "type": "compilation" }, { "product_id": 1001, "product_name": "lord of rings", "seller": "Home Production", "city": "london", "category": "book", "type": "one book" }, { "product_id": 1002, "product_name": "lord of rings", "seller": "Some", "city": "dublin", "category": "dvd", "type": "disk" } ]
查询逻辑非常简单:
- 第一步用
term查询做硬过滤,直接筛掉所有非目标城市的文档,这部分数据完全不参与相关性计算 - 第二步对剩下的文档用
match查询匹配商品名、分类、类型等文本字段即可
这种结构完全不会出现跨属性匹配的问题,查询性能比嵌套查询高30%以上,相关性计算也不会出现偏差。搜索lord of rings dvd in dublin时,只有第二个卖家的DVD条目会同时满足「城市是都柏林」+「分类匹配dvd」的条件,自然会排在正确的位置。如果需要返回商品维度的去重结果,加个collapse参数按product_id折叠就行,不需要额外处理。
方案2:保留单商品单文档结构,使用nested嵌套类型
如果你业务上必须把同一个商品的所有信息存在单个文档里,就把availability字段的类型改成nested。nested类型会把数组里的每个子对象存成独立的隐藏文档,保证子对象内部的字段绑定关系不会丢失。
首先定义mapping时指定字段类型:
{ "mappings": { "properties": { "name": {"type": "text"}, "seller": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, "availability": { "type": "nested", "properties": { "city": {"type": "keyword"}, "category": {"type": "text"}, "types": {"type": "text"} } } } } }
查询时必须用nested查询包裹可售属性的匹配条件,把城市过滤、分类、类型匹配放在同一个nested查询上下文中,确保匹配的是同一个可售条目下的属性:
{ "query": { "bool": { "must": [ {"match": {"name": "lord of the rings dvd"}} ], "filter": [ { "nested": { "path": "availability", "query": { "bool": { "must": [ {"term": {"availability.city": "dublin"}} ] } } } } ] } } }
这个方案可以实现需求,但查询性能比打平文档的方案差,后续做相关性调优、聚合统计的复杂度也更高,不是优先选项。
不要尝试用字符串拼接、copy_to等方式绕开结构问题,比如把city、category、type拼成一个长字符串做匹配,后续只要新增筛选维度就要改同步逻辑,维护成本极高,稳定性远不如上面两个方案。
内容的提问来源于stack exchange,提问作者terlimbombom

