Spring Boot 集成Elastic Search

这篇文章是将Spring Boot与ES的集成和ES API放在一起,方便大家理解和记忆;文章中是常用的API,大家可以去看官方文档自行拓展。

文档操作快速入门

1、依赖

这里引入的依赖要和安装ES版本一致

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>

2、客户端配置

@Configuration
public class EsConfig {
    //请求测试项,比如es添加了安全访问规则,访问es需要添加一个安全头,就可以通过requestOptions设置
    //官方建议把requestOptions创建成单实例
    public static final RequestOptions COMMON_OPTIONS;

    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();

        COMMON_OPTIONS = builder.build();
    }

    @Bean
    public RestHighLevelClient esRestClient() {

        RestClientBuilder builder = null;
        // 可以指定多个es
        //第一给参数为主机名
        builder = RestClient.builder(new HttpHost("xxx.xxx.xxx.xxx", 9200, "http"));

        RestHighLevelClient client = new RestHighLevelClient(builder);
        return client;
    }
}

3、测试基本功能

1)新增文档

  • ES API

    # kibana输入
    PUT /test/_doc/2
    {
      "name": "Lily",
      "age": 14
    }
    或 POST /test/_doc/2
    {
      "name": "Lily",
      "age": 14
    }
    
    #输出
    {
      "_index" : "test",
      "_type" : "_doc",
      "_id" : "2",
      "_version" : 3,
      "result" : "updated",
      "_shards" : {
        "total" : 2,
        "successful" : 1,
        "failed" : 0
      },
      "_seq_no" : 3,
      "_primary_term" : 1
    }
    
  • java代码

        @Test
        public void index() throws IOException {
            //设置索引和id
            IndexRequest indexRequest = new IndexRequest("test");
            indexRequest.id("2");
    
            User user = new User();
            user.setName("Lily");
            user.setAge(15);
            String jsonString = JSON.toJSONString(user);
            //设置要存储的数据
            indexRequest.source(jsonString, XContentType.JSON);
            //调用索引接口
            client.index(indexRequest, EsConfig.COMMON_OPTIONS);
        }
    

2)查询文档——已知id查询,但一般都用条件查询

  • ES API

    GET /test/_doc/2查询结果:

    {
      "_index" : "test",
      "_type" : "_doc",
      "_id" : "2",
      "_version" : 22,
      "_seq_no" : 32,
      "_primary_term" : 3,
      "found" : true,
      "_source" : {
        "name" : "Lily",
        "age" : 14
      }
    }
    
    
  • java代码

        @Test
        public void get() throws IOException {
            //设置索引
            GetRequest getRequest = new GetRequest();
            getRequest.id("2");
            getRequest.index("test");
            //获取结果
            GetResponse fields = client.get(getRequest, EsConfig.COMMON_OPTIONS);
            String source = fields.getSourceAsString();
            User user = JSON.parseObject(source, User.class);
            System.out.println(user.toString());
        }
    

3)修改文档

  • ES API

    POST /test/_doc/2
    {
      "name": "Lily",
      "age": 100
    }
    或使用 _update ,但是要使用 doc ,将修改的字段包起来,比较麻烦,不建议使用
    POST /test/_update/2
    {
      "doc": {
        "age": 101
      }
    }
    
  • java 代码

           //设置索引
            UpdateRequest updateRequest = new UpdateRequest("test", "2");
    # json 已经不兼容了,会报错
    //        User user = new User();
    //        user.setName("hh");
    //        String jsonString = JSON.toJSONString(user);
    //        updateRequest.doc(XContentType.JSON, jsonString);
    # 可以使用map
            HashMap<String, Object> map = new HashMap<>();
            map.put("age", "199");
            updateRequest.doc(map);
    
            UpdateResponse update = client.update(updateRequest, EsConfig.COMMON_OPTIONS);
            System.out.println(update.toString());
    

4、总结

  • post 和 put 都可以做新增和修改,但是ES语法上和对文档的影响有区别
    • put操作需指定 id , post 做新增时不需要指定,会自动生成id;
    • 修改时:
      • put为文档全量替换,需要将文档所有数据提交,原文档将会被删除;
      • post带_update 就是局部替换,可以大大减少网络传输次数和流量,提升性能
    • 综上,可以在新增时使用put或post,修改用post
  • java代码上在理解的基础上很好记忆与restful风格呼应,查找用GetRequest,删除用DeleteRequest,修改用UpdateRequest,新增用IndexRequest;

