谷粒商城十四检索服务

搭建页面环境

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atlinxi.gulimall</groupId>
    <artifactId>gulimall-search</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-search</name>
    <description>elasticsearch检索服务</description>
    <properties>
        <java.version>1.8</java.version>
        <elasticsearch.version>7.4.2</elasticsearch.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.atlinxi.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
<!--        引入热启动-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>com.alibaba</groupId>-->
<!--            <artifactId>fastjson</artifactId>-->
<!--            <version>1.2.79</version>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--        由于SpringCloud Feign高版本不使用Ribbon而是使用spring-cloud-loadbalancer,
            所以需要引用spring-cloud-loadbalancer或者降版本-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
    </dependencies>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

关闭thymeleaf缓存

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-search
server.port=13000

spring.thymeleaf.cache=false

替换index.html中的内容,并将静态资源全部复制到nginx中

href="替换为href="/static/search/src="替换为src="/static/search/

修改windows本地hosts文件

192.168.56.10	gulimall.com
192.168.56.10	search.gulimall.com

修改gulimall.conf

server {
    
    
    listen       80;
    server_name  gulimall.com *.gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location /static {
    
    
        root   /usr/share/nginx/html;
    }

    location / {
    
    
       proxy_set_header Host $host;
       proxy_pass http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    
    
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    
    
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    
    
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    
    
    #    deny  all;
    #}
}

在gateway模块添加路由至末尾

- id: gulimall_search_route
  uri: lb://gulimall-search
  predicates:
   - Host=search.gulimall.com

product模块的index.html

//  href="/static/#" 去掉
<a href="/static/#" ><img src="/static/index/img/img_09.png" onclick="search()" /></a>


window.location.href="/static/http://search.gulimall.com/search.html?keyword="+keyword;
// 将上面的改为
window.location.href="http://search.gulimall.com/list.html?keyword="+keyword;

此时,就可以访问search.gulimall.com

商城检索-检索条件分析

在这里插入图片描述

nginx

gulimall.conf

server {
    
    
    listen       80;
    server_name  gulimall.com *.gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location /static {
    
    
        root   /usr/share/nginx/html;
    }

    location / {
    
    
       proxy_set_header Host $host;
       proxy_pass http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    
    
        root   /usr/share/nginx/html;
    }

es dsl语句

之前的es索引应该是product,我们在做映射的时候,brandImg、brandName等字段我们只是来看一下,不做聚合和查询,所以当时这些字段的index、doc_values都为false,我们就得做索引的数据迁移(es不能直接修改映射)。

扫描二维码关注公众号,回复: 16036077 查看本文章
# 更新映射
# 映射更新之后,之前的数据映射还是不会改变的
# 所以我们建一个新的索引用来迁移
PUT 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"
        }
      }
    }
  }



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




# 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析

# 如果是嵌入式的属性,查询,聚合,分析都应该用嵌入式的
# dsl完整查询语句
{
    
    
  "query": {
    
    
    "bool": {
    
    
      "must": [
        {
    
    
          "match": {
    
    
            "skuTitle": "华为"
          }
        }
      ],
      "filter": [
        {
    
    
          "term": {
    
    
            "catalogId": "225"
          }
        },
        {
    
    
          "terms": {
    
    
            "brandId": [
              "5",
              "6",
              "7"
            ]
          }
        },
        {
    
    
          "nested": {
    
    
            "path": "attrs",
            "query": {
    
    
              "bool": {
    
    
                "must": [
                  {
    
    
                    "term": {
    
    
                      "attrs.attrId": {
    
    
                        "value": "1"
                      }
                    }
                  },
                  {
    
    
                    "terms": {
    
    
                      "attrs.attrValue": [
                        "CET-AL00",
                        "balabala"
                      ]
                    }
                  }
                ]
              }
            }
          }
        },
        {
    
    
          "term": {
    
    
            "hasStock": {
    
    
              "value": "false"
            }
          }
        },
        {
    
    
          "range": {
    
    
            "skuPrice": {
    
    
              "gte": 0,
              "lte": 6000
            }
          }
        }
      ]
    }
  },
  "sort": [
    {
    
    
      "skuPrice": {
    
    
        "order": "desc"
      }
    }
  ],
  "from": 0,
  "size": 2,
  "highlight": {
    
    
    "fields": {
    
    
      "skuTitle": {
    
    }
    },
    "pre_tags": "<b style='color:yellow'>",
    "post_tags": "</b>"
  },
  "aggs": {
    
    
    "brand_agg": {
    
    
      "terms": {
    
    
        "field": "brandId",
        "size": 100
      },
      "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": "catalogName",
            "size": 10
          }
        }
      }
    },
    "attr_agg": {
    
    
      "nested": {
    
    
        "path": "attrs"
      },
      "aggs": {
    
    
        "attr_id": {
    
    
          "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
              }
            }
          }
        }
      }
    }
  }
}

