Grain Mall Notes (13) - Mall Business - Search Service

Table of contents

1. [Search Module] Build a page environment

1.1 Separation of dynamic and static search pages

1.2 Using thymeleaf template engine

1.2.1 Import thymeleaf dependencies

1.2.2 index.html imports the namespace of thymeleaf

1.2.3 Add "/static/search" to the static path prefix of the home page 

1.3 Configure Nginx and gateway

1.3.1 hosts file configuration domain name mapping address

1.3.2 Configure Nginx configuration file

1.3.3 Configure gateway

2. Page jump after search

3. Extract retrieval model vo class

3.1 request model class, SearchParam 

3.2 Response model class, SearchResult 

4. Retrieve the DSL statement 

4.1 Review index library

4.2 Query part

4.2.1 Analysis

4.2.2 Retrieval of product titles

4.2.3 Retrieval by Mobile Phone Classification

4.2.4 Brand search

4.2.5 Search by attribute. bool-filter

4.2.6 Whether there is stock

4.2.7 Price Range Search

4.2.8 Sorting

4.2.9 Page numbers

4.2.10 Highlight

4.2.11 Final DSL statement

4.3 Aggregation part

4.3.1 Analysis

4.3.2 Create an index library that allows indexing

4.3.3 Index library data migration

4.3.4 Modify the "index library name" in the constant class to the new index library

4.3.5 Brand aggregation, sub-aggregation

4.3.6 Classification Aggregation

4.3.7 Attribute aggregation, nested aggregation

4.3.8 Full DSL

4.3.9 Save gulimall_product mapping and DSL

5. SearchRequest construction

5.1 Environment preparation

5.1.1 controller

5.1.2 service import ES client object

5.1.3 Overall business process and extraction method

5.2 Realize query service

5.2.1 Query

5.2.2 Processing query request DSL

5.2.3 Parsing the response result

6. Page rendering 

6.1 Basic data rendering of page number

6.2 Mall Business - Retrieval Service - Page Filter Condition Rendering

6.3 Page pagination data rendering

6.4 Page sorting function

6.5 Page sort field echo

6.6 Page price range search

7. Breadcrumbs

8. Conditional deletion and URL encoding issues

9. Add filter linkage


1. [Search Module] Build a page environment

1.1 Separation of dynamic and static search pages

Upload the static resources in the search page to the /static/search folder, and store the index.html search homepage in the templates of the gulimall-search service

 

cd /mydata/nginx/html/static
mkdir search

 

 

 ​​​​​​​

 

1.2 Using thymeleaf template engine

1.2.1 Import thymeleaf dependencies

<!--导入thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1.2.2 index.html imports the namespace of thymeleaf

xmlns:th="http://www.thymeleaf.org"

 

1.2.3 Add "/static/search" to the static path prefix of the home page 

index.html modify the request path of static resources, use CTRL+R to replace all

 

 

 

1.3 Configure Nginx and gateway

All dynamic requests to search.gulimall.com are forwarded to the gateway by Nginx.

1.3.1 hosts file configuration domain name mapping address

 

1.3.2 Configure Nginx configuration file

The http block of the main configuration file nginx.conf has been configured: 

