看下图的淘宝页面,可以看到搜索有多个条件及搜索产品,并且支持多种排序方式,例如按价格;其实这块有个特点,就是不管你搜索哪个商品他都是有分类的,以及他对应的品牌,这两个是固定的,但其它参数不一定所有商品都具有;这一块设计就涉及到动态变化数据的加载,设计是比较复杂的,这个可以在后面慢慢说,其实这次想分析的主要是es的搜索服务使用

一、es的搜索服务使用

  1. 完成关键字的搜索功能
  2. 完成商品分类过滤功能
  3. 完成品牌、规格过滤功能
  4. 完成价格区间过滤功能

二、ES服务的搭建

在搭建服务前先理下流程,其实流程也很简单,前台服务对数据库进行了操作后,canal会同步变化的数据,将数据发到ES搜索引擎上去,用户就可以在前台使用不同条件进行搜索,关键词、分类、价格区间、动态属性;因为搜索功能在很多模块会被调用,所以先在api模块下建一个子服务spring-cloud-search-api,然后导入包

<dependencies>
<!--ElasticSearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>

在接下来写前看下上图片,现在需要将数据库数据查询出来,再存入ES中,但中间需要有一个和ES索引库对应的JavaBean,为了不影响原来程序对象,所以会创建一个新的 JavaBean 对象

/**
* indexName是索引库中对应的索引名称
* type是当前实体类中对应的一个类型,可以它理解一个表的名字
*/
@Data
@Document(indexName = "shopsearch",type = "skues")
public class SkuEs { @Id
private String id;
//这里是因为要对商品进行模糊查询,要对它进行分词查找,所以要选择分词器,这里选择的是IK分词器
@Field(type = FieldType.Text,analyzer = "ik_smart",searchAnalyzer = "ik_smart")
private String name;
private Integer price;
private Integer num;
private String image;
private String images;
private Date createTime;
private Date updateTime;
private String spuId;
private Integer categoryId;
//Keyword:不分词,这是里分类名称什么的是不用分词拆分的所以选择不分词
@Field(type= FieldType.Keyword)
private String categoryName;
private Integer brandId;
@Field(type=FieldType.Keyword)
private String brandName;
@Field(type=FieldType.Keyword)
private String skuAttribute;
private Integer status;
//属性映射(动态创建域信息)
private Map<String,String> attrMap;
}

这一步搞定后就是要搭建搜索工程了,接下来在spring-cloud-service下面搭建子服务spring-cloud-search-service

<dependency>
<groupId>com.ghy</groupId>
<artifactId>spring-cloud-search-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
bootstrap.yml 
server:
port: 8084
spring:
application:
name: spring-cloud-search-service
cloud:
nacos:
config:
file-extension: yaml
server-addr: 192.168.32.135:8848
discovery:
#Nacos的注册地址
server-addr: 192.168.32.135:8848
#Elasticsearch服务配置 6.8.12
elasticsearch:
rest:
uris: http://192.168.32.135:9200
#日志配置
logging:
pattern:
console: "%msg%n"

上面配置工作做完后下面要的事就是写业务代码了,在业务场景中当数据库sku数据变更的时候,需要做的操作就是通过Canal微服务调用当前搜索微服务实现数据实时更新,原因在上面也画图说明了。接下来先先Mapper代码

public interface SkuSearchMapper extends ElasticsearchRepository<SkuEs,String> {
}
public interface SkuSearchService {

    //增加索引
void add(SkuEs skuEs);
//删除索引
void del(String id);
}
@Service
public class SkuSearchServiceImpl implements SkuSearchService { @Autowired
private SkuSearchMapper skuSearchMapper; /***
* 增加索引
* @param skuEs
*/
@Override
public void add(SkuEs skuEs) {
//获取属性
String attrMap = skuEs.getSkuAttribute();
if(!StringUtils.isEmpty(attrMap)){
//将属性添加到attrMap中
skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class));
}
skuSearchMapper.save(skuEs);
} /***
* 根据主键删除索引
* @param id
*/
@Override
public void del(String id) {
skuSearchMapper.deleteById(id);
}
}
@RestController
@RequestMapping(value = "/search")
public class SkuSearchController { @Autowired
private SkuSearchService skuSearchService; /*****
* 增加索引
*/
@PostMapping(value = "/add")
public RespResult add(@RequestBody SkuEs skuEs){
skuSearchService.add(skuEs);
return RespResult.ok();
} /***
* 删除索引
*/
@DeleteMapping(value = "/del/{id}")
public RespResult del(@PathVariable(value = "id")String id){
skuSearchService.del(id);
return RespResult.ok();
}
}

