分布式搜索引擎Elasticsearch(二)SpringBoot整合Elasticsearch查询

前言:本文为原创 若有错误欢迎评论!

准备工作

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
  • 如果启动,发现报错:
    'elasticsearchClient' threw exception; nested exception is java.lang.IllegalStateException: availableProcessors is already set to [3], rejecting [3]
    
    原因是整合了Redis后,引发了netty的冲突,需要在启动类中加入:
    @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的实体类

  1. @Document:作用在类,标记实体类为文档对象

    四个属性
    indexName:对应索引库名称
    type:对应在索引库中的类型
    shards:分片数量,默认5
    replicas:副本数量,默认1

  2. @Id: 作用在成员变量,标记一个字段作为id主键

  3. @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

准备工作

  1. 新建一个interface 然后extends ElasticsearchRepository

public interface ItemRepository extends ElasticsearchRepository<Item,Long> { }

  1. 新建一个类 并注入刚才那个接口

@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):通过名字获得聚合
      

案例

/**
 * 商品在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;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值