include /etc/nginx/conf.d/*.conf;    #该路径下的配置文件会全部合并到这里

cd /mydata/nginx/conf.d
vi gulimall.conf

The monitored domain name server_name is changed from "gulimall.com" to "*.gulimall.com"

 

Restart the nginx service 

docker restart  nginx

1.3.3 Configure gateway

        - id: gulimall_host_route
          uri: lb://gulimall-product  # lb:负载均衡
          predicates:
            - Host=gulimall.com   # **.xxx  子域名
 
        - id: gulimall_search_route
          uri: lb://gulimall-search  # lb:负载均衡
          predicates:
            - Host=search.gulimall.com   # **.xxx  子域名

Test passed: 

2. Page jump after search

① Import hot deployment dependencies

<!--导入热部署依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <optional>true</optional>
</dependency>

② The cache is turned off by default during development

Click here to jump to the search homepage

Right mouse click, click inspect

Modify the request path

CTRL+F9 recompile 

An error occurred: access to port 80

The reason for the problem: nginx configuration error cannot route jump correctly 

Solution: modify the nginx configuration file

cd /mydata/nginx/conf/conf.d
vi gulimall.conf

restart nginx 

docker restart nginx

Close the cache of the Product service and restart the service

  

Home page, click the search button to go to the search page

 Click on the mobile phone 1111111 to come to the search page

 

The request path is http://search.gmall.com/list.html?catalog3Id=225 , which is a wrong request path, gulimall is missing instead of gumall

①Modify index.html to list.html

② Write control class

③The search bar on the home page is changed to

④ Modify js and upload nginx, restart nginx

 result:

3. Extract retrieval model vo class

DTO (Data Transfer Object) data transfer object, usually refers to the transfer between the front and back ends.

VO (Value Object) value object, we regard it as a view object, used in the presentation layer, its function is to encapsulate all the data of a specified page.

 

3.1 request model class, SearchParam 

①Retrieve through the search bar on the home page and pass the keyword

 ②Search by category. Pass catalog3Id

 ③ complex query

Sorting: ①Comprehensive sorting ②Sales ③Price, for example: sort by sales in descending order or ascending order, sort=saleCount_desc/saleCount_asc

Filter: ①Stock, for example: in stock->hasStock=1, no stock-> hasStock=0 ②Price range, for example: the price is between 400 -900 -> skuPrice=400_900, the price is lower than 900 -> skuPrice= _900, the price Higher than 900 -> skuPrice=900_ ③Brand: You can filter by multiple brands

Aggregation: attribute: Multiple attributes are separated by:, attribute No. 1 network can be 4G or 5G -> attrs=1_4G:5G 

Pagination: page number

Create Vo to encapsulate query conditions

@Data
public class SearchParam {

    /**
     * 页面传递过来的全文匹配关键字
     */
    private String keyword;

    /**
     * 品牌id,可以多选
     */
    private List<Long> brandId;

    /**
     * 三级分类id
     */
    private Long catalog3Id;

    /**
     * 排序条件:sort=price/salecount/hotscore_desc/asc
     */
    private String sort;

    /**
     * 是否显示有货
     */
    private Integer hasStock;

    /**
     * 价格区间查询
     */
    private String skuPrice;

    /**
     * 按照属性进行筛选
     */
    private List<String> attrs;

    /**
     * 页码
     */
    private Integer pageNum = 1;

    /**
     * 原生的所有查询条件
     */
    private String _queryString;


}

3.2 Response model class, SearchResult 

Take JD.com as an example, search Xiaomi

Default: Query all product information

1. The brand that Xiaomi belongs to 2. The category that Xiaomi belongs to 3. The attribute that Xiaomi belongs to

Write the Vo that returns the result

@Data
public class SearchResult {

    /**
     * 查询到的所有商品信息
     */
    private List<SkuEsModel> product;


    /**
     * 当前页码
     */
    private Integer pageNum;

    /**
     * 总记录数
     */
    private Long total;

    /**
     * 总页码
     */
    private Integer totalPages;

    private List<Integer> pageNavs;

    /**
     * 当前查询到的结果,所有涉及到的品牌
     */
    private List<BrandVo> brands;

    /**
     * 当前查询到的结果,所有涉及到的所有属性
     */
    private List<AttrVo> attrs;

    /**
     * 当前查询到的结果,所有涉及到的所有分类
     */
    private List<CatalogVo> catalogs;


    //===========================以上是返回给页面的所有信息============================//


    /* 面包屑导航数据 */
    private List<NavVo> navs;

    @Data
    public static class NavVo {
        private String navName;
        private String navValue;
        private String link;
    }


    @Data
    public static class BrandVo {

        private Long brandId;

        private String brandName;

        private String brandImg;
    }


    @Data
    public static class AttrVo {

        private Long attrId;

        private String attrName;

        private List<String> attrValue;
    }


    @Data
    public static class CatalogVo {

        private Long catalogId;

        private String catalogName;
    }
}

4. Retrieve the DSL statement 

Elasticsearch queries are implemented based on JSON-style DSL.

A domain-specific language (English: domain - specific  language , DSL ) refers to a computer language that focuses on a certain application domain.