修改commons R类

/**
 * Copyright (c) 2016-2019 人人开源 All rights reserved.
 *
 * https://www.renren.io
 *
 * 版权所有,侵权必究!
 */

package com.atlinxi.common.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.apache.http.HttpStatus;

import java.util.HashMap;
import java.util.Map;

/**
 * 返回数据
 *
 * @author Mark [email protected]
 *
 *
 * 老师是用泛型的方式封装的data,
 * 在feign接口中返回泛型类时,由于java的泛型机制,在实例化之前无法得到具体的类型 ,
 * 因此,虽然服务提供方返回的是具体实例的数据,但是在客户端decode时,无法转化为具体的类。
 *
 * 上面的话看不太懂,翻译成人话就是,feign在被远程调用返回结果的时候,泛型是null
 *
 * 因为R继承了HashMap,我们写的所有私有属性都没用,只能存键值对,具体原因未知,
 *
 * public class R<T> extends HashMap<String, Object> {
 * 	private static final long serialVersionUID = 1L;
 *
 * 	private T data;
 *
 * 	public T getData() {
 * 		return this.data;
 *        }
 *
 *
 * 	public void setData(T data) {
 * 		this.data = data;
 *    }
 */
public class R extends HashMap<String, Object> {
    
    
	private static final long serialVersionUID = 1L;


	public R setData(Object data){
    
    
		put("data",data);
		return this;
	}



	public <T> T getData(TypeReference<T> typeReference) {
    
    
		Object data = get("data");	//默认是map
		String jsonString = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonString, typeReference);
		return t;
	}

	//利用fastjson进行反序列化
	public <T> T getData(String key,TypeReference<T> typeReference) {
    
    
		Object data = get(key);	//默认是map
		String jsonString = JSON.toJSONString(data);
		T t = JSON.parseObject(jsonString, typeReference);
		return t;
	}

	public R() {
    
    
		put("code", 0);
		put("msg", "success");
	}
	
	public static R error() {
    
    
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
	}
	
	public static R error(String msg) {
    
    
		return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
	}
	
	public static R error(int code, String msg) {
    
    
		R r = new R();
		r.put("code", code);
		r.put("msg", msg);
		return r;
	}

	public static R ok(String msg) {
    
    
		R r = new R();
		r.put("msg", msg);
		return r;
	}
	
	public static R ok(Map<String, Object> map) {
    
    
		R r = new R();
		r.putAll(map);
		return r;
	}
	
	public static R ok() {
    
    
		return new R();
	}

	public R put(String key, Object value) {
    
    
		super.put(key, value);
		return this;
	}

	public int getCode(){
    
    

		return (Integer) this.get("code");

	}
}

所有实体类、常量等

package com.atlinxi.gulimall.search.constant;

public class EsConstant {
    
    

	// 这儿写错了,常量名应该都是大写
    public static final String Product_INDEX = "gulimall_product"; // sku数据在es中的索引
    public static final Integer Product_PAGESIZE = 2; // 前期为了方便测试,分页只两个
}

// 查询条件实体类
package com.atlinxi.gulimall.search.vo;

import lombok.Data;

import java.util.List;

/**


 * 封装页面所有可能传递过来的查询条件
 *
 * catalog3Id=225&keyword=小米&sort=saleCount_asc&hasStock=0/1&brandId=1&brandId=2
 * &attrs=1_5寸:8寸&attrs=2_16G:8G
 */
@Data
public class SearchParam {
    
    

    private String keyword; // 页面传递过来的全文匹配关键字
    private Long catalog3Id; // 页面传递过来的三级分类id
    /**
     * sort=saleCount_asc/desc
     * sort=skuPrice_asc/desc
     * sort=hotScore_asc/desc
     */
    private String sort; // 排序条件

    /**
     * 好多的过滤条件
     * hasStock(是否有货)、skuPrice(区间)、brandId、catalog3Id、attrs
     *
     * hasStock 0/1
     * skuPrice 1_500/_500/500_
     * brandId=1
     * attrs=1_其他:安卓&attrs=2_5寸:6寸(_前面代表属性,后面代表值,多个值之间用:分割)
     */