进阶——search搜索入门

1、无条件查询

GET /test/_search

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {      #分片信息
    "total" : 1,     #分片个数
    "successful" : 1,#查询分片成功个数
    "skipped" : 0,
    "failed" : 0     #查询分片失败个数
  },
  "hits" : {         #命中信息
    "total" : {     
      "value" : 2,   #命中个数
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [        #命中记录的详细信息
      {
        "_index" : "test",
        "_type" : "user",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "name" : "小芳",
          "age" : 101
        }
      },
      {
        "_index" : "test",
        "_type" : "user",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "name" : "小明",
          "age" : 10
        }
      }
    ]
  }
}

2、简单条件查询

DSL语句介绍:

  • match_all
  • match
  • match_phrase: 类似于sql中的like
  • 名词.keyword :完全匹配
  • multi_match:多个查询目标字段共用同一个查询条件
  • term:精确查询,默认查询的目标字段不分词,查询条件要和查询目标的字段完全匹配
  • nested
GET /test/_search
{
  "query":{
    "match_all": {}, 
  //或者
  //"match":{
    //"name": "李",
    //"age": 10
    //}
    },
    "sort": [        #排序字段,是个数组
      {"age": "desc"}],
   "from": 0,        #分页
   "size": 1,
    "_source":["age"] #指定查询结果的字段
}
public void simpleSearch() throws IOException {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("test");
    # 指定查询条件, quey\sort\from\size 是同一级别,是SearchSourceBuilder的方法
        #再下一层级,就使用对应的构造器 QueryBuildersSortBuilders
        
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        searchSourceBuilder.from(0).size(10);
        searchSourceBuilder.sort(SortBuilders.fieldSort("age").order(SortOrder.ASC));

        searchRequest.source(searchSourceBuilder);
        SearchResponse search = client.search(searchRequest, EsConfig.COMMON_OPTIONS);
        System.out.println(search);
    }

3、query/bool/must复合查询

复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

  • must:必须达到must所列举的所有条件

  • must_not:必须不匹配must_not所列举的所有条件。

  • should:应该满足should所列举的条件。满足条件最好,不满足也可以,满足得分更高,不影响查询的结果;

    如果没有must条件,就按照should的条件来

  • filter:过滤

GET /test_index/_search
{
    "query": {   #"query" 是 Elasticsearch 查询的顶级查询部分。
        "bool": {# "bool" 是一个布尔查询,可组合多个查询条件,包括"must"、"should"、"must_not"等 
            "must": { "match":{ "gender": "男" }}, #性别必须是男的
            "should": [
                { "match":{ "age": 18 }},     # 最好 年龄是18岁,是就加分,不是也行
                { "bool": {
                    "must":{ "match": { "personality": "good" }}, #最好 性格好,不能粗鲁
                    "must_not": { "match": { "rude": true }}
                }}
            ],
            "minimum_should_match": 1 #是指定在 "should" 查询子句中至少要满足几个条件的参数,这里设置为 1,表示至少满足一个 "should" 子句中的条件即可。即:年龄和性格条件至少有一个符合
        }
    }
}

3.1 Java代码

   public void multiSearch(){
        //查询请求
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("test");
        //构建查询DSL
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //bool查询构造器
        BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        //must\should\must not
        boolQueryBuilder.must(QueryBuilders.matchQuery("gender", "男"));
        boolQueryBuilder.should(QueryBuilders.matchQuery("age", 18));
        BoolQueryBuilder builder2 = new BoolQueryBuilder();
        builder2.must(QueryBuilders.matchQuery("personality", "good"));
        builder2.mustNot(QueryBuilders.matchQuery("rude", "yes"));
        boolQueryBuilder.should(builder2);
        boolQueryBuilder.minimumShouldMatch(1);
        //query bool must
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);

        client.search(searchRequest,, EsConfig.COMMON_OPTIONS);
    }

聚合入门

聚合提供了从数据中分组和提取数据的能力。所以我们要做的就是先查出数据,再进行分析。

