乐优商城第十三天,十四天(搜索功能)

乐优商城来到了搜索功能,这个功能很难。。。。。。。。。。。。其实没什么时间写博客,课程的内容太多了,但是感觉写博客可以让自己感觉到清醒,所以还是得写

Elastic官网:https://www.elastic.co/cn/

Elasticsearch官网:https://www.elastic.co/cn/products/elasticsearch

我们的工具是kibana。索引库数据统计工具

Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具,可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。

rest风格的Api

https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html


kibana的操作

查询索引库 GET /goods

查看索引库是否存在 HEAD goods

查看映射关系 GET /goods/_mapping

字段的类型

  • String类型,又分两种:

    • text:可分词,不可参与聚合

    • keyword:不可分词,数据会作为完整字段进行匹配,可以参与聚合

  • Numerical:数值类型,分两类

    • 基本数据类型:long、interger、short、byte、double、float、half_float

    • 浮点数的高精度类型:scaled_float

      • 需要指定一个精度因子,比如10或100。elasticsearch会把真实值乘以这个因子后存储,取出时再还原。

  • Date:日期类型

    elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。

2)index

index影响字段的索引情况。

  • true:字段会被索引,则可以用来进行搜索。默认值就是true

  • false:字段不会被索引,不能用来搜索

index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。

但是有些字段是我们不希望被索引的,比如商品的图片信息,就需要手动设置index为false。

3)store

是否将数据进行额外存储。

在学习lucene和solr时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。

但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。

原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做_source的属性中。而且我们可以通过过滤_source来选择哪些要显示,哪些不显示。

而如果设置store为true,就会在_source以外额外存储一份数据,多余,因此一般我们都会将store设置为false,事实上,store的默认值就是false。

接下来是重头戏,索引库的查询1.查询所有

GET /heima/_search
{
    "query":{
        "match_all": {}
    }

}

2.or关系的查询

GET /heima/_search
{
    "query":{
        "match":{
            "title":"小米电视"
        }
    }

}


3.And关系的查询GET /goods/_search
{
    "query":{
        "match":{
            "title":{"query":"小米电视","operator":"and"}
        }
    }

}

4.最小匹配数查询GET /heima/_search
{
    "query":{
        "match":{
            "title":{
            "query":"小米曲面电视",
            "minimum_should_match": "75%"
            }
        }
    }
}

5.多字段查询GET /heima/_search
{
    "query":{
        "multi_match": {
            "query":    "小米",
            "fields":   [ "title", "subTitle" ]
        }
}

}


词条匹配(term精确值查询)1.单词条

GET /heima/_search
{
    "query":{
        "term":{
            "price":2699.00
        }
    }

}


2.多词条匹配

GET /heima/_search
{
    "query":{
        "terms":{
            "price":[2699.00,2899.00,3899.00]
        }
    }

}

过滤,得到想要的字段GET /heima/_search
{
  "_source": ["title","price"],
  "query": {
    "term": {
      "price": 2699
    }
  }

}


高级查询1.bool查询

GET /heima/_search
{
    "query":{
        "bool":{
        "must":     { "match": { "title": "大米" }},
        "must_not": { "match": { "title":  "电视" }},
        "should":   { "match": { "title": "手机" }}
        }
    }

}


2.范围查询GET /heima/_search
{
    "query":{
        "range": {
            "price": {
                "gte":  1000.0,
                "lt":   2800.00
            }
    }
    }
}

3.模糊查询GET /heima/_search
{
  "query": {
    "fuzzy": {
      "title": "appla"
    }
  }
}

过滤

查询条件的过滤

