1.多条件检索,结果高亮处理 (业务场景 索引库设计 实现流程)

业务场景:酒店系统需要根据用户查询的关键字(酒店名、地址、星级)等搜索出对应的酒店信息。
设计思考:考虑到MySQL对海量数据的搜索效率不高,无法进行及时的结果反馈给用户,故选择elasticsearch技术来实现数据的快速搜索。
实现流程:首先建立索引库
将name字段设计为text支持分词;band,city等设置为poswprd,添加拼音分词器等。

  1. PUT /hotel
  2. {
  3. "settings": {
  4. "analysis": {
  5. "analyzer": {
  6. "text_anlyzer": {
  7. "tokenizer": "ik_max_word",
  8. "filter": "py"
  9. },
  10. "completion_analyzer": {
  11. "tokenizer": "keyword",
  12. "filter": "py"
  13. }
  14. },
  15. "filter": {
  16. "py": {
  17. "type": "pinyin",
  18. "keep_full_pinyin": false,
  19. "keep_joined_full_pinyin": true,
  20. "keep_original": true,
  21. "limit_first_letter_length": 16,
  22. "remove_duplicated_term": true,
  23. "none_chinese_pinyin_tokenize": false
  24. }
  25. }
  26. }
  27. },
  28. "mappings": {
  29. "properties": {
  30. "id":{
  31. "type": "keyword"
  32. },
  33. "name":{
  34. "type": "text",
  35. "analyzer": "text_anlyzer",
  36. "search_analyzer": "ik_smart",
  37. "copy_to": "all"
  38. },
  39. "address":{
  40. "type": "keyword",
  41. "index": false
  42. },
  43. "price":{
  44. "type": "integer"
  45. },
  46. "score":{
  47. "type": "integer"
  48. },
  49. "brand":{
  50. "type": "keyword",
  51. "copy_to": "all"
  52. },
  53. "city":{
  54. "type": "keyword"
  55. },
  56. "starName":{
  57. "type": "keyword"
  58. },
  59. "business":{
  60. "type": "keyword",
  61. "copy_to": "all"
  62. },
  63. "location":{
  64. "type": "geo_point"
  65. },
  66. "pic":{
  67. "type": "keyword",
  68. "index": false
  69. },
  70. "all":{
  71. "type": "text",
  72. "analyzer": "text_anlyzer",
  73. "search_analyzer": "ik_smart"
  74. },
  75. "suggestion":{
  76. "type": "completion",
  77. "analyzer": "completion_analyzer"
  78. }
  79. }
  80. }
  81. }

从数据库将数据导入索引库,建立相应文档,一条数据就是一个文档。

  1. //批量导入文档 BulkRequest
  2. @Test
  3. void testcreateAll() throws IOException {
  4. //1.在mysql中查询出所以字段
  5. List<Hotel> list = hotelService.list();
  6. //遍历
  7. //2.创建请求对象
  8. BulkRequest request = new BulkRequest();
  9. for (Hotel hotel : list) {
  10. //3.将字段封装成hotelDoc
  11. HotelDoc hotelDoc = new HotelDoc(hotel);
  12. //4.将hotelDoc转化成JSON格式(FastJson)
  13. String json = JSON.toJSONString(hotelDoc);
  14. //5.准备请求参数(一般都是request.source(json参数,参数格式))
  15. request.add(new IndexRequest("hotel")
  16. .id(hotelDoc.getId().toString())
  17. .source(json, XContentType.JSON));
  18. }
  19. //6.发送请求,处理结果
  20. client.bulk(request, RequestOptions.DEFAULT);
  21. }