    private Integer hasStock; // 是否只显示有货 0(无库存) 1(有库存)
    private String skuPrice; // 价格区间查询
    private List<Long> brandId; // 按照品牌进行查询,可以多选
    private List<String> attrs; // 按照属性进行筛选
    private Integer pageNum = 1; // 页码


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


}

// 返回结果实体类
package com.atlinxi.gulimall.search.vo;

import com.atlinxi.common.to.es.SkuEsModel;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

@Data
public class SearchResult {
    
    

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

    /**
     * 分页信息
     */
    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 = new ArrayList<>();
    private List<Long> attrIds = new ArrayList<>();

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









package com.atlinxi.gulimall.search.vo;

import lombok.Data;

@Data
public class AttrResponseVo {
    
    

    // 所属分类名字
    private String catelogName;

    // 所属分组名字
    private String groupName;

    // 三级分类路径
    private Long[] catelogPath;


    /**
     * 属性id
     */
    private Long attrId;
    /**
     * 属性名
     */
    private String attrName;
    /**
     * 是否需要检索[0-不需要,1-需要]
     */
    private Integer searchType;
    /**
     * 属性图标
     */
    private String icon;
    /**
     * 可选值列表[用逗号分隔]
     */
    private String valueSelect;
    /**
     * 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]
     */
    private Integer attrType;
    /**
     * 启用状态[0 - 禁用,1 - 启用]
     */
    private Long enable;
    /**
     * 所属分类
     */
    private Long catelogId;
    /**
     * 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整
     */
    private Integer showDesc;

    private Long attrGroupId;
}












package com.atlinxi.gulimall.search.vo;

import lombok.Data;

@Data
public class BrandVo {
    
    

    private Long brandId;
    private String brandName;
}


feign远程调用

package com.atlinxi.gulimall.search.feign;

import com.atlinxi.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient("gulimall-product")
public interface ProductFeignService {
    
    

    // 在写feign接口的时候,请求路径必须是完整的,函数命可以与原服务的函数名不一样
    @RequestMapping("/product/attr/info/{attrId}")
    R attrInfo(@PathVariable("attrId") Long attrId);


    @GetMapping("/product/brand//infos")
    R BrandsInfo(@RequestParam("brandIds") List<Long> brandIds);
}








/**
     *
     * @param attrId
     * @return
     */
    // search远程调用的时候耗时太长,我们把返回的结果放进缓存
    @Cacheable(value = "attr",key = "'attrInfo:' + #root.args[0]")
    @Override
    public AttrResVo getAttrInfo(Long attrId) {
    
    

        AttrResVo attrResVo = new AttrResVo();
        AttrEntity attrEntity = this.getById(attrId);
        BeanUtils.copyProperties(attrEntity,attrResVo);

        if (attrEntity.getAttrType()==ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
    
    
            // 1.设置分组信息
            AttrAttrgroupRelationEntity attrgroupRelation = attrAttrgroupRelationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));

            if (attrgroupRelation!=null){
    
    
                attrResVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());

                if (attrGroupEntity!=null){
    
    
                    attrResVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }


            }
        }



        // 2. 设置分类信息
        Long catelogId = attrEntity.getCatelogId();

        Long[] catelogPath = categoryService.findCatelogPath(catelogId);

        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);

        if (categoryEntity!=null){
    
    
            attrResVo.setCatelogPath(catelogPath);
            attrResVo.setCatelogName(categoryEntity.getName());
        }



        return attrResVo;
    }

controller

package com.atlinxi.gulimall.search.controller;

import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;

@Controller
public class SearchController {
    
    

    @Autowired
    MallSearchService mallSearchService;

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

        String queryString = request.getQueryString();

        param.set_queryString(queryString);

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

        model.addAttribute("result",result);
        // 我们整合了thymeleaf,所以不用谢templates和.html
        return "list";
    }
}

启动类

package com.atlinxi.gulimall.search;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients(basePackages = "com.atlinxi.gulimall.search.feign")
public class GulimallSearchApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(GulimallSearchApplication.class, args);
    }

}

service