和上一篇一样,这个搜索功能在很多模块会被调用,所以要在对应的API中写上feign接口

@FeignClient(value = "spring-cloud-search-service")
public interface SkuSearchFeign { /*****
* 增加索引
*/
@PostMapping(value = "/search/add")
RespResult add(@RequestBody SkuEs skuEs); /***
* 删除索引
*/
@DeleteMapping(value = "/search/del/{id}")
RespResult del(@PathVariable(value = "id")String id);
}

索引服务的删除和添加功能做好了,但是这样还没完,前面说过ES的更新是由Canal推过来的,所以需要在Canal服务调用刚刚上面写的两个接口,在spring-cloud-canal-service引入search的api

        <dependency>
<groupId>com.ghy</groupId>
<artifactId>spring-cloud-search-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

然后和上一篇一样在canal服务中写一个监听事件

@CanalTable(value = "sku")
@Component
public class Search implements EntryHandler<Sku> { @Resource
private SkuSearchFeign skuSearchFeign; /***
* 增加数据监听
* @param sku
*/
@Override
public void insert(Sku sku) {
if(sku.getStatus().intValue()==1){
//将Sku转成JSON,再将JSON转成SkuEs
skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(sku), SkuEs.class));
}
} /****
* 修改数据监听
* @param before
* @param after
*/
@Override
public void update(Sku before, Sku after) {
if(after.getStatus().intValue()==2){
//删除索引
skuSearchFeign.del(after.getId());
}else{
//更新
skuSearchFeign.add(JSON.parseObject(JSON.toJSONString(after), SkuEs.class));
}
} /***
* 删除数据监听
* @param sku
*/
@Override
public void delete(Sku sku) {
skuSearchFeign.del(sku.getId());
}
}

现在看似功能做好了,数据也能监听推送到es了,但是还有一个问题,启动程序测试一下就可以发现,由于实体类与数据库映射关系问题导致,所以需要在api中导入以下包

<!--JPA-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>

然后在对应的实体类上加上@Column注解就解决了

然后打开es控制面板,在数据库随便操作一条数据会发现控制面板有更新,做到这一步就说明实时更新已经完成

添加和删除搞定后,接下来就来搞下查询功能,也就是关键词搜索功能,实现也很简单,就是用户输入关键词后,将关键词一起传入后台,需要根据商品名字进行搜索。以后也有可能根据别的条件查询,所以传入后台的数据可以用Map接收,响应页面的数据包含列表、分页等信息,可以用Map封装。

public interface SkuSearchService {

    /****
* 搜索数据
*/
Map<String,Object> search(Map<String,Object> searchMap); //增加索引
void add(SkuEs skuEs);
//删除索引
void del(String id);
}
 /****
* 关键词搜索
* @param searchMap
* 关键词:keywords->name
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//QueryBuilder->构建搜索条件
NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //skuSearchMapper进行搜索
Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数
Map<String,Object> resultMap = new HashMap<String,Object>(); List<SkuEs> list = page.getContent();
resultMap.put("list",list);
resultMap.put("totalElements",page.getTotalElements());
return resultMap;
} /****
* 搜索条件构建
* @param searchMap
* @return
*/
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件,关键词前后台要统一
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); }
return builder;
}
@RestController
@RequestMapping(value = "/search")
public class SkuSearchController { @Autowired
private SkuSearchService skuSearchService; /***
* 商品搜索
*/
@GetMapping
public RespResult<Map<String,Object>> search(@RequestParam(required = false)Map<String,Object> searchMap){
Map<String, Object> resultMap = skuSearchService.search(searchMap);
return RespResult.ok(resultMap);
} /*****
* 增加索引
*/
@PostMapping(value = "/add")
public RespResult add(@RequestBody SkuEs skuEs){
skuSearchService.add(skuEs);
return RespResult.ok();
} /***
* 删除索引
*/
@DeleteMapping(value = "/del/{id}")
public RespResult del(@PathVariable(value = "id")String id){
skuSearchService.del(id);
return RespResult.ok();
}
}

