跳到主要内容

ElasticSearch全文检索

基本概念

  1. 索引index

动词,相当于mysql的insert

名词,相当于mysql的databse

  1. 类型type

在index索引中,可以定义一个或者多个类型

类似于mysql的table,每一种类型的数据放在一起

  1. 文档Document

文档是json格式

  1. 倒排索引

image-20240126093019794

Docker安装ES环境

docker pull elasticsearch:7.17.17 #存储和检索数据
docker pull kibana:7.17.17 #可视化检索数据

创建挂载目录~/tools/docker-volumes/elasticsearch

  • config文件夹,里面放一个elasticsearch.yml文件,写入http.host: 0.0.0.0注意冒号后面要加空格
  • data文件夹

运行容器

docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v ~/tools/docker-volumes/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v ~/tools/docker-volumes/elasticsearch/data:/usr/share/elasticsearch/data \
-v ~/tools/docker-volumes/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.17.17

访问:http://localhost:9200/,看到下面界面成功

image-20240126102933596

运行kibana:

docker run --name kibana -e ELASTICSEARCH_HOSTS="http://192.168.1.11:9200" -p 5601:5601 \
-d kibana:7.17.17

注意:

ip不可以使用localhost,否则会一直出现Kibana server is not ready yet

使用ifconfig查看本机ip

初步检索

  1. cat
GET http://localhost:9200/_cat/nodes 查看所有节点
GET http://localhost:9200/_cat/health 查看es健康状况
GET http://localhost:9200/_cat/master 查看主节点信息
GET http://localhost:9200/_cat/indices 查看所有的索引
  1. 索引文档(保存)POST/PUT
PUT http://localhost:9200/customer/external/1
{
"name": "John Doe"
}

PUT 和 POST 都可以,

  • POST 新增。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号PUT 可以新增可以修改。
  • PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改操作,不指定 id 会报错。
  1. 查询文档GET
{
"_index": "customer", //在哪个索引
"_type": "external", //在哪个类型
"_id": "1", //记录 id
"_version": 2, //版本号
"_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1, //同上,主分片重新分配,如重启,就会变化
"found": true,
"_source": { //真正的内容
"name": "John Doe"
}
}
  1. 更新文档POST/PUT
POST customer/external/1/_update
{
"doc": {
"name": "John Doew"
}
}

或者:

POST customer/external/1
{
"name": "John Doe2"
}

POST 操作会对比源文档数据,如果相同不会有什么操作,文档version 不增加

PUT 操作总会将数据重新保存并增加 version 版本;

带_update 对比元数据如果一样就不进行任何操作

或者:

PUT customer/external/1
{
"name": "John Doe"
}
  1. 删除DELETE

删除文档

DELETE customer/external/1

删除索引

DELETE customer 
  1. bulk批量API
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }

使用kibana

image-20240126174728873

复杂操作:

POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123" } }
{ "doc" : {"title" : "My updated blog post"} }

image-20240126174959213

测试数据:

https://github.com/elastic/elasticsearch/blob/7.4/docs/src/test/resources/accounts.json

POST /bank/account/_bulk
+数据

进阶检索

SearchAPI

  • 请求方式检索:
GET bank/_search?q=*&sort=account_number:asc
  • uri+请求体进行检索
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": {
"order": "desc"
}
},
{
"balance": "desc"
}
]
}

Query DSL

基本语法:

GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"balance": {
"order": "desc"
}
}
],
"from": 1,
"size": 3,
"_source": ["balance","firstname"]
}

说明:

  1. GET bank/_search: 这是Elasticsearch中执行搜索操作的基本语法。正在请求bank索引中的文档。
  2. "query": {"match_all": {}}: 这个查询部分指定了搜索的条件。在这里,使用了match_all查询,表示你想要匹配所有文档,即检索所有数据。
  3. "sort": [{"balance": {"order": "desc"}}]: 这个部分用于对结果进行排序。希望按照balance字段降序排序,这意味着余额最高的文档将排在前面。
  4. "from": 1: 这个参数指定了从搜索结果的第几条文档开始返回。在这里,从第二条文档开始返回(因为Elasticsearch使用0-based索引)。
  5. "size": 3: 这个参数定义了返回的文档数量,即最多返回3条匹配的文档。
  6. "_source": ["balance", "firstname"]: 这个参数用于指定返回结果中包含的字段。在这里,只关心balancefirstname字段的值,其他字段将不会包含在结果中。

