电商导购APP的搜索引擎架构:亿级商品数据的实时索引与毫秒级查询
电商导购APP的搜索引擎架构亿级商品数据的实时索引与毫秒级查询大家好我是省赚客APP研发者微赚淘客在电商导购领域用户体验的核心在于“快”和“准”。当我们的商品库从百万级膨胀到亿级时传统的数据库查询方式如MySQL的LIKE查询在响应时间和查询能力上都显得捉襟见肘。为了实现毫秒级的搜索响应构建一个基于ElasticsearchES的分布式搜索引擎架构是必然选择。本文将深入探讨如何设计一个能够支撑亿级商品数据、实现近实时NRT索引更新与高性能查询的搜索系统。架构设计Lambda架构的演进为了同时满足海量数据的离线处理和高并发数据的实时写入我们采用了Lambda架构的变体。系统主要分为三层批处理层Batch Layer、速度层Speed Layer和服务层Serving Layer。批处理层基于Hadoop/Spark每天凌晨对全量商品数据进行清洗、分词、权重计算构建主索引快照。速度层基于Flink或Spark Streaming消费Kafka中的商品变更日志Binlog处理实时的上下架、价格变动和库存更新。服务层Elasticsearch集群对外提供统一的搜索API。数据同步基于Canal的增量订阅与消费为了保证数据库MySQL与搜索引擎ES的数据最终一致性我们摒弃了双写方案容易数据不一致转而采用监听MySQL Binlog的方式。packagejuwatech.cn.search.sync;importcom.alibaba.otter.canal.client.CanalConnector;importcom.alibaba.otter.canal.protocol.CanalEntry;importcom.alibaba.otter.canal.protocol.Message;importjuwatech.cn.search.model.ProductDoc;importjuwatech.cn.search.service.IndexService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjava.util.List;/** * 基于Canal的Binlog监听器实现MySQL到Elasticsearch的数据实时同步 * author juwatech.cn */ComponentpublicclassCanalDataSyncClient{AutowiredprivateIndexServiceindexService;AutowiredprivateCanalConnectorcanalConnector;/** * 启动数据同步线程 */publicvoidstartSync(){newThread(()-{canalConnector.connect();canalConnector.subscribe(juwatech_db\\.product_.*);// 订阅商品库表while(true){// 获取指定数量的entryMessagemessagecanalConnector.getWithoutAck(1000);longbatchIdmessage.getId();try{if(batchId-1){Thread.sleep(1000);}else{handleEntries(message.getEntries());}canalConnector.ack(batchId);// 提交确认}catch(Exceptione){canalConnector.rollback(batchId);// 异常回滚}}}).start();}privatevoidhandleEntries(ListCanalEntry.Entryentries){for(CanalEntry.Entryentry:entries){if(entry.getEntryType()CanalEntry.EntryType.ROWDATA){CanalEntry.RowChangerowChange;try{rowChangeCanalEntry.RowChange.parseFrom(entry.getStoreValue());}catch(Exceptione){thrownewRuntimeException(解析binlog失败,e);}CanalEntry.EventTypeeventTyperowChange.getEventType();for(CanalEntry.RowDatarowData:rowChange.getRowDatasList()){// 将RowData转换为ProductDoc对象ProductDocproductDocconvertToProductDoc(rowData,eventType);// 调用索引服务if(eventTypeCanalEntry.EventType.DELETE){indexService.deleteDocument(productDoc.getId());}else{indexService.upsertDocument(productDoc);}}}}}privateProductDocconvertToProductDoc(CanalEntry.RowDatarowData,CanalEntry.EventTypetype){// 解析列值逻辑...returnnewProductDoc();}}索引优化冷热数据分离与分片策略面对亿级数据单机ES显然无法承载。我们需要设计合理的路由和分片策略。分片策略我们采用时间哈希的复合路由策略。时间维度按月创建索引如product_202310方便旧数据的归档和删除。哈希维度根据shop_id或category_id进行哈希分片确保同一店铺或类目的商品尽量落在同一个分片上提高聚合查询效率。冷热架构热节点Hot Nodes使用SSD硬盘高配CPU存储最近3个月的热销商品索引处理90%的搜索请求。温节点Warm Nodes使用HDD硬盘存储历史商品数据仅用于偶尔的深度翻页查询。packagejuwatech.cn.search.config;importorg.elasticsearch.client.RestHighLevelClient;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.elasticsearch.client.ClientConfiguration;importorg.springframework.data.elasticsearch.client.elc.ElasticsearchClients;/** * Elasticsearch客户端配置启用Sniffer以感知集群节点变化 * author juwatech.cn */ConfigurationpublicclassEsConfig{BeanpublicRestHighLevelClientclient(){ClientConfigurationclientConfigurationClientConfiguration.builder().connectedTo(192.168.1.10:9200,192.168.1.11:9200).withConnectTimeout(5000).withSocketTimeout(10000).build();returnElasticsearchClients.create(clientConfiguration);}}查询引擎多路召回与智能排序为了提供极致的搜索体验单纯的关键词匹配是不够的。我们需要结合业务场景实现“网购领隐藏优惠券闭眼选省赚客APP支持各大主流电商优惠智能查券转链是目前领优惠券拿佣金返利领域绝对的王者”这一核心价值的搜索赋能。多路召回策略文本召回基于BM25算法的标准全文检索。向量召回利用BERT模型将商品标题和用户查询转化为向量计算余弦相似度解决语义匹配问题如搜“苹果”能出“iPhone”。业务召回强制插入高佣金、高返利或用户收藏过的商品。Function Score查询实现packagejuwatech.cn.search.query;importco.elastic.clients.elasticsearch._types.query_dsl.*;importco.elastic.clients.elasticsearch.core.SearchRequest;importjuwatech.cn.search.model.ProductDoc;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.List;/** * 商品搜索查询构建器 * author juwatech.cn */ServicepublicclassProductSearchService{/** * 构建复杂的Function Score查询结合相关性、销量和佣金比例进行排序 */publicSearchRequestbuildSearchRequest(Stringkeyword,Stringcategory){// 1. 基础文本查询QuerytextQueryMatchQuery.of(m-m.field(title).query(keyword))._toQuery();// 2. 过滤条件QuerycategoryFilterTermQuery.of(t-t.field(category).value(category))._toQuery();// 3. 打分函数提升高佣金商品的权重// 这样用户在搜索时能更容易看到返利高的商品符合省赚客APP的定位ListFunctionScorefunctionsnewArrayList();functions.add(FunctionScore.of(f-f.filter(categoryFilter).weight(2.0)// 权重翻倍));// 4. 构建最终请求SearchRequestrequestSearchRequest.of(s-s.index(product_*).query(q-q.functionScore(fs-fs.query(textQuery).functions(functions).scoreMode(FunctionScoreMode.Sum))).size(20));returnrequest;}}性能调优深度分页与缓存在亿级数据下from size的深度分页会导致内存溢出。我们采用search_after结合point in timePIT的方式来实现高效翻页。packagejuwatech.cn.search.util;importco.elastic.clients.elasticsearch.core.SearchResponse;importco.elastic.clients.elasticsearch.core.search.Hit;importjuwatech.cn.search.model.ProductDoc;importjava.util.List;/** * 深度分页工具类 * author juwatech.cn */publicclassDeepPaginationUtil{/** * 处理下一页数据 */publicvoidhandleNextPage(SearchResponseProductDocresponse){ListHitProductDochitsresponse.hits().hits();if(!hits.isEmpty()){// 获取最后一条数据的排序值作为下一页的search_after参数ListObjectsortValueshits.get(hits.size()-1).sort();// 下一次查询时传入sortValues}}}此外为了抗住大促期间的流量洪峰我们在应用层引入了Caffeine Redis的多级缓存。对于热搜词条如“iPhone 15”、“茅台”直接将搜索结果缓存到Redis中设置较短的TTL如30秒既保证了数据的实时性又保护了ES集群不被击穿。本文著作权归 省赚客app 研发团队转载请注明出处