条件回显问题:

看上图可知,当每次执行搜索的时候,页面会显示不同搜索条件,例如:品牌,这些搜索条件都不是固定的,其实他们是没执行搜索的时候,符合搜索条件的商品所有品牌和所有分类,以及所有属性,把他们查询出来,然后页面显示。但是这些条件都没有重复的,也就是说要去重,去重一般采用分组查询即可,所以我们要想动态获取这样的搜索条件,需要在后台进行分组查询。 这个也很简单,只用修改上面写的search方法的业务层代码就好。

/****
* 关键词搜索
* @param searchMap
* 关键词:keywords->name
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//QueryBuilder->构建搜索条件
NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分组搜索调用
group(queryBuilder,searchMap);
//skuSearchMapper进行搜索
//Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build());
AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数
Map<String,Object> resultMap = new HashMap<String,Object>();
//分组数据解析
parseGroup(page.getAggregations(),resultMap); List<SkuEs> list = page.getContent();
resultMap.put("list",list);
resultMap.put("totalElements",page.getTotalElements());
return resultMap;
}
/***
* 分组结果解析
*/
public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){
if(aggregations!=null){
for (Aggregation aggregation : aggregations) {
//强转ParsedStringTerms
ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循环结果集对象
List<String> values = new ArrayList<String>();
for (Terms.Bucket bucket : terms.getBuckets()) {
values.add(bucket.getKeyAsString());
}
//名字
String key = aggregation.getName();
resultMap.put(key,values);
}
}
}
/***
* 分组查询
*/
public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){
//用户如果没有输入分类条件,则需要将分类搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("category"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("categoryList")//别名,类似Map的key
.field("categoryName")//根据categoryName域进行分组
.size(100) //分组结果显示100个
);
}
//用户如果没有输入品牌条件,则需要将品牌搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("brand"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("brandList")//别名,类似Map的key
.field("brandName")//根据brandName域进行分组
.size(100) //分组结果显示100个
);
}
//属性分组查询
queryBuilder.addAggregation(
AggregationBuilders
.terms("attrmaps")//别名,类似Map的key
.field("skuAttribute")//根据skuAttribute域进行分组
.size(100000) //分组结果显示100000个
);
}
/****
* 搜索条件构建
* @param searchMap
* @return
*/
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件,关键词前后台要统一
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); }
return builder;
}

