)
十六、Java RestHighLevelClient 初始化ES 官方提供了 Java 客户端本课程使用的是 7.12.1 对应的 High Level Rest Client。1. 引入依赖dependency groupIdorg.elasticsearch.client/groupId artifactIdelasticsearch-rest-high-level-client/artifactId /dependency2. 指定 ES 版本Spring Boot 默认可能使用 7.17.x 版本因此需要覆盖版本properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target elasticsearch.version7.12.1/elasticsearch.version /properties3. 初始化客户端RestHighLevelClient client new RestHighLevelClient( RestClient.builder(HttpHost.create(http://192.168.150.101:9200)) );单元测试模板public class IndexTest { private RestHighLevelClient client; BeforeEach void setUp() { this.client new RestHighLevelClient(RestClient.builder( HttpHost.create(http://192.168.150.101:9200) )); } Test void testConnect() { System.out.println(client); } AfterEach void tearDown() throws IOException { this.client.close(); } }十七、Java API 操作索引库1. 创建索引库Test void testCreateIndex() throws IOException { CreateIndexRequest request new CreateIndexRequest(items); request.source(MAPPING_TEMPLATE, XContentType.JSON); client.indices().create(request, RequestOptions.DEFAULT); }其中MAPPING_TEMPLATE就是 Mapping 的 JSON 字符串。2. 删除索引库Test void testDeleteIndex() throws IOException { DeleteIndexRequest request new DeleteIndexRequest(items); client.indices().delete(request, RequestOptions.DEFAULT); }3. 判断索引库是否存在Test void testExistsIndex() throws IOException { GetIndexRequest request new GetIndexRequest(items); boolean exists client.indices().exists(request, RequestOptions.DEFAULT); System.out.println(exists ? 索引库已经存在 : 索引库不存在); }4. 索引库操作步骤总结Java API 操作索引库一般分为三步创建 Request 对象准备请求参数通过client.indices()发送请求。十八、Java API 操作文档1. 定义 ItemDoc 实体类由于 ES 索引库字段和数据库表字段不一定完全一致所以需要定义一个专门用于 ES 文档的实体类。Data ApiModel(description 索引库实体) public class ItemDoc { ApiModelProperty(商品id) private String id; ApiModelProperty(商品名称) private String name; ApiModelProperty(价格单位为分) private Integer price; ApiModelProperty(商品图片) private String image; ApiModelProperty(类目名称) private String category; ApiModelProperty(品牌名称) private String brand; ApiModelProperty(销量) private Integer sold; ApiModelProperty(评论数) private Integer commentCount; ApiModelProperty(是否是推广广告) private Boolean isAD; ApiModelProperty(更新时间) private LocalDateTime updateTime; }2. 新增文档Test void testAddDocument() throws IOException { Item item itemService.getById(100002644680L); ItemDoc itemDoc BeanUtil.copyProperties(item, ItemDoc.class); String json JSONUtil.toJsonStr(itemDoc); IndexRequest request new IndexRequest(items).id(itemDoc.getId()); request.source(json, XContentType.JSON); client.index(request, RequestOptions.DEFAULT); }3. 查询文档Test void testGetDocumentById() throws IOException { GetRequest request new GetRequest(items).id(100002644680); GetResponse response client.get(request, RequestOptions.DEFAULT); String json response.getSourceAsString(); ItemDoc itemDoc JSONUtil.toBean(json, ItemDoc.class); System.out.println(itemDoc); }4. 删除文档Test void testDeleteDocument() throws IOException { DeleteRequest request new DeleteRequest(items, 100002644680); client.delete(request, RequestOptions.DEFAULT); }5. 局部修改文档Test void testUpdateDocument() throws IOException { UpdateRequest request new UpdateRequest(items, 100002644680); request.doc( price, 58800, commentCount, 1 ); client.update(request, RequestOptions.DEFAULT); }6. 批量导入文档实际项目中商品数据可能有几十万条不能一条一条导入应该使用 BulkRequest 批量导入。Test void testLoadItemDocs() throws IOException { int pageNo 1; int size 1000; while (true) { PageItem page itemService.lambdaQuery() .eq(Item::getStatus, 1) .page(new Page(pageNo, size)); ListItem items page.getRecords(); if (CollUtils.isEmpty(items)) { return; } BulkRequest request new BulkRequest(items); for (Item item : items) { ItemDoc itemDoc BeanUtil.copyProperties(item, ItemDoc.class); request.add(new IndexRequest() .id(itemDoc.getId()) .source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON)); } client.bulk(request, RequestOptions.DEFAULT); pageNo; } }十九、DSL 查询基础Elasticsearch 查询使用 JSON 风格的 DSL 语法。基本格式GET /items/_search { query: { 查询类型: { 查询条件: 查询值 } } }二十、常见叶子查询1. match_all 查询查询所有文档GET /items/_search { query: { match_all: {} } }注意ES 默认只返回前 10 条数据。2. match 查询match 是全文检索查询会对用户输入内容进行分词。GET /items/_search { query: { match: { name: 手机 } } }适合搜索text类型字段例如商品名称。3. multi_match 查询multi_match 可以同时搜索多个字段GET /items/_search { query: { multi_match: { query: 手机, fields: [name, category] } } }4. term 查询term 是精确查询不会对搜索条件分词。GET /items/_search { query: { term: { brand: { value: 华为 } } } }适合搜索keyword数值日期boolean。5. range 查询range 用于范围查询。GET /items/_search { query: { range: { price: { gte: 90000, lte: 159900 } } } }范围关键字关键字含义gte大于等于gt大于lte小于等于lt小于二十一、复合查询1. bool 查询bool 查询可以组合多个查询条件。支持四种逻辑逻辑含义是否参与算分must必须匹配类似 AND参与should选择匹配类似 OR参与must_not必须不匹配类似 NOT不参与filter必须匹配不参与例如搜索手机并且品牌必须是华为价格在 900 到 1599 元之间GET /items/_search { query: { bool: { must: [ { match: { name: 手机 } } ], filter: [ { term: { brand: { value: 华为 } } }, { range: { price: { gte: 90000, lt: 159900 } } } ] } } }在实际项目中关键词搜索用must品牌、分类、价格过滤用filter。这样性能更好因为 filter 不参与相关性算分。二十二、function_score 竞价排名Elasticsearch 默认按照相关性_score排序。但是在电商项目中可能需要让广告商品排在前面。例如商品表中有字段isAD如果isAD true说明这个商品是广告商品。可以使用function_score修改算分。GET /items/_search { query: { function_score: { query: { match: { name: 手机 } }, functions: [ { filter: { term: { isAD: true } }, weight: 10 } ], boost_mode: multiply } } }含义先根据原始查询条件搜索商品找到isAD true的广告商品给这些商品的分数乘以 10最终广告商品排名会更加靠前。二十三、排序ES 默认按照_score相关度排序也可以指定字段排序。例如按照价格降序GET /items/_search { query: { match_all: {} }, sort: [ { price: { order: desc } } ] }可以参与排序的字段通常包括keyword数值日期地理坐标。text 类型字段不能直接用于排序。二十四、分页ES 默认只返回前 10 条数据可以通过from和size控制分页。GET /items/_search { query: { match_all: {} }, from: 0, size: 10 }分页公式from (pageNo - 1) * pageSize例如查询第 2 页每页 10 条from (2 - 1) * 10 10深度分页问题ES 默认限制from size 10000因为 ES 是分片存储如果查询很深的分页需要从每个分片取大量数据再汇总排序会消耗大量内存和 CPU。常见解决方案方式说明限制最大页数电商项目常用search_after官方推荐深度分页方案scroll旧方案不推荐用于实时搜索一般电商搜索页面限制分页深度即可。二十五、高亮显示高亮就是搜索结果中把用户搜索的关键词用特殊标签包裹起来。例如em手机/em前端再给em标签添加红色样式。DSL 写法GET /items/_search { query: { match: { name: 手机 } }, highlight: { fields: { name: { pre_tags: em, post_tags: /em } } } }注意高亮必须配合全文检索查询高亮字段必须是text类型默认情况下高亮字段要和搜索字段一致。二十六、Java RestClient 查询1. match_all 查询Test void testMatchAll() throws IOException { SearchRequest request new SearchRequest(items); request.source().query(QueryBuilders.matchAllQuery()); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }解析响应private void handleResponse(SearchResponse response) { SearchHits searchHits response.getHits(); long total searchHits.getTotalHits().value; System.out.println(共搜索到 total 条数据); SearchHit[] hits searchHits.getHits(); for (SearchHit hit : hits) { String source hit.getSourceAsString(); ItemDoc item JSONUtil.toBean(source, ItemDoc.class); System.out.println(item); } }2. match 查询Test void testMatch() throws IOException { SearchRequest request new SearchRequest(items); request.source().query(QueryBuilders.matchQuery(name, 脱脂牛奶)); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }3. multi_match 查询Test void testMultiMatch() throws IOException { SearchRequest request new SearchRequest(items); request.source().query( QueryBuilders.multiMatchQuery(脱脂牛奶, name, category) ); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }4. range 查询Test void testRange() throws IOException { SearchRequest request new SearchRequest(items); request.source().query( QueryBuilders.rangeQuery(price).gte(10000).lte(30000) ); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }5. term 查询Test void testTerm() throws IOException { SearchRequest request new SearchRequest(items); request.source().query(QueryBuilders.termQuery(brand, 华为)); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }6. bool 查询Test void testBool() throws IOException { SearchRequest request new SearchRequest(items); BoolQueryBuilder bool QueryBuilders.boolQuery(); bool.must(QueryBuilders.matchQuery(name, 脱脂牛奶)); bool.filter(QueryBuilders.termQuery(brand, 德亚)); bool.filter(QueryBuilders.rangeQuery(price).lte(30000)); request.source().query(bool); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }7. 分页和排序Test void testPageAndSort() throws IOException { int pageNo 1; int pageSize 5; SearchRequest request new SearchRequest(items); request.source().query(QueryBuilders.matchQuery(name, 脱脂牛奶)); request.source().sort(price, SortOrder.ASC); request.source() .from((pageNo - 1) * pageSize) .size(pageSize); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }8. 高亮查询Test void testHighlight() throws IOException { SearchRequest request new SearchRequest(items); request.source().query(QueryBuilders.matchQuery(name, 脱脂牛奶)); request.source().highlighter( SearchSourceBuilder.highlight() .field(name) .preTags(em) .postTags(/em) ); SearchResponse response client.search(request, RequestOptions.DEFAULT); handleResponse(response); }高亮结果解析private void handleResponse(SearchResponse response) { SearchHits searchHits response.getHits(); long total searchHits.getTotalHits().value; System.out.println(共搜索到 total 条数据); SearchHit[] hits searchHits.getHits(); for (SearchHit hit : hits) { String source hit.getSourceAsString(); ItemDoc item JSONUtil.toBean(source, ItemDoc.class); MapString, HighlightField hfs hit.getHighlightFields(); if (CollUtils.isNotEmpty(hfs)) { HighlightField hf hfs.get(name); if (hf ! null) { String hfName hf.getFragments()[0].string(); item.setName(hfName); } } System.out.println(item); } }二十七、数据聚合聚合可以用来做数据统计和分析例如商品有哪些品牌商品有哪些分类每个品牌有多少商品每个品牌商品的最高价、最低价、平均价每个月商品销量情况。聚合主要分为三类聚合类型作用Bucket 聚合分组统计Metric 聚合求最大值、最小值、平均值等Pipeline 聚合基于聚合结果继续计算注意参与聚合的字段通常必须是keyword数值日期boolean。text 类型字段一般不能直接聚合。二十八、Bucket 聚合例如统计商品中有哪些分类GET /items/_search { size: 0, aggs: { category_agg: { terms: { field: category, size: 20 } } } }说明参数说明size: 0不返回文档只返回聚合结果aggs聚合category_agg聚合名称terms按字段值分组field聚合字段size返回多少个聚合结果二十九、带条件聚合例如统计价格高于 3000 元的手机品牌GET /items/_search { query: { bool: { filter: [ { term: { category: 手机 } }, { range: { price: { gte: 300000 } } } ] } }, size: 0, aggs: { brand_agg: { terms: { field: brand, size: 20 } } } }这里query的作用是限定聚合范围。三十、Metric 聚合例如统计每个品牌的价格最小值、最大值、平均值GET /items/_search { query: { bool: { filter: [ { term: { category: 手机 } }, { range: { price: { gte: 300000 } } } ] } }, size: 0, aggs: { brand_agg: { terms: { field: brand, size: 20 }, aggs: { stats_metric: { stats: { field: price } } } } } }stats可以同时统计minmaxavgsumcount。三十一、Java RestClient 实现聚合Test void testAgg() throws IOException { SearchRequest request new SearchRequest(items); BoolQueryBuilder bool QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery(category, 手机)) .filter(QueryBuilders.rangeQuery(price).gte(300000)); request.source().query(bool).size(0); request.source().aggregation( AggregationBuilders.terms(brand_agg) .field(brand) .size(5) ); SearchResponse response client.search(request, RequestOptions.DEFAULT); Aggregations aggregations response.getAggregations(); Terms brandTerms aggregations.get(brand_agg); List? extends Terms.Bucket buckets brandTerms.getBuckets(); for (Terms.Bucket bucket : buckets) { String brand bucket.getKeyAsString(); long count bucket.getDocCount(); System.out.println(brand brand ; count count); } }三十二、黑马商城搜索服务拆分由于搜索业务访问量较高如果继续放在 item-service 中会影响商品服务本身的稳定性。因此可以拆分一个新的微服务search-service它专门负责商品搜索商品过滤条件聚合商品索引库数据导入商品索引库数据同步广告商品竞价排名。拆分后的结构可以理解为前端 | | 搜索请求 v search-service | | 查询 ES v Elasticsearch item-service | | 增删改商品 v MySQL | | MQ 通知 v search-service 同步 ES三十三、item-service 提供商品查询接口search-service 需要从 item-service 获取商品数据因此 item-service 应该提供根据 id 查询商品的接口。例如GetMapping(/{id}) public ItemDTO queryItemById(PathVariable(id) Long id) { return itemService.queryItemById(id); }然后在 search-service 中编写 FeignClientFeignClient(item-service) public interface ItemClient { GetMapping(/items/{id}) ItemDTO queryItemById(PathVariable(id) Long id); }这样 search-service 就可以通过 Feign 调用 item-service 查询商品信息。三十四、商品数据同步方案当商品发生新增、修改、删除时MySQL 和 ES 中的数据都需要保持一致。推荐使用 MQ 异步通知。1. 商品新增或修改流程item-service 修改 MySQL | v 发送 MQ 消息携带商品 id | v search-service 监听消息 | v 根据 id 远程调用 item-service 查询商品 | v 写入或更新 ES 文档2. 商品删除流程item-service 删除 MySQL 商品 | v 发送 MQ 消息携带商品 id | v search-service 监听消息 | v 根据 id 删除 ES 文档3. 为什么使用 MQ使用 MQ 的好处解耦商品服务和搜索服务提高接口响应速度避免搜索同步失败影响主业务后续可以重试提高数据最终一致性。三十五、实现搜索接口黑马商城搜索接口信息如下项目内容请求方式GET请求路径/search/list请求参数参数说明key搜索关键字pageNo页码pageSize每页大小sortBy排序字段isAsc是否升序category分类brand品牌minPrice最小价格maxPrice最大价格查询逻辑分析搜索接口需要实现根据关键字搜索商品名称根据分类过滤根据品牌过滤根据价格区间过滤支持排序支持分页支持高亮支持广告商品优先展示。核心查询可以使用bool查询组合条件function_score实现广告排名from size实现分页sort实现排序highlight实现高亮。三十六、搜索条件构建思路伪代码如下BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StrUtil.isNotBlank(key)) { boolQuery.must(QueryBuilders.matchQuery(name, key)); } else { boolQuery.must(QueryBuilders.matchAllQuery()); } if (StrUtil.isNotBlank(category)) { boolQuery.filter(QueryBuilders.termQuery(category, category)); } if (StrUtil.isNotBlank(brand)) { boolQuery.filter(QueryBuilders.termQuery(brand, brand)); } if (minPrice ! null || maxPrice ! null) { RangeQueryBuilder range QueryBuilders.rangeQuery(price); if (minPrice ! null) { range.gte(minPrice); } if (maxPrice ! null) { range.lte(maxPrice); } boolQuery.filter(range); }如果要让广告商品优先可以在 bool 查询外面包一层 function_scoreFunctionScoreQueryBuilder functionScoreQuery QueryBuilders.functionScoreQuery( boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery(isAD, true), ScoreFunctionBuilders.weightFactorFunction(10) ) } ).boostMode(CombineFunction.MULTIPLY);三十七、过滤条件聚合接口搜索页面的过滤项不能写死而应该根据当前搜索结果动态变化。例如用户搜索电视那么分类过滤项中就不应该出现手机、拉杆箱等无关分类。所以需要根据当前搜索条件做聚合统计当前搜索结果中有哪些分类和品牌。接口信息项目内容请求方式POST请求路径/search/filters请求参数与搜索接口类似keycategorybrandminPricemaxPrice。返回格式{ category: [手机, 曲面电视, 拉杆箱], brand: [小米, 华为, Apple] }聚合 DSL 示例GET /items/_search { query: { bool: { must: [ { match: { name: 手机 } } ] } }, size: 0, aggs: { category_agg: { terms: { field: category, size: 20 } }, brand_agg: { terms: { field: brand, size: 20 } } } }Java API 示例SearchRequest request new SearchRequest(items); BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StrUtil.isNotBlank(key)) { boolQuery.must(QueryBuilders.matchQuery(name, key)); } request.source().query(boolQuery).size(0); request.source().aggregation( AggregationBuilders.terms(category_agg) .field(category) .size(20) ); request.source().aggregation( AggregationBuilders.terms(brand_agg) .field(brand) .size(20) ); SearchResponse response client.search(request, RequestOptions.DEFAULT);解析聚合结果Aggregations aggregations response.getAggregations(); Terms categoryTerms aggregations.get(category_agg); ListString categories categoryTerms.getBuckets() .stream() .map(bucket - bucket.getKeyAsString()) .collect(Collectors.toList()); Terms brandTerms aggregations.get(brand_agg); ListString brands brandTerms.getBuckets() .stream() .map(bucket - bucket.getKeyAsString()) .collect(Collectors.toList());三十八、竞价排名实现思路商品表中已经有isAD字段用来标记是否是广告商品。需求在原本搜索结果的基础上让isAD true的商品排名更靠前。可以使用function_score查询。DSL 示例GET /items/_search { query: { function_score: { query: { bool: { must: [ { match: { name: 手机 } } ] } }, functions: [ { filter: { term: { isAD: true } }, weight: 10 } ], boost_mode: multiply } } }Java API 思路BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); boolQuery.must(QueryBuilders.matchQuery(name, key)); FunctionScoreQueryBuilder functionScoreQuery QueryBuilders.functionScoreQuery( boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ new FunctionScoreQueryBuilder.FilterFunctionBuilder( QueryBuilders.termQuery(isAD, true), ScoreFunctionBuilders.weightFactorFunction(10) ) } ).boostMode(CombineFunction.MULTIPLY); request.source().query(functionScoreQuery);这样广告商品的相关性分数会被放大从而排在更前面。三十九、本阶段重点总结1. ES 为什么快因为 ES 使用倒排索引。MySQL 模糊查询通常是文档 - 词条ES 倒排索引是词条 - 文档根据词条查找文档速度更快。2. 分词器有什么作用分词器主要用于两个阶段创建倒排索引时对文档内容分词用户搜索时对输入关键字分词。中文搜索一般使用 IK 分词器。3. IK 分词器有几种模式模式说明ik_smart智能切分粗粒度ik_max_word最细粒度切分4. text 和 keyword 的区别是什么类型是否分词使用场景text分词商品名称、文章内容keyword不分词品牌、分类、状态、邮箱5. 索引库操作有哪些操作DSL创建索引库PUT /索引库名查询索引库GET /索引库名删除索引库DELETE /索引库名添加字段PUT /索引库名/_mapping6. 文档操作有哪些操作DSL新增文档POST /索引库名/_doc/id查询文档GET /索引库名/_doc/id删除文档DELETE /索引库名/_doc/id全量修改PUT /索引库名/_doc/id局部修改POST /索引库名/_update/id批量操作POST /_bulk7. Java RestClient 操作步骤基本步骤都是创建 Request 对象准备请求参数发送请求如果是查询还需要解析响应结果。8. DSL 查询包含哪些部分常见 DSL 查询结构{ query: {}, from: 0, size: 10, sort: [], highlight: {}, aggs: {} }9. 黑马商城搜索服务最终要实现什么需要完成拆分search-service创建items商品索引库将 MySQL 商品数据批量导入 ES商品新增、修改、删除时同步 ES实现/search/list搜索接口实现/search/filters过滤条件聚合接口使用function_score实现广告商品竞价排名。四十、结语通过本阶段学习可以发现 Elasticsearch 并不是用来完全替代 MySQL 的而是用来解决 MySQL 不擅长的搜索问题。在真实项目中常见架构是MySQL 负责数据写入和事务一致性 Elasticsearch 负责海量数据搜索和分析 MQ 负责两者之间的数据同步对于黑马商城这样的电商项目来说商品搜索是非常核心的功能。将数据库模糊查询改造为 Elasticsearch 搜索之后不仅可以提升查询性能还可以实现分词搜索、条件过滤、动态聚合、高亮显示、广告排名等更加完整的搜索体验。因此Elasticsearch 是微服务项目中非常重要的一项搜索中间件技术也是后端开发必须掌握的核心技术之一。