一、ElasticSearch回顾与应用场景
1.1 ElasticSearch简介
ElasticSearch(简称ES)是一个分布式、RESTful风格的搜索和数据分析引擎,采用Java开发,是当前最流行的开源企业级搜索引擎。它具有近实时搜索、稳定、可靠、快速、安装使用方便等特点,支持Java、.NET、PHP、Python、Ruby等多种语言的客户端。
官方网站:https://www.elastic.co/
1.2 主要应用场景
站内搜索
日志管理与分析
大数据分析
应用性能监控
机器学习
1.3 业务场景:图灵商城商品搜索
在实际电商项目中,商品搜索需要支持多种查询条件:
根据关键字查询
根据品牌查询
商品类别筛选
商品属性信息筛选
价格区间筛选
是否有库存
多种排序方式(销量、价格、上架时间等)
二、文档建模与索引设计
2.1 商品文档结构分析
从文档中的示例数据可以看出,商品文档包含以下字段:
商品基本信息:
id:商品ID
name:商品名称
keywords:关键词
subTitle:副标题
price:价格
promotionPrice:促销价
originalPrice:原价
pic:图片地址
销售信息:
sale:销量
hasStock:是否有库存
salecount:销售数量
putawayDate:上架日期
品牌分类信息:
brandId:品牌ID
brandName:品牌名称
brandImg:品牌图片
categoryId:分类ID
categoryName:分类名称
商品属性:
attrs:属性数组,包含attrId、attrName、attrValue
2.2 建模分析
分词字段处理:
name、keywords、subTitle字段需要使用中文分词器(ik_max_word)
精确匹配字段:
categoryName、brandName等字段类型设置为keyword
关联关系处理:
商品属性attrs采用nested类型,因为属性与商品存在关联关系且不频繁更新
2.3 索引映射定义
json
{ "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text", "analyzer": "ik_max_word" }, "keywords": { "type": "text", "analyzer": "ik_max_word" }, "subTitle": { "type": "text", "analyzer": "ik_max_word" }, "salecount": { "type": "long" }, "putawayDate": { "type": "date" }, "price": { "type": "double" }, "promotionPrice": { "type": "keyword" }, "originalPrice": { "type": "keyword" }, "pic": { "type": "keyword" }, "sale": { "type": "long" }, "hasStock": { "type": "boolean" }, "brandId": { "type": "long" }, "brandName": { "type": "keyword" }, "brandImg": { "type": "keyword" }, "categoryId": { "type": "long" }, "categoryName": { "type": "keyword" }, "attrs": { "type": "nested", "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword" }, "attrValue": { "type": "keyword" } } } } } }2.4 索引文档与数据同步
文档中展示了多个商品数据的索引示例,数据同步可以使用canal等工具实现。以下是部分示例数据:
json
PUT /product_db/_doc/1 { "id": "26", "name": "小米 11 手机", "keywords": "小米手机", "subTitle": "AI智慧全面屏 6GB +64GB 亮黑色 全网通版 移动联通电信4G手机 双卡双待", "price": "3999", "promotionPrice": "2999", "originalPrice": "5999", "pic": "http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20180615/xiaomi.jpg", "sale": 999, "hasStock": true, "salecount": 999, "putawayDate": "2021-04-01", "brandId": 6, "brandName": "小米", "brandImg": "http://macro-oss.oss-cn-shenzhen.aliyuncs.com/mall/images/20190129/1e34aef2a409119018a4c6258e39ecfb_222_222.png", "categoryId": 19, "categoryName": "手机通讯", "attrs": [ { "attrId": 1, "attrName": "cpu", "attrValue": "2核" }, { "attrId": 2, "attrName": "颜色", "attrValue": "黑色" } ] }三、DSL查询语句构建
3.1 基础搜索查询
文档中提供了两种查询示例:
示例1:基础条件查询
json
POST /product_db/_doc/_search { "from": 0, "size": 8, "query": { "bool": { "must": [ { "match": { "name": { "query": "手机" } } } ] } }, "filter": [ { "term": { "hasStock": { "value": true } } }, { "range": { "price": { "from": "1", "to": "5000" } } } ], "sort": [{ "salecount": { "order": "asc" } }] }示例2:多字段搜索与聚合
json
GET product_db/_search { "from": 0, "size": 20, "query": { "bool": { "must": [ { "multi_match": { "query": "手机", "fields": ["name", "keywords", "subTitle"] } } ], "filter": [ { "term": { "hasStock": "true" } }, { "range": { "price": { "gte": 2000, "lte": 5000 } } } ] } } }3.2 聚合分析
搜索查询中包含了多种聚合分析:
品牌聚合:按brandId分组,统计品牌信息
分类聚合:按categoryId分组,统计分类信息
属性聚合:使用nested类型对商品属性进行聚合分析
高亮显示:对匹配的关键词进行高亮标记
四、Java代码实现商品搜索功能
4.1 环境准备
引入Spring Boot Elasticsearch依赖:
xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
4.2 核心搜索服务实现
4.2.1 主搜索方法
java
@Override public ESResponseResult search(ESRequestParam param) { try { // 1、构建检索对象-封装请求相关参数信息 SearchRequest searchRequest = startBuildRequestParam(param); // 2、进行检索操作 SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); // 3、分析响应数据,封装成指定的格式 ESResponseResult responseResult = startBuildResponseResult(response, param); return responseResult; } catch (Exception e) { e.printStackTrace(); } return null; }4.2.2 请求参数构建
java
private SearchRequest startBuildRequestParam(ESRequestParam param) { SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); // 1、关键字查询 if (!StringUtils.isEmpty(param.getKeyword())) { boolQueryBuilder.must(QueryBuilders.multiMatchQuery( param.getKeyword(), "name", "keywords", "subTitle")); } // 2、根据类目ID过滤 if (null != param.getCategoryId()) { boolQueryBuilder.filter(QueryBuilders.termQuery("categoryId", param.getCategoryId())); } // 3、根据品牌ID过滤 if (null != param.getBrandId() && param.getBrandId().size() > 0) { boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId", param.getBrandId())); } // 4、根据属性过滤 if (param.getAttrs() != null && param.getAttrs().size() > 0) { param.getAttrs().forEach(item -> { String[] s = item.split("_"); String attrId = s[0]; String[] attrValues = s[1].split(":"); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId)); boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues)); NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery( "attrs", boolQuery, ScoreMode.None); boolQueryBuilder.filter(nestedQueryBuilder); }); } // 5、是否有库存过滤 if (null != param.getHasStock()) { boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1)); } // 6、价格区间过滤 if (!StringUtils.isEmpty(param.getPrice())) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("price"); String[] price = param.getPrice().split("_"); if (price.length == 2) { rangeQueryBuilder.gte(price[0]).lte(price[1]); } else if (price.length == 1) { if (param.getPrice().startsWith("_")) { rangeQueryBuilder.lte(price[1]); } if (param.getPrice().endsWith("_")) { rangeQueryBuilder.gte(price[0]); } } boolQueryBuilder.filter(rangeQueryBuilder); } searchSourceBuilder.query(boolQueryBuilder); // 排序处理 if (!StringUtils.isEmpty(param.getSort())) { String sort = param.getSort(); String[] sortFields = sort.split("_"); if (!StringUtils.isEmpty(sortFields[0])) { SortOrder sortOrder = "asc".equalsIgnoreCase(sortFields[1]) ? SortOrder.ASC : SortOrder.DESC; searchSourceBuilder.sort(sortFields[0], sortOrder); } } // 分页处理 searchSourceBuilder.from((param.getPageNum() - 1) * SearchConstant.PAGE_SIZE); searchSourceBuilder.size(SearchConstant.PAGE_SIZE); // 高亮显示 if (!StringUtils.isEmpty(param.getKeyword())) { HighlightBuilder highlightBuilder = new HighlightBuilder(); highlightBuilder.field("name"); highlightBuilder.preTags("<b style='color:red'>"); highlightBuilder.postTags("</b>"); searchSourceBuilder.highlighter(highlightBuilder); } // 聚合分析 // 品牌聚合 TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg"); brand_agg.field("brandId").size(50); brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg") .field("brandName").size(1)); brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg") .field("brandImg").size(1)); searchSourceBuilder.aggregation(brand_agg); // 分类聚合 TermsAggregationBuilder category_agg = AggregationBuilders.terms("category_agg"); category_agg.field("categoryId").size(50); category_agg.subAggregation(AggregationBuilders.terms("category_name_agg") .field("categoryName").size(1)); searchSourceBuilder.aggregation(category_agg); // 属性聚合 NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs"); TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg") .field("attrs.attrId"); attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg") .field("attrs.attrName")); attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg") .field("attrs.attrValue")); attr_agg.subAggregation(attr_id_agg); searchSourceBuilder.aggregation(attr_agg); return new SearchRequest(new String[]{"product_db"}, searchSourceBuilder); }4.2.3 响应结果处理
java
private ESResponseResult startBuildResponseResult(SearchResponse response, ESRequestParam param) { ESResponseResult result = new ESResponseResult(); // 1、获取商品信息 SearchHit[] hits = response.getHits().getHits(); List<ESResponseResult.ProductVo> productVos = new ArrayList<>(); for (SearchHit hit : hits) { ESResponseResult.ProductVo productVo = new ESResponseResult.ProductVo(); String sourceAsString = hit.getSourceAsString(); productVo = JSON.parseObject(sourceAsString, ESResponseResult.ProductVo.class); // 设置高亮 if (hit.getHighlightFields().get("name") != null) { String name = hit.getHighlightFields().get("name").getFragments()[0].string(); productVo.setName(name); } productVos.add(productVo); } result.setProducts(productVos); // 2、获取品牌聚合信息 ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg"); List<ESResponseResult.BrandVo> brandVos = new ArrayList<>(); for (Terms.Bucket bucket : brandAgg.getBuckets()) { ESResponseResult.BrandVo brandVo = new ESResponseResult.BrandVo(); brandVo.setBrandId(Long.parseLong(bucket.getKeyAsString())); ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg"); brandVo.setBrandName(brandNameAgg.getBuckets().get(0).getKeyAsString()); ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg"); brandVo.setBrandImg(brandImgAgg.getBuckets().get(0).getKeyAsString()); brandVos.add(brandVo); } result.setBrands(brandVos); // 3、获取分类聚合信息 ParsedLongTerms categoryAgg = response.getAggregations().get("category_agg"); List<ESResponseResult.CategoryVo> categoryVos = new ArrayList<>(); for (Terms.Bucket bucket : categoryAgg.getBuckets()) { ESResponseResult.CategoryVo categoryVo = new ESResponseResult.CategoryVo(); categoryVo.setCategoryId(Long.parseLong(bucket.getKeyAsString())); ParsedStringTerms categoryNameAgg = bucket.getAggregations().get("category_name_agg"); categoryVo.setCategoryName(categoryNameAgg.getBuckets().get(0).getKeyAsString()); categoryVos.add(categoryVo); } result.setCategories(categoryVos); // 4、获取属性聚合信息 List<ESResponseResult.AttrVo> attrVos = new ArrayList<>(); ParsedNested attrsAgg = response.getAggregations().get("attr_agg"); ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg"); for (Terms.Bucket bucket : attrIdAgg.getBuckets()) { ESResponseResult.AttrVo attrVo = new ESResponseResult.AttrVo(); attrVo.setAttrId(bucket.getKeyAsNumber().longValue()); ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg"); attrVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString()); ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg"); List<String> attrValues = attrValueAgg.getBuckets().stream() .map(item -> item.getKeyAsString()) .collect(Collectors.toList()); attrVo.setAttrValue(attrValues); attrVos.add(attrVo); } result.setAttrs(attrVos); // 5、分页信息 result.setPageNum(param.getPageNum()); long total = response.getHits().getTotalHits().value; result.setTotal(total); int totalPages = (int) total % SearchConstant.PAGE_SIZE == 0 ? (int) total / SearchConstant.PAGE_SIZE : ((int) total / SearchConstant.PAGE_SIZE + 1); result.setTotalPages(totalPages); List<Integer> pageNaws = new ArrayList<>(); for (int i = 1; i <= totalPages; i++) { pageNaws.add(i); } result.setPageNaws(pageNaws); return result; }4.3 测试示例
文档中提供了测试URL示例:
text
http://localhost:8054/searchList?price=1_5000&keyword=手机&sort=salecount_asc&hasStock=1&pageNum=1&pageSize=20&categoryId=19&attrs=2_蓝色&attrs=1_2核
五、总结
通过本文的完整解析,我们了解了电商项目中ElasticSearch高性能搜索的完整实现流程:
数据建模:合理设计文档结构,区分需要分词的字段和精确匹配的字段
索引设计:使用合适的字段类型,对于关联数据使用nested类型
查询构建:灵活运用bool查询、多字段匹配、范围查询等DSL语法
聚合分析:实现品牌、分类、属性等多维度聚合统计
Java集成:通过RestHighLevelClient实现完整的搜索服务
这种架构设计能够支持电商平台复杂的产品搜索需求,提供高性能、高可用的搜索服务,为用户提供良好的购物体验。