经过上面的步骤就完成了搜索功能中的分类和品牌的操作,这两块相对来说还是比较简单的,因为他们是固定的,但接下来的什么价格呀、款式呀什么的不是固定的,是动态的。下面就说下这块属性回显的做法;属性条件其实就是当前搜索的所有商品属性信息,所以我们可以把所有属性信息全部查询出来,然后把属性名作为key,属性值用集合存起来,就是我们页面要的属性条件了。

 /****
* 关键词搜索
* @param searchMap
* 关键词:keywords->name
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//QueryBuilder->构建搜索条件
NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分组搜索调用
group(queryBuilder,searchMap);
//skuSearchMapper进行搜索
//Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build());
AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build()); //获取结果集:集合列表、总记录数
Map<String,Object> resultMap = new HashMap<String,Object>();
//分组数据解析
parseGroup(page.getAggregations(),resultMap);
//动态属性解析
attrParse(resultMap);
List<SkuEs> list = page.getContent();
resultMap.put("list",list);
resultMap.put("totalElements",page.getTotalElements());
return resultMap;
}
/****
* 将属性信息合并成Map对象
*/
public void attrParse(Map<String,Object> searchMap){
//先获取attrmaps
Object attrmaps = searchMap.get("attrmaps");
if(attrmaps!=null){
//集合数据
List<String> groupList= (List<String>) attrmaps; //定义一个集合Map<String,Set<String>>,存储所有汇总数据
Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>(); //循环集合
for (String attr : groupList) {
Map<String,String> attrMap = JSON.parseObject(attr,Map.class); for (Map.Entry<String, String> entry : attrMap.entrySet()) {
//获取每条记录,将记录转成Map 就业薪资 学习费用
String key = entry.getKey();
Set<String> values = allMaps.get(key);
//空表示没有这个对象
if(values==null){
values = new HashSet<String>();
}
values.add(entry.getValue());
//覆盖之前的数据
allMaps.put(key,values);
}
}
//覆盖之前的attrmaps
searchMap.put("attrmaps",allMaps);
}
}
/***
* 分组结果解析
*/
public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){
if(aggregations!=null){
for (Aggregation aggregation : aggregations) {
//强转ParsedStringTerms
ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循环结果集对象
List<String> values = new ArrayList<String>();
for (Terms.Bucket bucket : terms.getBuckets()) {
values.add(bucket.getKeyAsString());
}
//名字
String key = aggregation.getName();
resultMap.put(key,values);
}
}
}
/***
* 分组查询
*/
public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){
//用户如果没有输入分类条件,则需要将分类搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("category"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("categoryList")//别名,类似Map的key
.field("categoryName")//根据categoryName域进行分组
.size(100) //分组结果显示100个
);
}
//用户如果没有输入品牌条件,则需要将品牌搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("brand"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("brandList")//别名,类似Map的key
.field("brandName")//根据brandName域进行分组
.size(100) //分组结果显示100个
);
}
//属性分组查询
queryBuilder.addAggregation(
AggregationBuilders
.terms("attrmaps")//别名,类似Map的key
.field("skuAttribute")//根据skuAttribute域进行分组
.size(100000) //分组结果显示100000个
);
}
/****
* 搜索条件构建
* @param searchMap
* @return
*/
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件,关键词前后台要统一
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
builder.withQuery(QueryBuilders.termQuery("name",keywords.toString())); }
return builder;
}

前面的做法还停留在单条件,但用户在前端执行条件搜索的时候,有可能会选择分类、品牌、价格、属性,每次选择条件传入后台,后台按照指定参数进行条件查询,这里制定一个传参数的规则:

1、分类参数:category
2、品牌参数:brand
3、价格参数:price
4、属性参数:attr_属性名:属性值
5、分页参数:page

现在来做的是获取category,brand,price的值,并根据这三个只分别实现分类过滤、品牌过滤、价格过滤,其中价格过滤传入的数据以-分割,修改的实现代码如下:

 public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
//builder.withQuery(QueryBuilders.termQuery("name",keywords.toString()));
boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString()));
} //分类查询
Object category = searchMap.get("category");
if(!StringUtils.isEmpty(category)){
boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString()));
} //品牌查询
Object brand = searchMap.get("brand");
if(!StringUtils.isEmpty(brand)){
boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString()));
} //价格区间查询 price=0-500元 500-1000元 1000元以上
Object price = searchMap.get("price");
if(!StringUtils.isEmpty(price)){
//价格区间
String[] prices = price.toString().replace("元","").replace("以上","").split("-");
//price>x
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0])));
//price<=y
if(prices.length==2){
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1])));
}
} //动态属性查询
for (Map.Entry<String, Object> entry : searchMap.entrySet()) {
//以attr_开始,动态属性 attr_网络:移动5G
if(entry.getKey().startsWith("attr_")){
String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword";
boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString()));
}
} } return builder;
}

上面查询搞完了准备收尾工作了,加上前面说的排序问题和分页代码

  public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
