这篇文章是将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的方法
#再下一层级,就使用对应的构造器 QueryBuilders、SortBuilders
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 修改字段映射
-
创建新的索引映射: 创建一个新的索引映射,将
skuPrice
字段的类型更改为keyword
。PUT your_new_index { "mappings": { "your_type": { "properties": { "phone": { "type": "keyword" }, // 其他字段的映射... } } } }
-
重新索引: 使用 Elasticsearch 的 Reindex API 将数据从旧索引复制到新索引。
POST _reindex { "source": { "index": "your_old_index" }, "dest": { "index": "your_new_index" } }
-
切换别名: 如果你使用别名来引用索引,可以通过切换别名指向新的索引来实现平滑的过渡。
POST /_aliases { "actions": [ { "remove": { "index": "your_old_index", "alias": "your_alias" } }, { "add": { "index": "your_new_index", "alias": "your_alias" } } ] }
-
删除旧索引(可选): 如果确认数据迁移成功,你可以删除旧的索引。
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的一致性,在涉及到更新数据的时候,要保持双方 的一致性。