关键字搜索
1.请求对象
2.设置参数
3.发送请求
4.解析查询结果

  1. /**
  2. * @功能: 查询所有
  3. * @return: cn.itcast.hotel.pojo.PageResult
  4. */
  5. @Override
  6. public PageResult searchAll(RequestParams requestParams, SearchRequest request) {
  7. //requestParams:{key: "", page: 1, size: 5, sortBy: "default"}
  8. //创建索引库客户端(提取到bean里)
  9. // RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
  10. // HttpHost.create("http://192.168.200.130:9200")
  11. // ));
  12. PageResult pageResult = null;
  13. try {
  14. //1.创建请求对象
  15. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
  16. //2.设置请求参数 "query": "match": {"all": "外滩如家"}}
  17. if (StringUtils.isNotBlank(requestParams.getKey())) {
  18. boolQuery.must(QueryBuilders.matchQuery("all", requestParams.getKey()));
  19. } else {
  20. boolQuery.must(QueryBuilders.matchAllQuery());
  21. }
  22. //过滤条件 brand city startName minPrice maxPrice
  23. if (StringUtils.isNotBlank(requestParams.getCity())) {
  24. boolQuery.filter(QueryBuilders.termQuery("city", requestParams.getCity()));
  25. }
  26. if (StringUtils.isNotBlank(requestParams.getBrand())) {
  27. boolQuery.filter(QueryBuilders.termQuery("brand", requestParams.getBrand()));
  28. }
  29. if (StringUtils.isNotBlank(requestParams.getStarName())) {
  30. boolQuery.filter(QueryBuilders.termQuery("starName", requestParams.getStarName()));
  31. }
  32. //价格用range
  33. if (requestParams.getMinPrice() != null && requestParams.getMinPrice() != null) {
  34. boolQuery.filter(QueryBuilders.rangeQuery("price")
  35. .gte(requestParams.getMinPrice())
  36. .lte(requestParams.getMaxPrice()));
  37. }
  38. request.source().query(boolQuery);
  39. //分页
  40. int page = requestParams.getPage();
  41. int size = requestParams.getSize();
  42. request.source().from((page - 1) * size).size(size);
  43. //排序 "sort": [{"_geo_distance":
  44. // {"location": {"lat": 31.146538,"lon": 121.671663},
  45. // "order": "asc"}}]
  46. String location = requestParams.getLocation();
  47. if (StringUtils.isNotBlank(location)) {
  48. request.source().sort(SortBuilders.geoDistanceSort("location",
  49. new GeoPoint(location)).order(SortOrder.ASC)
  50. .unit(DistanceUnit.KILOMETERS));
  51. }
  52. //竞价排名GET /hotel/_search{"query": {"function_score": {
  53. // "query": {"match": {"name": "如家"}},
  54. // "functions": [{"filter": {"term": {"name": "如家"}},
  55. // "weight": 2}],
  56. // "boost_mode": "multiply"}}}
  57. FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
  58. boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
  59. new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  60. //加分条件
  61. QueryBuilders.termQuery("isAD", true),
  62. //加分权重
  63. ScoreFunctionBuilders.weightFactorFunction(15)
  64. )
  65. }
  66. );
  67. request.source().query(functionScoreQuery);
  68. //排序 判断sortBy default 默认 score 按评分自定义排序 price 按价格自定义排序
  69. String sortBy = requestParams.getSortBy();
  70. if (sortBy.equals("score")) {
  71. //评分排序
  72. request.source().sort("score", SortOrder.DESC);
  73. } else if (sortBy.equals("price")) {
  74. //价格排序
  75. request.source().sort("price", SortOrder.DESC);
  76. }
  77. //高亮 "highlight": {
  78. // "fields": {"name": {
  79. // "pre_tags": "<em>",
  80. // "post_tags": "</em>"}}}
  81. if (StringUtils.isNotBlank(requestParams.getKey())) {
  82. HighlightBuilder highlightBuilder = new HighlightBuilder();
  83. highlightBuilder.field("name");
  84. highlightBuilder.requireFieldMatch(false);
  85. highlightBuilder.preTags("<em>");
  86. highlightBuilder.postTags("</em>");
  87. request.source().highlighter(highlightBuilder);
  88. }
  89. //3.发送请求
  90. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  91. //获取结果
  92. pageResult = getRestult(response);
  93. } catch (IOException e) {
  94. log.error("查询出错信息:{}", e.getMessage());
  95. }
  96. return pageResult;
  97. }
  98. /**
  99. * @功能: 解析处理结果
  100. * @return: cn.itcast.hotel.pojo.PageResult
  101. */
  102. public PageResult getRestult(SearchResponse response) {
  103. //4.解析结果
  104. SearchHits hits = response.getHits();
  105. SearchHit[] searchHits = hits.getHits();
  106. long total = hits.getTotalHits().value; //总页数
  107. //遍历 拿到 source字段
  108. List<HotelDoc> hotels = new ArrayList<>(); //存储查询返回的酒店信息
  109. for (SearchHit hit : searchHits) {
  110. String sourceAsString = hit.getSourceAsString(); //JSON 格式
  111. HotelDoc hotelDoc = JSON.parseObject(sourceAsString, HotelDoc.class); //转换为HotelDoc对象
  112. if(hit.getHighlightFields().size()>0){
  113. //处理高亮结果 hits: highlight:name
  114. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  115. HighlightField highlightField = highlightFields.get("name");
  116. Text[] fragments = highlightField.fragments();
  117. String newname = "";
  118. for (Text fragment : fragments) {
  119. newname += fragment;
  120. }
  121. hotelDoc.setName(newname);
  122. }
  123. hotels.add(hotelDoc);
  124. //获取地址排序
  125. Object[] sortValues = hit.getSortValues();
  126. if (sortValues.length > 0) {
  127. Object sortValue = sortValues[0];
  128. hotelDoc.setDistance(sortValue
  129. );
  130. }
  131. }
  132. return new PageResult(total, hotels);
  133. }

