elasticsearch 搜索引擎工具的高级使用
聚合
聚合功能可以将数据进行统计,例如:叫小明/小红/小蓝/...的有多少人;叫小明/小红/小蓝/...的人平均年龄是多少
类似于MySQL中的group by,常见的聚合有以下三种:
Bucket:对文档数据分组,并统计每组数量
Metric:对文档数据做计算,例如avg
Pipeline:基于其他聚合结果再做聚合
不过,我们需要注意的是,参与聚合的字段类型无法是被可分词的字段,例如text
GET /lsty/_search
{
"size": 0,
"aggs": {
"brandAgg": { #自己取的聚合名称,按照brand字段进行分组就是Bucket聚合
"terms": {
"field": "brand",
"size": 20,
"order": {
"scoreAgg.avg": "desc" #按照子聚合scoreAgg的avg值降序排序
}
},
"aggs": { #brandAgg聚合的子聚合,也就是brandAgg分组后对每组分别计算
"scoreAgg": { #自己取的聚合名称
"stats": { #聚合方法,还可以是avg,max等,stats可以计算count,min,max,avg,sum几种信息
"field": "score" #score为聚合字段
}
}
}
},
"priceAgg": { #此处聚合我为了和scoreAgg子聚合做对比,这两聚合都属于Metric聚合
"stats": {
"field": "price"
}
}
}
}
使用JavaRestClient进行聚合,查找
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.List;
@SpringBootTest
public class HotelAggTest {
private RestHighLevelClient client;
@Test
void testAggregation() throws IOException {
SearchRequest request = new SearchRequest("lsty");
request.source().size(0); //我们只需要聚合数据,无需正常的查询数据,所以size设置0
request.source().aggregation(
AggregationBuilders
.terms("brandAgg") //terms表示精确查询,brandAgg是对本次聚合取名
.field("brand") //对brand字段进行聚合
.size(20) //最多读取20条信息
);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response);
}
//输出数据处理
private static void handleResponse(SearchResponse response) {
Aggregations aggregations = response.getAggregations();
Terms brandTerms = aggregations.get("brandAgg");
List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
for (Terms.Bucket bucket:buckets){
System.out.println(bucket.getKeyAsString());
}
}
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("192.168.230.100:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
自定义分词器和拼音分词器
为了方便用户体验,除了IK这种中文分词器,我们还会使用pinyin分词器
安装方式和IK分词器一样,将文件放入plugins文件夹然后重启ES即可
我们需要将IK分词器和pinyin分词器配合使用,所以我们直接自定义分词器,将二者结合
首先我们需要知道,分词器分为三部分:
character filters:对文本进行处理,例如替换,删除文字
tokenizer:对文本进行分词。IK就是这部分
tokenizer filter:将tokenizer分词后的词条进一步处理,大小写转换,拼音处理,同义词处理。pinyin分词器就是这部分
我们在创建索引库时配置了自定义分词器,这里我们只设置了tokenizer和tokenizer filter两部分
PUT /test
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": { #自己取得自定义分词器名
"tokenizer": "ik_max_word", #配置tokenizer
"filter": "pinyin" #配置tokenizer filter
}
}
}
}
}
上述的配置中,pinyin分词器会有些问题,所以我们采用如下的方式,配置pinyin分词器。
有些时候,我们需要建立索引库时和查询关键字时,所用的分词器不一样。
例如因为有同音字的存在,我们不希望在搜索时进行分词,只希望在建立索引库时进行分词。这样在搜索中文时,就不会出现所搜到同音字的情况。
那么我们可以设置索引字段的search_analyzer属性,来调整搜索关键字时的分词器。如果没设置,会使用analyzer属性的分词器,都没设置则使用默认的分词器。
PUT /lsty
{
"settings": {
"analysis": {
"analyzer": {
"text_anlyzer": { #自己取得自定义分词器名
"tokenizer": "ik_max_word",
"filter": "py"
},
"completion_analyzer": {
"tokenizer": "keyword",
"filter": "py"
}
},
"filter": { #自定义tokenizer filter
"py": { #过滤名称
"type": "pinyin", #过滤器类型
"keep_full_pinyin": false, #是否保留每个汉字的完整拼音
"keep_joined_full_pinyin": true, #是否加入每个词组的完整拼音,例如刘德华——liudehua
"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": "text_anlyzer", #设置创建索引时使用的分词器
"search_analyzer": "ik_smart" #设置搜索时使用的分词器
},
"address":{
"type": "keyword",
"index": false #是否创建索引
},
"suggestion":{
"type": "completion",
"analyzer": "completion_analyzer" #该字段类型因为是个数组,数组中我用的是brand这种无需分割的元素,所以分词器中的tokenizer属性我们选择keyword
}
}
}
}
自动补全
自动补全的字段有些要求,必须是completion类型,而且字段内容是数组
PUT test #创建索引库
{
"mappings": {
"properties": {
"title":{
"type": "completion"
}
}
}
}
POST test/_doc #插入数据
{
"title": ["Sony", "WH-1000XM3"]
}
POST test/_doc
{
"title": ["SK-II", "PITERA"]
}
POST test/_doc
{
"title": ["Nintendo", "switch"]
}
自动补全查询
POST /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", #查询的关键字
"completion": {
"field": "title", #补全查询的字段
"skip_duplicates": true, #跳过重复的字段
"size": 10 #查询前10条数据
}
}
}
}
利用JAVA进行操作
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.List;
@SpringBootTest
public class HotelSuggestTest {
private RestHighLevelClient client;
@Test
void testSuggest() throws IOException {
SearchRequest request = new SearchRequest("lsty"); //查询的索引库
request.source().suggest(new SuggestBuilder().addSuggestion(
"hotelSuggestion", //取一个查询名,用于后面取查询结果
SuggestBuilders.completionSuggestion("suggestion") //suggestion为字段名
.prefix("slt") //查询的关键字
.skipDuplicates(true) //去除重复信息
.size(20)
));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//处理查询结果
Suggest suggest = response.getSuggest();
CompletionSuggestion suggestion = suggest.getSuggestion("hotelSuggestion");
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
for (CompletionSuggestion.Entry.Option option:options){
System.out.println(option.getText().toString());
}
}
//与ES创建链接
@BeforeEach
void setUp(){
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.230.100:9200")
));
}
//断开与ES的链接
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}