4.1 Review index library

PUT product
{
    "mappings":{
        "properties": {
            "skuId":{ "type": "long" },    #商品sku
            "spuId":{ "type": "keyword" },  #当前sku所属的spu。
            "skuTitle": {
                "type": "text",
                "analyzer": "ik_smart"      #只有sku的标题需要被分词
            },
            "skuPrice": { "type": "keyword" },  
            "skuImg"  : { "type": "keyword" },  
            "saleCount":{ "type":"long" },
            "hasStock": { "type": "boolean" },    #是否有库存。在库存模块添加此商品库存后,此字段更为true
            "hotScore": { "type": "long"  },
            "brandId":  { "type": "long" },
            "catalogId": { "type": "long"  },
            "brandName": {"type": "keyword"}, 
            "brandImg":{
                "type": "keyword",
                "index": false,  
                "doc_values": false 
            },
            "catalogName": {"type": "keyword" }, 
            "attrs": {
                "type": "nested",    #对象数组防止扁平化,不能用object类型
                "properties": {
                    "attrId": {"type": "long"  },
                    "attrName": {
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    },
                    "attrValue": {"type": "keyword" }
                }
            }
        }
    }
}
 

 

4.2 Query part

4.2.1 Analysis

First of all, this is a bool query, write the search conditions that need to be scored in must , and write the search conditions that do not need to be scored in filter

Review Boolean Queries

A Boolean query is a combination of one or more query clauses, each of which is a subquery. Subqueries can be combined in the following ways:

  • must: must match each subquery, similar to "and". Generally match with match, check the text type.
  • should: Selective matching subquery, similar to "or"
  • must_not: must not match, does not participate in scoring, similar to "not"
  • filter: Must match, not involved in scoring. Generally match with term and range, check values, keywords, geography, etc.

The more fields involved in scoring, the worse the query performance is. It is recommended to use must_not and filter more .

Therefore, when querying with multiple conditions, it is recommended to do this:

  • The keyword search in the search box is a full-text search query, use must query, and participate in scoring
  • For other filter conditions, use filter query. Do not participate in scoring

4.2.2  Retrieval of product titles

Must be used for scoring, for example: keyword=iphone

4.2.3  Retrieval of Mobile Phone Classification

For example: catalogId=225, use term for non-text field retrieval

4.2.4 Brand search

4.2.5  Search by attribute. bool-filter

The attribute is declared as nested in order to prevent flattening, so nested query needs to be used

Nested query document address: Nested query | Elasticsearch Guide [8.2] | Elastic

Example embedded query:

Create an index library

PUT /my-index-000001
{
  "mappings": {
    "properties": {
      "obj1": {
        "type": "nested"
      }
    }
  }
}

Inquire:

GET /my-index-000001/_search
{
  "query": {
    "nested": {
      "path": "obj1",
      "query": {
        "bool": {
          "must": [
            { "match": { "obj1.name": "blue" } },
            { "range": { "obj1.count": { "gt": 5 } } }
          ]
        }
      },
      "score_mode": "avg"
    }
  }
}

Flattening of es array: When es stores an object array , it will flatten the array, that is to say , extract each property of the object array as an array. Therefore, there will be a problem of query disorder.

 

 

 

4.2.6  Whether there is stock

4.2.7  Price Range Search

4.2.8  Sorting

4.2.9  Page numbers

4.2.10 Highlight

, if the title content contains search content, the search content contained in the title will be marked red

 

4.2.11  Final DSL statement

GET /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>"
   }
}

 

4.3 Aggregation part

4.3.1 Analysis

Aggregation purpose: Dynamically display attributes:

 

 

There are three common types of aggregation:

  • Bucket aggregation: used to group documents
    • TermAggregation: group by document field value, such as group by brand value, group by country
    • Date Histogram: Group by date ladder, for example, a week as a group, or a month as a group
  • Metric aggregation: used to calculate some values, such as: maximum value, minimum value, average value, etc.
    • Avg: Average
    • Max: find the maximum value
    • Min: Find the minimum value
    • Stats: Simultaneously seek max, min, avg, sum, etc.
  • Pipeline (pipeline) aggregation: aggregation based on the results of other aggregations