match匹配查询:

  • 基本类型,非字符串精确匹配
GET /bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
  • 字符串,全文检索(倒排索引)计算相关性得分
GET /bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
}
  • 字符串,分词检索
GET /bank/_search
{
"query": {
"match": {
"address": "mill road"
}
}
}
  • match_phrase短语匹配
GET /bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}

和上面的区别是:

  • match_phrase 匹配 必须包含mill road
  • match匹配 mill或者road或者mill road
  • multi_match 多字段匹配
GET /bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["state","address"]
}
}
}
  • bool复合查询

must必须达到的所有条件

GET /bank/_search
{
"query": {
"bool": {
"must": [
{"match": {"address": "mill"}},
{"match": {"gender": "M"}}
]
}
}
}

should:应该达到的条件,符合会加分

GET /bank/_search
{
"query": {
"bool": {
"must": [
{"match": {"address": "mill"}},
{"match": {"gender": "M"}}
],
"should": [
{"match": {
"address": "lane"
}}
]
}
}
}

must_not必须不是指定的情况:

GET /bank/_search
{
"query": {
"bool": {
"must": [
{"match": {"address": "mill"}},
{"match": {"gender": "M"}}
],
"should": [
{"match": {
"address": "lane"
}}
],
"must_not": [
{"match": {
"email": "baluba.com"
}}
]
}
}
}
  • filter结果过滤
GET /bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "mill"
}}
],
"filter": [
{"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}}
]
}
}
}
  • term,匹配某个属性的值,精确匹配

全文检索字段用match,其他非text用term

GET /bank/_search
{
"query": {
"bool": {
"must": [
{"term": {
"age": {
"value": "28"
}
}
}
]
}
}
}
  • aggregations执行聚合
GET /bank/_search
{
"query": {
"match": {
"address": "mill"
}},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg":{
"avg": {
"field": "age"
}
},
"balanceAvg":{
"avg": {
"field": "balance"
}
}
},
"size": 0
}
  1. "aggs": {...}: 这是聚合部分,用于对结果进行汇总分析。
    • "ageAgg": {...}: 这是一个名为ageAgg的聚合,使用了terms聚合,它将文档按照age字段的值分组,并且设置了size为10,表示只返回前10个分组。
    • "ageAvg": {...}: 这是计算age字段的平均值的聚合。
    • "balanceAvg": {...}: 这是计算balance字段的平均值的聚合。
  2. "size": 0: 这个参数表示不返回文档,只返回聚合结果。

复杂聚合:

查年龄分布和这个年龄的平均薪资

GET /bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"ageAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}

查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资

GET /bank/_search
{
"query": {"match_all": {}}
, "aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword",
"size": 10
}
,
"aggs": {
"balanceAgg": {
"avg": {
"field": "balance"
}
}
}
},
"ageBalanceAvg":{
"avg": {
"field": "balance"
}
}
}
}
}
}

Mapping映射

创建时可以指定字段的类型

PUT /my_index
{
"mappings": {
"properties": {
"age":{
"type": "integer"
},
"email":{
"type": "keyword"
},
"name":{
"type": "text"
}
}
}
}

添加新的映射:

PUT /my_index/_mapping
{
"properties":{
"employee_id":{
"type":"keyword",
"index":false
}
}
}

更新:

索引不可以更新,只可以重新创建

数据迁移:

POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}

分词器

POST _analyze
{
"analyzer": "standard",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}

这种分词器只能对英文有用,需要安装中文分词器ik

https://github.com/medcl/elasticsearch-analysis-ik/releases

image-20240127173227626

将下载好的文件放到自己电脑挂载的plugins目录下面,如果版本不一致可以修改 plugin-descriptor.properties文件中的最后一行。

