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
3. Extract retrieval model vo class
3.1 request model class, SearchParam
3.2 Response model class, SearchResult
4.2.2 Retrieval of product titles
4.2.3 Retrieval by Mobile Phone Classification
4.2.5 Search by attribute. bool-filter
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.9 Save gulimall_product mapping and DSL
5.1.2 service import ES client object
5.1.3 Overall business process and extraction method
5.2.2 Processing query request DSL
5.2.3 Parsing the response result
6.1 Basic data rendering of page number
6.2 Mall Business - Retrieval Service - Page Filter Condition Rendering
6.3 Page pagination data rendering
8. Conditional deletion and URL encoding issues
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