test aggregation

Aggregation by brand id 

 

You can see that there are two buckets found, there are 12 products of the brand with id 12, and 9 products of the brand 18:

 

 

4.3.2  Create an index library that allows indexing

① Some products do not allow indexing, so a new mapping needs to be created to allow indexing

Mainly modified the "skuImg", "attrName", and "attrValue" in the original index library so that they can be indexed and aggregated

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

Compare product table:

PUT product
{
    "mappings":{
        "properties": {
            "skuId":{ "type": "long" },    #商品sku
            "spuId":{ "type": "keyword" },  #当前sku所属的spu。
            "skuTitle": {
                "type": "text",
                "analyzer": "ik_smart"      #只有sku的标题需要被分词
            },
            "skuPrice": { "type": "keyword" },  
            "skuImg"  : { "type": "keyword" },  
            "saleCount":{ "type":"long" },
            "hasStock": { "type": "boolean" },    #是否有库存。在库存模块添加此商品库存后,此字段更为true
            "hotScore": { "type": "long"  },
            "brandId":  { "type": "long" },
            "catalogId": { "type": "long"  },
            "brandName": {"type": "keyword"}, 
            "brandImg":{
                "type": "keyword",
                "index": false,  
                "doc_values": false     #禁止被聚合
            },
            "catalogName": {"type": "keyword" }, 
            "attrs": {
                "type": "nested",    #对象数组防止扁平化,不能用object类型
                "properties": {
                    "attrId": {"type": "long"  },
                    "attrName": {
                        "type": "keyword",
                        "index": false,
                        "doc_values": false
                    },
                    "attrValue": {"type": "keyword" }
                }
            }
        }
    }
}
 

 

4.3.3 Index library data migration

4.3.4 Modify the "index library name" in the constant class to the new index library

4.3.5 Brand aggregation, sub-aggregation

First aggregate the brand id, and then sub-aggregate the brand name and picture on the aggregation result.

search result

 

 

4.3.6 Classification Aggregation

4.3.7 Attribute aggregation, nested aggregation

nested aggregations文档地址:Nested Aggregations | Elasticsearch: The Definitive Guide [2.x] | Elastic

4.3.8 Full DSL

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
              }
            }
          }
        }
      }
    }
  }
}

4.3.9 Save gulimall_product mapping and DSL

 

5. SearchRequest construction

5.1 Environment preparation

5.1.1 controller

query module

package com.xunqi.gulimall.search.controller;
@Controller
public class SearchController {

    @Autowired
    private MallSearchService mallSearchService;

    /**
     * 自动将页面提交过来的所有请求参数封装成我们指定的对象
     * @param param
     * @return
     */
    @GetMapping(value = "/list.html")
    public String listPage(SearchParam param, Model model, HttpServletRequest request) {

        param.set_queryString(request.getQueryString());

        //1、根据传递来的页面的查询参数,去es中检索商品
        SearchResult result = mallSearchService.search(param);

        model.addAttribute("result",result);

        return "list";
    }

}

5.1.2 service import ES client object

    @Autowired
    private RestHighLevelClient restHighLevelClient;

5.1.3  Overall business process and extraction method

1. Process query request DSL. extraction method

2. Query

3. Parse the query response. extraction method