package com.atlinxi.gulimall.search.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atlinxi.common.to.es.SkuEsModel;
import com.atlinxi.common.utils.R;
import com.atlinxi.gulimall.search.config.GulimallElasticSearchConfig;
import com.atlinxi.gulimall.search.constant.EsConstant;
import com.atlinxi.gulimall.search.feign.ProductFeignService;
import com.atlinxi.gulimall.search.service.MallSearchService;
import com.atlinxi.gulimall.search.vo.AttrResponseVo;
import com.atlinxi.gulimall.search.vo.BrandVo;
import com.atlinxi.gulimall.search.vo.SearchParam;
import com.atlinxi.gulimall.search.vo.SearchResult;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class MallSearchServiceImpl implements MallSearchService {
    
    

    @Autowired
    private RestHighLevelClient client;


    @Autowired
    private ProductFeignService productFeignService;

    // 去es中进行检索
    @Override
    public SearchResult search(SearchParam param) {
    
    
        // 1. 动态构建出查询需要的DSL语句
        SearchResult result = null;

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


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

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

        return result;
    }


    /**
     * 构建结果数据
     *
     * @param response
     * @return
     */
    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 s = skuTitle.getFragments()[0].toString();
                    esModel.setSkuTitle(s);
                }


                esModels.add(esModel);

            }
        }

        result.setProducts(esModels);


        // 2. 当前所有商品涉及到的所有属性信息
        List<SearchResult.AttrVo> attrVos = new ArrayList<>();

        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");

        for (Terms.Bucket bucket : attr_id_agg.getBuckets()) {
    
    
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();

            // 得到属性的id
            long attrId = bucket.getKeyAsNumber().longValue();

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


            // 得到属性的所有制值
            List<String> attrValues = ((ParsedStringTerms) bucket.getAggregations().get("attr_value_agg")).getBuckets().stream().map(
                    item -> {
    
    
                        String keyAsString = ((Terms.Bucket) item).getKeyAsString();
                        return keyAsString;
                    }).collect(Collectors.toList());

            attrVo.setAttrId(attrId);
            attrVo.setAttrName(attrName);
            attrVo.setAttrValue(attrValues);


            attrVos.add(attrVo);
        }

        result.setAttrs(attrVos);

        // 3. 当前所有商品涉及到的所有品牌信息
        List<SearchResult.BrandVo> brandVos = new ArrayList<>();

        ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
        for (Terms.Bucket bucket : brand_agg.getBuckets()) {
    
    
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();

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

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

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

            brandVo.setBrandId(brandId);
            brandVo.setBrandName(brandName);
            brandVo.setBrandImg(brandImg);

            brandVos.add(brandVo);

        }


        result.setBrands(brandVos);

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

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

        // 5. 分页信息 - 页码
        result.setPageNum(param.getPageNum());
        // 分页信息 - 总记录数
        long total = hits.getTotalHits().value;
        result.setTotal(total);
        // 分页信息 - 总页码 - 计算得到
        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. 构建面包屑导航功能

        // 面包屑导航只限于属性,不包括分类和keyword(检索条件)
        // 因为我们如果去掉分类或者检索条件的话,属性则无意义

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

            List<SearchResult.NavVo> collect = param.getAttrs().stream().map(attr -> {
    
    

                SearchResult.NavVo navVo = new SearchResult.NavVo();
                // 1. 分析每个attrs传过来的查询参数值
                // attrs=1_其他:安卓&attrs=2_5寸:6寸
                String[] s = attr.split("_");
                navVo.setNavValue(s[1]);

                R r = productFeignService.attrInfo(Long.parseLong(s[0]));
                result.getAttrIds().add(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 replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);
                return navVo;
            }).collect(Collectors.toList());


            // todo 分类,不需要导航取消

            result.setNavs(collect);


        }


        // 品牌,分类
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
    
    
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();

            navVo.setNavName("品牌");
            R r = productFeignService.BrandsInfo(param.getBrandId());

            if (r.getCode() == 0) {
    
    
                List<BrandVo> brand = r.getData("brand", new TypeReference<List<BrandVo>>() {
    
    
                });

                StringBuffer buffer = new StringBuffer();
                String replace = "";
                for (BrandVo brandVo : brand) {
    
    
                    buffer.append(brandVo.getBrandName() + ";");
                    replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
                }

                navVo.setNavValue(buffer.toString());
                navVo.setLink(replace);
            }

            navs.add(navVo);
        }


        return result;
    }

    private String replaceQueryString(SearchParam param, String value, String key) {
    
    
        String encode = null;
        try {
    
    
            // 中文需要编码
            encode = URLEncoder.encode(value, "UTF-8");
            encode = encode.replace("+", "%20"); // 浏览器对空格编码和java不一样,浏览器是%20,java是+
        } catch (UnsupportedEncodingException e) {
    
    
            e.printStackTrace();
        }

        String replace = param.get_queryString().replace("&" + key + "=" + encode, "");
        return replace;
    }

    /**
     * 准备检索请求
     * <p>
     * 模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存),排序,分页,高亮,聚合分析
     *
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {
    
    

        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); // 构建DSL语句

        /**
         * 查询:模糊匹配,过滤(按照属性,分类,品牌,价格区间,库存)
         */

        // 1. 构建bool query
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();

        // 1.1 must 模糊匹配
        if (!StringUtils.isEmpty(param.getKeyword())) {
    
    

            boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }

        // 1.2 bool filter 按照三级分类id查询
        if (param.getCatalog3Id() != null) {
    
    

            boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }
        // 1.2 bool filter 按照品牌id查询
        if (param.getBrandId() != null && param.getBrandId().size() > 0) {
    
    

            boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }
        // 1.2 bool filter 按照所有指定的属性进行查询
        if (param.getAttrs() != null && param.getAttrs().size() > 0) {
    
    


            for (String attrStr : param.getAttrs()) {
    
    
                // attrs=1_5寸:8寸&attrs=2_16G:8G
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
                // attrs=1_5寸:8寸&
                String[] s = attrStr.split("_");
                String attrId = s[0]; // 检索的属性id
                String[] attrValue = s[1].split(":"); // 这个属性检索用的值
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValue));

                // param3 聚合的这些结果以什么方式参与评分
                // 在这儿我们先不让它参与评分

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

                boolQuery.filter(nestedQuery);
            }


        }

        // 1.2 bool filter 按照是否有库存进行查询
        // 字段是用0,1代表,es是用bool值代表
        if (param.getHasStock() != null) {
    
    
            boolQuery.filter(QueryBuilders.termsQuery("hasStock", param.getHasStock() == 1));
        }


        // 1.2 bool filter 按照价格区间 skuPrice 1_500/_500/500_
        if (!StringUtils.isEmpty(param.getSkuPrice())) {
    
    

            /**
             *         {
             *           "range":{
             *             "skuPrice":{
             *               "gte":0,
             *               "lte":6000
             *             }
             *           }
             *         }
             */
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
            String[] s = param.getSkuPrice().split("_");
            if (s.length == 2) {
    
    
                // 区间
                rangeQuery.gte(s[0]).lt(s[1]);
            } else if (s.length == 1) {
    
    
                if (param.getSkuPrice().startsWith("_")) {
    
    
                    rangeQuery.lte(s[0]);
                }

                if (param.getSkuPrice().endsWith("_")) {
    
    
                    rangeQuery.gte(s[0]);
                }
            }
            boolQuery.filter(rangeQuery);
        }


        sourceBuilder.query(boolQuery);


        /**
         * 排序,分页,高亮,
         */
        // 2.1 排序
        if (!StringUtils.isEmpty(param.getSort())) {
    
    
            String sort = param.getSort();
            // sort=saleCount_asc/desc
            String[] s = sort.split("_");

            SortOrder order = s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC;
            sourceBuilder.sort(s[0], order);
        }

        // 2.2 分页
        // pageNum:1 from:0 size:2
        // pageNum:2 from:2 size:2
        // from = (pageNum-1)*size
        sourceBuilder.from((param.getPageNum() - 1) * EsConstant.Product_PAGESIZE);
        sourceBuilder.size(EsConstant.Product_PAGESIZE);

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

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

        }

        /**
         * 聚合分析
         */

        // 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));
        sourceBuilder.aggregation(brand_agg);
        // 2. 分类聚合
        TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").field("catalogId").size(2);
        catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1));
        sourceBuilder.aggregation(catalog_agg);
        // 3. 属性聚合
        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);
        sourceBuilder.aggregation(attr_agg);


        String s = sourceBuilder.toString();
        System.out.println("构建的DSL" + s);


        SearchRequest searchRequest = new SearchRequest(new String[]{
    
    EsConstant.Product_INDEX}, sourceBuilder);

        return searchRequest;
    }
}

刘怡婷知道当小孩最大的好处,就是没有人会认真看待她的话。她大可吹牛、食言,甚至说谎。也是大人反射性的自我保护,因为小孩最初说的往往是雪亮真言,大人只好安慰自己,小孩子懂什么。挫折之下,小孩从说实话的孩子进化为可以选择说实话的孩子,在话语的民主中,小孩才长成大人。

房思琪的初恋乐园
林奕含

猜你喜欢

转载自blog.csdn.net/weixin_44431371/article/details/128824160
今日推荐