//builder.withQuery(QueryBuilders.termQuery("name",keywords.toString()));
boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString()));
} //分类查询
Object category = searchMap.get("category");
if(!StringUtils.isEmpty(category)){
boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString()));
} //品牌查询
Object brand = searchMap.get("brand");
if(!StringUtils.isEmpty(brand)){
boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString()));
} //价格区间查询 price=0-500元 500-1000元 1000元以上
Object price = searchMap.get("price");
if(!StringUtils.isEmpty(price)){
//价格区间
String[] prices = price.toString().replace("元","").replace("以上","").split("-");
//price>x
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0])));
//price<=y
if(prices.length==2){
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1])));
}
} //动态属性查询
for (Map.Entry<String, Object> entry : searchMap.entrySet()) {
//以attr_开始,动态属性 attr_网络:移动5G
if(entry.getKey().startsWith("attr_")){
String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword";
boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString()));
}
} //排序
Object sfield = searchMap.get("sfield");
Object sm = searchMap.get("sm");
if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){
builder.withSort(
SortBuilders.fieldSort(sfield.toString()) //指定排序域
.order(SortOrder.valueOf(sm.toString())) //排序方式
);
}
} //分页查询
builder.withPageable(PageRequest.of(currentPage(searchMap),5));
return builder.withQuery(boolQueryBuilder);
}
搜索高亮实现:
高亮是指搜索商品的时候,商品列表中如何和你搜索的关键词相同,那么它会高亮展示,也就是变色展示,京东搜索其实就是给关键词增加了样式,所以是红色,ES搜索引擎也是一样,也可以实现关键词高亮展示,原理和京东搜索高亮原理一样。高亮搜索实现有2个步骤:
  • 配置高亮域以及对应的样式
  • 从结果集中取出高亮数据,并将非高亮数据换成高亮数据

接下来按这个思路来玩下,在search方法中加入下面一段代码就好了

 //1.设置高亮信息   关键词前(后)面的标签、设置高亮域
HighlightBuilder.Field field = new HighlightBuilder
.Field("name") //根据指定的域进行高亮查询
.preTags("<span style=\"color:red;\">") //关键词高亮前缀
.postTags("</span>") //高亮关键词后缀
.fragmentSize(100); //碎片长度
queryBuilder.withHighlightFields(field);
创建一个结果映射转换对象将非高亮转换成高亮数据
public class HighlightResultMapper extends DefaultResultMapper {

    /***
* 映射转换,将非高亮数据替换成高亮数据
* @param response
* @param clazz
* @param pageable
* @param <T>
* @return
*/
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
//1、获取所有非高亮数据
SearchHits hits = response.getHits();
//2、循环非高亮数据集合
for (SearchHit hit : hits) {
//非高亮数据
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
//3、获取高亮数据
for (Map.Entry<String, HighlightField> entry : hit.getHighlightFields().entrySet()) {
//4、将非高亮数据替换成高亮数据
String key = entry.getKey();
//如果当前非高亮对象中有该高亮数据对应的非高亮对象,则进行替换
if(sourceAsMap.containsKey(key)){
//高亮碎片
String hlresult = transTxtToArrayToString(entry.getValue().getFragments());
if(!StringUtils.isEmpty(hlresult)){
//替换高亮
sourceAsMap.put(key,hlresult);
}
}
}
//更新hit的数据
hit.sourceRef(new ByteBufferReference(ByteBuffer.wrap(JSONObject.toJSONString(sourceAsMap).getBytes())));
}
return super.mapResults(response, clazz, pageable);
} /***
* Text转成字符串
* @param fragments
* @return
*/
public String transTxtToArrayToString(Text[] fragments){
if(fragments!=null){
StringBuffer buffer = new StringBuffer();
for (Text fragment : fragments) {
buffer.append(fragment.toString());
}
return buffer.toString();
}
return null;
}
}
注入对象 ElasticsearchRestTemplate
@Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate;
将之前的搜索换成用 elasticsearchRestTemplate 来实现搜索: 
AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper());

完整类代码