Specific methods for extracting queries and constructing query results: 

    @Override
    public SearchResult search(SearchParam param) {

        //1、动态构建出查询需要的DSL语句
        SearchResult result = null;

        //1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);

        try {
            //2、执行检索请求
            SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

            //3、分析响应数据,封装成我们需要的格式
            result = buildSearchResult(response,param);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }

    private SearchRequest buildSearchRequest(SearchParam param)
    {return null;}
    private SearchResult buildSearchResult(SearchResponse response,SearchParam param)
    {return null;}

5.2 Realize query service

5.2.1 Query

    @Override
    public SearchResult search(SearchParam param) {

        //1、动态构建出查询需要的DSL语句
        SearchResult result = null;

        //1、准备检索请求
        SearchRequest searchRequest = buildSearchRequest(param);

        try {
            //2、执行检索请求
            SearchResponse response = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);

            //3、分析响应数据,封装成我们需要的格式
            result = buildSearchResult(response,param);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }

5.2.2 Processing query request DSL

    private SearchRequest buildSearchRequest(SearchParam param) {

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        /**
         * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
         */
        //1. 构建bool-query
        BoolQueryBuilder boolQueryBuilder=new BoolQueryBuilder();

        //1.1 bool-must
        if(!StringUtils.isEmpty(param.getKeyword())){
            boolQueryBuilder.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
        }

        //1.2 bool-fiter
        //1.2.1 catelogId
        if(null != param.getCatalog3Id()){
            boolQueryBuilder.filter(QueryBuilders.termQuery("catalogId",param.getCatalog3Id()));
        }

        //1.2.2 brandId
        if(null != param.getBrandId() && param.getBrandId().size() >0){
            boolQueryBuilder.filter(QueryBuilders.termsQuery("brandId",param.getBrandId()));
        }

        //1.2.3 attrs
        if(param.getAttrs() != null && param.getAttrs().size() > 0){

            param.getAttrs().forEach(item -> {
                //attrs=1_5寸:8寸&2_16G:8G
                BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();


                //attrs=1_5寸:8寸
                String[] s = item.split("_");
                String attrId=s[0];
                String[] attrValues = s[1].split(":");//这个属性检索用的值
                boolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
                boolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));

                NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery("attrs",boolQuery, ScoreMode.None);
                boolQueryBuilder.filter(nestedQueryBuilder);
            });

        }

        //1.2.4 hasStock
        if(null != param.getHasStock()){
            boolQueryBuilder.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
        }


        //1.2.5 skuPrice
        if(!StringUtils.isEmpty(param.getSkuPrice())){
            //skuPrice形式为:1_500或_500或500_
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("skuPrice");
            String[] price = param.getSkuPrice().split("_");
            if(price.length==2){
                rangeQueryBuilder.gte(price[0]).lte(price[1]);
            }else if(price.length == 1){
                if(param.getSkuPrice().startsWith("_")){
                    rangeQueryBuilder.lte(price[1]);
                }
                if(param.getSkuPrice().endsWith("_")){
                    rangeQueryBuilder.gte(price[0]);
                }
            }
            boolQueryBuilder.filter(rangeQueryBuilder);
        }

        //封装所有的查询条件
        searchSourceBuilder.query(boolQueryBuilder);


        /**
         * 排序,分页,高亮
         */

        //排序
        //形式为sort=hotScore_asc/desc
        if(!StringUtils.isEmpty(param.getSort())){
            String sort = param.getSort();
            String[] sortFileds = sort.split("_");

            SortOrder sortOrder="asc".equalsIgnoreCase(sortFileds[1])?SortOrder.ASC:SortOrder.DESC;

            searchSourceBuilder.sort(sortFileds[0],sortOrder);
        }

        //分页
        searchSourceBuilder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
        searchSourceBuilder.size(EsConstant.PRODUCT_PAGESIZE);

        //高亮
        if(!StringUtils.isEmpty(param.getKeyword())){

            HighlightBuilder highlightBuilder = new HighlightBuilder();
            highlightBuilder.field("skuTitle");
            highlightBuilder.preTags("<b style='color:red'>");
            highlightBuilder.postTags("</b>");

            searchSourceBuilder.highlighter(highlightBuilder);
        }



        /**
         * 聚合分析
         */
        //1. 按照品牌进行聚合
        TermsAggregationBuilder brand_agg = AggregationBuilders.terms("brand_agg");
        brand_agg.field("brandId").size(50);


        //1.1 品牌的子聚合-品牌名聚合
        brand_agg.subAggregation(AggregationBuilders.terms("brand_name_agg")
                .field("brandName").size(1));
        //1.2 品牌的子聚合-品牌图片聚合
        brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg")
                .field("brandImg").size(1));

        searchSourceBuilder.aggregation(brand_agg);

        //2. 按照分类信息进行聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg");
        catalog_agg.field("catalogId").size(20);

        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));

        searchSourceBuilder.aggregation(catalog_agg);

        //2. 按照属性信息进行聚合
        NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
        //2.1 按照属性ID进行聚合
        TermsAggregationBuilder attr_id_agg = AggregationBuilders.terms("attr_id_agg").field("attrs.attrId");
        attr_agg.subAggregation(attr_id_agg);
        //2.1.1 在每个属性ID下,按照属性名进行聚合
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1));
        //2.1.1 在每个属性ID下,按照属性值进行聚合
        attr_id_agg.subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50));
        searchSourceBuilder.aggregation(attr_agg);

        log.debug("构建的DSL语句 {}",searchSourceBuilder.toString());

        SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},searchSourceBuilder);

        return searchRequest;
    }

 