改为自己的elasticsearch版本,此时可以进行分词

POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}

使用docker安装nginx

html/es目录下面创建fenci.txt文件

编辑配置:

image-20240128161920804

修改分词文件位置:

image-20240128162015634

此时重启elasticsearch

Elasticsearch整合springboot

导入maven依赖:

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

注意需要更换版本

<elasticsearch.version>7.17.17</elasticsearch.version>

创建配置类官网

@Configuration
public class GulimallElasticSearchConfig {

public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}

@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.1.11", 9200, "http")));
return client;
}
}

测试,检索数据:

    @Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
// indexRequest.source("userName","zhangsan","age",18,"gender","男");
User user = new User();
user.setUserName("zhangsan");
user.setAge(18);
user.setGender("男");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);


}
@Data
class User{
private String userName;
private Integer age;
private String gender;
}

复杂搜索:

    @Test
public void searchData() throws Exception {
//创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//构建检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));//匹配查询
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
searchSourceBuilder.aggregation(ageAgg);//聚合查询
searchSourceBuilder.aggregation(balanceAvg);//聚合查询
System.out.println("检索条件" + searchSourceBuilder.toString());

searchRequest.source(searchSourceBuilder);

//执行检索
SearchResponse searchResponse = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(searchResponse.toString());
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String sourceAsString = hit.getSourceAsString();
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println("account = " + account);
}

//获取分析数据
Aggregations aggregations = searchResponse.getAggregations();
for (Aggregation aggregation : aggregations.asList()) {
String name = aggregation.getName();
System.out.println("当前聚合name = " + name);
}
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄 = " + keyAsString);
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资 = " + balanceAvg1.getValue());
}

商品上架

@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}

逻辑:

    @Override
public void up(Long spuId) {
//1.查出当前spuId对应的所有sku信息,品牌的名字
List<SkuInfoEntity> skuInfoEntities = skuInfoService.getSkuInfoBySpuId(spuId);

//todo 4.查询当前sku的所有可以被用来检索的规格属性
List<ProductAttrValueEntity> baseAttr = productAttrValueService.baseAttrlistforspu(spuId);
List<Long> attrIds = baseAttr.stream().map(attr -> {
return attr.getAttrId();
}).collect(Collectors.toList());
List<Long> searchAttrIds = attrService.selectSearchAttrIds(attrIds);
HashSet<Long> idSet = new HashSet<>(searchAttrIds);


List<SkuEsModel.Attrs> attrsList = baseAttr.stream().filter(item -> {
return idSet.contains(item.getAttrId());
}).map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());


//todo 1.发送远程调用 ,查询库存系统是否有库存 hasStock
Map<Long, Boolean> collect = null;
try {
List<Long> skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
R<List<SkuHasStockVo>> skuHasStock = wareFeignService.getSkuHasStock(skuIdList);
collect = skuHasStock.getData()
.stream().collect(Collectors.toMap(SkuHasStockVo::getSkuId, SkuHasStockVo::getHasStock));
} catch (Exception e) {
log.error("库存服务查询异常,原因:{}",e);
}


//2.封装每个sku的信息
Map<Long, Boolean> finalCollect = collect;
List<SkuEsModel> upProducts = skuInfoEntities.stream().map(sku -> {
SkuEsModel esModel = new SkuEsModel();
BeanUtils.copyProperties(sku, esModel);
//skuPrice,skuImg,hasStock,hotScore,brandName,brandImg,catalogName,attrs
// public static class Attr {
// private Long attrId;
// private String attrName;
// private String attrValue;
// }
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
if (finalCollect ==null){
esModel.setHasStock(true);
}else{
esModel.setHasStock(finalCollect.get(sku.getSkuId()));
}
//todo 2.热度评分 0
esModel.setHotScore(0L);
//todo 3.查询品牌和分类的名字信息
BrandEntity brand = brandService.getById(esModel.getBrandId());
esModel.setBrandName(brand.getName());
esModel.setBrandImg(brand.getLogo());
CategoryEntity category = categoryService.getById(esModel.getCatalogId());
esModel.setCatalogName(category.getName());
esModel.setAttrs(attrsList);
return esModel;
}).collect(Collectors.toList());
// todo 5.将数据发送给es进行保存
R r = searchFeignService.productStatusUp(upProducts);
if (r.getCode() == 0) {
//远程调用成功
//todo 6.修改当前spu的状态
this.baseMapper.updateSpuStatus(spuId, ProductConstant.StatusEnum.SPU_UP.getCode());
} else {
//远程调用失败
//todo 7.重复调用?接口幂等性,重试机制
}

}