1.1 语法

  • terms:看值的可能性分布,会合并锁查字段,给出计数
  • avg:看值的分布平均
 # 简单聚合
"aggs":{

    "aggs_name":{ # 这次聚合的名字,方便展示在结果集中

        "AGG_TYPE":{} # 聚合的类型(avg,terms,filter)

     }
}
#子聚合
{
  "aggs": {
    "NAME": {
      "AGG_TYPE": {},
      "aggs": { #子聚合和父聚合的“Agg_type”对齐,语法和父聚合的语法一样
        "NAME": {
          "AGG_TYPE": {}
        }
      }
    }
  }
}
#嵌入式聚合
{
  "aggs" : { //取了名字以后声明是聚合
    "<aggregation-name>" : { 
      "nested" : {
        "path" : "<nested-object-path>"
      },
      "aggs" : {
        "<nested-aggregation-name>": {
          "<aggregation-type>" : { <aggregation-properties> },
          "aggs": { //嵌入也可以有子聚合
             
            }
          }
        }
      }
    }
  }
}

1.2 示例

GET /test/_search
{
  "query": {"match_all": {}}
  , "aggs": {
    "ageAgg": {
      "terms": {         #“terms”是查看分布,查看age的分布
        "field": "age", 
        "size": 10
      }
      , "aggs": {         
        "heightAgg": {
          "avg": {       #“avg”是查看平均值,查看每个age下的身高平均值
            "field": "height"
          }
        }
      }
    },
    "phones":{
      "nested": {
        "path": "phones"
      },
      "aggs": {
        "phone": {
          "terms": {
            "field": "phones.phone",
            "size": 10
          }
        }
      }
    }
  },
  "size": 0  #query的查询结果不展示
}

1.3 Java代码

 @Test
    public void aggregation() throws IOException {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("test");

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("ageAgg")
                .field("age")
                .size(10)
                .subAggregation(AggregationBuilders.avg("heightAgg").field("height"));

        NestedAggregationBuilder nestedAggregationBuilder = AggregationBuilders.nested("phonesAgg", "phones")
                .subAggregation(AggregationBuilders.terms("phoneAgg").field("phones.phone").size(10));
        searchSourceBuilder.aggregation(aggregationBuilder)
                .aggregation(nestedAggregationBuilder);
        searchRequest.source(searchSourceBuilder);
        System.out.println(searchRequest);
        client.search(searchRequest, EsConfig.COMMON_OPTIONS);
    }

映射

映射指定了文档中字段的存储类型、分词器、是否被索引等,是一种数据结构。可以手动和自动创建。

简单来说,映射指定了文档字段的存储和索引。

1.字段类型

1.1核心类型

  • 字符串:text and keyword
  • 数值型:byte,short,integer,long,float,double
  • ture or false :boolean
  • 日期: date

在Elasticsearch中,keyword 类型的字段默认不进行分词。keyword 类型适用于精确匹配的场景,通常用于存储关键字、标签、ID等不需要分词的文本信息。当你将字段映射为 keyword 类型时,Elasticsearch会将整个字段作为一个单独的项进行索引,而不会进行分词处理。

如果只是需要精确匹配、排序和聚合,那么使用 keyword 类型可能更合适。

1.2 嵌入式类型

https://blog.csdn.net/qq_42200163/article/details/113704087 可以看一下这个文章,有详细讲解

nested:嵌套对象,,用于数组中的元素是对象的[{}, {}],该nested类型是object数据类型的专用版本,它允许可以彼此独立地查询它们的方式对对象数组进行索引。

总结来说:es自动的映射会把对象数组的层次打平,找不到对象的属性之间的联系,所以需要用 nested 类型来定义对象数组。nested 类型的对象数组在存储每一个对象时,类似于 es 对文档的存储,保留了属性之间的联系。

1.2.1 嵌入式 类型的定义 PUT test/_mapping
{
  "properties":{
    "phones": {
      "type": "nested",
       "properties":{
         "phone": {
           "type": "integer"
          },
         "addr": {
           "type": "text",
           "analyzer": "english"
          }
        }
    }
  }
}
1.2.1 嵌入式 类型的查询和聚合方式
POST /test/_search
{
  "query": {
    "nested": {
      "path": "phones",
      "query": {
        "bool": {
          "must": [ //这里 must 是一个数组
            {
              "match": {
                "phones.phone": "19975262233"
              }
            },
            {
              "match": {
                "phones.addr": "湖北宜昌"
              }
            }
          ]
        }
      }
    }
  }
}

