文章目录
- 前言
- 一、前置知识
- 1、Elasticsearch 的结构
- 2、倒排索引 (Inverted Index)
- 2.1、 索引阶段
- 2.2、查询阶段
- 二、环境准备
- 1、安装Es
- 2、安装Kibana
- 3、安装 ik 分词器
- 三、项目整合
- 1、引入依赖
- 2、整合业务
- 2.1、创建索引、文档、构建查询语句
- 2.2、整合业务代码
- 后记
前言
本篇介绍谷粒商城项目检索服务,从搭建es环境到商城检索业务的实现。(不考虑freeMarker模版中的jquery部分)
对应视频:P173-P192
一、前置知识
1、Elasticsearch 的结构
同传统的关系型数据库进行类比:
索引 (Index):
相当于关系数据库中的表,由多个文档
组成。文档 (Document):
文档是 Elasticsearch 中存储的基本数据单位,相当于关系数据库中的一行。文档以JSON
格式存储。每个文档属于一个特定的索引,并具有唯一的 ID。映射 (Mapping):
类似于数据库中的表结构定义,定义了文档的字段及其数据类型。(DDL建表语句)
2、倒排索引 (Inverted Index)
在倒排索引中,每个词项都关联到一个倒排列表(Posting List),该列表存储着包含这个词项的所有文档的 ID。倒排索引的构建和查询主要分为以下两个阶段:
2.1、 索引阶段
当文档被添加到系统中时,首先会进行文档解析(分词),然后将每个词项添加到倒排索引的词典中。词典存储的是文档中所有唯一词项的列表。最后对于每个词项,记录该词项在哪些文档中出现,存储这些文档的 ID 以及该词项在文档中的位置(可选)。这些信息称为倒排列表。
例如我现在有两个文档:文档一:布偶猫吃鱼
和文档二:加菲猫吃鱼
,分词后可以得到以下倒排索引(假设目前使用的是ik分词器):
词 | 文档ID |
---|---|
布偶 | 1 |
加菲猫 | 2 |
猫 | 1,2 |
吃鱼 | 1,2 |
2.2、查询阶段
首先会进行分词,布偶猫吃鱼
会得到词项【布偶
,猫
,吃鱼
】,加菲猫吃鱼
会得到词项【加菲猫
,吃鱼
】。然后根据分出的词去查找其文档ID:
布偶
的文档ID是1。猫
的文档ID是1,2。吃鱼
的文档ID是1,2。加菲猫
的文档ID是2。
最后找到同时包含所有查询词项的文档,例如我要搜索布偶猫吃鱼
,文档1会作为结果返回。
二、环境准备
1、安装Es
在本项目中采用docker安装es的方式。es和kibana均采用7.4.2
版本
首先执行:
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
然后执行:
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
这条命令的含义是,在 /mydata/elasticsearch/config/elasticsearch.yml
文件的末尾添加一行配置,内容是http.host: 0.0.0.0
,会允许服务通过任何 IP 地址访问Elasticsearch 。
chmod -R 777 /mydata/elasticsearch/
这条命令的含义是,修改指定目录下的所有文件和子目录的权限。777
表示赋予所有用户(文件的所有者、同组用户、其他用户)读取(r)
、写入(w)
和 执行(x)
的权限。(权限的 777 是通过组合 rwx(读、写、执行)的权限位来得到的:7 = r + w + x = 4 + 2 + 1
,即读、写、执行权限都有。)
最后执行:
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
- docker run:
启动一个新的 Docker 容器。
---name elasticsearch:
给容器命名为 elasticsearch。
--p 9200:9200 -p 9300:9300:
-p 选项用于将主机的端口与容器内的端口进行映射。9200:9200:将主机的 9200 端口映射到容器的 9200 端口(Elasticsearch 默认的 HTTP REST API 端口)。9300:9300:将主机的 9300 端口映射到容器的 9300 端口(Elasticsearch 默认的内部节点通信端口)。
- -e "discovery.type=single-node":
-e 选项用于设置环境变量。“discovery.type=single-node”:指定 Elasticsearch 以单节点模式运行,即不需要集群节点的发现(通常用于开发或测试环境)。
--e ES_JAVA_OPTS="-Xms64m -Xmx512m":
ES_JAVA_OPTS 是用于配置 JVM 的选项。-Xms64m:设置 JVM 的最小堆内存为 64MB。-Xmx512m:设置 JVM 的最大堆内存为 512MB。
--v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:
-v 选项用于将主机上的目录或文件挂载到容器中。将主机上 /mydata/elasticsearch/config/elasticsearch.yml 文件挂载到容器内的 /usr/share/elasticsearch/configelasticsearch.yml,用于替换容器中的默认配置文件。
- -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:
-v 选项用于将主机上的目录或文件挂载到容器中。将主机上/mydata/elasticsearch/config/elasticsearch.yml
文件挂载到容器内的 /usr/share/elasticsearch/config/elasticsearch.yml
,用于替换容器中的默认配置文件。
--v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins:
将主机上的/mydata/elasticsearch/plugins
目录挂载到容器的/usr/share/elasticsearch/plugins
目录,用于存储和管理 Elasticsearch 插件。
- -d elasticsearch:7.4.2:
-d 选项让容器在后台运行(守护模式)。elasticsearch:7.4.2:指定要使用的 Elasticsearch 镜像及其版本号 7.4.2。
2、安装Kibana
Kibana相当于es的可视化界面和控制台:
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://自己的虚拟机地址:9200 -p 5601:5601 \
-d kibana:7.4.2
- docker run:
启动一个新的 Docker 容器。
---name kibana:
给容器命名为kibana。
-e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200:
指定 Kibana 连接的 Elasticsearch 集群地址为 http://192.168.56.10:9200。
-p 5601:5601:
5601:5601:将主机的 5601 端口映射到容器的 5601 端口。
-d kibana:7.4.2:
指定使用 7.4.2 版本的 Kibana 镜像,并使容器在后台运行。
3、安装 ik 分词器
将ik分词器拷贝到/mydata/elasticsearch/plugins/ik/
下,重启es容器,可使用下面的命令验证是否安装成功:
curl -X GET "http://localhost:9200/_cat/plugins?v"
三、项目整合
1、引入依赖
在gulimall-search
模块中引入依赖:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.4.2</version>
</dependency>
后续在项目中使用,只需要注入RestHighLevelClient
即可
@Autowired
private RestHighLevelClient client;
2、整合业务
2.1、创建索引、文档、构建查询语句
在分布式基础篇的后台管理系统中,点击上架,会将商品信息保存在gulimall_product
索引中,gulimall_product
的映射:(注意,分布式基础篇中,映射是product
,和现在的gulimall_product
有所不同,需要进行数据迁移)
{
"gulimall_product" : {
"mappings" : {
"properties" : {
"attrs" : {
"type" : "nested",
"properties" : {
"attrId" : {
"type" : "long"
},
"attrName" : {
"type" : "keyword"
},
"attrValue" : {
"type" : "keyword"
}
}
},
"brandId" : {
"type" : "long"
},
"brandImg" : {
"type" : "keyword"
},
"brandName" : {
"type" : "keyword"
},
"catalogId" : {
"type" : "long"
},
"catalogName" : {
"type" : "keyword"
},
"hasStock" : {
"type" : "boolean"
},
"hotScore" : {
"type" : "long"
},
"saleCount" : {
"type" : "long"
},
"skuId" : {
"type" : "long"
},
"skuImg" : {
"type" : "keyword"
},
"skuPrice" : {
"type" : "keyword"
},
"skuTitle" : {
"type" : "text",
"analyzer" : "ik_smart"
},
"spuId" : {
"type" : "keyword"
}
}
}
}
}
数据迁移:
# 迁移数据
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gulimall_product"
}
}
在页面上点击搜索时,相当于带着搜索条件去ES中进行检索,并且将检索的结果封装成对象返回给前端页面进行展示,我们需要:
- 对
skuTitle
进行模糊匹配,高亮显示。 - 对
catalogId
,brandId
,attrs
,hasStock
,range
进行过滤。 - 对
skuPrice
进行排序。 - 进行分页。
- 根据上面查询的结果进行聚合分析,按照
brandId
,catalogId
,attrId
,查询出共有的部分。
其中must
和filter
的区别:must
中的条件会参与相关性评分(_score)
的计算。如果多个条件都出现在must
中,满足的条件越多,相关性评分就越高。filter
只用于过滤文档,满足条件的文档会返回,但不会参与相关性评分的计算。因此它比 must 更高效,特别适合用于不需要计算评分的精确匹配或过滤。
什么是相关性评分?
相关性评分是 Elasticsearch 在返回搜索结果时,用来衡量每个文档与查询条件的匹配程度的一个分值。每个文档返回时都有一个 _score 值,表示这个文档与查询的匹配程度。分数越高,意味着这个文档与查询条件越相关。
构建的查询语句:
GET /gulimall_product/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "iphone"
}
}
],
"filter": [
{
"term": {
"catalogId": {
"value": "225"
}
}
},
{
"terms": {
"brandId": [
"8",
"9"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "1"
}
}
},
{
"terms": {
"attrs.attrValue": [
"5G",
"4G"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "false"
}
}
},
{
"range": {
"skuPrice": {
"gte": 4999,
"lte": 5400
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 10,
"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": "catelogName",
"size": 10
}
}
}
},
"attr_agg":{
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id_agg": {
"terms": {
"field": "attrs.attrId",
"size": 10
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName",
"size": 10
}
},
"attr_value_agg":{
"terms": {
"field": "attrs.attrValue",
"size": 10
}
}
}
}
}
}
}
}
对于其中一些关键点的解释:
nested:
用于处理文档中的嵌套字段
,嵌套字段
指的是ES文档中存储的对象类型的数据,而普通的对象的ES中会被扁平化处理,可能会导致错误匹配的现象。而定义一个字段为嵌套类型时,Elasticsearch 会将每个嵌套对象视为独立的小文档,但它仍然与父文档保持关联。
#定义嵌套字段
{
"mappings": {
"properties": {
"attrs": {
"type": "nested",
"properties": {
"attrId": { "type": "integer" },
"attrValue": { "type": "text" }
}
}
}
}
}
```bash
# 查询嵌套字段
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{ "term": { "attrs.attrId": 1 } },
{ "term": { "attrs.attrValue": "红色" } }
]
}
}
}
}
aggs:
是一种用于计算统计信息、汇总数据的功能。可以对查询到的结果进行分析、分组、统计等操作,aggs
还可以嵌套使用,即在一个聚合内部定义另一个聚合:
#先根据 brandId 字段对文档进行分组,然后对每个 brandId 组计算该组中文档的 price 字段的平均值。
{
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
而对于嵌套字段
的聚合,需要进行特殊处理:
# 首先对嵌套字段 attrs 进行聚合,然后对 attrs.attrId 进行 terms 聚合
{
"aggs": {
"nested_attrs": {
"nested": {
"path": "attrs"
},
"aggs": {
"attr_id": {
"terms": {
"field": "attrs.attrId"
}
}
}
}
}
}
2.2、整合业务代码
到这里查询语句就已经构建完毕了,但是还需要将查询语句转化成Java语言,利用RestHighLevelClient
发送请求进行查询并且解析返回结果:
/**
* 商品上架后信息保存在es->根据前端传递的搜索条件构建dsl去es中搜索->解析并封装查询结果给前端页面
* @param searchDTO 条件
* @return
*/
@Override
public SearchVO searchForCondition(SearchDTO searchDTO) {
//构建查询请求
SearchRequest searchRequest = this.getSearchRequest(searchDTO);
SearchVO vo;
try {
//查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("返回的结果"+searchResponse.toString());
//解析查询结果,封装成SearchVO
vo = this.parseSearchRequest(searchResponse,searchDTO);
} catch (Exception e) {
throw new RuntimeException(e);
}
return vo;
}
查询:
private SearchRequest getSearchRequest(SearchDTO searchDTO) {
// 构建查询
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// Bool查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 模糊匹配 skuTitle
this.likeSearch(searchDTO, boolQuery);
//过滤查询
this.filter(searchDTO, boolQuery, searchSourceBuilder);
//分页高亮排序
this.sortPageAndHighlight(searchDTO, searchSourceBuilder);
//对搜索的结果进行聚合分析
this.termsAggregation(searchSourceBuilder);
//测试 打印构建结果
String s = searchSourceBuilder.toString();
System.out.println("构建的 DSL" + s);
//创建搜索请求
SearchRequest searchRequest = new SearchRequest(new
String[]{ESConstants.PRODUCT_INDEX}, searchSourceBuilder);
return searchRequest;
}
标题模糊匹配:
private void likeSearch(SearchDTO searchDTO, BoolQueryBuilder boolQuery) {
String keyword = searchDTO.getKeyword();
if (StringUtils.isNotBlank(keyword)) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword)); //对应es中的must
}
}
过滤查询:
private void filter(SearchDTO searchDTO, BoolQueryBuilder boolQuery, SearchSourceBuilder searchSourceBuilder) {
// catalogId 查询
Long catalog3Id = searchDTO.getCatalog3Id();
if (!ObjectUtils.isEmpty(catalog3Id)) {
boolQuery.filter(QueryBuilders.termsQuery("catalogId", new long[]{catalog3Id})); //对应es中的filter
}
//bool - filter - 按照品牌 id 查询
List<Long> brandId = searchDTO.getBrandId();
if (!CollectionUtils.isEmpty(brandId)) {
boolQuery.filter(QueryBuilders.termsQuery("brandId",
brandId));
}
//bool - filter - 按照所有指定的属性进行查询
List<String> attrs = searchDTO.getAttrs();
if (!CollectionUtils.isEmpty(attrs)) {
BoolQueryBuilder nestedboolQuery = QueryBuilders.boolQuery(); //对应es中的nested
//进行处理
//attr=1_5寸:8寸
for (String attr : attrs) {
String attrId = attr.split("_")[0];
String value = attr.split("_")[1];
// String[] attrValues = s.split(":");
nestedboolQuery.must(QueryBuilders.termQuery("attrs.attrId",
attrId));
nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",
value));
//每一个必须都得生成一个 nested 查询
NestedQueryBuilder nestedQuery =
QueryBuilders.nestedQuery("attrs", nestedboolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
//bool - filter - 按照库存是否有进行查询
if (searchDTO.getHasStock() != null) {
boolQuery.filter(QueryBuilders.termQuery("hasStock",
searchDTO.getHasStock() == 1));
}
//1.2、bool - filter - 按照价格区间
if (!StringUtils.isEmpty(searchDTO.getSkuPrice())) {
//1_500/_500/500_
/**
* "range": {
* "skuPrice": {
* "gte": 0,
* "lte": 6000
* }
* }
*/
RangeQueryBuilder rangeQuery =
QueryBuilders.rangeQuery("skuPrice"); //对应es中的range
String[] s = searchDTO.getSkuPrice().split("_");
if (s.length == 2) {
//区间
rangeQuery.gte(s[0]).lte(s[1]);
} else if (s.length == 1) {
if (searchDTO.getSkuPrice().startsWith("_")) {
rangeQuery.lte(s[0]);
}
if (searchDTO.getSkuPrice().endsWith("_")) {
rangeQuery.gte(s[0]);
}
}
boolQuery.filter(rangeQuery);
}
//把以前的所有条件都拿来进行封装
searchSourceBuilder.query(boolQuery);
}
分页高亮排序:
private void sortPageAndHighlight(SearchDTO searchDTO, SearchSourceBuilder searchSourceBuilder) {
//排序
//sort=hotScore_asc/desc
String sort = searchDTO.getSort();
if (StringUtils.isNotBlank(sort)) {
String sortStr = sort.split("_")[1];
SortOrder order = sortStr.equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
searchSourceBuilder.sort(sort.split("_")[0], order);
}
//分页
searchSourceBuilder.from((searchDTO.getPageNum() - 1) *
ESConstants.PRODUCT_PAGESIZE);
searchSourceBuilder.size(ESConstants.PRODUCT_PAGESIZE);
//高亮
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
HighlightBuilder builder = new HighlightBuilder();
builder.field("skuTitle");
builder.preTags("<b style='color:red'>");
builder.postTags("</b>");
searchSourceBuilder.highlighter(builder);
}
}
聚合分析:
private void termsAggregation(SearchSourceBuilder searchSourceBuilder) {
/**
* 聚合分析
*/
//1、品牌聚合
TermsAggregationBuilder brand_agg =
AggregationBuilders.terms("brand_agg");
brand_agg.field("brandId").size(50);
//品牌聚合的子聚合
brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
/*
1、聚合 brand
*/
searchSourceBuilder.aggregation(brand_agg);
//2、分类聚合 catalog_agg
TermsAggregationBuilder catalog_agg =
AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg"
).field("catalogName").size(1));
/*
2、聚合 catalog
*/
searchSourceBuilder.aggregation(catalog_agg);
//3、属性聚合 attr_agg
NestedAggregationBuilder attr_agg =
AggregationBuilders.nested("attr_agg", "attrs");
//聚合出当前所有的 attrId
TermsAggregationBuilder attr_id_agg =
AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
//聚合分析出当前 attr_id 对应的名字
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
//聚合分析出当前 attr_id 对应的所有可能的属性值 attrValue
attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").
field("attrs.attrValue").size(50));
attr_agg.subAggregation(attr_id_agg);
/*
3、聚合 attr
*/
searchSourceBuilder.aggregation(attr_agg);
}
解析返回结果:
private SearchVO parseSearchRequest(SearchResponse resp, SearchDTO searchDTO) {
SearchVO vo = new SearchVO();
//外层hits
SearchHits hits = resp.getHits();
//里层hits
SearchHit[] hitsArr = hits.getHits();
List<ESPojo> esPojos = Arrays.stream(hitsArr).map(searchHit -> {
//_source
String sourceAsString = searchHit.getSourceAsString();
ESPojo esPojo = JSON.parseObject(sourceAsString, ESPojo.class);
//判断是否按关键字检索,若是就显示高亮,否则不显示
if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
//拿到高亮信息显示标题
HighlightField skuTitle = searchHit.getHighlightFields().get("skuTitle");
String skuTitleValue = skuTitle.getFragments()[0].string();
esPojo.setSkuTitle(skuTitleValue);
}
return esPojo;
}).collect(Collectors.toList());
//返回的所有商品
vo.setProducts(esPojos);
//聚合信息
ParsedLongTerms brandAgg = resp.getAggregations().get("brand_agg");
List<? extends Terms.Bucket> brandAggBuckets = brandAgg.getBuckets();
List<SearchVO.BrandsVO> brandsVOS = brandAggBuckets.stream().map(bucket -> {
SearchVO.BrandsVO brandsVO = new SearchVO.BrandsVO();
//得到品牌id
long brandId = bucket.getKeyAsNumber().longValue();
brandsVO.setBrandId(brandId);
//得到品牌名字
ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
brandsVO.setBrandName(brandName);
//3、得到品牌的图片
ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
brandsVO.setBrandImg(brandImg);
return brandsVO;
}).collect(Collectors.toList());
//封装品牌信息
vo.setBrands(brandsVOS);
ParsedLongTerms catalogAgg = resp.getAggregations().get("catalog_agg");
List<? extends Terms.Bucket> catalogAggBuckets = catalogAgg.getBuckets();
List<SearchVO.CatalogsVO> catalogsVOS = catalogAggBuckets.stream().map(bucket -> {
SearchVO.CatalogsVO catalogsVO = new SearchVO.CatalogsVO();
//得到分类ID
//得到品牌id
long catelogId = bucket.getKeyAsNumber().longValue();
catalogsVO.setCatalogId(catelogId);
//得到品牌名称
ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
catalogsVO.setCatalogName(catalogName);
return catalogsVO;
}).collect(Collectors.toList());
//封装分类信息
vo.setCatalogs(catalogsVOS);
ParsedNested attrsAgg = resp.getAggregations().get("attr_agg");
ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
List<? extends Terms.Bucket> attrsAggBuckets = attrIdAgg.getBuckets();
List<SearchVO.AttrsVo> attrsVoList = attrsAggBuckets.stream().map(bucket -> {
SearchVO.AttrsVo attrsVo = new SearchVO.AttrsVo();
//属性ID
long attrId = bucket.getKeyAsNumber().longValue();
attrsVo.setAttrId(attrId);
//属性名称
ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
attrsVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());
//属性值
ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
attrsVo.setAttrValue(attrValues);
return attrsVo;
}).collect(Collectors.toList());
//封装属性信息
vo.setAttrs(attrsVoList);
//封装分页参数
vo.setPageNum(searchDTO.getPageNum());
int total = (int) resp.getHits().getTotalHits().value;
vo.setTotal(total);
int totalPages = (int)total % ESConstants.PRODUCT_PAGESIZE == 0 ?
(int)total / ESConstants.PRODUCT_PAGESIZE : ((int)total / ESConstants.PRODUCT_PAGESIZE + 1);
vo.setTotalPages(totalPages);
return vo;
}
到这里为止,构建查询语句,查询ES,解析返回结果的操作就完成了。在高亮展示
这一块,有一个很坑的点:如果在对标题进行匹配时,是这样写的:boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword)).fuzziness(Fuzziness.AUTO));
实测高亮展示会失效。因为.fuzziness(Fuzziness.AUTO)
是模糊匹配的一个配置选项,表示自动确定模糊匹配的程度。下面简单说一下它的工作机制:
对于长度较短的词(1到2个字符)
,不进行模糊匹配。对于长度为3到5个字符
的词,允许最多一个字符不同。也就是说,输入的词和索引中的词之间最多可以有一个字符的差异。对于长度超过5个字符
的词,允许最多两个字符不同,即输入的词和索引中的词之间可以有两处字符差异。
为什么加上了会使高亮展示失效?因为模糊匹配会容忍一定的字符变化,比如拼写错误或词形变化。高亮显示依赖于精确匹配的词,只有当查询中的词语与索引中的词精确匹配时,ES才会高亮显示。
后记
本篇主要介绍了ES的组成和基本概念,以及环境搭建,项目业务整合ES。因为项目的重点是后端的逻辑,所以前端模板页面的改造没有写入本篇。在做这个项目之前,去年有曾经专门去看过某马的关于ES的专题教学视频,语法介绍的很详细,当时还跟着敲了一遍。但是工作中至今未遇到使用场景,在做这个项目的时候不出意外地发现几乎全部遗忘了。在这里想说的是,语法并非重点,重点是理解各自项目的业务逻辑,做好笔记,能根据案例和实际的业务场景举一反三。
下一篇:认证服务