前言:本文为原创 若有错误欢迎评论!
准备工作
1.依赖:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-elasticsearch</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
2.配置文件
spring:
data:
elasticsearch:
cluster-name: elasticsearch
# 如果是集群用" , "隔开 ip1:port1,ip2:port2
cluster-nodes: 192.168.56.101:9300
- 如果启动,发现报错:
原因是整合了Redis后,引发了netty的冲突,需要在启动类中加入:'elasticsearchClient' threw exception; nested exception is java.lang.IllegalStateException: availableProcessors is already set to [3], rejecting [3]
@SpringBootApplication public class Application { public static void main(String[] args) { System.setProperty("es.set.netty.runtime.available.processors","false"); SpringApplication.run(DubboApiApplication.class, args); } }
3.新建es的实体类
-
@Document:作用在类,标记实体类为文档对象
四个属性
indexName:对应索引库名称
type:对应在索引库中的类型
shards:分片数量,默认5
replicas:副本数量,默认1 -
@Id: 作用在成员变量,标记一个字段作为id主键
-
@Filed: 作用在成员变量,标记为文档的字段
字段映射属性
type:字段类型,取值是枚举:FieldType.?
index:是否索引,布尔类型,默认是true
store:是否存储,布尔类型,默认是false
analyzer:分词器名称:ik_max_word
- 示例
@Document(indexName = "item",type = "test", shards = 1, replicas = 0) public class Item { @Id private Long id; @Field(type = FieldType.Text, analyzer = "ik_max_word") private String title; //标题 @Field(type = FieldType.Keyword) private String category;// 分类 @Field(type = FieldType.Keyword) private String brand; // 品牌 @Field(type = FieldType.Double) private Double price; // 价格 @Field(index = false, type = FieldType.Keyword) private String images; // 图片地址 }
4.建立查询返回实体类
- SearchResult
@Data @AllArgsConstructor @NoArgsConstructor public class SearchResult<T> { private Integer totalPage; private List<T> list; }
使用方式一:ElasticsearchTemplate
创建索引
// 创建索引,会根据Item类的@Document注解信息来创建
elasticsearchTemplate.createIndex(Item.class);
创建映射
//配置映射,会根据Item类中的id、Field等字段来自动完成映射
elasticsearchTemplate.putMapping(Item.class);
删除索引
elasticsearchTemplate.deleteIndex(“heima”);
@Service
public class SearchService {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
//防止爬虫与一次拉取过多 影响性能
public static final Integer ROWS = 10;
public SearchResult search(String keyWord, Integer page) {
//设置分页参数 (注意 springdata的分页都是从0开始)
PageRequest pageRequest = PageRequest.of(page - 1, ROWS);
SearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(
QueryBuilders.matchQuery("title", keyWord).operator(Operator.AND)) // match查询
.withPageable(pageRequest) //分页查询
.withHighlightFields(new HighlightBuilder.Field("title")) // 设置高亮
.build();
AggregatedPage<HouseData> housePage = this.elasticsearchTemplate.queryForPage(searchQuery, HouseData.class);
return new SearchResult(housePage.getTotalPages(), housePage.getContent());
}
}
使用方式二:extends ElasticsearchRepository
准备工作
- 新建一个interface 然后extends ElasticsearchRepository
public interface ItemRepository extends ElasticsearchRepository<Item,Long> { }
- 新建一个类 并注入刚才那个接口
@Autowired private
ItemRepository itemRepository;
Spring Data Elasticsearch的文档操作(即crud)
新增文档
Item item = new Item(1L, "小米手机7", " 手机","小米", 3499.00, "test.png")
itemRepository.save(item);
批量新增
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "坚果手机R1", " 手机", "锤子", 3699.00, "test.png")
list.add(new Item(3L, "华为META10", " 手机", "华为", 4499.00, "test.png")
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
修改文档
//修改和新增是同一个接口,区分的依据就是id,这一点跟我们在页面发起PUT请求是类似的
itemRepository.save(item);
基本查询
- 根据id查询
Optional<Item> optional = this.itemRepository.findById(1l);
Item item=optional.get();
- 查询全部并排序
Iterable<Item> items =itemRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(item-> System.out.println(item));
- 根据自动生成的每个字段的查询方法
类似mybatis的逆向工程 生成entity每个字段的各种查询方法
List<Item> list = itemRepository.findByPriceBetween(2000.00, 3500.00);
高级查询(QueryBuilders)
- QueryBuilders:
Repository的search方法需要QueryBuilder参数,通过QueryBuilders的静态方法可获得多个不同QueryBuilder对象 如MatchQueryBuilder、TermQueryBulider等)
//如:词条查询:
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery(“title”, “小米”);
//执行查询
Iterable items = this.itemRepository.search(queryBuilder);
items.forEach(System.out::println);
高级查询(NativeSearchQueryBuilder 最常用查询)
- NativeSearchQueryBuilder:Spring提供的一个查询条件构建器,帮助构建json格式的请求体
1.自定义查询
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.matchQuery("title", "小米").operator(Operator.AND));
// 设置高亮
queryBuilder.withHighlightFields(new HighlightBuilder.Field("title"))
// 执行搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
Page :默认是分页查询,因此返回的是一个分页的结果对象
常用属性:
page.getTotalElements():总条数
page.getTotalPages():总页数
page.getSize():每页文档大小(每页文档数量)
page.getNumber():当前第几页
page.forEach():page实现了Iterator可以直接用forEach遍历每条数据
page.iterator():返回迭代器,不常用 因为Page本身实现了Iterator接口
2.分页查询(Elasticsearch中的分页是从第0页开始)
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 初始化分页参数
int page = 0;
int size = 3;
// 设置分页参数
queryBuilder.withPageable(PageRequest.of(page, size));
//执行查询
Page<Item> items = this.itemRepository.search(queryBuilder.build());
items.forEach(System.out::println);
3.排序
// 构建查询条件
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 添加基本的分词查询
queryBuilder.withQuery(QueryBuilders.termQuery("category", "手机"));
// 排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
// 执行搜索,获取结果
Page<Item> items = this.itemRepository.search(queryBuilder.build());
4.聚合
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 不查询任何结果(因为没有size所以操作sourceFilter 但是include和exclude不可以同时使用 若同时为null无法设置展示字段 所以要想一个字段都没有必须include为new string[]{""} exclude为null)
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
// 1、添加一个新的聚合,聚合类型为terms,聚合名称为brands,聚合字段为brand
queryBuilder.addAggregation(
AggregationBuilders.terms("brands").field("brand")
// 可以聚合嵌套举和 在品牌聚合桶内通过" . "进行连续调用嵌套聚合,该处为求平均值
.subAggregation(AggregationBuilders.avg("priceAvg").field("price"))
);
// 2、查询,需要把结果强转为AggregatedPage类型
AggregatedPage<Item> aggPage = (AggregatedPage<Item>)this.itemRepository.search(queryBuilder.build());
// 3、解析
// 3.1、从结果中取出名为brands的那个聚合,
// 因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
StringTerms agg = (StringTerms) aggPage.getAggregation("brands");
// 3.2、获取桶
List<StringTerms.Bucket> buckets = agg.getBuckets();
// 3.3、遍历
for (StringTerms.Bucket bucket : buckets) {
// 3.4、获取桶中的key,即品牌名称 3.5、获取桶中的文档数量
System.out.println(bucket.getKeyAsString() + ",共" + bucket.getDocCount() + "台");
// 3.6.获取子聚合结果:bucket.getAggregations().asMap()
InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("priceAvg");
System.out.println("平均售价:" + avg.getValue());
}
- 聚合查询小结:
- AggregationBuilders:聚合工厂
常用静态方法: 桶: terms、dateHistogram、histogram 度量: avg、max、min、stats
- AggregatedPage :聚合查询的结果类( Page 的子接口)
包装的方法: bool hasAggregations():判断查询结果是否有聚合 Aggregations getAggregations():把所有聚合变成map 对应的key是聚合的名称(用于度量类型这种聚合里面没有多个子类 只有一个value) Aggregations getAggregation(String name):根据指定聚合名称获取聚合
- 返回的Aggregation类型对象:
不同对象 由terms聚合: InternalTerms(由字段聚合就有三种key的类型):LongTerms、StringTerms、DoubleTerms 由histogram聚合 InternalHistogram(由数值分段 只有一种key的类型) 由dateHistogram聚合 InternalDateHistogram(由日期分段聚合 也只有一种key的类型) 方法 .getKeyAsString():获取key的值 .getDocCount():获得doc的个数 .getAggregations():获得所有集合的map形式(和".asMap().get(String name)"连续调用可以获得度量聚合的"Internal"对象) .getAggregation(Stirng name):通过名字获得聚合
- AggregationBuilders:聚合工厂
案例
/**
* 商品在es中的操作
*/
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private GoodsClient goodsClient;
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private SpecificationClient specificationClient;
@Autowired
private GoodsRepository goodsRepository;
/**
* 构建导入es的商品
*/
@Override
public Goods buildGoods(Spu spu) throws IOException {
Goods goods=new Goods();
goods.setId(spu.getId());
goods.setSubTitle(spu.getSubTitle());
goods.setBrandId(spu.getBrandId());
//商品 一级、二级、三级分类
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(new Date());
//all字段 该字段用作分词查询
List<String> categoryNameList=categoryClient.queryNamesByIds(Arrays.asList(spu.getCid1(),spu.getCid2(),spu.getCid3()));
String categoryNames= StringUtils.join(categoryNameList," ");
String all=categoryNames+" "+spu.getTitle()+" "+brandClient.queryBrandById(spu.getBrandId()).getName();
goods.setAll(all);
//prices、skus
ObjectMapper objectMapper=new ObjectMapper();
List<Sku> skuList=goodsClient.querySkuListBySpuId(spu.getId());
//一个spu下的多个sku的price可能不同
List<Long> pricesList=new ArrayList<>();
//设置sku自己属性值
List<Map<String,Object>> skuMapList=new ArrayList<>();
skuList.forEach(s->{
pricesList.add(s.getPrice());
Map<String ,Object> skuParamMap=new HashMap<>();
skuParamMap.put("id",s.getId());
skuParamMap.put("image",StringUtils.isNotBlank(s.getImages())?StringUtils.split(s.getImages(),",")[0]:"");
skuParamMap.put("price",s.getPrice());
skuParamMap.put("tittle",s.getTitle());
skuMapList.add(skuParamMap);
});
goods.setPrice(pricesList);
goods.setSkus(objectMapper.writeValueAsString(skuMapList));
//params
SpuDetail spuDetail = goodsClient.querySpuDetailBySid(spu.getId());
//spu对应具体sku的通用属性
Map<String,Object> genericSpecMap = objectMapper.readValue(spuDetail.getGenericSpec(), new TypeReference<Map<String, Object>>() {});
//spu对应具体sku的不同属性
Map<String,List<Object>> specialSpecMap=objectMapper.readValue(spuDetail.getSpecialSpec(),new TypeReference<Map<String,List<Object>>>(){});
//一个spu的所有属性值一定和三级分类对应的参数组下所有参数相同
List<SpecParam> specParams = specificationClient.querySpecParanmByGidOrCid3(null, spu.getCid3());
Map<String,Object> specsMap=new HashMap<>();
specParams.forEach(specParam -> {
//如果是通用属性
if(specParam.getGeneric()){
String value=genericSpecMap.get(specParam.getId().toString()).toString();
if(specParam.getNumeric()){
value=chooseSegment(value,specParam);
}
specsMap.put(specParam.getName(),value);
}else {
if(specParam.getNumeric()){
//不通用属性的存储结构是list
List<String> values=new ArrayList<>();
specialSpecMap.get(specParam.getId().toString()).forEach(s->{
values.add(chooseSegment(s.toString(),specParam));
});
specsMap.put(specParam.getName(),values);
}else {
specsMap.put(specParam.getName(),specialSpecMap.get(specParam.getId().toString()));
}
}
});
goods.setSpecs(specsMap);
return goods;
}
@Override
public SearchResult queryGoods(SearchRequest searchRequest) {
if(StringUtils.isBlank(searchRequest.getKey())){
return null;
}
NativeSearchQueryBuilder nativeSearchQueryBuilder=new NativeSearchQueryBuilder();
//nativeSearchQueryBuilder.withQuery(QueryBuilders.matchQuery("all",searchRequest.getKey()).operator(Operator.AND));
//构建查询条件
BoolQueryBuilder basicBuilder=getQueryBuilbulider(searchRequest);
nativeSearchQueryBuilder.withQuery(basicBuilder);
nativeSearchQueryBuilder.withPageable(PageRequest.of(searchRequest.getPage()-1,searchRequest.getSize()));
nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "skus", "subTitle"},null));
//聚合查询构建(用于返回剩余的可筛选字段)
String brandAggName="brands";
String categoryAggName="categorys";
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
AggregatedPage<Goods> goods = (AggregatedPage<Goods>) goodsRepository.search(nativeSearchQueryBuilder.build());
//获取品牌的聚合结果 然后封装成list 作为可筛选条件
List<Brand> brandAggRes = getBrandAggRes(goods.getAggregation(brandAggName));
//获取三级分类的聚合结果 然后封装成list 作为可筛选条件
List<Map<String, Object>> categoryAggRes = getCategoryAggRes(goods.getAggregation(categoryAggName));
List<Map<String,Object>> specAggRes = null;
//获取参数的聚合结果 然后封装成list 作为可筛选条件(只有选了分类才去获取参数的 不然参数太多)
if(!CollectionUtils.isEmpty(categoryAggRes) && categoryAggRes.size()==1){
specAggRes=getSpecAggRes((Long) categoryAggRes.get(0).get("id"),basicBuilder);
}
//将查到的数据和品牌、三级分类、参数的筛选条件返回
return new SearchResult(goods.getTotalElements(),goods.getTotalPages(),goods.getContent(),brandAggRes,categoryAggRes,specAggRes);
}
/**
* 增加索引(商品)
*/
@Override
public void createIndex(Long id) throws IOException {
Spu spu = goodsClient.querySpuById(id);
Goods goods = buildGoods(spu);
goodsRepository.save(goods);
}
/**
* 删除索引(商品)
*/
@Override
public void deleteIndex(Long id) {
goodsRepository.deleteById(id);
}
/**
* 构建通过SearchRequest对象获得的查询条件
*/
BoolQueryBuilder getQueryBuilbulider(SearchRequest searchRequest){
BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();
boolQueryBuilder.must(QueryBuilders.matchQuery("all",searchRequest.getKey()).operator(Operator.AND));
if(searchRequest.getFilter()!=null){
for (Map.Entry<String, Object> filter : searchRequest.getFilter().entrySet()) {
String f=filter.getKey();
if(f.equals("品牌")){
f="brandId";
}else if(f.equals("分类")){
f="cid3";
}else {
//嵌套对象且不做分词的话 该字段名会加上.keyword
//因为规格参数保存时不做分词,因此其名称会自动带上一个.keyword后缀:
f="specs."+filter.getKey()+".keyword";
}
boolQueryBuilder.filter(QueryBuilders.termQuery(f,filter.getValue()));
}
}
return boolQueryBuilder;
}
/**
* 将品牌所有聚合结果返回
*/
public List<Brand> getBrandAggRes(Aggregation agg){
LongTerms longTerms= (LongTerms) agg;
//获得品牌聚合所有Buckets
return longTerms.getBuckets().stream().map(bucket ->
brandClient.queryBrandById(bucket.getKeyAsNumber().longValue())).collect(Collectors.toList());
}
/**
* 将分类所有聚合结果返回
*/
public List<Map<String,Object>> getCategoryAggRes(Aggregation agg){
LongTerms longTerms= (LongTerms) agg;
//获取通过brandId聚合所有Buckets
return longTerms.getBuckets().stream().map(bucket -> {
Map<String,Object> map=new HashMap<>();
long id=bucket.getKeyAsNumber().longValue();
List<String> names = categoryClient.queryNamesByIds(Arrays.asList(id));
map.put("id",id);
map.put("name", names.get(0));
return map;
}).collect(Collectors.toList());
}
/**
* 对参数param的name进行聚合 返回可查询条件
*/
public List<Map<String,Object>> getSpecAggRes(long cid, QueryBuilder queryBuilder){
List<SpecParam> specParamList = specificationClient.querySpecParanmByGidOrCid3(null, cid);
NativeSearchQueryBuilder nativeSearchQueryBuilder=new NativeSearchQueryBuilder();
nativeSearchQueryBuilder.withQuery(queryBuilder);
//添加聚合
specParamList.forEach(specParam -> {
if(specParam.getSearching()){
//注意:.keyword
nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(specParam.getName()).field("specs."+specParam.getName()+".keyword"));
}
});
nativeSearchQueryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{},null));
NativeSearchQuery build = nativeSearchQueryBuilder.build();
AggregatedPage<Goods> goodsPage = (AggregatedPage<Goods>) goodsRepository.search(build);
//获得所有条件的聚合
Map<String, Aggregation> aggregationMap = goodsPage.getAggregations().asMap();
List<Map<String ,Object>> specResList=new ArrayList<>();
//遍历分别使用所有param参数名进行聚合的情况
for(Map.Entry<String, Aggregation> agg:aggregationMap.entrySet()){
Map<String,Object> map=new HashMap<>();
List<Object> options=new ArrayList<>();
map.put("k",agg.getKey());
StringTerms aggValue = (StringTerms) agg.getValue();
//获取该parm条件下的所有Buckets
List<StringTerms.Bucket> buckets = aggValue.getBuckets();
buckets.forEach(bucket -> {
options.add(bucket.getKeyAsString());
});
map.put("options",options);
specResList.add(map);
}
return specResList;
}
/**
* 将数字类型的可筛选查询条件 转化为中文形式 方便用户选择
*/
public String chooseSegment(String value,SpecParam specParam){
String result="其他";
Double val;
try{
val= Double.parseDouble(value);
}catch (Exception e){
return result;
}
for(String segment:StringUtils.split(specParam.getSegments(),",")){
Double begin=Double.MIN_VALUE;
Double end=Double.MAX_VALUE;
String[] nums=StringUtils.split(segment,"-");
if(nums.length==2){
begin=Double.parseDouble(nums[0]);
end=Double.parseDouble(nums[1]);
}else {
if(StringUtils.endsWith(segment,"-")){
begin=Double.parseDouble(nums[0]);
}
if(StringUtils.startsWith(segment,"-")){
end=Double.parseDouble(nums[0]);
}
}
if(val>=begin && val<end){
if(StringUtils.startsWith(segment,"-")){
result=end.toString()+specParam.getUnit()+"以下";
}else if(StringUtils.endsWith(segment,"-")){
result=begin.toString()+specParam.getUnit()+"以上";
}else {
result=segment+specParam.getUnit();
}
break;
}
}
return result;
}
}