谷粒商城のElasticsearch

文章目录

  • 前言
  • 一、前置知识
    • 1、Elasticsearch 的结构
    • 2、倒排索引 (Inverted Index)
      • 2.1、 索引阶段
      • 2.2、查询阶段
  • 二、环境准备
    • 1、安装Es
    • 2、安装Kibana
    • 3、安装 ik 分词器
  • 三、项目整合
    • 1、引入依赖
    • 2、整合业务
      • 2.1、创建索引、文档、构建查询语句
      • 2.2、整合业务代码
  • 后记


前言

  本篇介绍谷粒商城项目检索服务,从搭建es环境到商城检索业务的实现。(不考虑freeMarker模版中的jquery部分)
  对应视频:P173-P192


一、前置知识

1、Elasticsearch 的结构

  同传统的关系型数据库进行类比:

  1. 索引 (Index):相当于关系数据库中的表,由多个文档组成。
  2. 文档 (Document):文档是 Elasticsearch 中存储的基本数据单位,相当于关系数据库中的一行。文档以 JSON 格式存储。每个文档属于一个特定的索引,并具有唯一的 ID。
  3. 映射 (Mapping):类似于数据库中的表结构定义,定义了文档的字段及其数据类型。(DDL建表语句)

2、倒排索引 (Inverted Index)

  在倒排索引中,每个词项都关联到一个倒排列表(Posting List),该列表存储着包含这个词项的所有文档的 ID。倒排索引的构建和查询主要分为以下两个阶段:

2.1、 索引阶段

  当文档被添加到系统中时,首先会进行文档解析(分词),然后将每个词项添加到倒排索引的词典中。词典存储的是文档中所有唯一词项的列表。最后对于每个词项,记录该词项在哪些文档中出现,存储这些文档的 ID 以及该词项在文档中的位置(可选)。这些信息称为倒排列表。
  例如我现在有两个文档:文档一:布偶猫吃鱼和文档二:加菲猫吃鱼,分词后可以得到以下倒排索引(假设目前使用的是ik分词器):

文档ID
布偶1
加菲猫2
1,2
吃鱼1,2

2.2、查询阶段

  首先会进行分词,布偶猫吃鱼会得到词项【布偶吃鱼】,加菲猫吃鱼会得到词项【加菲猫吃鱼】。然后根据分出的词去查找其文档ID:

  • 布偶的文档ID是1。
  • 的文档ID是1,2。
  • 吃鱼的文档ID是1,2。
  • 加菲猫的文档ID是2。
      最后找到同时包含所有查询词项的文档,例如我要搜索布偶猫吃鱼,文档1会作为结果返回。

二、环境准备

1、安装Es

  在本项目中采用docker安装es的方式。es和kibana均采用7.4.2版本
  首先执行:

mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data

  然后执行:

echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml

  这条命令的含义是,在 /mydata/elasticsearch/config/elasticsearch.yml 文件的末尾添加一行配置,内容是http.host: 0.0.0.0,会允许服务通过任何 IP 地址访问Elasticsearch 。

chmod -R 777 /mydata/elasticsearch/

  这条命令的含义是,修改指定目录下的所有文件和子目录的权限。777 表示赋予所有用户(文件的所有者、同组用户、其他用户)读取(r)写入(w) 执行(x) 的权限。(权限的 777 是通过组合 rwx(读、写、执行)的权限位来得到的:7 = r + w + x = 4 + 2 + 1,即读、写、执行权限都有。)
  最后执行:

docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2