2. 按距离排序 查询离我最近的XXX (业务场景 索引库设计 实现流程)

业务场景:客户需要查询离他最近的酒店,按距离排序的需求就有了
image.png
设计思考:由前端发送当前客户坐标地址,后台接受数据后,利用es的“geo_distance”功能查询后按距离进行排序。
实现流程:

  1. //排序 "sort": [{"_geo_distance":
  2. // {"location": {"lat": 31.146538,"lon": 121.671663},
  3. // "order": "asc"}}]
  4. String location = requestParams.getLocation();
  5. if (StringUtils.isNotBlank(location)) {
  6. request.source().sort(SortBuilders.geoDistanceSort("location",
  7. new GeoPoint(location)).order(SortOrder.ASC)
  8. .unit(DistanceUnit.KILOMETERS));
  9. }
  10. //获取地址排序
  11. Object[] sortValues = hit.getSortValues();
  12. if (sortValues.length > 0) {
  13. Object sortValue = sortValues[0];
  14. hotelDoc.setDistance(sortValue
  15. );
  16. }

3. XXX竞价排名? (业务场景 索引库设计 实现流程)

业务场景:可以根据商家的广告费,在搜索的结果里排到首条显示,是增收的功能,比较重要。
image.png
设计思考:给酒店增加一个字段属性isAD(boolean类型),true表示有广告,再根据es的自定义算分函数function_score进行分数加权,这样保证有广告的酒店分数最高,排在首页。前端可以根据此字段显示广告的标记。function_score三要素:过滤条件,加权值,加权方式。
实现流程:

  1. //竞价排名GET /hotel/_search{"query": {"function_score": {
  2. // "query": {"match": {"name": "如家"}},
  3. // "functions": [{"filter": {"term": {"name": "如家"}},
  4. // "weight": 2}],
  5. // "boost_mode": "multiply"}}}
  6. FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(
  7. boolQuery, new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
  8. new FunctionScoreQueryBuilder.FilterFunctionBuilder(
  9. //加分条件
  10. QueryBuilders.termQuery("isAD", true),
  11. //加分权重
  12. ScoreFunctionBuilders.weightFactorFunction(15)
  13. )
  14. }
  15. );

4. 聚合查询搜索条件? (业务场景 索引库设计 实现流程)