@Service
public class SkuSearchServiceImpl implements SkuSearchService { @Autowired
private SkuSearchMapper skuSearchMapper; @Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate; /****
* 关键词搜索
* @param searchMap
* 关键词:keywords->name
* @return
*/
@Override
public Map<String, Object> search(Map<String, Object> searchMap) {
//QueryBuilder->构建搜索条件
NativeSearchQueryBuilder queryBuilder =queryBuilder(searchMap); //分组搜索调用
group(queryBuilder,searchMap); //1.设置高亮信息 关键词前(后)面的标签、设置高亮域
HighlightBuilder.Field field = new HighlightBuilder
.Field("name") //根据指定的域进行高亮查询
.preTags("<span style=\"color:red;\">") //关键词高亮前缀
.postTags("</span>") //高亮关键词后缀
.fragmentSize(100); //碎片长度
queryBuilder.withHighlightFields(field); //2.将非高亮数据替换成高亮数据 //skuSearchMapper进行搜索
//Page<SkuEs> page = skuSearchMapper.search(queryBuilder.build());
//AggregatedPage<SkuEs> page = (AggregatedPage<SkuEs>) skuSearchMapper.search(queryBuilder.build());
AggregatedPage<SkuEs> page = elasticsearchRestTemplate.queryForPage(queryBuilder.build(), SkuEs.class,new HighlightResultMapper()); //获取结果集:集合列表、总记录数
Map<String,Object> resultMap = new HashMap<String,Object>();
//分组数据解析
parseGroup(page.getAggregations(),resultMap);
//动态属性解析
attrParse(resultMap);
List<SkuEs> list = page.getContent();
resultMap.put("list",list);
resultMap.put("totalElements",page.getTotalElements());
return resultMap;
}
/****
* 将属性信息合并成Map对象
*/
public void attrParse(Map<String,Object> searchMap){
//先获取attrmaps
Object attrmaps = searchMap.get("attrmaps");
if(attrmaps!=null){
//集合数据
List<String> groupList= (List<String>) attrmaps; //定义一个集合Map<String,Set<String>>,存储所有汇总数据
Map<String,Set<String>> allMaps = new HashMap<String,Set<String>>(); //循环集合
for (String attr : groupList) {
Map<String,String> attrMap = JSON.parseObject(attr,Map.class); for (Map.Entry<String, String> entry : attrMap.entrySet()) {
//获取每条记录,将记录转成Map 就业薪资 学习费用
String key = entry.getKey();
Set<String> values = allMaps.get(key);
//空表示没有这个对象
if(values==null){
values = new HashSet<String>();
}
values.add(entry.getValue());
//覆盖之前的数据
allMaps.put(key,values);
}
}
//覆盖之前的attrmaps
searchMap.put("attrmaps",allMaps);
}
}
/***
* 分组结果解析
*/
public void parseGroup(Aggregations aggregations,Map<String,Object> resultMap){
if(aggregations!=null){
for (Aggregation aggregation : aggregations) {
//强转ParsedStringTerms
ParsedStringTerms terms = (ParsedStringTerms) aggregation; //循环结果集对象
List<String> values = new ArrayList<String>();
for (Terms.Bucket bucket : terms.getBuckets()) {
values.add(bucket.getKeyAsString());
}
//名字
String key = aggregation.getName();
resultMap.put(key,values);
}
}
}
/***
* 分组查询
*/
public void group(NativeSearchQueryBuilder queryBuilder,Map<String, Object> searchMap){
//用户如果没有输入分类条件,则需要将分类搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("category"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("categoryList")//别名,类似Map的key
.field("categoryName")//根据categoryName域进行分组
.size(100) //分组结果显示100个
);
}
//用户如果没有输入品牌条件,则需要将品牌搜索出来,作为条件提供给用户
if(StringUtils.isEmpty(searchMap.get("brand"))){
queryBuilder.addAggregation(
AggregationBuilders
.terms("brandList")//别名,类似Map的key
.field("brandName")//根据brandName域进行分组
.size(100) //分组结果显示100个
);
}
//属性分组查询
queryBuilder.addAggregation(
AggregationBuilders
.terms("attrmaps")//别名,类似Map的key
.field("skuAttribute")//根据skuAttribute域进行分组
.size(100000) //分组结果显示100000个
);
}
/****
* 搜索条件构建
* @param searchMap
* @return
*/
/****
* 搜索条件构建
* @param searchMap
* @return
*/
public NativeSearchQueryBuilder queryBuilder(Map<String, Object> searchMap){
NativeSearchQueryBuilder builder= new NativeSearchQueryBuilder(); //组合查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); //判断关键词是否为空,不为空,则设置条件
if(searchMap!=null && searchMap.size()>0){
//关键词条件
Object keywords = searchMap.get("keywords");
if(!StringUtils.isEmpty(keywords)){
//builder.withQuery(QueryBuilders.termQuery("name",keywords.toString()));
boolQueryBuilder.must(QueryBuilders.termQuery("name",keywords.toString()));
} //分类查询
Object category = searchMap.get("category");
if(!StringUtils.isEmpty(category)){
boolQueryBuilder.must(QueryBuilders.termQuery("categoryName",category.toString()));
} //品牌查询
Object brand = searchMap.get("brand");
if(!StringUtils.isEmpty(brand)){
boolQueryBuilder.must(QueryBuilders.termQuery("brandName",brand.toString()));
} //价格区间查询 price=0-500元 500-1000元 1000元以上
Object price = searchMap.get("price");
if(!StringUtils.isEmpty(price)){
//价格区间
String[] prices = price.toString().replace("元","").replace("以上","").split("-");
//price>x
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.valueOf(prices[0])));
//price<=y
if(prices.length==2){
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.valueOf(prices[1])));
}
} //动态属性查询
for (Map.Entry<String, Object> entry : searchMap.entrySet()) {
//以attr_开始,动态属性 attr_网络:移动5G
if(entry.getKey().startsWith("attr_")){
String key = "attrMap."+entry.getKey().replaceFirst("attr_","")+".keyword";
boolQueryBuilder.must(QueryBuilders.termQuery(key,entry.getValue().toString()));
}
} //排序
Object sfield = searchMap.get("sfield");
Object sm = searchMap.get("sm");
if(!StringUtils.isEmpty(sfield) && !StringUtils.isEmpty(sm)){
builder.withSort(
SortBuilders.fieldSort(sfield.toString()) //指定排序域
.order(SortOrder.valueOf(sm.toString())) //排序方式
);
}
} //分页查询
builder.withPageable(PageRequest.of(currentPage(searchMap),5));
return builder.withQuery(boolQueryBuilder);
} /***
* 分页参数
*/
public int currentPage(Map<String,Object> searchMap){
try {
Object page = searchMap.get("page");
return Integer.valueOf(page.toString())-1;
} catch (Exception e) {
return 0;
}
} /***
* 增加索引
* @param skuEs
*/
@Override
public void add(SkuEs skuEs) {
//获取属性
String attrMap = skuEs.getSkuAttribute();
if(!StringUtils.isEmpty(attrMap)){
//将属性添加到attrMap中
skuEs.setAttrMap(JSON.parseObject(attrMap, Map.class));
}
skuSearchMapper.save(skuEs);
} /***
* 根据主键删除索引
* @param id
*/
@Override
public void del(String id) {
skuSearchMapper.deleteById(id);
}
}

