1. 引出问题:前端搜索累了,后端 ES 上场
最近有个球友星球提问,问了个挺常见但又有点棘手的问题。他说他们有个商品系统,商品都存在 Elasticsearch(简称 ES)里,现在要把这些商品塞进各种营销活动里。
——https://t.zsxq.com/boPTY
比如“双11大促”“618狂欢”之类的,一个活动可能涉及成千上万的商品。
之前他们的做法是把所有商品数据一股脑儿拉到前端,然后前端自己搜,想找啥找啥。
但问题来了:活动商品太多,前端扛不住了,加载慢、搜索卡,用户体验直线下降。
现在他们想改成后端用 ES 直接搜,把重活交给服务器干。
朋友还问了一句:“这种场景能用 ES 的索引 join
吗?一般咋搞?”
听完我心里一琢磨,这事儿不简单,但也不是没招儿。
咱们今天就来聊聊怎么用 ES 优雅地解决这个问题。
2. 方案探讨:怎么把商品和活动绑一块儿?
在 ES 里处理这种“商品和营销活动”的关系,得先想清楚数据咋存、咋查。
球友提到 join
,咱们先看看这招行不行,再聊聊其他招数。
用 join
行不行?
ES 确实有 join
功能,能实现类似数据库的父子关系。
干货 | Elasticsearch多表关联设计指南
比如可以建两个索引,一个存商品,一个存活动,然后用 parent-child
把它们连起来。听起来挺美,但实际用起来有坑:
慢 :
join
查询比普通查询费劲,数据多的时候性能掉得厉害。麻烦 :得提前定义好父子关系,写数据时还得多操心。
不灵活 :分布式环境下,查多了分片多,效率更低。
想象一下,一个活动几万商品,查的时候还得跨索引拼数据,服务器不得累吐血?
所以,**join
这招不太香,咱们得另想办法** 。
其他招数有哪些?
在 ES 里,这种问题一般靠“数据建模”解决,也就是调整数据存的方式,让查询更顺手。
干货 | Elasticsearch 数据建模指南
以下是几个常见的方案:
招数 1:嵌套字段(Nested Fields)
咋干 :在商品索引里加个嵌套字段,把活动信息塞进去。比如:
{"product_id": "123","name": "手机","price": 2000,"activities": [{ "activity_id": "act001", "activity_name": "双11促销" },{ "activity_id": "act002", "activity_name": "新年特惠" }] }
好处 :查的时候直接用
nested
查询,过滤出某个活动的商品,简单粗暴。坑 :活动信息变了,得更新整个商品文档,数据多的话存起来也占地方。
适合 :活动不多,关系不咋变的场景。
招数 2:反向建模(活动-商品索引)
咋干 :另起一个索引,按活动存数据,每条记录是一个活动和商品的组合。比如:
{"activity_id": "act001","activity_name": "双11促销","product_id": "123","product_name": "手机","price": 2000 }
好处 :查某个活动下的商品贼快,直接按
activity_id
过滤就行。坑 :数据会重复存(需要理解一下空间换时间),占地方,得保证写入时同步好。
适合 :活动商品巨多,查询主要看活动的场景。
招数 3:宽表模式(Flattened Data)
咋干 :把活动信息展平塞进商品索引,用数组存。比如:
{"product_id": "123","name": "手机","price": 2000,"activity_ids": ["act001", "act002"],"activity_names": ["双11促销", "新年特惠"] }
好处 :查起来简单,
terms
一句搞定,性能也不错。坑 :活动信息复杂的话不好维护,更新也麻烦。
适合 :活动信息简单,更新不频繁的场景。
3. 推荐方案:反向建模最靠谱
球友说他们有些活动商品特别多,动辄几万条,那我觉得反向建模(活动-商品索引)是最佳选择。为什么呢?
查得快 :按活动 ID 过滤,ES 天生擅长这种操作,再多商品也不怕。
扩展强 :活动规模大了也没压力,分布式环境下扛得住。
业务匹配 :从前端搜改成后端搜,正好让 ES 发挥搜索优势。
如果活动信息简单、更新少,也可以试试宽表模式 ,实现起来更省事儿。不过考虑到“商品特别多”这点,反向建模更稳。
4. 实现建议:DSL 代码实操一把
光说不练假把式,咱们直接上代码,看看咋用 ES 实现。如下内容在 Elasticsearch 8.15 版本完全验证ok!
建索引
先建一个“活动-商品”索引,映射(mapping)长这样:
PUT /activity_products
{"mappings": {"properties": {"activity_id": { "type": "keyword" },"activity_name": { "type": "text" },"product_id": { "type": "keyword" },"product_name": { "type": "text" },"price": { "type": "float" }}}
}
写数据
批量塞点数据进去:
POST /activity_products/_bulk
{ "index": {} }
{ "activity_id": "act001", "activity_name": "双11促销", "product_id": "123", "product_name": "小米手机14", "price": 3999 }
{ "index": {} }
{ "activity_id": "act002", "activity_name": "新年特惠", "product_id": "123", "product_name": "小米手机14", "price": 3999 }
{ "index": {} }
{ "activity_id": "act001", "activity_name": "双11促销", "product_id": "456", "product_name": "华为耳机Pro", "price": 499 }
{ "index": {} }
{ "activity_id": "act003", "activity_name": "618年中大促", "product_id": "456", "product_name": "华为耳机Pro", "price": 499 }
{ "index": {} }
{ "activity_id": "act001", "activity_name": "双11促销", "product_id": "789", "product_name": "苹果笔记本M2", "price": 9999 }
{ "index": {} }
{ "activity_id": "act002", "activity_name": "新年特惠", "product_id": "789", "product_name": "苹果笔记本M2", "price": 9999 }
{ "index": {} }
{ "activity_id": "act004", "activity_name": "黑色星期五", "product_id": "789", "product_name": "苹果笔记本M2", "price": 9999 }
{ "index": {} }
{ "activity_id": "act003", "activity_name": "618年中大促", "product_id": "101", "product_name": "索尼电视65寸", "price": 5999 }
{ "index": {} }
{ "activity_id": "act002", "activity_name": "新年特惠", "product_id": "101", "product_name": "索尼电视65寸", "price": 5999 }
{ "index": {} }
{ "activity_id": "act001", "activity_name": "双11促销", "product_id": "202", "product_name": "戴森吹风机", "price": 2999 }
{ "index": {} }
{ "activity_id": "act003", "activity_name": "618年中大促", "product_id": "202", "product_name": "戴森吹风机", "price": 2999 }
{ "index": {} }
{ "activity_id": "act004", "activity_name": "黑色星期五", "product_id": "202", "product_name": "戴森吹风机", "price": 2999 }
{ "index": {} }
{ "activity_id": "act002", "activity_name": "新年特惠", "product_id": "456", "product_name": "华为耳机Pro", "price": 499 }
{ "index": {} }
{ "activity_id": "act003", "activity_name": "618年中大促", "product_id": "123", "product_name": "小米手机14", "price": 3999 }
{ "index": {} }
{ "activity_id": "act004", "activity_name": "黑色星期五", "product_id": "101", "product_name": "索尼电视65寸", "price": 5999 }
查数据
想找“双11促销”里的商品,用这个 DSL:
GET /activity_products/_search
{"query": {"term": {"activity_id": "act001"}},"size": 10,"sort": [{ "price": "asc" }]
}
返回结果会按价格排序,最多 10 条。
分页优化
商品太多咋办?用 search_after
分页:
GET /activity_products/_search
{
"query": {"term": {"activity_id": "act001"}},
"size": 10,
"sort": [{ "price": "asc" },{ "product_id": "asc" }],
"search_after": [2000, "123"]
}
search_after
用上一页的最后一条记录定位下一页,效率比传统 from/size
高。
干货 | 全方位深度解读 Elasticsearch 分页查询
额外小贴士
缓存 :热门活动查得多?加个 Redis 缓存,少烦 ES。
精简字段 :索引里只存关键信息,其他细节可以用商品 ID 再查数据库。
5.小结
后端ES搜起来,省心又高效这事儿总结下来,ES的join
不太适合这种“活动商品多”的场景。
更好的办法是调整数据模型,推荐用** 反向建模建一个“活动-商品”索引,查询快、扩展强。如果活动简单,也可以试试 宽表模式**。
具体咋选,看你数据量和业务需求。
大家看完估计能松口气了,前端搜索的担子终于可以卸给后端 ES。
想试试代码或者有啥细节问题,欢迎随时留言交流。
《一本书讲透 Elasticsearch》被清华、北大等多所知名高校图书馆收录
干货 | Elasticsearch 数据建模指南
Elasticsearch Nested 选型,先看这一篇!
干货 | 拆解一个 Elasticsearch Nested 类型复杂查询问题
更短时间更快习得更多干货!
和全球超2000+ Elastic 爱好者一起精进!
elastic6.cn——ElasticStack进阶助手
抢先一步学习进阶干货!