业务场景:在进行相关酒店搜索后,可选项应该只包含此类酒店的信息,实现动态搜索。比如搜索如家的时候,如果杭州没有如家,则不会再城市选项中出现。
image.png
设计思考:按城市分桶,存入map集合,key=”上海”,value=list;采用es的aggregatetion功能设置相应的桶,再从查询的返回结果里的buckets中取出数据,返回给前端。
实现流程:

  1. /**
  2. * @功能: 聚合索引库
  3. * @return: java.util.Map<java.lang.String, java.util.List < java.lang.String>>
  4. */
  5. @Override
  6. public Map<String, List<String>> getFilters(RequestParams params, SearchRequest request) {
  7. Map<String, List<String>> result = new HashMap<>();
  8. try {
  9. // { “aggs” :{ "brand_agg" :{"terms":{"field": "brand","size":20 }} }}
  10. //1.创建请求
  11. //2.设置请求参数
  12. //先按之前的进行搜索
  13. searchAll(params, request);
  14. //全文搜索不显示
  15. request.source().size(0);
  16. //按品牌分桶
  17. request.source().aggregation(AggregationBuilders
  18. .terms("brandAgg")
  19. .field("brand")
  20. .size(100));
  21. //按城市分桶
  22. request.source().aggregation(AggregationBuilders
  23. .terms("cityAgg")
  24. .field("city")
  25. .size(100));
  26. //按商圈分桶
  27. request.source().aggregation(AggregationBuilders
  28. .terms("starAgg")
  29. .field("starName")
  30. .size(100));
  31. //3.发送请求
  32. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  33. //4.解析结果
  34. Aggregations aggregations = response.getAggregations();
  35. //根据名称获取结果
  36. List<String> brandAgg = getAggByName(aggregations, "brandAgg");
  37. result.put("brand", brandAgg);
  38. List<String> cityAgg = getAggByName(aggregations, "cityAgg");
  39. result.put("city", cityAgg);
  40. List<String> starAgg = getAggByName(aggregations, "starAgg");
  41. result.put("starName", starAgg);
  42. } catch (IOException e) {
  43. log.error("聚合搜索出错{}", e.getMessage());
  44. }
  45. return result;
  46. }
  47. /**
  48. * @功能: 获取聚合结果桶里的数据
  49. * @return: java.util.List<java.lang.String>
  50. */
  51. public List<String> getAggByName(Aggregations aggregations, String aggname) {
  52. Terms terms = aggregations.get(aggname);
  53. //获取桶 buckets
  54. List<? extends Terms.Bucket> buckets = terms.getBuckets();
  55. List<String> list = new ArrayList<>();
  56. for (Terms.Bucket bucket : buckets) {
  57. String key = bucket.getKeyAsString();
  58. list.add(key);
  59. }
  60. return list;
  61. }

5. 自动补全及拼音查询? (业务场景 索引库设计 实现流程)

业务场景:当在搜索框里输入拼音时在下拉框自动生成相应相关字段提示
image.png
设计思考:按name进行分词,再使用拼音分词器转成拼音分词,当输入相应拼音后能查出对应的汉字词组。再使用es的suggestion功能实现自动补全。
实现流程:
修改索引库结构增加拼音分词器

  1. // 酒店数据索引库
  2. PUT /hotel
  3. {
  4. "settings": {
  5. "analysis": {
  6. "analyzer": {
  7. "text_anlyzer": {
  8. "tokenizer": "ik_max_word",
  9. "filter": "py"
  10. },
  11. "completion_analyzer": {
  12. "tokenizer": "keyword",
  13. "filter": "py"
  14. }
  15. },
  16. "filter": {
  17. "py": {
  18. "type": "pinyin",
  19. "keep_full_pinyin": false,
  20. "keep_joined_full_pinyin": true,
  21. "keep_original": true,
  22. "limit_first_letter_length": 16,
  23. "remove_duplicated_term": true,
  24. "none_chinese_pinyin_tokenize": false
  25. }
  26. }
  27. }
  28. },
  29. "mappings": {
  30. "properties": {
  31. "id":{
  32. "type": "keyword"
  33. },
  34. "name":{
  35. "type": "text",
  36. "analyzer": "text_anlyzer",
  37. "search_analyzer": "ik_smart",
  38. "copy_to": "all"
  39. },
  40. "address":{
  41. "type": "keyword",
  42. "index": false
  43. },
  44. "price":{
  45. "type": "integer"
  46. },
  47. "score":{
  48. "type": "integer"
  49. },
  50. "brand":{
  51. "type": "keyword",
  52. "copy_to": "all"
  53. },
  54. "city":{
  55. "type": "keyword"
  56. },
  57. "starName":{
  58. "type": "keyword"
  59. },
  60. "business":{
  61. "type": "keyword",
  62. "copy_to": "all"
  63. },
  64. "location":{
  65. "type": "geo_point"
  66. },
  67. "pic":{
  68. "type": "keyword",
  69. "index": false
  70. },
  71. "all":{
  72. "type": "text",
  73. "analyzer": "text_anlyzer",
  74. "search_analyzer": "ik_smart"
  75. },
  76. "suggestion":{
  77. "type": "completion",
  78. "analyzer": "completion_analyzer"
  79. }
  80. }
  81. }
  82. }