ElasticSearchController

@RequestMapping("/search/save")
@RestController
@Slf4j
public class ElasticSaveController {

@Autowired
private ProductSaveService productSaveService;

//上架商品
@PostMapping("/product")
public R productStatusUp(@RequestBody List<SkuEsModel> skuEsModels) {
try {
boolean b = productSaveService.productStatusUp(skuEsModels);
if (b) {
return R.ok();
} else {
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
} catch (IOException e) {
log.error("ElasticSaveController controller商品上架错误:{}", e);
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
}
}
}

逻辑实现:

@Service
@Slf4j
public class ProductSaveServiceImpl implements ProductSaveService {

@Autowired
private RestHighLevelClient restHighLevelClient;

@Override
public boolean productStatusUp(List<SkuEsModel> skuEsModels) throws IOException {

//保存到es
//1.给es中建立索引。product,建立好映射关系

//2.给es中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
indexRequest.id(skuEsModel.getSkuId().toString());
String jsonString = JSON.toJSONString(skuEsModel);
indexRequest.source(jsonString, XContentType.JSON);
bulkRequest.add(indexRequest);
}
BulkResponse bulk = restHighLevelClient.bulk(bulkRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//TODO 如果批量错误
boolean b = bulk.hasFailures();
List<String> collect = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
log.error("商品上架错误:{}",collect);
return b;
}
}

检索服务

导入页面到search目录下面,编辑host文件加上最后一行

image-20240131193252288

nginx修改:

    server_name  *.gulimall.com;

配置网关:

        - id: gulimall_host_route # gulimall.com
uri: lb://gulimall-product
predicates:
- Host=gulimall.com
- id: gulimall_search_route # search.gulimall.com
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com

修改mapping映射

//PUT gulimall_product
{
"mappings": {
"properties": {
"skuId": {
"type": "long"
},
"spuId": {
"type": "long"
},
"skuTitle": {
"type": "text",
"analyzer": "ik_smart"
},
"skuPrice": {
"type": "keyword"
},
"skuImg": {
"type": "keyword"
},
"saleCount": {
"type": "long"
},
"hosStock": {
"type": "boolean"
},
"hotScore": {
"type": "long"
},
"brandId": {
"type": "long"
},
"catelogId": {
"type": "long"
},
"brandName": {
"type": "keyword"
},
"brandImg": {
"type": "keyword"
},
"catalogName": {
"type": "keyword"
},
"attrs": {
"type": "nested",
"properties": {
"attrId": {
"type": "long"
},
"attrName": {
"type": "keyword"
},
"attrValue": {
"type": "keyword"
}
}
}
}
}
}

构建检索dsl语句:

//GET gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"1",
"2",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"valu e": "9"
}
}
},
{
"terms": {
"attrs.attrValue": [
"高通",
"海思"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "true"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6500
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 5,
"highlight": {
"fields": {
"skuTitle": {}
},
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 10
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName",
"size": 10
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg",
"size": 10
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId",
"size": 10
},
"aggs": {
"catalog_name_agg": {
"terms": {
"field": "catalogName",
"size": 10
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 100
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
  1. 查询条件
    • 使用bool查询,包含must(必须匹配)和filter(过滤)子句。
    • match子句匹配包含关键字"华为"的skuTitle字段。
    • term子句过滤catalogId为"225"的商品。
    • terms子句过滤brandId为"1"、"2"或"9"的商品。
    • nested子句对attrs字段进行嵌套查询,要求attrs.attrId为"9",并且attrs.attrValue为"高通"或"海思"。
    • term子句过滤hasStock字段为"true"的商品。
    • range子句过滤skuPrice在0到6500之间的商品。
  2. 排序
    • 结果按照skuPrice字段降序排列。
  3. 分页
    • 从搜索结果的第0条记录开始,获取5条记录。
  4. 高亮显示
    • 对匹配的skuTitle字段进行高亮显示,用红色标签。
  5. 聚合(Aggregations)
    • brand_agg聚合按照brandId字段进行分组,同时计算每个分组内的品牌名称和品牌图片。
    • catalog_agg聚合按照catalogId字段进行分组,同时计算每个分组内的目录名称。
    • attr_agg聚合对attrs字段进行嵌套分组,计算每个属性(attrId)下的属性名称(attrName)和属性值(attrValue)。

转化为Java 代码:

@Service
@Slf4j
public class MallSearchServiceImpl implements MallSearchService {

@Autowired
private RestHighLevelClient restHighLevelClient;

@Autowired
private ProductFeignService productFeignService;

@Override
public SearchResult search(SearchParam param) {
SearchRequest searchRequest = buildSearchRequest(param);
SearchResult searchResult = null;
try {
SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
searchResult = buildSearchResult(response, param);
} catch (IOException e) {
throw new RuntimeException(e);
}
return searchResult;
}

private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
SearchResult searchResult = new SearchResult();
//1.返回的所有查询到的商品
SearchHits hits = response.getHits();
List<SkuEsModel> esModels = new ArrayList<>();
if (hits.getHits() != null && hits.getHits().length > 0) {
for (SearchHit hit : hits.getHits()) {
String sourceAsString = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);
//判断是否按照关键字搜索,如果是,高亮显示
if (!StringUtils.isEmpty(param.getKeyword())) {
String skuTitle = hit.getHighlightFields().get("skuTitle").getFragments()[0].string();
esModel.setSkuTitle(skuTitle);
}
esModels.add(esModel);
}
}
searchResult.setProducts(esModels);

//2.当前所有商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attrAgg = response.getAggregations().get("attr_agg");
ParsedLongTerms attrIdAgg = attrAgg.getAggregations().get("attrId_agg");
for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//属性id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);

//属性名
ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrName_agg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);

//属性值
ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValue_agg");
List<String> attrValues = attrValueAgg.getBuckets().stream()
.map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
searchResult.setAttrs(attrVos);

//3.当前所有商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
//获取到品牌的聚合
ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brandAgg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

//1、得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
brandVo.setBrandId(brandId);

//2、得到品牌的名字
ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brandName_agg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(brandName);

//3、得到品牌的图片
ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brandImg_agg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandImg(brandImg);

brandVos.add(brandVo);
}
searchResult.setBrands(brandVos);

//4.当前所有商品涉及到的所有分类信息
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
//1、得到分类的id
long catalogId = bucket.getKeyAsNumber().longValue();
catalogVo.setCatalogId(catalogId);

//2、得到分类的名字
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalogName_agg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalogName);

catalogVos.add(catalogVo);
}
searchResult.setCatalogs(catalogVos);

//5.分页信息
searchResult.setPageNum(param.getPageNum());
//5.1 当前页码
long value = hits.getTotalHits().value;
searchResult.setTotal(value);
//5.2 总记录数
int totalPage = (int) (value % EsConstant.PRODUCT_PAGESIZE == 0 ?
(int) value / EsConstant.PRODUCT_PAGESIZE : (int) value / EsConstant.PRODUCT_PAGESIZE + 1);
searchResult.setTotalPages(totalPage);


//5.3 总页码
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPage; i++) {
pageNavs.add(i);
}
searchResult.setPageNavs(pageNavs);

//6、构建面包屑导航
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
//1、分析每一个attrs传过来的参数值
SearchResult.NavVo navVo = new SearchResult.NavVo();
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
if (r.getCode() == 0) {
AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
});
navVo.setNavName(data.getAttrName());
} else {
navVo.setNavName(s[0]);
}

