ES服务的搭建(八)
看下图的淘宝页面,可以看到搜索有多个条件及搜索产品,并且支持多种排序方式,例如按价格;其实这块有个特点,就是不管你搜索哪个商品他都是有分类的,以及他对应的品牌,这两个是固定的,但其它参数不一定所有商品都具有;这一块设计就涉及到动态变化数据的加载,设计是比较复杂的,这个可以在后面慢慢说,其实这次想分析的主要是es的搜索服务使用
一、es的搜索服务使用
- 完成关键字的搜索功能
- 完成商品分类过滤功能
- 完成品牌、规格过滤功能
- 完成价格区间过滤功能
二、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>
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);
}
- 配置高亮域以及对应的样式
- 从结果集中取出高亮数据,并将非高亮数据换成高亮数据
接下来按这个思路来玩下,在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;
}
}
@Autowired private 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服务的搭建(八)的更多相关文章
- Centos7安装ES 和 Docker搭建ES
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.linux centos7.x安装ES 1.下载java sudo yum instal ...
- redis在Windows下以后台服务一键搭建集群(多机器)
redis在Windows下以后台服务一键搭建集群(多机器) 一.概述 此教程介绍如何在windows系统中多台机器之间布置redis集群,同时要以后台服务的模式运行.布置以脚本的形式,一键完成.多台 ...
- redis在Windows下以后台服务一键搭建集群(单机--伪集群)
redis在Windows下以后台服务一键搭建集群(单机--伪集群) 一.概述 此教程介绍如何在windows系统中同一台机器上布置redis伪集群,同时要以后台服务的模式运行.布置以脚本的形式,一键 ...
- nginx服务傻瓜搭建
nginx服务傻瓜搭建 安装步骤: 一.先准备好相关源码包和程序包,如下图 所有包都在云服务器的/src目录下. 二.安装 1.安装nginx服务器,支持vod stream.fileupload c ...
- 基于SpringMVC下的Rest服务框架搭建【1、集成Swagger】
基于SpringMVC下的Rest服务框架搭建[1.集成Swagger] 1.需求背景 SpringMVC本身就可以开发出基于rest风格的服务,通过简单的配置,即可快速开发出一个可供客户端调用的re ...
- dubbo服务简单搭建
一.初识dubbo: 架构图: Provider: 暴露服务的服务提供方. Consumer: 调用远程服务的服务消费方. Registry: 服务注册与发现的注册中心. Monitor: 统计服务的 ...
- Centos 6.5 pptpd服务端搭建过程
首先检测有没有启用ppp和tun cat /dev/ppp cat /dev/net/tun 如果显示是这样的 cat: /dev/ppp: No such device or address cat ...
- [Visual Studio] SOA服务框架搭建
1.服务框架搭建 2.服务模板创建 3.Nuget引用 4.客户端调用 任务点: 1.分析SOA 2.修改SOA架构名称以及关键字 3.使用Nuget添加引用 4.选择服务模板进行创建 5.尝试调用 ...
- 【转载】Redis Sentinel 高可用服务架构搭建
作者:田园里的蟋蟀 出处:http://www.cnblogs.com/xishuai/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接. 阅读 ...
随机推荐
- 快速熟悉windows操作
快捷键 win + E : 打开我的电脑 Ctrl+Shift+Esc:打开资源管理器 Alt +F4 :关闭当前窗口 Win + R:打开命令窗口 DOS 命令 打开CMD 的方式 Win+R:输入 ...
- 如何设计一个高性能 Elasticsearch mapping
目录 前言 mapping mapping 能做什么 Dynamic mapping dynamic=true dynamic=runtime dynamic=false dynamic=strict ...
- python类内部方法__setattr__ __getattr_ __delattr__ hasattr __getattribute__ __getitem__(),__setitem__(), __delitem__()
主要讲类的内部方法 __setattr__ __getattr_ __delattr__ hasattr __getattribute__ __getitem__(),__setitem__ ...
- 单臂路由实现不同vlan间通信
单臂路由实现不同vlan间通信 拓扑图 PC配置 PC1 :192.168.1.1 vlan10 192.168.1.254 PC2 :192.168.2.1 vlan20 192.168.2.254 ...
- Zabbix 监控过程详解
Zabbix 监控过程详解 一.修改密码及中文语言 修改密码 修改中文语言 如果复选框中没有 Chinese(zh_CN) 选项,需要安装中文包 [root@Zabbix-server ~]# yum ...
- Redis 哨兵模式配置
搭建步骤 第一步: 在 redis.conf 配置文件目录下拷贝三份 sentinel.conf 文件 [root@node-01 redis-5.0.9]# cp sentinel.conf sen ...
- 9.11 strace:跟踪进程的系统调用 、ltrace:跟踪进程调用库函数
strace 是Linux环境下的一款程序调试工具,用于检查一个应用程序所使用的系统调用以及它所接收的系统信息.strace会追踪程序运行时的整个生命周期,输出每一个系统调用的名字.参数.返回值和执行 ...
- Centos 重置root密码
# cat /etc/system-release #查看版本 开机后在内核grub.2上敲击 e 在linux16 行(倒数第二行)末加入 " ...
- 上传靶机实战之upload-labs解题
前言 我们知道对靶机的渗透可以提高自己对知识的掌握能力,这篇文章就对上传靶机upload-labs做一个全面的思路分析,一共21个关卡.让我们开始吧,之前也写过关于上传的专题,分别为浅谈文件上传漏洞( ...
- TcaplusDB祝大家端午安康!
"五月五,端午到,赛龙舟,真热闹.吃粽子,带香包,蚊虫不来身边闹."这首脍炙人口.描绘着端午节风俗的儿歌,想必大家都听过. 每年的农历五月初五,是我国四大传统节日(春节.清明节.端 ...