源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/spring-cloud-alibaba1.git

ES服务的搭建(八)的更多相关文章

  1. Centos7安装ES 和 Docker搭建ES

    本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.linux centos7.x安装ES 1.下载java sudo yum instal ...

  2. redis在Windows下以后台服务一键搭建集群(多机器)

    redis在Windows下以后台服务一键搭建集群(多机器) 一.概述 此教程介绍如何在windows系统中多台机器之间布置redis集群,同时要以后台服务的模式运行.布置以脚本的形式,一键完成.多台 ...

  3. redis在Windows下以后台服务一键搭建集群(单机--伪集群)

    redis在Windows下以后台服务一键搭建集群(单机--伪集群) 一.概述 此教程介绍如何在windows系统中同一台机器上布置redis伪集群,同时要以后台服务的模式运行.布置以脚本的形式,一键 ...

  4. nginx服务傻瓜搭建

    nginx服务傻瓜搭建 安装步骤: 一.先准备好相关源码包和程序包,如下图 所有包都在云服务器的/src目录下. 二.安装 1.安装nginx服务器,支持vod stream.fileupload c ...

  5. 基于SpringMVC下的Rest服务框架搭建【1、集成Swagger】

    基于SpringMVC下的Rest服务框架搭建[1.集成Swagger] 1.需求背景 SpringMVC本身就可以开发出基于rest风格的服务,通过简单的配置,即可快速开发出一个可供客户端调用的re ...

  6. dubbo服务简单搭建

    一.初识dubbo: 架构图: Provider: 暴露服务的服务提供方. Consumer: 调用远程服务的服务消费方. Registry: 服务注册与发现的注册中心. Monitor: 统计服务的 ...

  7. Centos 6.5 pptpd服务端搭建过程

    首先检测有没有启用ppp和tun cat /dev/ppp cat /dev/net/tun 如果显示是这样的 cat: /dev/ppp: No such device or address cat ...

  8. [Visual Studio] SOA服务框架搭建

    1.服务框架搭建 2.服务模板创建 3.Nuget引用 4.客户端调用 任务点: 1.分析SOA 2.修改SOA架构名称以及关键字 3.使用Nuget添加引用 4.选择服务模板进行创建 5.尝试调用 ...

  9. 【转载】Redis Sentinel 高可用服务架构搭建

    作者:田园里的蟋蟀 出处:http://www.cnblogs.com/xishuai/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接. 阅读 ...