在映射的实体类增加suggestion字段
重新导入索引库文档
代码实现功能

  1. /**
  2. * @功能: 搜索自动回显
  3. * @return: java.util.List<java.lang.String>
  4. */
  5. @Override
  6. public List<String> getSuggestions(String key) {
  7. List<String> list = new ArrayList<>();
  8. try {
  9. //1.准备请求
  10. SearchRequest request = new SearchRequest("hotel");
  11. //2.设置参数
  12. request.source().suggest(new SuggestBuilder().addSuggestion(
  13. "suggestions", SuggestBuilders.completionSuggestion("suggestion")
  14. .prefix(key)
  15. .skipDuplicates(true)
  16. .size(5)
  17. ));
  18. //3.发起请求
  19. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  20. //4.解析结果
  21. Suggest suggest = response.getSuggest();
  22. //5.获取补全结果
  23. CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
  24. List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
  25. //6.返回结果
  26. for (CompletionSuggestion.Entry.Option option : options) {
  27. String text = option.getText().toString();
  28. list.add(text);
  29. }
  30. return list;
  31. } catch (IOException e) {
  32. log.error("获取出错:原因为{}", e.getMessage());
  33. }
  34. return list;
  35. }

6.es和mysql数据同步思路?

可以有三种方案实现:
1)使用微服务间的同步调用,使用Feign组件实现。
2)使用消息队列进行异步通知,使用RabbitMQ组件实现。
3)使用监听binglog进行监听。
目前使用第二种方案进行实现
image.png
思路:
当酒店管理服务发生数据的增加、删除、修改时,将更改消息发送给RabbitMQ的主题交换机(hotel.exchange),与交换机绑定的有删除(deleta_queue)和增加修改队列(insert_queue)。酒店搜索服务对消息队列进行监听,当消息过来后,依据消息的类型和参数,调用服务层方法,通过相应es的API对索引库中的片段进行相应的操作,从而达到MySQL数据库与Elasticsearch文档中的数据保持一致。
代码实现:
a.在hotel_domo与hotel_admin中导入rabbitMQ的依赖。
image.png

b.在hotel_domo与hotel_admin中配置连接信息。
image.png

c.在hotel_domo与hotel_admin中声明exchange、queue、bing实例与常量名。
image.png
image.png
d.在hotel_admin中发消息。
image.png

e.在hotel_demo中j接收消息,并调用服务层执行相关操作。
image.png

7.es如何保证高可用?(集群介绍)

  • 集群(cluster):一组拥有共同的 cluster name 的 节点。
  • 节点(node) :集群中的一个 Elasticearch 实例
  • 分片(shard):索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中
  • 解决问题:数据量太大,单点存储量有限的问题。

    8.es集群中分片和副本介绍?

  • 主分片(Primary shard):相对于副本分片的定义。

  • 副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。

image.png

9.es集群中的脑裂现象?

脑裂是由于网络阻塞,分节点与主节点失联,所有分节点重新选出新节点作为主节点,而网络正常后,之前的主节点恢复通讯,造成一个集群出现2个主节点,形成脑裂现象。不过一般这种情况很少出现。
image.png
image.png

10.es集群中的故障转移? (集群容错)

集群分布式储存
image.png
故障转移:当节点node1宕机后,node2和node3会重选主节点,并创建P-0主分片和R-1副主分片。流程图如下:
image.png