1.3 地理类型

经纬度: geo_point

详细操作请看: 其他相关ES操作及介绍请参考《ElasticSearch6.5.4快速入门》

2.分词器

作用:切分词语,normalization(提升recall召回率),能够搜索到更多的结果;

2.1种类

  • standard analyzer标准分词器:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是standard)

  • simple analyzer简单分词器:set, the, shape, to, semi, transparent, by, calling, set, trans

  • whitespace analyzer:Set, the, shape, to, semi-transparent, by, calling, set_trans(5)

  • language analyzer(特定的语言的分词器,比如说,english,英语分词器):set, shape, semi, transpar, call, set_tran, 5

所有的语言分词,默认使用的都是“Standard Analyzer”,但是这些分词器针对于中文的分词,并不友好。为此需要安装中文的分词器。

2.2 中文分词器安装

官网:https://github.com/medcl/elasticsearch-analysis-ik

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases

根据es版本下载相应版本包。

解压到 es/plugins/ik中。

重启es

3.查看映射

GET /test/_mapping

{#自动生成的映射
  "test" : {
    "mappings" : {
      "properties" : {
        "age" : {
          "type" : "long" #age存储整数型
        },
        "name" : {
          "type" : "text", #name存储文本
          "fields" : {
            "keyword" : {        #支持精确查询
              "type" : "keyword",#当查询为name.keyword,该字段需要完全匹配到。
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

4.手动创建映射 PUT /test1

{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "age": {
        "type": "long"
      },
      "height": {
        "type": "integer"
      },
      //手动创建的映射,可以将objects类型的数组设置为嵌入式类型,这样objects类型的数组就不会被打平,而是
      //作为独立的的一个对象
      "friends":{
        "type": "nested",
        "properties": {
          "name":{
            "type": "text"
          },
          "gender": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

5.修改映射

5.1添加字段映射 PUT test/_mapping

 {
  "properties":{
    "phones": {
      "type": "nested",
       "properties":{
         "phone": {
           "type": "integer"
          },
         "addr": {
           "type": "text",
           "analyzer": "english"
          }
        }
    }
  }
}

5.2 修改字段映射

  1. 创建新的索引映射: 创建一个新的索引映射,将 skuPrice 字段的类型更改为 keyword

    PUT your_new_index
    {
      "mappings": {
        "your_type": {
          "properties": {
            "phone": {
              "type": "keyword"
            },
            // 其他字段的映射...
          }
        }
      }
    }
    
  2. 重新索引: 使用 Elasticsearch 的 Reindex API 将数据从旧索引复制到新索引。

    POST _reindex
    {
      "source": {
        "index": "your_old_index"
      },
      "dest": {
        "index": "your_new_index"
      }
    }
    
  3. 切换别名: 如果你使用别名来引用索引,可以通过切换别名指向新的索引来实现平滑的过渡。

    POST /_aliases
    {
      "actions": [
        { "remove": { "index": "your_old_index", "alias": "your_alias" } },
        { "add": { "index": "your_new_index", "alias": "your_alias" } }
      ]
    }
    
  4. 删除旧索引(可选): 如果确认数据迁移成功,你可以删除旧的索引。

    DELETE your_old_index
    

实战

1.根据需求建立索引

在这里插入图片描述

需求:根据关键字、分类id、品牌id、属性id能检索出数据,有价格、库存的筛选条件,并且分页

​ 页面展示信息: 图片、价格、title,品牌、分类等信息

es中的数据结构:

{
  skuId
  spuId
  skuTitle
  hasStock
  price
  
  brandId
  brandPicture
  brandName
  
  categoryID
  categoryName
  
  attrs:[
  {
  attrId
  attrName
  attrValue:[]
  },
  {
  attrId
  attrName
  attrValue:[] 
  }
  ]  
}

故索引为:

  • “index”: false 不建立索引, 就不支持query和sort,但是可以被过滤和聚合
  • “doc_values” : false,不可以被聚合
PUT test_productt
{
  "mappings": {
    "properties": {
      "spuId":{
        "type":"long"
      },
      "skuId": {
        "type": "long"
      },
      "skuImage":{
        "type": "keyword",
        "index": false,      
        "doc_values" : false
      },
      "skuPrice":{
        "type": "keyword"
      },
      "hotScore":{
        "type": "long"
      },
      "saleCount":{
        "type": "long"
      },
      "brandId":{
        "type": "long"
      },
      "brandImage":{
        "type": "keyword"
      },
      "brandName":{
        "type": "keyword"
      },
      "categoryId":{
        "type": "long"
      },
      "categoryName":{
        "type": "keyword"
      },
      "attrs":{
        "type": "nested",
        "properties": {
          "attrId":{
            "type": "long"
          },
          "attrName":{
            "type": "keyword"
          },
          "attrValue":{
            "type": "keyword"
          }
        }
      }
    }
  }
}

2.将数据上传到es

/**
	 * 上架商品
	 */
@PostMapping("/product") // ElasticSaveController
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels){

    boolean status;
    try {
        status = productSaveService.productStatusUp(skuEsModels);
    } catch (IOException e) {
        log.error("ElasticSaveController商品上架错误: {}", e);
        return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
    }
    if(!status){
        return R.ok();
    }
    return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}

/**
	 * 将数据保存到ES
	 * 用bulk代替index,进行批量保存
	 * BulkRequest bulkRequest, RequestOptions options
	 */
@Override // ProductSaveServiceImpl
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {
    // 1. 批量保存
    BulkRequest bulkRequest = new BulkRequest();
    // 2.构造保存请求
    for (SkuEsModel esModel : skuEsModels) {
        // 设置es索引 gulimall_product
        IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
        // 设置索引id
        indexRequest.id(esModel.getSkuId().toString());
        // json格式
        String jsonString = JSON.toJSONString(esModel);
        indexRequest.source(jsonString, XContentType.JSON);
        // 添加到文档
        bulkRequest.add(indexRequest);
    }
    // bulk批量保存
    BulkResponse bulk = client.bulk(bulkRequest, GuliESConfig.COMMON_OPTIONS);
    // TODO 是否拥有错误
    boolean hasFailures = bulk.hasFailures();
    if(hasFailures){
        List<String> collect = Arrays.stream(bulk.getItems()).map(item -> item.getId()).collect(Collectors.toList());
        log.error("商品上架错误:{}",collect);
    }
    return hasFailures;
}

3. 检索,封装返回数据

{
  "query":{
    "bool": {
      "must": [
        {"match": {
          "skuTitle": "华为"
        }}
      ],
      "filter": [
        {
        "term": {
          "brandId": "2"
        }
      },
      {
        "term": {
          "hasStock": "false"
        }
      },
       {
          "range": {
            "skuPrice": {
              "gte": 6800,
              "lte": 7800
            }
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "term":{
                "attrs.attrValue": "海思(Hisilicon)"
              }
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "size": 1,
  "aggs": {
    "brandAgg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brandNameAgg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brandImageAgg": {
          "terms": {
            "field": "brandImage",
            "size": 10
          }
        }
      }
    },
    "attrs":{
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attrsAgg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attrNameAgg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              },
              "aggs": {
                "attrValueAgg": {
                  "terms": {
                    "field": "attrs.attrValue",
                    "size": 10
                  }
                }
              }
            }
            
        }
      }
    }
  }
  }
  
  
}

java创建DSL语句

 public void buildRequest() throws IOException {

        // 创建查询请求
        SearchRequest searchRequest = new SearchRequest("test_productt"); // 替换为你的索引名称
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

        // 构建查询部分
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        boolQuery.must(new TermQueryBuilder("skuTitle", "华为"));
        boolQuery.filter(new TermQueryBuilder("brandId", "4"));
        boolQuery.filter(new TermQueryBuilder("categoryId", "9"));
        boolQuery.filter(new TermQueryBuilder("hasStock", "false"));
        boolQuery.filter(new RangeQueryBuilder("skuPrice").gte(10).lte(200));

        NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery(
                "attrs",
                new TermQueryBuilder("attrs.attrValue", "200"),
                ScoreMode.None
        );
        boolQuery.filter(nestedQuery);

        sourceBuilder.query(boolQuery);

        // 构建排序部分
        sourceBuilder.sort("skuPrice", SortOrder.DESC);

        // 设置分页
        sourceBuilder.from(0);
        sourceBuilder.size(10);

        // 构建聚合部分
        TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brandAgg")
                .field("brandId")
                .size(10)
                .subAggregation(
                        AggregationBuilders.terms("brandNameAgg").field("brandName").size(10)
                ).subAggregation(AggregationBuilders.terms("brandImageAgg").field("brandImage").size(10));

        TermsAggregationBuilder categoryAgg = AggregationBuilders.terms("categoryAgg")
                .field("categoryId")
                .size(10)
                .subAggregation(
                        AggregationBuilders.terms("categoryNameAgg").field("categoryName").size(10)
                );

        NestedAggregationBuilder attrsAgg = AggregationBuilders.nested("attrs", "attrs")
                .subAggregation(
                        AggregationBuilders.terms("attrsAgg").field("attrs.attrId").size(10)
                                .subAggregation(
                                        AggregationBuilders.terms("attrNameAgg").field("attrs.attrName").size(10)
                                ).subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue").size(10))
                );

        sourceBuilder.aggregation(brandAgg);
        sourceBuilder.aggregation(categoryAgg);
        sourceBuilder.aggregation(attrsAgg);

        // 将 SearchSourceBuilder 添加到 SearchRequest
        searchRequest.source(sourceBuilder);

        // 执行查询
        SearchResponse searchResponse = client.search(searchRequest, EsConfig.COMMON_OPTIONS);
        System.out.println(searchResponse);
        // 处理查询结果
        // 可以从 searchResponse 中提取查询结果、聚合结果和其他信息

    }

封装返回数据,将es中的数据封装

ES返回:

{
  "took" : 17,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "test_productt",
        "_type" : "_doc",
        "_id" : "46",
        "_score" : null,
        "_source" : {
          "attrs" : [
            {
              "attrId" : 1,
              "attrName" : "入网参数",
              "attrValue" : "4G"
            },
            {
              "attrId" : 2,
              "attrName" : "上市年份",
              "attrValue" : "2021"
            },
            {
              "attrId" : 3,
              "attrName" : "颜色",
              "attrValue" : "极光蓝"
            },
            {
              "attrId" : 9,
              "attrName" : "电池容量",
              "attrValue" : "3500mAh"
            },
            {
              "attrId" : 10,
              "attrName" : "机身长度(mm)",
              "attrValue" : "168"
            },
            {
              "attrId" : 7,
              "attrName" : "CPU型号",
              "attrValue" : "麒麟990"
            },
            {
              "attrId" : 8,
              "attrName" : "CPU工艺",
              "attrValue" : "5nm"
            },
            {
              "attrId" : 12,
              "attrName" : "CPU品牌",
              "attrValue" : "海思(Hisilicon)"
            }
          ],
          "brandId" : 2,
          "brandImg" : "",
          "brandName" : "华为",
          "catalogId" : 225,
          "catalogName" : "手机2",
          "hasStock" : false,
          "hotScore" : 0,
          "saleCount" : 0,
          "skuId" : 46,
          "skuImg" : "",
          "skuPrice" : 6800.0,
          "skuTitle" : "华为mate60 6G 套餐一 12GB+256GB 白 1000*1000",
          "spuId" : 19
        },
        "sort" : [
          "6800.0"
        ]
      }
    ]
  },
  "aggregations" : {
    "brandAgg" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : 2,
          "doc_count" : 4,
          "brandNameAgg" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [
              {
                "key" : "华为",
                "doc_count" : 4
              }
            ]
          },
          "brandImageAgg" : {
            "doc_count_error_upper_bound" : 0,
            "sum_other_doc_count" : 0,
            "buckets" : [ ]
          }
        }
      ]
    },
    "attrs" : {
      "doc_count" : 32,
      "attrsAgg" : {
        "doc_count_error_upper_bound" : 0,
        "sum_other_doc_count" : 0,
        "buckets" : [
          {
            "key" : 1,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "入网参数",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "4G",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 2,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "上市年份",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "2021",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 3,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "颜色",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "极光蓝",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 7,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "CPU型号",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "麒麟990",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 8,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "CPU工艺",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "5nm",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 9,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "电池容量",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "3500mAh",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 10,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "机身长度(mm)",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "168",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          },
          {
            "key" : 12,
            "doc_count" : 4,
            "attrNameAgg" : {
              "doc_count_error_upper_bound" : 0,
              "sum_other_doc_count" : 0,
              "buckets" : [
                {
                  "key" : "CPU品牌",
                  "doc_count" : 4,
                  "attrValueAgg" : {
                    "doc_count_error_upper_bound" : 0,
                    "sum_other_doc_count" : 0,
                    "buckets" : [
                      {
                        "key" : "海思(Hisilicon)",
                        "doc_count" : 4
                      }
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    }
  }
}

根据返回结果将数据封装为:

 private SearchResult buildSearchResult(SearchParam request, SearchResponse searchResponse) {
        SearchResult searchResult = new SearchResult();

        //1.包装skuModules
        SearchHits hits = searchResponse.getHits();
        if(hits.getHits() != null && hits.getHits().length > 0 ){
            ArrayList<SkuEsModel> skuEsModels = new ArrayList<>();
            for (SearchHit hit : hits) {
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel skuEsModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
                //设置高亮
                if(!StringUtils.isEmpty(request.getKeyword())){
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String highLight = skuTitle.getFragments()[0].string();
                    skuEsModel.setSkuTitle(highLight);
                }
                skuEsModels.add(skuEsModel);
            }
            searchResult.setProduct(skuEsModels);
        }

        //2.封装分页信息
        //2.1总记录数
        searchResult.setTotal(hits.getTotalHits().value);

        //2.2 页数
        long pageNum = hits.getTotalHits().value % EsConstant.PAGE_SIZE == 0 ? hits.getTotalHits().value % EsConstant.PAGE_SIZE :
                hits.getTotalHits().value % EsConstant.PAGE_SIZE + 1;
        searchResult.setPageNum((int)pageNum);

        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= pageNum; i++) {
            pageNavs.add(i);
        }

        //3.取出聚合信息
        //3.1 品牌
        List<SearchResult.BrandVo> brandVoList = new ArrayList<>();
        Aggregations aggregations = searchResponse.getAggregations();
        //ParsedLongTerms用于接收terms聚合的结果,并且可以把key转化为Long类型的数据
        ParsedLongTerms brandIdAgg = aggregations.get("brandIdAgg");
        for (Terms.Bucket bucket : brandIdAgg.getBuckets()) {
            long brandId = bucket.getKeyAsNumber().longValue();
            ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brandImgAgg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandNameAgg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo(brandId, brandImg, brandName);
            brandVoList.add(brandVo);
        }
        searchResult.setBrands(brandVoList);
        //3.2 分类
        ArrayList<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalogAgg = aggregations.get("catalogAgg");
        for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
            long catalogId = bucket.getKeyAsNumber().longValue();
            Aggregations subCatalogAgg = bucket.getAggregations();
            ParsedStringTerms catalogNameAgg = subCatalogAgg.get("catalogNameAgg");
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            catalogVo.setCatalogId(catalogId);
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }
        searchResult.setCatalogs(catalogVos);

        //3.3 属性
        List<SearchResult.AttrVo>  attrVos = new ArrayList<>();
        ParsedNested attrsAgg = aggregations.get("attrs");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");
        SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            long attrId = bucket.getKeyAsNumber().longValue();
            Aggregations subAggregations = bucket.getAggregations();
            ParsedStringTerms attrNameAgg = subAggregations.get("attrNameAgg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            ParsedStringTerms attrValueAgg = subAggregations.get("attrValueAgg");
            ArrayList<String> values = new ArrayList<>();
            for (Terms.Bucket attrValueAggBucket : attrValueAgg.getBuckets()) {
                String value = attrValueAggBucket.getKeyAsString();
                values.add(value);
            }
            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(values);
            attrVos.add(attrVo);
        }
        searchResult.setAttrs(attrVos);
        return searchResult;
    }

总结

1、语法固然要熟悉,但是和学习mysql类似,在实战中要注意字段的类型、分词器、是否索引和聚合等属性的设置,要兼顾性能和存储。

2、ES又和Redis相似,使用的时候要注意ES和Mysql的一致性,在涉及到更新数据的时候,要保持双方 的一致性。

  • 45
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值