GET /heima/_search
{
    "query":{
        "constant_score":   {
            "filter": {
            "range":{"price":{"gt":2000.00,"lt":3000.00}}
            }
        }
}

度量,分桶后求价格的平均值get /goods/_search
{
  "size":0,
  "aggs":{
    "category":{
      "terms":{
        "field":"brandId"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
    
  }

桶内嵌套桶get /goods/_search
{
  "size":0,
  "aggs":{
    "category":{
      "terms":{
        "field":"brandId"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        },
        "rand":{
          "terms": {
            "field": "specs.主屏幕尺寸(英寸)",
            "size": 10
          }
        }
      }
    }
    
  }

  阶梯分桶

GET /goods/_search
{
  "size": 0, 
  "aggs": {
    "price": {
      "histogram": {
        "field": "price",
        "interval": 50000,
         "min_doc_count": 1
      }
    }
  }

}

基本的语法完成后,我们需要在java中操作了

我们不选择使用 elasticSearch提供的客户端,而使用sping data elasticSearch,因为原生的需要频繁的json操作。

这一块我的博客写在第十二天。


接下来就是搜索的微服务首先是索引类

@Document(indexName = "goods", type = "docs", shards = 1, replicas = 0)
public class Goods {
    @Id
    private Long id; // spuId

    @Field(type = FieldType.text, analyzer = "ik_max_word")
    private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌

    @Field(type = FieldType.keyword, index = false)
    private String subTitle;// 卖点

    private Long brandId;// 品牌id

    private Long cid1;// 1级分类id

    private Long cid2;// 2级分类id

    private Long cid3;// 3级分类id

    private Date createTime;// 创建时间

    private List<Long> price;// 价格

    @Field(type = FieldType.keyword, index = false)
    private String skus;// sku信息的json结构

    private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值我们

我们用一个测试类,将数据写入索引库

@Test
public void add() {
    int page = 1;
    int rows = 100;
    //数据库中的每次查出的记录数
    int size = 0;
    //目标,填充Goods中的数据
    do {
        //分页查询所有数据
        ResponseEntity<PageResult<SpuBo>> pageResultResponseEntity = spuClient.querySpuByPage(null, page, rows, 1);
        if (pageResultResponseEntity.getBody()==null) {
            return;
        }
        PageResult<SpuBo> pageResult = pageResultResponseEntity.getBody();
        List<SpuBo> items = pageResult.getItems();
            List<Goods> goodsList = new ArrayList<>();

            for (SpuBo spu : items) {
                //创建一个goods对象
                Goods goods = new Goods();
                Long id = spu.getId();

                Long cid1 = spu.getCid1();
                Long cid2 = spu.getCid2();
                Long cid3 = spu.getCid3();

                ResponseEntity<SpuDetail> spuDetailResponseEntity = goodsClient.querySpuDetailById(id);
                ResponseEntity<List<Sku>> listResponseEntity = goodsClient.querySkuList(spu.getId());
                ResponseEntity<List<String>> categoryNames = categoryClient.queryCategoryNamesBycids(Arrays.asList(cid1, cid2, cid3));
                if (!spuDetailResponseEntity.hasBody()||!listResponseEntity.hasBody()||!categoryNames.hasBody()){
                    break;
                }

                //查询spuDetail
                SpuDetail spuDetail = spuDetailResponseEntity.getBody();
                //将可搜索的属性导入
                String specifications = spuDetail.getSpecifications();
                //将字符串转为对象
                List<Map<String, Object>> maps = JsonUtils.nativeRead(specifications, new TypeReference<List<Map<String, Object>>>() {
                });
                //map用来存储可搜索属性
                Map<String, Object> specMap = new HashMap<>();
                for (Map<String, Object> map : maps) {
                    List<Map<String,Object>>  paramsList = (List<Map<String, Object>>) map.get("params");
                    for (Map<String, Object> paramsMap : paramsList) {
                        Boolean searchable = (Boolean) paramsMap.get("searchable");
                        if (searchable){
                            if (paramsMap.get("v")!=null)
                            specMap.put((String) paramsMap.get("k"),paramsMap.get("v"));
                            else if (paramsMap.get("options")!=null)
                            specMap.put((String) paramsMap.get("k"),paramsMap.get("options"));
                        }
                    }
                }

                //获取sku的信息
                List<Sku> skuList = listResponseEntity.getBody();
                //sku的信息是一个json对象,里面有很多对象
                List<Map<String,Object>> skuData = new ArrayList<>();
                //准备价格的集合,价格不能重复
                HashSet<Long> prices = new HashSet<>();
                for (Sku sku : skuList) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("id",sku.getId());
                    map.put("title",sku.getTitle());
                    map.put("image", StringUtils.isBlank(sku.getImages())?"":sku.getImages().split(",")[0]);
                    map.put("price",sku.getPrice());
                    prices.add(sku.getPrice());
                    skuData.add(map);
                }
                //sku的集合转为json
                String skuDatas = JsonUtils.serialize(skuData);

                //查询分类的集合
                List<String> categoryNamesBody = categoryNames.getBody();
                goods.setSubTitle(spu.getSubTitle());
                goods.setSpecs(specMap);

                goods.setSkus(skuDatas);

                goods.setPrice(new ArrayList<>(prices));
                goods.setAll(spu.getTitle()+StringUtils.join(categoryNamesBody," "));//todo
                goods.setBrandId(spu.getBrandId());

                goods.setCreateTime(spu.getCreateTime());
                goods.setId(spu.getId());

                goods.setCid1(cid1);
                goods.setCid2(cid2);
                goods.setCid3(cid3);

                goodsList.add(goods);
            }
            goodsRepository.saveAll(goodsList);

        //pageResultgettotal是每页显示的条数,listsize是个数
        size = items.size();//TODO赋值
        //本页完成后,查询下一页的数据
        page++;
    } while (size == 100);
}

接下来就是搜索的方法,当用户点击搜索的时候,会向后端发送一个请求

我们准备好接收请求的实体类

public class SearchRequest {
    private String key;// 搜索条件

    private Integer page;// 当前页

    private String sortBy;//排序条件

    private Boolean descending;//升序降序

    private Map<String,String> filter;//过滤条件

然后是service层的search方法

public SearchResult<Goods> search(SearchRequest request) {
    // 创建查询构建器
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 1、构建查询条件
    // 1.1.对搜索的结果进行过滤
    queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));
    // 1.2.基本查询
    QueryBuilder basicQuery = this.buildBasicQueryWithFilter(request);
    queryBuilder.withQuery(basicQuery);

    // 1.3、分页
    queryBuilder.withPageable(PageRequest.of(request.getPage() - 1, request.getSize()));
    // 1.4、聚合
    // 对分类聚合
    String categoryAggName = "categoryAgg";
    String brandAggName = "brandAgg";
    queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
    queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));

    // 2、查询
    AggregatedPage<Goods> aggResult =
            (AggregatedPage<Goods>) this.goodsRepository.search(queryBuilder.build());


    // 3、解析结果:
    // 3.1、总条数和总页数
    long total = aggResult.getTotalElements();
    long totalPage = (total + request.getSize() - 1) / request.getSize();

    // 3.2、解析商品分类
    List<Category> categories = getCategoryAgg(aggResult, categoryAggName);
    // 3.3、解析品牌
    List<Brand> brands = getBrandAgg(aggResult, brandAggName);

    // 3.4、处理规格参数
    List<Map<String, Object>> specs = null;
    if (categories.size() == 1) {
        specs = getSpecifications(categories.get(0).getId(), basicQuery);
    }

    return new SearchResult<>(total, totalPage, aggResult.getContent(), categories, brands, specs);
}

查询分为3个步骤,1.构建查询条件 2.查询 2.解析结果

一.构建查询条件

1.基本条件的构建

// 构建基本查询条件
private QueryBuilder buildBasicQueryWithFilter(SearchRequest request) {
    BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
    // 基本查询条件
    queryBuilder.must(QueryBuilders.matchQuery("all", request.getKey()).operator(Operator.AND));
    // 过滤条件构建器
    BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
    // 整理过滤条件
    Map<String, String> filter = request.getFilter();
    for (Map.Entry<String, String> entry : filter.entrySet()) {
        // 判断是否是数值类型
        String key = entry.getKey();
        String value = entry.getValue();

        String regex = "^(\\d+\\.?\\d*)-(\\d+\\.?\\d*)$";

        if (value.matches(regex)) {
            Double[] nums = NumberUtils.searchNumber(value, regex);
            // 数值类型进行范围查询
            filterQueryBuilder.must(QueryBuilders.rangeQuery("specs." + key).gte(nums[0]).lt(nums[1]));
        } else {
            if (key != "cid3" && key != "brandId") {
                key = "specs." + key + ".keyword";
            }
            // 字符串类型,进行term查询
            filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
        }
    }
    // 添加过滤条件
    queryBuilder.filter(filterQueryBuilder);

    return queryBuilder;
}

这里将关键词与过滤条件结合在了一起

,然后,我们还需要加一个分页的条件,因为我们的品牌和分类是一定显示的,所以这里还要加上品牌和分类

,所以要对分类进行聚合,然后就可以查询了,查询完成,会得到一个聚合后的结果

三.解析查询结果

对聚合后的结果进行解析解析品牌

private List<Brand> getBrandAgg(AggregatedPage<Goods> aggResult, String brandAggName) {
    LongTerms terms = (LongTerms) aggResult.getAggregation(brandAggName);
    List<Long> ids = new ArrayList<>();
    for (LongTerms.Bucket bucket : terms.getBuckets()) {
        ids.add(bucket.getKeyAsNumber().longValue());
    }
    ResponseEntity<List<Brand>> resp = this.brandsClient.queryBrandsByBrandIds(ids);
    if (resp.hasBody()) {
        return resp.getBody();
    }
    return null;
}

解析分类

private List<Category> getCategoryAgg(AggregatedPage<Goods> aggResult, String categoryAggName) {
    LongTerms terms = (LongTerms) aggResult.getAggregation(categoryAggName);
    List<Long> ids = new ArrayList<>();
    for (LongTerms.Bucket bucket : terms.getBuckets()) {
        ids.add(bucket.getKeyAsNumber().longValue());
    }
    // 获取分类名称
    ResponseEntity<List<String>> resp = this.categoryClient.queryCategoryNamesBycids(ids);
    if (!resp.hasBody()) {
        return null;
    }
    List<String> names = resp.getBody();
    List<Category> categories = new ArrayList<>();
    for (int i = 0; i < ids.size(); i++) {
        Category c = new Category();
        c.setId(ids.get(i));
        c.setName(names.get(i));
        categories.add(c);
    }
    return categories;
}

如果分类只有一个的话,我们还需要对其下的过滤条件进行聚合

private List<Map<String, Object>> getSpecifications(Long id, QueryBuilder basicQuery) {
    // 1、根据分类查询规格
    ResponseEntity<String> specResp = this.specificationClient.querySpecificationsBycid(id);
    if (!specResp.hasBody()) {
        logger.error("查询规格参数出错,cid={}", id);
        return null;
    }
    String jsonSpec = specResp.getBody();
    // 将规格反序列化为集合
    List<Map<String, Object>> specs = null;
    try {
        specs = JsonUtils.nativeRead(jsonSpec, new TypeReference<List<Map<String, Object>>>() {
        });
    } catch (Exception e) {
        logger.error("解析规格参数json出错,json={}", jsonSpec, e);
        return null;
    }


    // 2、过滤出可以搜索的哪些规格参数的名称,分成数值类型、字符串类型
    // 准备集合,保存字符串规格参数名
    Set<String> strSpec = new HashSet<>();
    // 准备map,保存数值规格参数名及单位
    Map<String, String> numericalUnits = new HashMap<>();
    // 解析规格
    for (Map<String, Object> spec : specs) {
        List<Map<String, Object>> params = (List<Map<String, Object>>) spec.get("params");
        for (Map<String, Object> param : params) {
            Boolean searchable = (Boolean) param.get("searchable");
            if (searchable) {
                // 判断是否是数值类型
                if (param.containsKey("numerical") && (boolean) param.get("numerical")) {
                    numericalUnits.put(param.get("k").toString(), param.get("unit").toString());
                } else {
                    strSpec.add(param.get("k").toString());
                }
            }
        }
    }

    // 3、聚合计算数值类型的interval
    Map<String, Double> numericalInterval = getNumericalInterval(id, numericalUnits.keySet());

    // 4、利用interval聚合计算数值类型的分段
    // 5、对字符串类型的参数进行聚合
    return this.aggForSpec(strSpec, numericalInterval, numericalUnits, basicQuery);
}
根据传过来的spu的id查询规格模版,拿到其中的课搜索属性,再对其中的可搜索属性进行判断,判断其是数值类型还是字符串类型,如果是数值类型,我们后续要阶梯分桶,如果是字符串,我们直接关键字分桶,最后,将解析的结果,做处理,处理成前端想要的格式,比如,数值类型的格式是100-200

细节如下

1.首先是查询规格模版的方法

private List<Map<String, Object>> getSpecifications(Long id, QueryBuilder basicQuery) {
    // 1、根据分类查询规格
    ResponseEntity<String> specResp = this.specificationClient.querySpecificationsBycid(id);
    if (!specResp.hasBody()) {
        logger.error("查询规格参数出错,cid={}", id);
        return null;
    }
    String jsonSpec = specResp.getBody();
    // 将规格反序列化为集合
    List<Map<String, Object>> specs = null;
    try {
        specs = JsonUtils.nativeRead(jsonSpec, new TypeReference<List<Map<String, Object>>>() {
        });
    } catch (Exception e) {
        logger.error("解析规格参数json出错,json={}", jsonSpec, e);
        return null;
    }

根据数值类型的key,对数值类型的结果做度量

// 聚合得到interval
private Map<String, Double> getNumericalInterval(Long cid, Set<String> keySet) {
    Map<String, Double> numbericalSpecs = new HashMap<>();
    // 准备查询条件
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    // 不查询任何数据
    queryBuilder.withQuery(QueryBuilders.termQuery("cid3", cid.toString()))
            .withSourceFilter(new FetchSourceFilter(new String[]{""}, null))
            .withPageable(PageRequest.of(0, 1));
    // 添加stats类型的聚合
    for (String key : keySet) {
        queryBuilder.addAggregation(AggregationBuilders.stats(key).field("specs." + key));
    }
    Map<String, Aggregation> aggs = this.template.query(queryBuilder.build(),
            new ResultsExtractor<Map<String, Aggregation>>() {
                @Override
                public Map<String, Aggregation> extract(SearchResponse response) {
                    return response.getAggregations().asMap();
                }
            });

    for (String key : keySet) {
        InternalStats stats = (InternalStats) aggs.get(key);
        double interval = this.getInterval(stats.getMin(), stats.getMax(), stats.getSum());
        numbericalSpecs.put(key, interval);
    }
    return numbericalSpecs;
}
所有条件准备好了,最终解析
// 根据规格参数,聚合得出过滤条件
private List<Map<String, Object>> aggForSpec(Set<String> strSpec, Map<String, Double> numericalInterval,
                                             Map<String, String> numericalUnits, QueryBuilder query) {
    List<Map<String, Object>> specs = new ArrayList<>();
    // 准备查询条件
    NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
    queryBuilder.withQuery(query);
    // 聚合数值类型
    for (Map.Entry<String, Double> entry : numericalInterval.entrySet()) {
        queryBuilder.addAggregation(
                AggregationBuilders.histogram(entry.getKey())
                        .field("specs." + entry.getKey())
                        .interval(entry.getValue())
                        .minDocCount(1)
        );
    }
    // 聚合字符串
    for (String key : strSpec) {
        queryBuilder.addAggregation(
                AggregationBuilders.terms(key).field("specs." + key + ".keyword"));
    }

    // 解析聚合结果
    Map<String, Aggregation> aggs = this.template.query(
            queryBuilder.build(),SearchResponse::getAggregations).asMap();

    // 解析数值类型
    for (Map.Entry<String, Double> entry : numericalInterval.entrySet()) {
        Map<String, Object> spec = new HashMap<>();
        String key = entry.getKey();
        spec.put("k", key);
        spec.put("unit", numericalUnits.get(key));
        // 获取聚合结果
        InternalHistogram histogram = (InternalHistogram) aggs.get(key);
        spec.put("options", histogram.getBuckets().stream().map(bucket -> {
            Double begin = (double) bucket.getKey();
            Double end = begin + numericalInterval.get(key);
            // beginend取整
            if (NumberUtils.isInt(begin) && NumberUtils.isInt(end)) {
                // 确实是整数,需要取整
                return begin.intValue() + "-" + end.intValue();
            } else {
                // 小数,取2位小数
                begin = NumberUtils.scale(begin, 2);
                end = NumberUtils.scale(end, 2);
                return begin + "-" + end;
            }
        }));
        specs.add(spec);
    }

    // 解析字符串类型
    strSpec.forEach(key -> {
        Map<String, Object> spec = new HashMap<>();
        spec.put("k", key);
        StringTerms terms = (StringTerms) aggs.get(key);
        spec.put("options", terms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()));
        specs.add(spec);
    });
    return specs;
}
乐优商城,乐优商城,乐优商城,乐优商城

猜你喜欢

转载自blog.csdn.net/qpc672456416/article/details/80645188