5.2.3 Parsing the response result

    private SearchResult buildSearchResult(SearchResponse response,SearchParam param) {

        SearchResult result = new SearchResult();

        //1、返回的所有查询到的商品
        SearchHits hits = response.getHits();

        List<SkuEsModel> esModels = new ArrayList<>();
        //遍历所有商品信息
        if (hits.getHits() != null && hits.getHits().length > 0) {
            for (SearchHit hit : hits.getHits()) {
                String sourceAsString = hit.getSourceAsString();
                SkuEsModel esModel = JSON.parseObject(sourceAsString, SkuEsModel.class);

                //判断是否按关键字检索,若是就显示高亮,否则不显示
                if (!StringUtils.isEmpty(param.getKeyword())) {
                    //拿到高亮信息显示标题
                    HighlightField skuTitle = hit.getHighlightFields().get("skuTitle");
                    String skuTitleValue = skuTitle.getFragments()[0].string();
                    esModel.setSkuTitle(skuTitleValue);
                }
                esModels.add(esModel);
            }
        }
        result.setProduct(esModels);

        //2、当前商品涉及到的所有属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();
        //获取属性信息的聚合
        ParsedNested attrsAgg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attr_id_agg");
        for (Terms.Bucket bucket : attrIdAgg.getBuckets()) {
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            //1、得到属性的id
            long attrId = bucket.getKeyAsNumber().longValue();
            attrVo.setAttrId(attrId);

            //2、得到属性的名字
            ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
            String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
            attrVo.setAttrName(attrName);

            //3、得到属性的所有值
            ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> item.getKeyAsString()).collect(Collectors.toList());
            attrVo.setAttrValue(attrValues);

            attrVos.add(attrVo);
        }

        result.setAttrs(attrVos);

        //3、当前商品涉及到的所有品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();
        //获取到品牌的聚合
        ParsedLongTerms brandAgg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brandAgg.getBuckets()) {
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

            //1、得到品牌的id
            long brandId = bucket.getKeyAsNumber().longValue();
            brandVo.setBrandId(brandId);

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

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

            brandVos.add(brandVo);
        }
        result.setBrands(brandVos);

        //4、当前商品涉及到的所有分类信息
        //获取到分类的聚合
        List<SearchResult.CatalogVo> catalogVos = new ArrayList<>();
        ParsedLongTerms catalogAgg = response.getAggregations().get("catalog_agg");
        for (Terms.Bucket bucket : catalogAgg.getBuckets()) {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            //得到分类id
            String keyAsString = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(keyAsString));

            //得到分类名
            ParsedStringTerms catalogNameAgg = bucket.getAggregations().get("catalog_name_agg");
            String catalogName = catalogNameAgg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            catalogVos.add(catalogVo);
        }

        result.setCatalogs(catalogVos);
        //===============以上可以从聚合信息中获取====================//
        //5、分页信息-页码
        result.setPageNum(param.getPageNum());
        //5、1分页信息、总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);

        //5、2分页信息-总页码-计算
        int totalPages = (int)total % EsConstant.PRODUCT_PAGESIZE == 0 ?
                (int)total / EsConstant.PRODUCT_PAGESIZE : ((int)total / EsConstant.PRODUCT_PAGESIZE + 1);
        result.setTotalPages(totalPages);

        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);


        //6、构建面包屑导航
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
            List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
                //1、分析每一个attrs传过来的参数值
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);
                R r = productFeignService.attrInfo(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
                    AttrResponseVo data = r.getData("attr", new TypeReference<AttrResponseVo>() {
                    });
                    navVo.setNavName(data.getAttrName());
                } else {
                    navVo.setNavName(s[0]);
                }

                //2、取消了这个面包屑以后,我们要跳转到哪个地方,将请求的地址url里面的当前置空
                //拿到所有的查询条件,去掉当前
                String encode = null;
                try {
                    encode = URLEncoder.encode(attr,"UTF-8");
                    encode.replace("+","%20");  //浏览器对空格的编码和Java不一样,差异化处理
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                String replace = param.get_queryString().replace("&attrs=" + attr, "");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);

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

            result.setNavs(collect);
        }


        return result;
    }