//2、取消了这个面包屑以后,我们要跳转到哪个地方,将请求的地址url里面的当前置空
//拿到所有的查询条件,去掉当前
String encode = null;
try {
encode = URLEncoder.encode(attr, "UTF-8");
encode.replace("+", "%20"); //浏览器对空格的编码和Java不一样,差异化处理
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = param.get_queryString().replace("&attrs=" + attr, "");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);

return navVo;
}).collect(Collectors.toList());

searchResult.setNavs(collect);
}


return searchResult;
}

private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
/**
* 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
*/
//1、构建检索请求
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

//1.1、must-模糊匹配
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
//1.2 bool - filter - term
if (param.getCatalog3Id() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}

//1.2.2 brandId
if (param.getBrandId() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("brandId", param.getBrandId()));
}

//1.2.3 attrs
if (param.getAttrs() != null && !param.getAttrs().isEmpty()) {
param.getAttrs().forEach(item -> {
//attrs=1_5寸:8寸 & attrs=2_16G:8G
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
String[] split = item.split("_");
String attrId = split[0];
String[] attrValues = split[1].split(":");
boolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", boolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
});
}

//1.2.4 hasStock
if (param.getHasStock() != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("hsStock", param.getHasStock() == 1));
}

//1.2.5 skuPrice
if (!StringUtils.isEmpty(param.getSkuPrice())) {
//skuPrice=1_500 skuPrice=500_ skuPrice=_500
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] split = param.getSkuPrice().split("_");
if (split.length == 2) {
rangeQuery.gte(split[0]).lte(split[1]);
} else if (split.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQuery.lte(split[0]);
} else {
rangeQuery.gte(split[0]);
}
}
boolQueryBuilder.filter(rangeQuery);
}
//封装所有的查询条件
searchSourceBuilder.query(boolQueryBuilder);

/**
* 排序,分页,高亮
*/
//排序
//形式为sort=hotScore_asc/desc
if (!StringUtils.isEmpty(param.getSort())) {
String[] sort = param.getSort().split("_");
searchSourceBuilder.sort(sort[0], "asc".equals(sort[1]) ? SortOrder.ASC : SortOrder.DESC);
}
//分页
searchSourceBuilder.from((param.getPageNum() - 1) * EsConstant.PRODUCT_PAGESIZE);
searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

//高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
searchSourceBuilder.highlighter(highlightBuilder);
}

/**
* 聚合分析
*/
//品牌聚合
TermsAggregationBuilder brandAgg = AggregationBuilders.terms("brand_agg");
brandAgg.field("brandId").size(50);

brandAgg.subAggregation(AggregationBuilders.terms("brandName_agg").field("brandName").size(1));
brandAgg.subAggregation(AggregationBuilders.terms("brandImg_agg").field("brandImg").size(1));
searchSourceBuilder.aggregation(brandAgg);

//分类聚合
TermsAggregationBuilder catalogAgg = AggregationBuilders.terms("catalog_agg");
catalogAgg.field("catalogId").size(50);
catalogAgg.subAggregation(AggregationBuilders.terms("catalogName_agg").field("catalogName").size(1));
searchSourceBuilder.aggregation(catalogAgg);

//属性聚合
NestedAggregationBuilder nested = AggregationBuilders.nested("attr_agg", "attrs");
//按照属性id聚合
TermsAggregationBuilder attrIdAgg = AggregationBuilders.terms("attrId_agg").field("attrs.attrId");
nested.subAggregation(attrIdAgg);
//在每个attrId下按照attrValue聚合
attrIdAgg.subAggregation(AggregationBuilders.terms("attrValue_agg").field("attrs.attrValue").size(50));
//在每个attrId下再聚合attrName
attrIdAgg.subAggregation(AggregationBuilders.terms("attrName_agg").field("attrs.attrName").size(1));
searchSourceBuilder.aggregation(nested);

log.info("构建的DSL语句:{}", searchSourceBuilder.toString());
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX}, searchSourceBuilder);
return searchRequest;
}


}

控制器:

@GetMapping("/list.html")
private String listPage(SearchParam param, Model model, HttpServletRequest request) {
param.set_queryString(request.getQueryString());
SearchResult result = mallSearchService.search(param);
model.addAttribute("result", result);
return "list";
}