此次跟随教程完成项目,主要目的是学习一些新的技术,掌握一些开发工具的使用。因此,并不会对事务逻辑进行细致记录。此篇博客仅用于博主本人日后搭建项目时的参考,并非教程
在商城中,用户通常会直接通过搜索来寻找自己想要的商品,由于商品数量非常多,所以使用传统数据库搜索就显得力不从心。我们使用全文检索技术:Elasticsearch,版本为6.2.4,需要虚拟机JDK1.8及以上。在虚拟机上安装jdk的详细步骤已经在另一篇博客中有所记录,这里就不再赘述。
Elasticsearch安装和配置:
首先上传安装包到/home/leyou目录,解压:
tar xvf elasticsearch-6.2.4.tar.gz
删除压缩包:
rm -rf elasticsearch-6.2.4.tar.gz
出于安全考虑,elasticsearch不允许使用root用户运行,因此我们需要修改权限:
chown leyou:leyou elasticsearch-6.2.4/ -R #-R代表将此文件及文件里的所有内容
进入解压后的文件夹:
cd elasticsearch-6.2.4
目录结构如图所示:
进入config目录:
cd config
发现有三个配置文件,我们只需要修改其中两个如图:
Elasticsearch基于Lucene的,而Lucene底层是java实现,因此我们需要配置jvm参数 。打开jvm配置文件:
vim jvm.options
找到如下默认配置:
-Xms1g
-Xmx1g
默认内存占用为1g,安装时我们选择虚拟机内存就是1g,所以这里内存占用太多了,我们调到512m
-Xms512m
-Xmx512m
保存退出
修改elasticsearch.yml:
vim elasticsearch.yml
修改数据和日志存放目录:
path.data: /home/leyou/elasticsearch-6.2.4/data # 数据目录位置
path.logs: /home/leyou/elasticsearch-6.2.4/logs # 日志目录位置
修改绑定的ip:
network.host: 0.0.0.0 # 绑定到0.0.0.0,允许任何ip来访问
保存后退出,在/home/leyou/elasticsearch-6.2.4/下,我们看到logs已经存在,但是data不存在,因此我们需要手动创建data并更改权限:
mkdir data
chown leyou:leyou . -R
现在我们切换至leyou账号(不能使用root账号启动elasticsearch)
进入相应目录并启动elasticsearch:
cd /home/leyou/elasticsearch-6.2.4/bin
./elasticsearch
发现有四个错误:
错误1:
我使用的CentOS6.5,其linux内核版本为2.6,而此插件要求内核至少3.5以上,在这里选择修改配置文件禁用此插件即可
修改elasticsearch.yml文件,在文件最下面添加如下内容:
bootstrap.system_call_filter: false
保存退出
错误2:
[1]: max file descriptors [4096] for elasticsearch process likely too low, increase to at least [65536]
我们用的是leyou用户,而不是root,所以文件权限不足。
首先用root用户登录。
然后修改配置文件:
vim /etc/security/limits.conf
在文件最下方添加如下内容(*不要去掉):
* soft nofile 65536
* hard nofile 131072
* soft nproc 4096
* hard nproc 4096
保存退出
错误3:
[1]: max number of threads [1024] for user [leyou] is too low, increase to at least [4096]
这是线程数不够。
继续修改配置:
vim /etc/security/limits.d/90-nproc.conf
将原内容:
* soft nproc 1024
改为:
* soft nproc 4096
保存退出
错误4:
[3]: max virtual memory areas vm.max_map_count [65530] likely too low, increase to at least [262144]
继续修改配置文件:
vim /etc/sysctl.conf
在文件末尾添加如下内容:
vm.max_map_count=655360
保存退出后执行如下命令使配置生效:
sysctl -p
重启终端窗口,否则无效。这里我们使用的终端为XShell.
以leyou账号登陆后启动成功:
打开浏览器输入:虚拟机ip:9200,可以看到如下内容:
安装ik分词器:
通过xftp将分词器压缩包上传至/home/leyou/elasticsearch-6.2.4/plugins/目录中,因为是.zip压缩文件,所以使用以下命令解压:
unzip elasticsearch-analysis-ik-6.2.4.zip
得到一个名为elasticsearch的目录:
删除压缩包并将elasticsearch改名(改名根据个人喜好,这里将其改为ik-analyzer)
rm -rf elasticsearch-analysis-ik-6.2.4.zip
mv elasticsearch ik-analyzer
重启elasticsearch:
安装Kibanan:
因为Kibana依赖于node,我们的虚拟机没有安装node,而window中安装过。所以我们选择在window下使用kibana。最新版本与elasticsearch保持一致,也是6.2.4。
解压后进入config目录打开kibana.yml,修改elasticsearch服务器地址:
elasticsearch.url: "http://虚拟机的Ip:9200"
保存后退出进入bin目录双击kibana.bat运行,发现kibana的监听端口是5601。
我们可以通过浏览器进行访问。
Elasticsearch的相关概念,语法,用法就不在此赘述了,需要时可以百度。
需要记录的是如何通过java代码来访问Elasticsearch,Elasticsearch为我们提供了客户端,但是并不好用,因此我们使用Spring提供的。
Spring Data Elasticsearch:
创建新工程,pom.xml文件如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.demo</groupId>
<artifactId>esdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>elasticsearch</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件:
spring:
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 虚拟机的ip地址:9300
#此处配置的是elasticsearch集群的地址
启动类:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EsApplication {
public static void main(String[] args) {
SpringApplication.run(EsApplication.class);
}
}
实体类:
package com.leyou.es.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "heima3",type = "item",shards = 1)
public class Item {
@Field(type = FieldType.Long)
@Id
Long id;
@Field(type = FieldType.Text,analyzer = "ik_smart")
String title; //标题
@Field(type = FieldType.Keyword)
String category;// 分类
@Field(type = FieldType.Keyword)
String brand; // 品牌
@Field(type = FieldType.Double)
Double price; // 价格
@Field(type = FieldType.Keyword,index = false)
String images; // 图片地址
}
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
-
@Document
作用在类,标记实体类为文档对象,一般有两个属性-
indexName:对应索引库名称
-
type:对应在索引库中的类型
-
shards:分片数量,默认5
-
replicas:副本数量,默认1
-
-
@Id
作用在成员变量,标记一个字段作为id主键 -
@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:-
type:字段类型,是是枚举:FieldType
-
index:是否索引,布尔类型,默认是true
-
store:是否存储,布尔类型,默认是false
-
analyzer:分词器名称
-
我们新建一个测试类并添加相应注解,用来测试索引库的创建和相关查询。
在测试类中注入Spring提供的ElasticsearchTemplate,通过template来创建索引库并添加映射关系(索引库相关信息和映射关系已通过在实体类上添加注解实现了)如下:
package com.leyou.es.demo;
import com.leyou.es.pojo.Item;
import com.leyou.es.repository.ItemRepository;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsTest {
@Autowired
private ElasticsearchTemplate template;
@Test
public void testCreate() {
//创建索引库
template.createIndex(Item.class);
//映射关系
template.putMapping(Item.class);
}
}
基本的增删改查我们不使用ElasticsearchTemplate来实现,自定义一个接口并继承spring提供的由ElasticsearchRepository接口。泛型中第一个为实体类的类型,第二个为id的类型,如下所示:
package com.leyou.es.repository;
import com.leyou.es.pojo.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ItemRepository extends ElasticsearchRepository<Item,Long>{
List<Item> findByPriceBetween(Double begin,Double end);
}
注入ItemRepository 到测试类中,并使用其提供的批量增添方法。测试类如下:
package com.leyou.es.demo;
import com.leyou.es.pojo.Item;
import com.leyou.es.repository.ItemRepository;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ArrayList;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class EsTest {
@Autowired
private ElasticsearchTemplate template;
@Autowired
private ItemRepository itemRepository;
@Test
public void testCreate() {
//创建索引库
template.createIndex(Item.class);
//映射关系
template.putMapping(Item.class);
}
@Test
public void insertIndex() {
List<Item> list = new ArrayList<>();
list.add(new Item(1L, "小米手机7", "手机", "小米", 3299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(2L, "坚果手机R1", "手机", "锤子", 3699.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(3L, "华为META10", "手机", "华为", 4499.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(4L, "小米Mix2S", "手机", "小米", 4299.00, "http://image.leyou.com/13123.jpg"));
list.add(new Item(5L, "荣耀V10", "手机", "华为", 2799.00, "http://image.leyou.com/13123.jpg"));
// 接收对象集合,实现批量新增
itemRepository.saveAll(list);
}
}
基本查询(查询所有):
@Test
public void testFind() {
Iterable<Item> all = itemRepository.findAll();
for(Item item : all) {
System.out.println("item = " + item);
}
}
自定义方法(根据价格查询):
在自定义接口中给出方法返回值,参数和方法名,底层将自动实现(命名方法必须按照spring所给出的规范)
package com.leyou.es.repository;
import com.leyou.es.pojo.Item;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import java.util.List;
public interface ItemRepository extends ElasticsearchRepository<Item,Long>{
//以下为新增的方法(根据价格区间查询)
List<Item> findByPriceBetween(Double begin,Double end);
}
在测试类中直接调用即可:
@Test
public void testFindBy() {
List<Item> list = itemRepository.findByPriceBetween(2000d, 4000d);
for(Item item : list) {
System.out.println("item : " + item);
}
}
也可以通过自定义方法实现分页,在传入参数中增加Pageable对象即可。
但是像一些复杂聚合查询仍需要我们使用elasticsearch原生查询自己实现
自定义查询:
@Test
public void testQuery() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
//结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[] {"id","title","price"},null));
//添加查询条件
queryBuilder.withQuery(QueryBuilders.matchQuery("title","小米手机"));
//排序
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
//分页
queryBuilder.withPageable(PageRequest.of(0,2));
Page<Item> result = itemRepository.search(queryBuilder.build());
//总条数
long total = result.getTotalElements();
System.out.println("total = " + total);
//总页数
int totalPages = result.getTotalPages();
System.out.println("totalPages = " + totalPages);
List<Item> list = result.getContent();
for(Item item : list) {
System.out.println("item = " + item);
}
}
@Test
public void testAgg() {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
String aggName = "popularBrand";
// 聚合
queryBuilder.addAggregation(AggregationBuilders.terms(aggName).field("brand"));
// 查询并返回带聚合结果
AggregatedPage<Item> result = template.queryForPage(queryBuilder.build(), Item.class);
// 解析聚合
Aggregations aggs = result.getAggregations();
// 获取指定名称的聚合
StringTerms terms = aggs.get(aggName);
// 获取桶
List<StringTerms.Bucket> buckets = terms.getBuckets();
for(StringTerms.Bucket bucket : buckets) {
System.out.println("key = " + bucket.getKey());
System.out.println("docCount = " + bucket.getDocCount());
}
}
接下来我们可以创建我们的搜索微服务:
pom.xml文件如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou</artifactId>
<groupId>com.leyou.parent</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.leyou.service</groupId>
<artifactId>ly-search</artifactId>
<dependencies>
<!--eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--elasticsearch-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.leyou.service</groupId>
<artifactId>ly-item-interface</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
因为我们在向索引库中增添数据时需调用商品微服务查询商品信息,所以应添加Feign有关注解,启动类如下:
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class);
}
}
配置文件:
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.114.129:9300
#下面一项的配置用于返回信息时
jackson:
default-property-inclusion: non_null
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
商品实体类:
package com.leyou.search.pojo;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Data
@Document(indexName = "goods",type = "docs",shards = 1,replicas = 0)
public class Goods {
@Id
private Long id; // spuId
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String all; // 所有需要被搜索的信息,包含标题,分类,甚至品牌
@Field(type = FieldType.Keyword, index = false)
private String subTitle;// 卖点
private Long brandId;// 品牌id
private Long cid1;// 1级分类id
private Long cid2;// 2级分类id
private Long cid3;// 3级分类id
private Date createTime;// 创建时间
private List<Long> price;// 价格
@Field(type = FieldType.Keyword, index = false)
private String skus;// sku信息的json结构
private Map<String, Object> specs;// 可搜索的规格参数,key是参数名,值是参数值
}
搜索条件请求类:
package com.leyou.search.pojo;
import lombok.Data;
public class SearchRequest {
private String key;// 搜索条件
private Integer page;// 当前页
private static final int DEFAULT_SIZE = 20;// 每页大小,不从页面接收,而是固定大小
private static final int DEFAULT_PAGE = 1;// 默认页
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public Integer getPage() {
if(page == null){
return DEFAULT_PAGE;
}
// 获取页码时做一些校验,不能小于1
return Math.max(DEFAULT_PAGE, page);
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return DEFAULT_SIZE;
}
}
因为搜索微服务要使用Feign调用商品微服务,所以应得到相应接口。但在实际开发中,两个微服务会由两个团队独立开发。所以搜索微服务团队并不能也无需知道商品微服务的相关请求路径。这样我们在商品微服务接口工程中给出相应接口但不添加相应注解,搜索微服务通过引入接口工程依赖并自定义接口继承相应接口并添加注解就可以解决问题。代码如下(接口工程中需要添加相应依赖否则注解无法生效):
interface工程新添加的依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
interface工程提供的api接口示例:
package com.leyou.item.api;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Sku;
import com.leyou.item.pojo.Spu;
import com.leyou.item.pojo.SpuDetail;
import org.springframework.web.bind.annotation.*;
import java.util.List;
public interface GoodsApi {
@GetMapping("/spu/page")
PageResult<Spu> querySpuByPage(
@RequestParam(value = "page",defaultValue="1")Integer page,
@RequestParam(value = "rows",defaultValue="5")Integer rows,
@RequestParam(value = "saleable",required = false)boolean saleable,
@RequestParam(value = "key",required = false)String key
);
@PostMapping("goods")
void saveGoods(@RequestBody Spu spu);
@GetMapping("/spu/detail/{id}")
SpuDetail querySpuDetailById(@PathVariable("id") Long id);
@GetMapping("/sku/list")
List<Sku> querySkuBySpuId(@RequestParam("id") Long id);
@PutMapping("/goods")
void updateGoods(@RequestBody Spu spu);
}
搜索微服务的相应接口(注意这里添加了@FeignClient):
package com.leyou.search.client;
import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient("item-service")
public interface GoodsClient extends GoodsApi {
}
这样我们就可以调用商品微服务来查询商品信息了。
商品的elasticsearchRepository接口:
package com.leyou.search.repository;
import com.leyou.search.pojo.Goods;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface GoodsRepository extends ElasticsearchRepository<Goods,Long>{
}
测试类中进行创建索引库并使用流处理批量新增数据:
package com.leyou.search.repository;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.Spu;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.service.SearchService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.stream.Collectors;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SearchService searchService;
@Test
public void testCreateIndex() {
template.createIndex(Goods.class);
template.putMapping(Goods.class);
}
@Test
public void loadData() {
//查询spu信息
int page = 1;
int rows = 100;
int size = 0;
do{
PageResult<Spu> result = goodsClient.querySpuByPage(page, rows, true,null);
List<Spu> spuList = result.getItems();
if(CollectionUtils.isEmpty(spuList)) {
break;
}
//构建成goods
List<Goods> goodsList = spuList.stream().map(searchService::buildGoods).collect(Collectors.toList());
//存入索引库
goodsRepository.saveAll(goodsList);
//翻页
page++;
size = spuList.size();
} while (size == 100);
}
}
service层的各种查询方法:
package com.leyou.search.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.leyou.common.enums.ExceptionEnum;
import com.leyou.common.exception.LyException;
import com.leyou.common.utils.JsonUtils;
import com.leyou.common.vo.PageResult;
import com.leyou.item.pojo.*;
import com.leyou.search.client.BrandClient;
import com.leyou.search.client.CategoryClient;
import com.leyou.search.client.GoodsClient;
import com.leyou.search.client.SpecificationClient;
import com.leyou.search.pojo.Goods;
import com.leyou.search.pojo.SearchRequest;
import com.leyou.search.pojo.SearchResult;
import com.leyou.search.repository.GoodsRepository;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SourceFilter;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class SearchService {
@Autowired
private CategoryClient categoryClient;
@Autowired
private BrandClient brandClient;
@Autowired
private GoodsClient goodsClient;
@Autowired
private SpecificationClient specificationClient;
@Autowired
private GoodsRepository goodsRepository;
@Autowired
private ElasticsearchTemplate template;
public Goods buildGoods(Spu spu) {
// 查询分类
List<Category> categories = categoryClient.queryCategoryByIds
(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
if (CollectionUtils.isEmpty(categories)) {
throw new LyException(ExceptionEnum.CATEGORY_NOT_FOUND);
}
List<String> names = categories.stream().map(Category::getName).collect(Collectors.toList());
// 查询品牌
Brand brand = brandClient.queryBrandById(spu.getBrandId());
if (brand == null) {
throw new LyException(ExceptionEnum.BRAND_NOT_FOUND);
}
// 搜索字段
String all = spu.getTitle() + StringUtils.join(names, " ") + brand.getName();
// 查询sku
List<Sku> skuList = goodsClient.querySkuBySpuId(spu.getId());
if (CollectionUtils.isEmpty(skuList)) {
throw new LyException(ExceptionEnum.GOODS_SKU_NOT_FOUND);
}
// 对sku进行处理
List<Map<String, Object>> skus = new ArrayList<>();
for (Sku sku : skuList) {
Map<String, Object> map = new HashMap<>();
map.put("id", sku.getId());
map.put("title", sku.getTitle());
map.put("price", sku.getPrice());
map.put("image", StringUtils.substringBefore(sku.getImages(), ","));
skus.add(map);
}
// 处理价格
List<Long> priceList = skuList.stream().map(Sku::getPrice).collect(Collectors.toList());
// 查询规格参数
List<SpecParam> params = specificationClient.queryParamByGid(null, spu.getCid3(), true);
if (CollectionUtils.isEmpty(params)) {
throw new LyException(ExceptionEnum.SPEC_PARAM_NOT_FOUND);
}
// 查询商品详情
SpuDetail spuDetail = goodsClient.querySpuDetailById(spu.getId());
// 获取通用规格参数
Map<Long, String> genericSpec = JsonUtils.parseMap(spuDetail.getGenericSpec(), Long.class, String.class);
// 获取特有规格参数
Map<Long, List<String>> specialSpec = JsonUtils.nativeRead
(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
});
// 规格参数
Map<String, Object> specs = new HashMap<>();
for (SpecParam param : params) {
// 规格名称
String key = param.getName();
Object value = "";
// 判断是否是通用规格
if (param.getGeneric()) {
value = genericSpec.get(param.getId());
// 判断是否是数值类型
if (param.getNumeric()) {
value = chooseSegment(value.toString(), param);
}
} else {
value = specialSpec.get(param.getId());
}
specs.put(key, value);
}
// 构建goods对象
Goods goods = new Goods();
goods.setBrandId(spu.getBrandId());
goods.setCid1(spu.getCid1());
goods.setCid2(spu.getCid2());
goods.setCid3(spu.getCid3());
goods.setCreateTime(spu.getCreateTime());
goods.setId(spu.getId());
goods.setSubTitle(spu.getSubTitle());
goods.setAll(all); // 搜索字段,包含标题,分类,品牌和规格
goods.setPrice(priceList); // 所有sku价格的集合
goods.setSkus(JsonUtils.serialize(skus)); // 所有sku的集合的json形式
goods.setSpecs(specs); // 所有的可搜索的规格参数
return goods;
}
private String chooseSegment(String value, SpecParam p) {
double val = NumberUtils.toDouble(value);
String result = "其它";
// 保存数值段
for (String segment : p.getSegments().split(",")) {
String[] segs = segment.split("-");
// 获取数值范围
double begin = NumberUtils.toDouble(segs[0]);
double end = Double.MAX_VALUE;
if (segs.length == 2) {
end = NumberUtils.toDouble(segs[1]);
}
// 判断是否在范围内
if (val >= begin && val < end) {
if (segs.length == 1) {
result = segs[0] + p.getUnit() + "以上";
} else if (begin == 0) {
result = segs[1] + p.getUnit() + "以下";
} else {
result = segment + p.getUnit();
}
break;
}
}
return result;
}
public PageResult<Goods> search(SearchRequest request) {
Integer page = request.getPage() - 1;
Integer size = request.getSize();
// 创建查询构建器
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{"id", "subTitle", "skus"}, null));
// 分页
queryBuilder.withPageable(PageRequest.of(page, size));
// 搜索条件
QueryBuilder basicQuery = buildBasicQuery(request);
queryBuilder.withQuery(basicQuery);
// 聚合分类和品牌
// 聚合分类
String categoryAggName = "category_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(categoryAggName).field("cid3"));
// 聚合品牌
String brandAggName = "brand_agg";
queryBuilder.addAggregation(AggregationBuilders.terms(brandAggName).field("brandId"));
// 查询
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
//解析分页结果
long total = result.getTotalElements();
long totalPages = result.getTotalPages();
List<Goods> goodsList = result.getContent();
//解析聚合结果
Aggregations aggs = result.getAggregations();
List<Category> categories = parseCategoryAgg(aggs.get(categoryAggName));
List<Brand> brands = parseBrandAgg(aggs.get(brandAggName));
// 完成规格参数聚合
List<Map<String, Object>> specs = null;
if (categories != null && categories.size() == 1) {
specs = buildSpecificationAgg(categories.get(0).getId(),basicQuery);
}
return new SearchResult(total, totalPages, goodsList, categories, brands, specs);
}
private QueryBuilder buildBasicQuery(SearchRequest request) {
// 创建布尔查询
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
// 查询条件
queryBuilder.must(QueryBuilders.matchQuery("all",request.getKey()));
// 过滤条件
Map<String,String> map = request.getFilter();
for(Map.Entry<String,String> entry : map.entrySet()) {
String key = entry.getKey();
// 处理key
if(!"cid3".equals(key) && !"brandId".equals(key)) {
key = "specs." + key + ".keyword";
}
queryBuilder.filter(QueryBuilders.termQuery(key,entry.getValue()));
}
return queryBuilder;
}
private List<Map<String, Object>> buildSpecificationAgg(Long cid, QueryBuilder basicQuery) {
List<Map<String, Object>> specs = new ArrayList<>();
// 查询需要聚合的规格参数
List<SpecParam> params = specificationClient.queryParamByGid(null, cid, true);
// 聚合
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(basicQuery);
for(SpecParam param : params) {
String name = param.getName();
queryBuilder.addAggregation(AggregationBuilders.terms(name).field("specs." + name + ".keyword"));
}
// 获取结果
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
// 解析结果
Aggregations aggregations = result.getAggregations();
for (SpecParam param : params) {
StringTerms terms = aggregations.get(param.getName());
List<String> options = terms.getBuckets().stream().map(t -> t.getKeyAsString()).collect(Collectors.toList());
Map<String,Object> map = new HashMap<>();
map.put("k",param.getName());
map.put("options",options);
specs.add(map);
}
return specs;
}
private List<Brand> parseBrandAgg(LongTerms terms) {
try {
List<Long> ids = terms.getBuckets().stream()
.map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Brand> brands = brandClient.queryBrandByIds(ids);
return brands;
} catch (Exception e) {
return null;
}
}
private List<Category> parseCategoryAgg(LongTerms terms) {
try {
List<Long> ids = terms.getBuckets().stream()
.map(b -> b.getKeyAsNumber().longValue()).collect(Collectors.toList());
List<Category> categories = categoryClient.queryCategoryByIds(ids);
return categories;
} catch (Exception e) {
return null;
}
}
}
第三部分到此为止