SpringCloud(十)——ElasticSearch简单了解(三)数据聚合和自动补全

文章目录

  • [1. 数据聚合](#1. 数据聚合)
    • [1.1 聚合介绍](#1.1 聚合介绍)
    • [1.2 Bucket 聚合](#1.2 Bucket 聚合)
    • [1.3 Metrics 聚合](#1.3 Metrics 聚合)
    • [1.4 使用 RestClient 进行聚合](#1.4 使用 RestClient 进行聚合)
  • [2. 自动补全](#2. 自动补全)
    • [2.1 安装补全包](#2.1 安装补全包)
    • [2.2 自定义分词器](#2.2 自定义分词器)
    • [2.3 自动补全查询](#2.3 自动补全查询)
    • [2.4 拼音自动补全查询](#2.4 拼音自动补全查询)
    • [2.5 RestClient 实现自动补全](#2.5 RestClient 实现自动补全)
      • [2.5.1 建立索引](#2.5.1 建立索引)
      • [2.5.2 修改数据定义](#2.5.2 修改数据定义)
      • [2.5.3 补全查询](#2.5.3 补全查询)
      • [2.5.4 解析结果](#2.5.4 解析结果)

1. 数据聚合

1.1 聚合介绍

聚合(aggregations)可以实现对文档数据的统计、分析、运算。

聚合常见的有三类:

  • 桶(Bucket)聚合 :用来对文档做分组
    • TermAggregation:按照文档字段值分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合 :用以计算一些值,比如:最大值、最小值、平均值等
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

参与聚合的字段为以下字段:

  • keyword
  • 数值
  • 日期
  • 布尔

注意,不能是 text 字段

1.2 Bucket 聚合

这里假如我们需要对不同品牌的酒店进行聚合人,那么我们就可以使用桶聚合,桶聚合的例子如下:

json 复制代码
GET /hotel/_search
{
  "size": 0,  // 设置size为0,结果中不包含文档,只包含聚合结果
  "aggs": { // 定义聚合
    "brandAgg": { //给聚合起个名字
      "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
        "field": "brand", // 参与聚合的字段
        "order": {
          "_count": "asc"// 按照聚合数量进行升序排列,默认降序
        },
        "size": 20 // 希望获取的聚合结果数量
      }
    }
  }
}

聚合结果如下:

如果我们需要在一些查询的条件下进行聚合,比如我们只对200元一下的酒店文档进行聚合,那么聚合条件如下:

json 复制代码
GET /hotel/_search
{
  "query": {
    "range": {
      "price": {
        "lte": 200 // 只对200元以下的文档聚合
      }
    }
  }, 
  "size": 0, 
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}

1.3 Metrics 聚合

如果我们需要按照每个品牌的用户的评分的最大值、最小值、平均值等进行排序,那么这就需要用到 Metrics 聚合了,我们使用 stats 查看所有的聚合属性,该聚合的实现如下:

json 复制代码
GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "size": 20
      },
      "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
        "scoreAgg": { // 聚合名称
          "stats": { // 聚合类型,这里stats可以计算min、max、avg等
            "field": "score" // 聚合字段,这里是score
          }
        }
      }
    }
  }
}

聚合结果如下:

如果我们想要对按照聚合的平均值进行排序,那么DSL语句如下:

json 复制代码
GET /hotel/_search
{
  "size": 0, 
  "aggs": {
    "brandAgg": { 
      "terms": { 
        "field": "brand", 
        "size": 20,
        "order": {
          "scoreAgg.avg": "asc"  //按照平均值进行排序
        }
      },
      "aggs": { 
        "scoreAgg": { 
          "stats": {
            "field": "score" 
          }
        }
      }
    }
  }
}

1.4 使用 RestClient 进行聚合

我们以各个酒店的品牌聚合为例,其中java语句与DSL语句的一一对应关系如下:

使用RestClient进行聚合的代码如下:

java 复制代码
    @Test
    void testAgg() throws IOException {
        //1.准备Request对象
        SearchRequest request = new SearchRequest("hotel");
        //2.准备size
        request.source().size(0);
        //3.进行聚合
        request.source().aggregation(AggregationBuilders
                .terms("brandAgg")
                .field("brand")
                .size(10));
        //4.发送请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        //解析聚合结果
        Aggregations aggregations = response.getAggregations();
        //根据名称获取聚合结果
        Terms brandterms = aggregations.get("brandAgg");
        //获取桶
        List<? extends Terms.Bucket> buckets = brandterms.getBuckets();
        // 遍历
        for (Terms.Bucket bucket: buckets){
            // 获取key,也就是品牌信息
            String brandName = bucket.getKeyAsString();
            System.out.println(brandName);
        }
    }

结果如下:

2. 自动补全

2.1 安装补全包

自动补全我们需要实现的效果是当我们输入拼音的时候,就有一些产品的提示,这种情况下就需要我们对拼音有一定的处理,所以我们在这里下载一个拼音分词器,下载的方式与上面下载 IK 分词器相差不大,都是首先进入容器内部,然后在容器插件目录下进行安装,

bash 复制代码
# 进入容器内部
docker exec -it es /bin/bash

# 在线下载并安装
/usr/share/elasticsearch/bin/elasticsearch-plugin install --batch \
    https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v7.12.1/elasticsearch-analysis-pinyin-7.12.1.zip

#退出
exit

#重启容器
docker restart es

重启后,使用拼音分词器试试效果,如下:

json 复制代码
POST /_analyze
{
  "text": ["你干嘛哎哟"],
  "analyzer": "pinyin"
}

2.2 自定义分词器

从上面的例子我们可以看出,拼音分词器是将一句话的每个字都进行分开,并且首字母的拼音全部都在一起的,这肯定不是我们想要看到的,我们想要的是对句子进行分词后还能根据词语来创建拼音的索引,所以,这就需要我们自定义分词器了。

首先,我们需要了解分词器的工作步骤,elasticsearch中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
  • tokenizer :将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart tokenizer
  • filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

自定义分词器的DSL代码如下:

json 复制代码
PUT /test   //针对的是test索引库
{
  "settings": {
    "analysis": {
      "analyzer": { 	//自定义分词器
        "my_analyzer": {	//分词器名称
          "tokenizer": "ik_max_word",
          "filter": "py"	//过滤器名称
        }
      },
      "filter": { 
        "py": { 
          "type": "pinyin", //拼音分词器
		   "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {	//创建的索引库的映射
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer",//创建索引时的
        "search_analyzer": "ik_smart"
      }
    }
  }
}

进行测试如下:

json 复制代码
POST /test/_doc/1
{
  "id": 1,
  "name": "下雪"
}

POST /test/_doc/2
{
  "id": 2,
  "name": "瞎学"
}

GET /test/_search
{
  "query": {
    "match": {
      "name": "武汉在下雪嘛"
    }
  }
}

查询结果如下:

2.3 自动补全查询

elasticsearch提供了 Completion Suggester 查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:

  • 参与补全查询的字段必须是 completion 类型。
  • 字段的内容一般是用来补全的多个词条形成的数组。

首先我们先建立索引库以及索引库约束,

json 复制代码
PUT test2
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}

test2 索引库中添加数据

json 复制代码
// 示例数据
POST test2/_doc
{
  "title": ["Sony", "WH-1000XM3"]
}

POST test2/_doc
{
  "title": ["SK-II", "PITERA"]
}

POST test2/_doc
{
  "title": ["Nintendo", "switch"]
}

进行自动补全查询,我们给出一个关键字 s ,对其进行补全查询如下,

json 复制代码
GET /test2/_search
{
  "suggest": {
    "title_suggest": {
      "text": "s", // 关键字
      "completion": {
        "field": "title", // 补全查询的字段
        "skip_duplicates": true, // 跳过重复的
        "size": 10 // 获取前10条结果
      }
    }
  }
}

查询结果如下:

2.4 拼音自动补全查询

如果想要使用拼音自动补全进行查询,那么就必须自定义分词器了,自定义分词器如下:

json 复制代码
PUT /test3
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": { 
        "py": { 
          "type": "pinyin", 
		      "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "completion",
        "analyzer": "completion_analyzer"
      }
    }
  }
}

然后创建一个索引库,并添加一些文档,如下:

json 复制代码
PUT /test3
{
  "mappings": {
    "properties": {
      "title":{
        "type": "completion"
      }
    }
  }
}

POST test3/_doc
{
  "title": ["上子", "熵字", "下雪"]
}

POST test3/_doc
{
  "title": ["赏金", "秀色", "猎人"]
}

然后就可以根据首字母缩写或者拼音来进行查询了,DSL代码如下:

json 复制代码
GET /test3/_search
{
  "suggest": {
    "suggestions": {
      "text": "xx",
      "completion": {
        "field": "title",
        "skip_duplicates": true,
        "size": 10
      }
    }
  }
}

如上,我们输入的是下雪的拼音缩写 xx ,进行查询时,结果如下:

当然,也可以对拼音进行按顺序的补全查询。

2.5 RestClient 实现自动补全

2.5.1 建立索引

要实现对索引库的内容进行自动补全,我们需要重新创建索引库,我们的索引库需要多出一个 completion 字段的类型,所以删除原有的索引库后创建新的索引库如下:

json 复制代码
DELETE /hotel

PUT /hotel
{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": { 
        "py": { 
          "type": "pinyin", 
		      "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "copy_to": "all"
      },
      "address": {
        "type": "keyword",
        "index": false
      },
      "price": {
        "type": "integer"
      },
      "score": {
        "type": "integer"
      },
      "brand": {
        "type": "keyword",
        "copy_to": "all"
      },
      "city": {
        "type": "keyword"
      },
      "starName": {
        "type": "keyword"
      },
      "bussiness": {
        "type": "keyword",
        "copy_to": "all"
      },
      "location": {
        "type": "geo_point"
      },
      "pic": {
        "type": "keyword",
        "index": false
      },
      "all": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "suggestion": {
        "type": "completion",
        "analyzer": "completion_analyzer"
      }
    }
  }
}

2.5.2 修改数据定义

除此之外,还需要将 Hotel 的定义进行修改,因为自动补全的字段不止一个字段,所以我们使用列表类型,定义如下:

java 复制代码
@Data
@NoArgsConstructor
@ToString
public class HotelDoc {
    private Long id;
    private String name;
    private String address;
    private Integer price;
    private Integer score;
    private String brand;
    private String city;
    private String starName;
    private String business;
    private String location;
    private String pic;
    private List<String> suggestion;

    public HotelDoc(Hotel hotel) {
        this.id = hotel.getId();
        this.name = hotel.getName();
        this.address = hotel.getAddress();
        this.price = hotel.getPrice();
        this.score = hotel.getScore();
        this.brand = hotel.getBrand();
        this.city = hotel.getCity();
        this.starName = hotel.getStarName();
        this.business = hotel.getBusiness();
        this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
        this.pic = hotel.getPic();
        this.suggestion = Arrays.asList(this.brand, this.business);
    }
}

以上定义就是将 brandbusiness 都进行补全的数据结构定义。

之后再按照之前的批量添加将数据库中的数据进行批量新增即可。

2.5.3 补全查询

以下是RestClient与DSL语句的一一对应关系,

补全查询的语句如下:

java 复制代码
    @Test
    void testSuggest() throws IOException{
        //1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        //2. 准备DSL
        request.source().suggest(new SuggestBuilder().addSuggestion(
                "suggestions",
                SuggestBuilders.completionSuggestion("suggestion")
                        .prefix("hu")
                        .skipDuplicates(true)
                        .size(10)
        ));
        //3. 发起请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        //4. 输出结果
        System.out.println(response);
    }

2.5.4 解析结果

输出的查询结果是一个包含很多形式的信息的Map类型,我们需要对其进行解析,获取其中想要的结果才行,解析的语句与DSL查询的结果对应关系如下:

解析的结果的语句如下:

java 复制代码
    @Test
    void testSuggest() throws IOException{
        //1. 准备Request
        SearchRequest request = new SearchRequest("hotel");
        //2. 准备DSL
        request.source().suggest(new SuggestBuilder().addSuggestion(
                "suggestions",
                SuggestBuilders.completionSuggestion("suggestion")
                        .prefix("hu")
                        .skipDuplicates(true)
                        .size(10)
        ));
        //3. 发起请求
        SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
        //4. 解析结果
        Suggest suggest = response.getSuggest();
        //4.1 根据补全查询名称,获取补全结果
        CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
        //4.2 获取options
        List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
        //4.3 遍历
        for (CompletionSuggestion.Entry.Option option : options) {
            String text = option.getText().toString();
            System.out.println(text);
        }
    }

处理后的结果输出如下:

相关推荐
瓜牛_gn27 分钟前
依赖注入注解
java·后端·spring
一元咖啡2 小时前
SpringCloud Gateway转发请求到同一个服务的不同端口
spring·spring cloud·gateway
天天扭码2 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
java亮小白19973 小时前
Spring循环依赖如何解决的?
java·后端·spring
跳跳的向阳花3 小时前
03-03、SpringCloud第三章,负载均衡Ribbon和Feign
spring cloud·ribbon·负载均衡
苏-言3 小时前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
草莓base4 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Mephisto.java4 小时前
【大数据学习 | Spark】Spark的改变分区的算子
大数据·elasticsearch·oracle·spark·kafka·memcache
mqiqe4 小时前
Elasticsearch 分词器
python·elasticsearch