搭建页面环境
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atlinxi.gulimall</groupId>
<artifactId>gulimall-search</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall-search</name>
<description>elasticsearch检索服务</description>
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atlinxi.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 引入热启动-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.79</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
所以需要引用spring-cloud-loadbalancer或者降版本-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
关闭thymeleaf缓存
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search
server.port=13000
spring.thymeleaf.cache=false
替换index.html中的内容,并将静态资源全部复制到nginx中
href="
替换为href="/static/search/
,src="
替换为src="/static/search/
修改windows本地hosts文件
192.168.56.10 gulimall.com
192.168.56.10 search.gulimall.com
修改gulimall.conf
server {
listen 80;
server_name gulimall.com *.gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
在gateway模块添加路由至末尾
- id: gulimall_search_route
uri: lb://gulimall-search
predicates:
- Host=search.gulimall.com
product模块的index.html
// href="/static/#" 去掉
<a href="/static/#" ><img src="/static/index/img/img_09.png" onclick="search()" /></a>
window.location.href="/static/http://search.gulimall.com/search.html?keyword="+keyword;
// 将上面的改为
window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;
此时,就可以访问search.gulimall.com
商城检索-检索条件分析
nginx
gulimall.conf
server {
listen 80;
server_name gulimall.com *.gulimall.com;
#charset koi8-r;
#access_log /var/log/nginx/log/host.access.log main;
location /static {
root /usr/share/nginx/html;
}
location / {
proxy_set_header Host $host;
proxy_pass http://gulimall;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
es dsl语句
之前的es索引应该是product,我们在做映射的时候,brandImg、brandName等字段我们只是来看一下,不做聚合和查询,所以当时这些字段的index、doc_values
都为false,我们就得做索引的数据迁移(es不能直接修改映射)。
# 更新映射
# 映射更新之后,之前的数据映射还是不会改变的
# 所以我们建一个新的索引用来迁移
PUT 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"
}
}
}
}
# 将product的数据迁移到gulimall_product
POST _reindex
{
"source": {
"index": "product"
},
"dest": {
"index": "gulimall_product"}
}
# 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
# 如果是嵌入式的属性,查询,聚合,分析都应该用嵌入式的
# dsl完整查询语句
{
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "华为"
}
}
],
"filter": [
{
"term": {
"catalogId": "225"
}
},
{
"terms": {
"brandId": [
"5",
"6",
"7"
]
}
},
{
"nested": {
"path": "attrs",
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId": {
"value": "1"
}
}
},
{
"terms": {
"attrs.attrValue": [
"CET-AL00",
"balabala"
]
}
}
]
}
}
}
},
{
"term": {
"hasStock": {
"value": "false"
}
}
},
{
"range": {
"skuPrice": {
"gte": 0,
"lte": 6000
}
}
}
]
}
},
"sort": [
{
"skuPrice": {
"order": "desc"
}
}
],
"from": 0,
"size": 2,
"highlight": {
"fields": {
"skuTitle": {
}
},
"pre_tags": "<b style='color:yellow'>",
"post_tags": "</b>"
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId",
"size": 100
},
"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": {
"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
}
}
}
}
}
}
}
}
修改commons R类
/**
* Copyright (c) 2016-2019 人人开源 All rights reserved.
*
* https://www.renren.io
*
* 版权所有,侵权必究!
*/
package com.atlinxi.common.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;
import java.util.HashMap;
import java.util.Map;
/**
* 返回数据
*
* @author Mark [email protected]
*
*
* 老师是用泛型的方式封装的data,
* 在feign接口中返回泛型类时,由于java的泛型机制,在实例化之前无法得到具体的类型 ,
* 因此,虽然服务提供方返回的是具体实例的数据,但是在客户端decode时,无法转化为具体的类。
*
* 上面的话看不太懂,翻译成人话就是,feign在被远程调用返回结果的时候,泛型是null
*
* 因为R继承了HashMap,我们写的所有私有属性都没用,只能存键值对,具体原因未知,
*
* public class R<T> extends HashMap<String, Object> {
* private static final long serialVersionUID = 1L;
*
* private T data;
*
* public T getData() {
* return this.data;
* }
*
*
* public void setData(T data) {
* this.data = data;
* }
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R setData(Object data){
put("data",data);
return this;
}
public <T> T getData(TypeReference<T> typeReference) {
Object data = get("data"); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
//利用fastjson进行反序列化
public <T> T getData(String key,TypeReference<T> typeReference) {
Object data = get(key); //默认是map
String jsonString = JSON.toJSONString(data);
T t = JSON.parseObject(jsonString, typeReference);
return t;
}
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
public int getCode(){
return (Integer) this.get("code");
}
}
所有实体类、常量等
package com.atlinxi.gulimall.search.constant;
public class EsConstant {
// 这儿写错了,常量名应该都是大写
public static final String Product_INDEX = "gulimall_product"; // sku数据在es中的索引
public static final Integer Product_PAGESIZE = 2; // 前期为了方便测试,分页只两个
}
// 查询条件实体类
package com.atlinxi.gulimall.search.vo;
import lombok.Data;
import java.util.List;
/**
* 封装页面所有可能传递过来的查询条件
*
* catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2
* &attrs=1_5寸:8寸&attrs=2_16G:8G
*/
@Data
public class SearchParam {
private String keyword; // 页面传递过来的全文匹配关键字
private Long catalog3Id; // 页面传递过来的三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort; // 排序条件
/**
* 好多的过滤条件
* hasStock(是否有货)、skuPrice(区间)、brandId、catalog3Id、attrs
*
* hasStock 0/1
* skuPrice 1_500/_500/500_
* brandId=1
* attrs=1_其他:安卓&attrs=2_5寸:6寸(_前面代表属性,后面代表值,多个值之间用:分割)
*/
private Integer hasStock; // 是否只显示有货 0(无库存) 1(有库存)
private String skuPrice; // 价格区间查询
private List<Long> brandId; // 按照品牌进行查询,可以多选
private List<String> attrs; // 按照属性进行筛选
private Integer pageNum = 1; // 页码
private String _queryString; // url原生的所有查询条件
}
// 返回结果实体类
package com.atlinxi.gulimall.search.vo;
import com.atlinxi.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class SearchResult {
// 查询到的所有商品信息
private List<SkuEsModel> products;
/**
* 分页信息
*/
private Integer pageNum; // 当前页码
private Long total; // 总记录数
private Integer totalPages; // 总页码
private List<Integer> pageNavs;
private List<BrandVo> brands; // 当前查询到的结果所有涉及到的品牌
private List<AttrVo> attrs; // 当前查询到的结果所有涉及到的属性
private List<CatalogVo> catalogs; // 当前查询到的结果所有涉及到的属性
// ==========================以上是返回给页面的所有信息=======================
// 面包屑导航数据
private List<NavVo> navs = new ArrayList<>();
private List<Long> attrIds = new ArrayList<>();
@Data
public static class NavVo{
private String navName;
private String navValue;
private String link;
}
@Data
public static class BrandVo{
private Long brandId;
private String brandName;
private String brandImg;
}
@Data
public static class AttrVo{
private Long attrId;
private String attrName;
private List<String> attrValue;
}
@Data
public static class CatalogVo{
private Long catalogId;
private String catalogName;
}
}
package com.atlinxi.gulimall.search.vo;
import lombok.Data;
@Data
public class AttrResponseVo {
// 所属分类名字
private String catelogName;
// 所属分组名字
private String groupName;
// 三级分类路径
private Long[] catelogPath;
/**
* 属性id
*/
private Long attrId;
/**
* 属性名
*/
private String attrName;
/**
* 是否需要检索[0-不需要,1-需要]
*/
private Integer searchType;
/**
* 属性图标
*/
private String icon;
/**
* 可选值列表[用逗号分隔]
*/
private String valueSelect;
/**
* 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
*/
private Integer attrType;
/**
* 启用状态[0 - 禁用,1 - 启用]
*/
private Long enable;
/**
* 所属分类
*/
private Long catelogId;
/**
* 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
*/
private Integer showDesc;
private Long attrGroupId;
}
package com.atlinxi.gulimall.search.vo;
import lombok.Data;
@Data
public class BrandVo {
private Long brandId;
private String brandName;
}
feign远程调用
package com.atlinxi.gulimall.search.feign;
import com.atlinxi.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("gulimall-product")
public interface ProductFeignService {
// 在写feign接口的时候,请求路径必须是完整的,函数命可以与原服务的函数名不一样
@RequestMapping("/product/attr/info/{attrId}")
R attrInfo(@PathVariable("attrId") Long attrId);
@GetMapping("/product/brand//infos")
R BrandsInfo(@RequestParam("brandIds") List<Long> brandIds);
}
/**
*
* @param attrId
* @return
*/
// search远程调用的时候耗时太长,我们把返回的结果放进缓存
@Cacheable(value = "attr",key = "'attrInfo:' + #root.args[0]")
@Override
public AttrResVo getAttrInfo(Long attrId) {
AttrResVo attrResVo = new AttrResVo();
AttrEntity attrEntity = this.getById(attrId);
BeanUtils.copyProperties(attrEntity,attrResVo);
if (attrEntity.getAttrType()==ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
// 1.设置分组信息
AttrAttrgroupRelationEntity attrgroupRelation = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
if (attrgroupRelation!=null){
attrResVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
if (attrGroupEntity!=null){
attrResVo.setGroupName(attrGroupEntity.getAttrGroupName());
}
}
}
// 2. 设置分类信息
Long catelogId = attrEntity.getCatelogId();
Long[] catelogPath = categoryService.findCatelogPath(catelogId);
CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
if (categoryEntity!=null){
attrResVo.setCatelogPath(catelogPath);
attrResVo.setCatelogName(categoryEntity.getName());
}
return attrResVo;
}
controller
package com.atlinxi.gulimall.search.controller;
import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
@Controller
public class SearchController {
@Autowired
MallSearchService mallSearchService;
/**
* springMVC 自动将页面提交过来的所有请求查询参数封装成指定的对象
* @param param
* @return
*/
@GetMapping("/list.html")
public String listPage(SearchParam param, Model model, HttpServletRequest request){
String queryString = request.getQueryString();
param.set_queryString(queryString);
// 1. 根据传递来的页面的查询参数,去es中检索商品
SearchResult result = mallSearchService.search(param);
model.addAttribute("result",result);
// 我们整合了thymeleaf,所以不用谢templates和.html
return "list";
}
}
启动类
package com.atlinxi.gulimall.search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients(basePackages = "com.atlinxi.gulimall.search.feign")
public class GulimallSearchApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallSearchApplication.class, args);
}
}
service
package com.atlinxi.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.to.es.SkuEsModel;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.search.config.GulimallElasticSearchConfig;
import com.atlinxi.gulimall.search.constant.EsConstant;
import com.atlinxi.gulimall.search.feign.ProductFeignService;
import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.AttrResponseVo;
import com.atlinxi.gulimall.search.vo.BrandVo;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
private RestHighLevelClient client;
@Autowired
private ProductFeignService productFeignService;
// 去es中进行检索
@Override
public SearchResult search(SearchParam param) {
// 1. 动态构建出查询需要的DSL语句
SearchResult result = null;
// 1. 准备检索请求
SearchRequest searchRequest = buildSearchRequest(param);
;
try {
// 2. 执行检索请求
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
// 3. 分析响应数据封装成我们需要的格式
result = buildSearchResult(response, param);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* 构建结果数据
*
* @param response
* @return
*/
private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
SearchResult result = 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())) {
HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
String s = skuTitle.getFragments()[0].toString();
esModel.setSkuTitle(s);
}
esModels.add(esModel);
}
}
result.setProducts(esModels);
// 2. 当前所有商品涉及到的所有属性信息
List<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attr_agg = response.getAggregations().get("attr_agg");
ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
// 得到属性的id
long attrId = bucket.getKeyAsNumber().longValue();
// 得到属性的名字
String attrName = ((ParsedStringTerms) bucket.getAggregations().get("attr_name_agg")).getBuckets().get(0).getKeyAsString();
// 得到属性的所有制值
List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(
item -> {
String keyAsString = ((Terms.Bucket) item).getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrId(attrId);
attrVo.setAttrName(attrName);
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
// 3. 当前所有商品涉及到的所有品牌信息
List<SearchResult.BrandVo> brandVos = new ArrayList<>();
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
for (Terms.Bucket bucket : brand_agg.getBuckets()) {
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
// 得到品牌的id
long brandId = bucket.getKeyAsNumber().longValue();
// 得到品牌的名字
String brandName = ((ParsedStringTerms) bucket.getAggregations().get("brand_name_agg")).getBuckets().get(0).getKeyAsString();
// 得到品牌的图片
String brandImg = ((ParsedStringTerms) bucket.getAggregations().get("brand_img_agg")).getBuckets().get(0).getKeyAsString();
brandVo.setBrandId(brandId);
brandVo.setBrandName(brandName);
brandVo.setBrandImg(brandImg);
brandVos.add(brandVo);
}
result.setBrands(brandVos);
// 4. 当前所有商品涉及到的所有分类信息
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
List<? extends Terms.Bucket> buckets = catalog_agg.getBuckets();
for (Terms.Bucket bucket : buckets) {
SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
// 得到分类id
String keyAsString = bucket.getKeyAsString();
catalogVo.setCatalogId(Long.parseLong(keyAsString));
// 得到分类名
ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
String catalog_name = catalog_name_agg.getBuckets().get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
// 5. 分页信息 - 页码
result.setPageNum(param.getPageNum());
// 分页信息 - 总记录数
long total = hits.getTotalHits().value;
result.setTotal(total);
// 分页信息 - 总页码 - 计算得到
int totalPages = (int) total % EsConstant.Product_PAGESIZE == 0 ? (int) total / EsConstant.Product_PAGESIZE : (int) total / EsConstant.Product_PAGESIZE + 1;
result.setTotalPages(totalPages);
List<Integer> pageNavs = new ArrayList<>();
for (int i = 1; i <= totalPages; i++) {
pageNavs.add(i);
}
result.setPageNavs(pageNavs);
// 6. 构建面包屑导航功能
// 面包屑导航只限于属性,不包括分类和keyword(检索条件)
// 因为我们如果去掉分类或者检索条件的话,属性则无意义
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
SearchResult.NavVo navVo = new SearchResult.NavVo();
// 1. 分析每个attrs传过来的查询参数值
// attrs=1_其他:安卓&attrs=2_5寸:6寸
String[] s = attr.split("_");
navVo.setNavValue(s[1]);
R r = productFeignService.attrInfo(Long.parseLong(s[0]));
result.getAttrIds().add(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 replace = replaceQueryString(param, attr, "attrs");
navVo.setLink("http://search.gulimall.com/list.html?" + replace);
return navVo;
}).collect(Collectors.toList());
// todo 分类,不需要导航取消
result.setNavs(collect);
}
// 品牌,分类
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
List<SearchResult.NavVo> navs = result.getNavs();
SearchResult.NavVo navVo = new SearchResult.NavVo();
navVo.setNavName("品牌");
R r = productFeignService.BrandsInfo(param.getBrandId());
if (r.getCode() == 0) {
List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
});
StringBuffer buffer = new StringBuffer();
String replace = "";
for (BrandVo brandVo : brand) {
buffer.append(brandVo.getBrandName() + ";");
replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
}
navVo.setNavValue(buffer.toString());
navVo.setLink(replace);
}
navs.add(navVo);
}
return result;
}
private String replaceQueryString(SearchParam param, String value, String key) {
String encode = null;
try {
// 中文需要编码
encode = URLEncoder.encode(value, "UTF-8");
encode = encode.replace("+", "%20"); // 浏览器对空格编码和java不一样,浏览器是%20,java是+
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String replace = param.get_queryString().replace("&" + key + "=" + encode, "");
return replace;
}
/**
* 准备检索请求
* <p>
* 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
*
* @return
*/
private SearchRequest buildSearchRequest(SearchParam param) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 构建DSL语句
/**
* 查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
*/
// 1. 构建bool query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 1.1 must 模糊匹配
if (!StringUtils.isEmpty(param.getKeyword())) {
boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
}
// 1.2 bool filter 按照三级分类id查询
if (param.getCatalog3Id() != null) {
boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
}
// 1.2 bool filter 按照品牌id查询
if (param.getBrandId() != null && param.getBrandId().size() > 0) {
boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
}
// 1.2 bool filter 按照所有指定的属性进行查询
if (param.getAttrs() != null && param.getAttrs().size() > 0) {
for (String attrStr : param.getAttrs()) {
// attrs=1_5寸:8寸&attrs=2_16G:8G
BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
// attrs=1_5寸:8寸&
String[] s = attrStr.split("_");
String attrId = s[0]; // 检索的属性id
String[] attrValue = s[1].split(":"); // 这个属性检索用的值
nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue));
// param3 聚合的这些结果以什么方式参与评分
// 在这儿我们先不让它参与评分
// 每一个必须都得生成一个nested查询
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
// 1.2 bool filter 按照是否有库存进行查询
// 字段是用0,1代表,es是用bool值代表
if (param.getHasStock() != null) {
boolQuery.filter(QueryBuilders.termsQuery("hasStock", param.getHasStock() == 1));
}
// 1.2 bool filter 按照价格区间 skuPrice 1_500/_500/500_
if (!StringUtils.isEmpty(param.getSkuPrice())) {
/**
* {
* "range":{
* "skuPrice":{
* "gte":0,
* "lte":6000
* }
* }
* }
*/
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] s = param.getSkuPrice().split("_");
if (s.length == 2) {
// 区间
rangeQuery.gte(s[0]).lt(s[1]);
} else if (s.length == 1) {
if (param.getSkuPrice().startsWith("_")) {
rangeQuery.lte(s[0]);
}
if (param.getSkuPrice().endsWith("_")) {
rangeQuery.gte(s[0]);
}
}
boolQuery.filter(rangeQuery);
}
sourceBuilder.query(boolQuery);
/**
* 排序,分页,高亮,
*/
// 2.1 排序
if (!StringUtils.isEmpty(param.getSort())) {
String sort = param.getSort();
// sort=saleCount_asc/desc
String[] s = sort.split("_");
SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
sourceBuilder.sort(s[0], order);
}
// 2.2 分页
// pageNum:1 from:0 size:2
// pageNum:2 from:2 size:2
// from = (pageNum-1)*size
sourceBuilder.from((param.getPageNum() - 1) * EsConstant.Product_PAGESIZE);
sourceBuilder.size(EsConstant.Product_PAGESIZE);
// 2.3 高亮
if (!StringUtils.isEmpty(param.getKeyword())) {
HighlightBuilder builder = new HighlightBuilder();
builder.field("skuTitle");
builder.preTags("<b style='color:red'>");
builder.postTags("</b>");
sourceBuilder.highlighter(builder);
}
/**
* 聚合分析
*/
// 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));
sourceBuilder.aggregation(brand_agg);
// 2. 分类聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").field("catalogId").size(2);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
sourceBuilder.aggregation(catalog_agg);
// 3. 属性聚合
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);
sourceBuilder.aggregation(attr_agg);
String s = sourceBuilder.toString();
System.out.println("构建的DSL" + s);
SearchRequest searchRequest = new SearchRequest(new String[]{
EsConstant.Product_INDEX}, sourceBuilder);
return searchRequest;
}
}
刘怡婷知道当小孩最大的好处,就是没有人会认真看待她的话。她大可吹牛、食言,甚至说谎。也是大人反射性的自我保护,因为小孩最初说的往往是雪亮真言,大人只好安慰自己,小孩子懂什么。挫折之下,小孩从说实话的孩子进化为可以选择说实话的孩子,在话语的民主中,小孩才长成大人。
房思琪的初恋乐园
林奕含