随机推荐

  1. mysql知识点归纳-锁(死锁)

    愿历尽千帆,归来仍是少年 所遇问题: MySql 更新死锁问题 Deadlock found when trying to get lock; try restarting transaction 场 ...

  2. [PTA]7-3 逆序的三位数 (10分)

    要求: 程序每次读入一个正3位数,然后输出按位逆序的数字.注意:当输入的数字含有结尾的0时,输出不应带有前导的0.比如输入700,输出应该是7. 正确思路: 拆分字符串后拼接成整数 1 #includ ...

  3. 折腾gcc/g++链接时.o文件及库的顺序问题

    gcc/g++链接时.o文件以及库的顺序问题 1 写在前面 最近换了xubuntu12.4,把原来的项目co出来编译的时候报"undefined reference to".猜测是 ...

  4. shell中 -eq,-ne,-gt,-lt,-ge,-le数字比较符

    使用说明:     -eq //equals等于    -ne //no equals不等于    -gt //greater than 大于    -lt //less than小于    -ge ...

  5. ipmitool使用手册

    ipmitool使用手册原创xinqidian_xiao 最后发布于2018-07-05 12:15:47 阅读数 17948 收藏展开一.查找安装包 查看ipmitool属于哪个安装包 #yum p ...

  6. VIM 三种模式和常用命令

    引言 大数据开发工作中,周围的同事不是用 VIM 就是 Emacs,你要是用 UltraEdit 或 notepad++ 都不好意思跟人家打招呼...什么插件呀.语法高亮呀.拼写检查呀,能给它开的都给 ...

  7. C++对象内存分布详解(包括字节对齐和虚函数表)

    转自:https://www.jb51.net/article/101122.htm 1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数 ...

  8. System Verilog设计例化和连接

  9. java内部类与静态内部类对比

    内部类 静态内部类 有一个隐式引用,指向实例化这个对象的外部类对象 没有这个附加指针 不支持静态字段(language15) 支持哦 不支持静态方法 (language15) 支持哦 接口中的内部类自 ...

  10. .NET5 WPF进阶教程

    一.概要 本系列将继<.net wpf快速入门教程>带领大家了解wpf,帮助各位在初级向中级过渡的中掌握基本该具备的能力.本系列视频长度大约在15分钟到30分钟左右,视频内容不仅仅会讲解技 ...