- docker run:启动一个新的 Docker 容器。
---name elasticsearch:给容器命名为 elasticsearch。
--p 9200:9200 -p 9300:9300:-p 选项用于将主机的端口与容器内的端口进行映射。9200:9200:将主机的 9200 端口映射到容器的 9200 端口(Elasticsearch 默认的 HTTP REST API 端口)。9300:9300:将主机的 9300 端口映射到容器的 9300 端口(Elasticsearch 默认的内部节点通信端口)。
- -e "discovery.type=single-node":-e 选项用于设置环境变量。“discovery.type=single-node”:指定 Elasticsearch 以单节点模式运行,即不需要集群节点的发现(通常用于开发或测试环境)。
--e ES_JAVA_OPTS="-Xms64m -Xmx512m":ES_JAVA_OPTS 是用于配置 JVM 的选项。-Xms64m:设置 JVM 的最小堆内存为 64MB。-Xmx512m:设置 JVM 的最大堆内存为 512MB。
--v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:-v 选项用于将主机上的目录或文件挂载到容器中。将主机上 /mydata/elasticsearch/config/elasticsearch.yml 文件挂载到容器内的 /usr/share/elasticsearch/configelasticsearch.yml,用于替换容器中的默认配置文件。
- -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:-v 选项用于将主机上的目录或文件挂载到容器中。将主机上/mydata/elasticsearch/config/elasticsearch.yml文件挂载到容器内的 /usr/share/elasticsearch/config/elasticsearch.yml,用于替换容器中的默认配置文件。
--v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins:将主机上的/mydata/elasticsearch/plugins目录挂载到容器的/usr/share/elasticsearch/plugins目录,用于存储和管理 Elasticsearch 插件。
- -d elasticsearch:7.4.2:-d 选项让容器在后台运行(守护模式)。elasticsearch:7.4.2:指定要使用的 Elasticsearch 镜像及其版本号 7.4.2。

2、安装Kibana

  Kibana相当于es的可视化界面和控制台:

docker run --name kibana -e ELASTICSEARCH_HOSTS=http://自己的虚拟机地址:9200 -p 5601:5601 \
-d kibana:7.4.2

- docker run:启动一个新的 Docker 容器。
---name kibana: 给容器命名为kibana。
-e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200:指定 Kibana 连接的 Elasticsearch 集群地址为 http://192.168.56.10:9200。
-p 5601:5601:5601:5601:将主机的 5601 端口映射到容器的 5601 端口。
-d kibana:7.4.2:指定使用 7.4.2 版本的 Kibana 镜像,并使容器在后台运行。

3、安装 ik 分词器

  将ik分词器拷贝到/mydata/elasticsearch/plugins/ik/下,重启es容器,可使用下面的命令验证是否安装成功:

curl -X GET "http://localhost:9200/_cat/plugins?v"

在这里插入图片描述


三、项目整合

1、引入依赖

  在gulimall-search模块中引入依赖:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.4.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>7.4.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>7.4.2</version>
</dependency>

  后续在项目中使用,只需要注入RestHighLevelClient即可

@Autowired
private RestHighLevelClient client;

2、整合业务

2.1、创建索引、文档、构建查询语句

  在分布式基础篇的后台管理系统中,点击上架,会将商品信息保存在gulimall_product索引中,gulimall_product的映射:(注意,分布式基础篇中,映射是product,和现在的gulimall_product有所不同,需要进行数据迁移)

{
  "gulimall_product" : {
    "mappings" : {
      "properties" : {
        "attrs" : {
          "type" : "nested",
          "properties" : {
            "attrId" : {
              "type" : "long"
            },
            "attrName" : {
              "type" : "keyword"
            },
            "attrValue" : {
              "type" : "keyword"
            }
          }
        },
        "brandId" : {
          "type" : "long"
        },
        "brandImg" : {
          "type" : "keyword"
        },
        "brandName" : {
          "type" : "keyword"
        },
        "catalogId" : {
          "type" : "long"
        },
        "catalogName" : {
          "type" : "keyword"
        },
        "hasStock" : {
          "type" : "boolean"
        },
        "hotScore" : {
          "type" : "long"
        },
        "saleCount" : {
          "type" : "long"
        },
        "skuId" : {
          "type" : "long"
        },
        "skuImg" : {
          "type" : "keyword"
        },
        "skuPrice" : {
          "type" : "keyword"
        },
        "skuTitle" : {
          "type" : "text",
          "analyzer" : "ik_smart"
        },
        "spuId" : {
          "type" : "keyword"
        }
      }
    }
  }
}

  数据迁移:

# 迁移数据
POST _reindex
{
  "source": {
    "index": "product"
  },
  "dest": {
    "index": "gulimall_product"
  }
}

  在页面上点击搜索时,相当于带着搜索条件去ES中进行检索,并且将检索的结果封装成对象返回给前端页面进行展示,我们需要:

  1. skuTitle进行模糊匹配,高亮显示。
  2. catalogIdbrandIdattrshasStockrange进行过滤。
  3. skuPrice进行排序。
  4. 进行分页。
  5. 根据上面查询的结果进行聚合分析,按照brandIdcatalogIdattrId,查询出共有的部分。
      其中mustfilter的区别:must 中的条件会参与相关性评分(_score)的计算。如果多个条件都出现在 must 中,满足的条件越多,相关性评分就越高。filter 只用于过滤文档,满足条件的文档会返回,但不会参与相关性评分的计算。因此它比 must 更高效,特别适合用于不需要计算评分的精确匹配或过滤。
      什么是相关性评分?
      相关性评分是 Elasticsearch 在返回搜索结果时,用来衡量每个文档与查询条件的匹配程度的一个分值。每个文档返回时都有一个 _score 值,表示这个文档与查询的匹配程度。分数越高,意味着这个文档与查询条件越相关。
      构建的查询语句:
GET /gulimall_product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "skuTitle": "iphone"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "catalogId": {
              "value": "225"
            }
          }
        },
        {
          "terms": {
            "brandId": [
              "8",
              "9"
            ]
          }
        },
        {
          "nested": {
            "path": "attrs",
            "query": {
              "bool": {
                "must": [
                  {
                    "term": {
                      "attrs.attrId": {
                        "value": "1"
                      }
                    }
                  },
                  {
                    "terms": {
                      "attrs.attrValue": [
                        "5G",
                        "4G"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
          "term": {
            "hasStock": {
              "value": "false"
            }
          }
        },
        {
          "range": {
            "skuPrice": {
              "gte": 4999,
              "lte": 5400
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
      "skuPrice": {
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 10,
  "highlight": {
    "fields": {"skuTitle":{}},
    "pre_tags": "<b style='color:red'>",
    "post_tags": "</b>"
  },
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brandId",
        "size": 10
      },
      "aggs": {
        "brand_name_agg": {
          "terms": {
            "field": "brandName",
            "size": 10
          }
        },
        "brand_img-agg": {
          "terms": {
            "field": "brandImg",
            "size": 10
          }
        }
      }
    },
    "catalog_agg":{
      "terms": {
        "field": "catalogId",
        "size": 10
      },
      "aggs": {
        "catalog_name_agg": {
          "terms": {
            "field": "catelogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg":{
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attr_id_agg": {
          "terms": {
            "field": "attrs.attrId",
            "size": 10
          },
          "aggs": {
            "attr_name_agg": {
              "terms": {
                "field": "attrs.attrName",
                "size": 10
              }
            },
            "attr_value_agg":{
              "terms": {
                "field": "attrs.attrValue",
                "size": 10
              }
            }
          }
        }
      }
    }
  }
}

  对于其中一些关键点的解释:

  • nested:用于处理文档中的嵌套字段嵌套字段指的是ES文档中存储的对象类型的数据,而普通的对象的ES中会被扁平化处理,可能会导致错误匹配的现象。而定义一个字段为嵌套类型时,Elasticsearch 会将每个嵌套对象视为独立的小文档,但它仍然与父文档保持关联。
#定义嵌套字段
{
  "mappings": {
    "properties": {
      "attrs": {
        "type": "nested", 
        "properties": {
          "attrId": { "type": "integer" },
          "attrValue": { "type": "text" }
        }
      }
    }
  }
}

```bash
# 查询嵌套字段
{
  "nested": {
    "path": "attrs",
    "query": {
      "bool": {
        "must": [
          { "term": { "attrs.attrId": 1 } },
          { "term": { "attrs.attrValue": "红色" } }
        ]
      }
    }
  }
}

  • aggs:是一种用于计算统计信息、汇总数据的功能。可以对查询到的结果进行分析、分组、统计等操作,aggs还可以嵌套使用,即在一个聚合内部定义另一个聚合:
#先根据 brandId 字段对文档进行分组,然后对每个 brandId 组计算该组中文档的 price 字段的平均值。
{
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brandId"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

而对于嵌套字段的聚合,需要进行特殊处理:

# 首先对嵌套字段 attrs 进行聚合,然后对 attrs.attrId 进行 terms 聚合
{
  "aggs": {
    "nested_attrs": {
      "nested": {
        "path": "attrs"
      },
      "aggs": {
        "attr_id": {
          "terms": {
            "field": "attrs.attrId"
          }
        }
      }
    }
  }
}

2.2、整合业务代码

  到这里查询语句就已经构建完毕了,但是还需要将查询语句转化成Java语言,利用RestHighLevelClient发送请求进行查询并且解析返回结果:

    /**
     * 商品上架后信息保存在es->根据前端传递的搜索条件构建dsl去es中搜索->解析并封装查询结果给前端页面
     * @param searchDTO 条件
     * @return
     */
    @Override
    public SearchVO searchForCondition(SearchDTO searchDTO) {
        //构建查询请求
        SearchRequest searchRequest = this.getSearchRequest(searchDTO);
        SearchVO vo;
        try {
            //查询
            SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
            System.out.println("返回的结果"+searchResponse.toString());
            //解析查询结果,封装成SearchVO
            vo = this.parseSearchRequest(searchResponse,searchDTO);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return vo;
    }

  查询:

    private SearchRequest getSearchRequest(SearchDTO searchDTO) {

        // 构建查询
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // Bool查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 模糊匹配 skuTitle
        this.likeSearch(searchDTO, boolQuery);
        //过滤查询
        this.filter(searchDTO, boolQuery, searchSourceBuilder);
        //分页高亮排序
        this.sortPageAndHighlight(searchDTO, searchSourceBuilder);
        //对搜索的结果进行聚合分析
        this.termsAggregation(searchSourceBuilder);
        //测试 打印构建结果
        String s = searchSourceBuilder.toString();
        System.out.println("构建的 DSL" + s);

        //创建搜索请求
        SearchRequest searchRequest = new SearchRequest(new
                String[]{ESConstants.PRODUCT_INDEX}, searchSourceBuilder);
        return searchRequest;
    }

  标题模糊匹配:

    private void likeSearch(SearchDTO searchDTO, BoolQueryBuilder boolQuery) {
        String keyword = searchDTO.getKeyword();
        if (StringUtils.isNotBlank(keyword)) {
            boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword)); //对应es中的must
        }
    }

  过滤查询:

    private void filter(SearchDTO searchDTO, BoolQueryBuilder boolQuery, SearchSourceBuilder searchSourceBuilder) {
        // catalogId 查询
        Long catalog3Id = searchDTO.getCatalog3Id();
        if (!ObjectUtils.isEmpty(catalog3Id)) {
            boolQuery.filter(QueryBuilders.termsQuery("catalogId", new long[]{catalog3Id})); //对应es中的filter
        }

        //bool - filter - 按照品牌 id 查询
        List<Long> brandId = searchDTO.getBrandId();
        if (!CollectionUtils.isEmpty(brandId)) {
            boolQuery.filter(QueryBuilders.termsQuery("brandId",
                    brandId));
        }

        //bool - filter - 按照所有指定的属性进行查询
        List<String> attrs = searchDTO.getAttrs();
        if (!CollectionUtils.isEmpty(attrs)) {
            BoolQueryBuilder nestedboolQuery = QueryBuilders.boolQuery(); //对应es中的nested
            //进行处理
            //attr=1_5寸:8寸
            for (String attr : attrs) {
                String attrId = attr.split("_")[0];
                String value = attr.split("_")[1];
//                String[] attrValues = s.split(":");

                nestedboolQuery.must(QueryBuilders.termQuery("attrs.attrId",
                        attrId));
                nestedboolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",
                        value));

                //每一个必须都得生成一个 nested 查询
                NestedQueryBuilder nestedQuery =
                        QueryBuilders.nestedQuery("attrs", nestedboolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);
            }
        }

        //bool - filter - 按照库存是否有进行查询
        if (searchDTO.getHasStock() != null) {
            boolQuery.filter(QueryBuilders.termQuery("hasStock",
                    searchDTO.getHasStock() == 1));
        }

        //1.2、bool - filter - 按照价格区间
        if (!StringUtils.isEmpty(searchDTO.getSkuPrice())) {
            //1_500/_500/500_
            /**
             * "range": {
             * "skuPrice": {
             * "gte": 0,
             * "lte": 6000
             * }
             * }
             */
            RangeQueryBuilder rangeQuery =
                    QueryBuilders.rangeQuery("skuPrice"); //对应es中的range
            String[] s = searchDTO.getSkuPrice().split("_");
            if (s.length == 2) {
                //区间
                rangeQuery.gte(s[0]).lte(s[1]);
            } else if (s.length == 1) {
                if (searchDTO.getSkuPrice().startsWith("_")) {
                    rangeQuery.lte(s[0]);
                }
                if (searchDTO.getSkuPrice().endsWith("_")) {
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }
        //把以前的所有条件都拿来进行封装
        searchSourceBuilder.query(boolQuery);
    }

  分页高亮排序:

    private void sortPageAndHighlight(SearchDTO searchDTO, SearchSourceBuilder searchSourceBuilder) {
        //排序
        //sort=hotScore_asc/desc
        String sort = searchDTO.getSort();
        if (StringUtils.isNotBlank(sort)) {
            String sortStr = sort.split("_")[1];
            SortOrder order = sortStr.equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            searchSourceBuilder.sort(sort.split("_")[0], order);
        }

        //分页
        searchSourceBuilder.from((searchDTO.getPageNum() - 1) *
                ESConstants.PRODUCT_PAGESIZE);
        searchSourceBuilder.size(ESConstants.PRODUCT_PAGESIZE);

        //高亮
        if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
            HighlightBuilder builder = new HighlightBuilder();
            builder.field("skuTitle");
            builder.preTags("<b style='color:red'>");
            builder.postTags("</b>");
            searchSourceBuilder.highlighter(builder);
        }
    }

  聚合分析:

    private void termsAggregation(SearchSourceBuilder searchSourceBuilder) {
        /**
         * 聚合分析
         */
        //1、品牌聚合
        TermsAggregationBuilder brand_agg =
                AggregationBuilders.terms("brand_agg");
        brand_agg.field("brandId").size(50);
        //品牌聚合的子聚合
        brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1));
        brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1));
        /*
        1、聚合 brand
         */
        searchSourceBuilder.aggregation(brand_agg);


        //2、分类聚合 catalog_agg
        TermsAggregationBuilder catalog_agg =
                AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg"
        ).field("catalogName").size(1));

        /*
        2、聚合 catalog
         */
        searchSourceBuilder.aggregation(catalog_agg);


        //3、属性聚合 attr_agg
        NestedAggregationBuilder attr_agg =
                AggregationBuilders.nested("attr_agg", "attrs");
        //聚合出当前所有的 attrId
        TermsAggregationBuilder attr_id_agg =
                AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
        //聚合分析出当前 attr_id 对应的名字
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
        //聚合分析出当前 attr_id 对应的所有可能的属性值 attrValue
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").
                field("attrs.attrValue").size(50));
        attr_agg.subAggregation(attr_id_agg);

        /*
        3、聚合 attr
         */
        searchSourceBuilder.aggregation(attr_agg);
    }

  解析返回结果:

    private SearchVO parseSearchRequest(SearchResponse resp, SearchDTO searchDTO) {
        SearchVO vo = new SearchVO();
        //外层hits
        SearchHits hits = resp.getHits();
        //里层hits
        SearchHit[] hitsArr = hits.getHits();

        List<ESPojo> esPojos = Arrays.stream(hitsArr).map(searchHit -> {
            //_source
            String sourceAsString = searchHit.getSourceAsString();
            ESPojo esPojo = JSON.parseObject(sourceAsString, ESPojo.class);
            //判断是否按关键字检索,若是就显示高亮,否则不显示
            if (!StringUtils.isEmpty(searchDTO.getKeyword())) {
                //拿到高亮信息显示标题
                HighlightField skuTitle = searchHit.getHighlightFields().get("skuTitle");
                String skuTitleValue = skuTitle.getFragments()[0].string();
                esPojo.setSkuTitle(skuTitleValue);
            }
            return esPojo;
        }).collect(Collectors.toList());
        //返回的所有商品
        vo.setProducts(esPojos);

        //聚合信息
        ParsedLongTerms brandAgg = resp.getAggregations().get("brand_agg");
        List<? extends Terms.Bucket> brandAggBuckets = brandAgg.getBuckets();
        List<SearchVO.BrandsVO> brandsVOS = brandAggBuckets.stream().map(bucket -> {
            SearchVO.BrandsVO brandsVO = new SearchVO.BrandsVO();
            //得到品牌id
            long brandId = bucket.getKeyAsNumber().longValue();
            brandsVO.setBrandId(brandId);

            //得到品牌名字
            ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
            String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
            brandsVO.setBrandName(brandName);

            //3、得到品牌的图片
            ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
            String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
            brandsVO.setBrandImg(brandImg);

            return brandsVO;
        }).collect(Collectors.toList());

        //封装品牌信息
        vo.setBrands(brandsVOS);

        ParsedLongTerms catalogAgg = resp.getAggregations().get("catalog_agg");
        List<? extends Terms.Bucket> catalogAggBuckets = catalogAgg.getBuckets();
        List<SearchVO.CatalogsVO> catalogsVOS = catalogAggBuckets.stream().map(bucket -> {
            SearchVO.CatalogsVO catalogsVO = new SearchVO.CatalogsVO();
            //得到分类ID
            //得到品牌id
            long catelogId = bucket.getKeyAsNumber().longValue();
            catalogsVO.setCatalogId(catelogId);

            //得到品牌名称
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            catalogsVO.setCatalogName(catalogName);

            return catalogsVO;
        }).collect(Collectors.toList());

        //封装分类信息
        vo.setCatalogs(catalogsVOS);

        ParsedNested attrsAgg = resp.getAggregations().get("attr_agg");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
        List<? extends Terms.Bucket> attrsAggBuckets = attrIdAgg.getBuckets();
        List<SearchVO.AttrsVo> attrsVoList = attrsAggBuckets.stream().map(bucket -> {
            SearchVO.AttrsVo attrsVo = new SearchVO.AttrsVo();
            //属性ID
            long attrId = bucket.getKeyAsNumber().longValue();
            attrsVo.setAttrId(attrId);
            //属性名称
            ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
            attrsVo.setAttrName(attrNameAgg.getBuckets().get(0).getKeyAsString());
            //属性值
            ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
            attrsVo.setAttrValue(attrValues);
            return attrsVo;
        }).collect(Collectors.toList());
        
        //封装属性信息
        vo.setAttrs(attrsVoList);
        
        //封装分页参数
        vo.setPageNum(searchDTO.getPageNum());
        int total = (int) resp.getHits().getTotalHits().value;
        vo.setTotal(total);

        int totalPages = (int)total % ESConstants.PRODUCT_PAGESIZE == 0 ?
                (int)total / ESConstants.PRODUCT_PAGESIZE : ((int)total / ESConstants.PRODUCT_PAGESIZE + 1);
        vo.setTotalPages(totalPages);
        return vo;
    }

  到这里为止,构建查询语句,查询ES,解析返回结果的操作就完成了。在高亮展示这一块,有一个很坑的点:如果在对标题进行匹配时,是这样写的:boolQuery.must(QueryBuilders.matchQuery("skuTitle", keyword)).fuzziness(Fuzziness.AUTO)); 实测高亮展示会失效。因为.fuzziness(Fuzziness.AUTO)是模糊匹配的一个配置选项,表示自动确定模糊匹配的程度。下面简单说一下它的工作机制:
  对于长度较短的词(1到2个字符),不进行模糊匹配。对于长度为3到5个字符的词,允许最多一个字符不同。也就是说,输入的词和索引中的词之间最多可以有一个字符的差异。对于长度超过5个字符的词,允许最多两个字符不同,即输入的词和索引中的词之间可以有两处字符差异。
  为什么加上了会使高亮展示失效?因为模糊匹配会容忍一定的字符变化,比如拼写错误或词形变化。高亮显示依赖于精确匹配的词,只有当查询中的词语与索引中的词精确匹配时,ES才会高亮显示。

后记

  本篇主要介绍了ES的组成和基本概念,以及环境搭建,项目业务整合ES。因为项目的重点是后端的逻辑,所以前端模板页面的改造没有写入本篇。在做这个项目之前,去年有曾经专门去看过某马的关于ES的专题教学视频,语法介绍的很详细,当时还跟着敲了一遍。但是工作中至今未遇到使用场景,在做这个项目的时候不出意外地发现几乎全部遗忘了。在这里想说的是,语法并非重点,重点是理解各自项目的业务逻辑,做好笔记,能根据案例和实际的业务场景举一反三。

  下一篇:认证服务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/877543.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

初识php库管理工具composer的体验【爽】使用phpword模板功能替换里面的字符串文本

需求&#xff1a; 做了一个租赁的项目&#xff0c;里面要求签署个人授权协议&#xff0c;里面要填写姓名&#xff0c;手机号&#xff0c;身份证号&#xff0c;签署日期等参数&#xff0c;格式如下图 格式&#xff1a; 如上图&#xff0c;word中的字符串模板变量使用${varname…

Java设计模式—面向对象设计原则(三) -----> 依赖倒转原则DIP(完整详解,附有代码+案例)

文章目录 3.3 依赖倒转原则(DIP)3.3.1概述3.3.2 案例 3.3 依赖倒转原则(DIP) 依赖倒转原则&#xff1a;Dependency Inversion Principle&#xff0c;DIP 3.3.1概述 高层模块不应该依赖低层模块&#xff0c;两者都应该依赖其抽象&#xff1b;抽象不应该依赖细节&#xff0c;细…

演示:基于WPF的自绘的中国地铁轨道控件

一、目的&#xff1a;演示一个基于WPF的自绘的中国地铁轨道控件 二、效果演示 北京地铁 成都地铁 上海地铁 深圳地铁 南京地铁 长春地铁 哈尔滨地铁 武汉地铁 厦门地铁 香港地铁 三、功能 支持平移、缩放等操作 鼠标悬停显示线路信息和站点信息 按表格显示&#xff0c;按纸张…

MySQL —— 索引

索引的概念 MySQL的索引是⼀种数据结构&#xff0c;它可以帮助数据库高效地查询、更新数据表中的数据。索引通过 ⼀定的规则排列数据表中的记录&#xff0c;使得对表的查询可以通过对索引的搜索来加快速度。 MySQL索引类似于书籍的目录&#xff0c;通过指向数据行的位置&…

PCIe进阶之TL:First/Last DW Byte Enables Rules Traffic Class Field

1 First/Last DW Byte Enables Rules & Attributes Field 1.1 First/Last DW Byte Enables Rules Byte Enable 包含在 Memory、I/O 和 Configuration Request 中。本文定义了相应的规则。Byte Enable 位于 header 的 byte 7 。对于 TH 字段值为 1 的 Memory Read Request…

Requests-HTML模块怎样安装和使用?

要安装和使用Requests-HTML模块&#xff0c;您可以按照以下步骤进行操作&#xff1a; 打开命令行界面&#xff08;如Windows的命令提示符或Mac的终端&#xff09;。 使用pip命令安装Requests-HTML模块。在命令行中输入以下命令并按回车键执行&#xff1a; pip install request…

前端网页代码编辑器 Monaco Editor

前端网页代码编辑器 Monaco Editor Monaco Editor Monaco Editor 是由 Microsoft 开发的一款基于 Web 技术的开源代码编辑器&#xff0c;它是 Visual Studio Code 编辑器的核心。Monaco Editor 可以嵌入到网页中&#xff0c;提供类似于 Visual Studio Code 的编辑体验。 官方…

数据结构 Java DS——分享部分链表题目 (2)

目录 前言 题目一 —— 链表的回文结构 题目二 —— 二进制链表转整数 题目三 —— 设计链表 结尾 前言 关于JAVA的链表,笔者已经写了两篇博客来介绍了,今天给笔者们带来第三篇,也是分享了一些笔者写过的,觉得挺好的题目,链接也已经挂上了,笔者们可以去看看…

redis 基本数据类型—string类型

一、介绍 Redis 中的字符串&#xff0c;直接就是按照二进制数据的方式存储的&#xff0c;不会做任何的编码转换。 Redis对于 string 类型&#xff0c;限制了大小最大是512M 二、命令 SET 将 string 类型的 value 设置到 key 中。如果 key 之前存在&#xff0c;则覆盖&#…

亚马逊、沃尔玛、敦煌网、Target塔吉特、Temu环境搭建测评技术!

海外跨境电商各大主要平台正不断力推半托管模式&#xff0c;不断对商家开出众多吸引和扶持政策。全托管是指电商平台全面负责店铺的运营&#xff0c;包括仓储、配送、售后等&#xff0c;而商家主要负责提供货品。半托管模式则基本由商家自主经营&#xff0c;平台只负责仓配物流…

java中Class文件的文件格式

无关性的基石 计算机底层只能识别二进制&#xff0c;由CPU直接处理二进制&#xff0c;在底层上面是操作系统&#xff0c;在操作系统上面就是虚拟机&#xff0c;java有一个口号&#xff0c;“一次编写&#xff0c;到处运行”这个不太可能在操作系统层面上实现&#xff0c;不同的…

俄罗斯方块——C语言实践(Dev-Cpp)

目录 1、创建项目(尽量不使用中文路径) 2、项目复制 3、项目配置 ​1、调整编译器 2、在配置窗口选择参数标签 3、添加头文件路径和库文件路径 4、代码实现 4.1、main.c 4.2、draw.h 4.3、draw.c 4.4、shape.h 4.5、shape.c 4.6、board.h 4.7、board.c 4.8、cont…

Vue.js入门系列(二十九):深入理解编程式路由导航、路由组件缓存与路由守卫

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

解锁编程潜力,从掌握GitHub开始

目录&#xff1a; 一、搜索开源项目 1、什么是Git 2、Github常用词含义 3、一个完整的项目界面 4、使用Github搜索项目 1&#xff09;in关键词 2&#xff09;star或fork数量去查找 3&#xff09;awesome加强搜索 二、访问速度慢的解决 1、使用网易UU加速器 2、使用…

Visual Studio(vs)下载安装C/C++运行环境配置和基本使用注意事项

基本安装 点击跳转到vs官网点击箭头所指的按钮进行下载双击运行刚才下载好的下载器点击继续勾选“使用C的桌面开发”和“Visual Studio扩展开发”点击“安装位置”&#xff0c;对vs的安装位置进行更改。你可以跟我一样只选择D盘或者其他你空闲的盘&#xff0c;然后将默认的路径…

响应式CSS 媒体查询——WEB开发系列39

CSS媒体查询&#xff08;Media Queries&#xff09;是响应式设计中的核心技术之一&#xff0c;帮助我们在不同设备上展示不同的样式。通过媒体查询&#xff0c;开发者可以检测用户设备的特性&#xff0c;如屏幕宽度、高度、分辨率、方向等&#xff0c;针对性地调整网页布局。 一…

架构师知识梳理(七):软件工程-工程管理与开发模型

软件工程概述 软件开发生命周期 软件定义时期&#xff1a;包括可行性研究和详细需求分析过程&#xff0c;任务是确定软件开发工程必须完成的总目标&#xff0c;具体可分成问题定义、可行性研究、需求分析等。软件开发时期&#xff1a;就是软件的设计与实现&#xff0c;可分成…

气压测试实验(用IIC)

I2C: 如果没有I2c这类总线&#xff0c;连接方法可能会如下图&#xff1a; 单片机所有的通讯协议&#xff0c;无非是建立在引脚&#xff08;高低电平的变换高低电平持续的时间&#xff09;这二者的组合上&#xff0c;i2c 多了一个clock线&#xff0c;负责为数据传输打节拍。 (i2…

如何删除git提交记录

今天在提交github时&#xff0c;不小心提交了敏感信息&#xff0c; 不要问我提交了啥&#xff0c;问就是不知道 查了下资料&#xff0c;终于找到简单粗暴的方式来删除提交记录。方法如下 git reset --soft HEAD~i i代表要恢复到多少次提交前的状态&#xff0c;如指定i 2&…

一文读懂:如何将广告融入大型语言模型(LLM)输出

本文是我翻译过来的&#xff0c;讨论了在线广告行业的现状以及如何将大型语言模型&#xff08;LLM&#xff09;应用于在线广告。 原文请参见”阅读原文“。 在2024年&#xff0c;预计全球媒体广告支出的69%将流向数字广告市场。这个数字预计到2029年将增长到79%。在Meta的2024…