6. Page rendering 

6.1 Basic data rendering of page number

Since there are very few products in stock, the default value of the stock is not set, and the query condition is assembled when the parameters passed in from the front end are not empty.

Set pagination size to 16 

Dynamically obtain page display data 

① Product display

 Pay attention to details: th:text will be escaped, th:utext will not be escaped

If you use th:text with keyword highlighting, the following results will appear:

 ②Brand display

③Category display 

④ Property display

6.2 Mall Business - Retrieval Service - Page Filter Condition Rendering

1. Filter by brand criteria, "="

2. Filter by classification criteria

3. Filter by attribute conditions

4. Write url splicing function

6.3 Page pagination data rendering

1. The search bar function is completed

 Create an id for the input, so that you can get the input in the input later; write a jump method

 

 The search box echoes the search content, th:value is the attribute setting value; param refers to the request parameter, and param.keyword refers to

 The keyword value in the request parameter

2. Perfect paging function

① The current page number > the first page can display the previous page, and the current page number < total page number can display the next page

② The custom attribute is used to save the current page number, function: used to replace the pageNum value in the request parameter

③Traverse display page number

 

④ The current page number displays a specific style

⑤ Replacement of request parameters

Delete all the href in the a tag, add the class of the a tag, bind events to it, and write a callback function

$(this) refers to the currently clicked element, return false function: disable the default behavior, a tag may jump

Replacement method 

function replaceParamVal(url,paramName,replaceVal){
        var oUrl = url.toString();
        var re = eval('/('+paramName+'=)([^&]*)/gi');
        var nUrl = oUrl.replace(re,paramName+'='+replaceVal);
        return nUrl;
    }

6.4 Page sorting function

Define class for a tag 

Bind the click event to the a tag 

Style the selected elements

 Before setting the style for the selected element, the style of all elements needs to be restored to the original style

Use toggleClass() to add desc to class, the default is descending order 

Add lift symbols 

$(this).text() gets the text content of the currently clicked element

The ascending and descending symbols that need to be cleared before adding the ascending and descending symbols

Extract style changes for selected elements into a method 

function changeStyle(ele){
        $(".sort_a").css({"color":"#333","border-color":"#CCC","background":"#FFF"})
        $(ele).css({"color":"#FFF","border-color":"#e4393c","background":"#e4393c"})
        $(ele).toggleClass("desc");
        $(".sort_a").each(function (){
            var text = $(this).text().replace("↓","").replace("↑","");
            $(this).text(text);
        });
        if ($(ele).hasClass("desc")){
            var text = $(ele).text().replace("↓","").replace("↑","");
            text = text+"↓";
            $(ele).text(text);
        }else {
            var text = $(ele).text().replace("↓","").replace("↑","");
            text = text+"↑";
            $(ele).text(text);
        }
    }

Assign custom attributes to some kind of sort

override replace method

    function replaceOrAddParamVal(url,paramName,replaceVal){
        var oUrl = url.toString();
        if (oUrl.indexOf(paramName)!=-1){
            var re = eval('/('+paramName+'=)([^&]*)/gi');
            var nUrl = oUrl.replace(re,paramName+'='+replaceVal);
            return nUrl;
        }else {
            if (oUrl.indexOf("?")!=-1){
                var nUrl = oUrl+"&"+paramName+"="+replaceVal;
                return nUrl;
            }else {
                var nUrl = oUrl+"?"+paramName+"="+replaceVal;
                return nUrl;
            }
        }
    }

Jump to the specified path

There is a problem: add desc to the class through toggleClass(), it will be lost after refreshing or jumping

6.5 Page sort field echo

The style is echoed after the page jumps, th:with is used to declare variables, and #strings calls the string tool class

Dynamically add class according to URL 

Dynamically add lift symbols 

6.6 Page price range search

Write price range search bar

Bind the click event to the button button 

price echo 

① Get the value of skuPirce

②Price range echo 

#strings.substringAfter(name,prefix): Get the string after prifix

#strings.substringBefore(name,suffix): Get the string before suffix

Whether splicing is in stock query conditions

Binding change event for radio button 

Get whether it is selected by calling prop('check'), if it is selected, it is true, otherwise it is false 

echo selected state 

 

7.  Breadcrumb Navigation

 

 ① Write the breadcrumb navigation bar Vo

② Encapsulate breadcrumb navigation bar data

The acquisition of the attribute name needs to be queried by calling the product service through the remote service 

①Import the cloud version

<spring-cloud.version>Hoxton.SR9</spring-cloud.version>

② Import cloud dependency management

  <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

③ Import the dependencies of openfeign

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

④ Enable remote service call function

⑤ Write the interface and configure the service name to call 

⑥ Write the interface to call the service, note: full path

⑦ Write your own key and return value type to get the data type method you want, the previous one can only get data data

⑧ Write the Vo of the return type, and the properties of Vo and AttrRespVo are consistent

 

⑨Encapsulation attribute name 

8. Conditional deletion and URL encoding issues

①Encapsulate native query conditions

The getQueryString() method of HttpServletRequest can get the request parameters of url

② Package link 

Something went wrong: path substitution failed

The reason for the problem: the browser will encode Chinese, and the attribute value queried is Chinese

Solution: Encode Chinese

Note: For some symbols, the browser's encoding is inconsistent with the java encoding

For example: '(': browser does not encode, java will encode to %28;')': browser does not encode, java will encode to %29; space browser will encode to %20, java will encode to' +'

  // 8.封装面包屑导航栏的数据
        if (param.getAttrs()!=null && param.getAttrs().size()>0){
            List<SearchResVo.NavVo> navVoList = param.getAttrs().stream().map(item -> {
                SearchResVo.NavVo navVo = new SearchResVo.NavVo();
                String[] s = item.split("_");
                // 封装属性值
                navVo.setAttrValue(s[1]);
 
                //封装属性名
                R r = productFeignService.info(Long.parseLong(s[0]));
                if (r.getCode() == 0){
                    AttrResponseVo responseVo = r.getData("attr", new TypeReference<AttrResponseVo>() {});
                    navVo.setAttrName(responseVo.getAttrName());
                }else {
                    // 出现异常则封装id
                    navVo.setAttrName(s[0]);
                }
 
                //封装链接即去掉当前属性的查询的url封装
                String encode=null;
                try {
                    encode = URLEncoder.encode(item,"UTF-8");
                    encode=encode.replace("%28","(").replace("%29",")").replace("+","%20");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                String replace = param.get_queryString().replace("&attrs=" + encode, "");
                navVo.setLink("http://search.gulimall.com/list.html?"+replace);
                return navVo;
            }).collect(Collectors.toList());
            searchResVo.setNavs(navVoList);
        }

Navigation bar echo writing

① Right-click to detect and find the element

 

Rewrite replaceOrAddParamVal is to replace the attribute by default, whether forceAdd is mandatory to add the logo

 

9. Add filter linkage

Improve the function of the brand breadcrumb navigation bar, the category breadcrumb navigation bar is similar, the difference is that it does not need to be removed, set the url

①Set a default value for breadcrumbs vo

② Remotely call the product service to query the brand name

 

Remote service calls, the query is time-consuming, and the query results can be saved in the cache, for example:

value: partition name, key: used to identify the number attribute

③Extract the method of encapsulating and replacing url 

④ Write breadcrumb navigation bar function

Brand breadcrumb navigation bar, brand filtering and elimination

⑤Create a list to encapsulate the filtered attribute id

 

Guess you like

Origin blog.csdn.net/qq_40991313/article/details/129825496