1. 引言
1.1 Elasticsearch
Elasticsearch:全文检索技术。
如上所述,Elasticsearch具备以下特点:
- 分布式,无需人工搭建集群(solr就需要人为配置,使用Zookeeper作为注册中心)
- Restful风格,一切API都遵循Rest原则,容易上手
- 近实时搜索,数据更新在Elasticsearch中几乎是完全同步的。
1.2 kibana
Kibana是一个基于Node.js的Elasticsearch索引库数据统计工具(发请求),可以利用Elasticsearch的聚合功能,生成各种图表,如柱形图,线状图,饼图等。
而且还提供了操作Elasticsearch索引数据的控制台,并且提供了一定的API提示
1.3 操作索引
Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。
对比关系:
索引(indices)--------------------------------Databases 数据库
类型(type)-----------------------------Table 数据表
索引(indices)--------------------------------Databases 数据库
类型(type)-----------------------------Table 数据表
文档(Document)----------------Row 行
字段(Field)-------------------Columns 列
Elasticsearch采用Rest风格API(http请求接口),因此其API就是一次http请求,可以用任何工具发起http请求
索引的请求格式:
- 请求方式:PUT(创建,修改合二为一)/ GET(查看)/ DELETE(删除)/ POST(可以向一个已经存在的索引库中添加数据)
- 请求路径:/索引库名
- 请求参数:json格式:
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
- settings:索引库的设置
- number_of_shards:分片数量
- number_of_replicas:副本数量
1.4 测试
1.4.1 查询
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
public void testQuery{
// 1 创建查询构建器(spring提供的)
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
// 2 结果过滤
queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{
"id", "subTitle", "skus"}, null));
// 3 添加查询条件
queryBuilder.withQuery(QueryBuilders.matchQuery(name:"title",text:"小米手机"));
// 4 排序
queryBuilder.withSort(SortBuilders.fieldSort("price"").order(SortOrder.DESC));
// 5 分页
queryBuilder.withPageable(PageRequest.of(page,size));
// 6 查询
Page<Goods> result = repository.search(queryBuilder.build());
long total = result.getTotalElements();
........
}
}
采用类的字节码信息创建索引并映射:
Spring Data通过注解来声明字段的映射属性,有下面的三个注解:
@Document
作用在类(Goods),标记实体类为文档对象,一般有两个属性- indexName:对应索引库名称
- type:对应在索引库中的类型
- shards:分片数量,默认5
- replicas:副本数量,默认1
@Id
作用在成员变量,标记一个字段作为id主键@Field
作用在成员变量,标记为文档的字段,并指定字段映射属性:- type:字段类型,是是枚举:FieldType
- index:是否索引,布尔类型,默认是true
- store:是否存储,布尔类型,默认是false
- analyzer:分词器名称
- 增删改不用
ElasticsearchTemplate
,ElasticsearchTemplate一般会用来做原生的复杂查询,比如聚合,我们一般的普通增删改查用不到,而spring给我们提供了ElasticsearchRepository
( Spring Data 的强大之处,就在于你不用写任何DAO处理,自动根据方法名或类的信息进行CRUD操作。只要你定义一个接口,然后继承Repository提供的一些子接口,就能具备各种基本的CRUD功能。) - 因此我们应该写个
GoodsRepository
接口继承ElasticsearchRepository,第一个泛型是实体类,第二个是id类型,接下来就可以直接用了 - Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。当然,方法名称要符合一定的约定
public interface GoodsRepository extends ElasticsearchRepository<Goods, Long> {
}
QueryBuilder(spring提供的)可整合Elasticsearch原生的结果过滤、查询、排序、分页等,还有结果过滤,整合完之后利用spring data做一个搜索,它会帮我们封装成一个结果
1.4.2 聚合
@RunWith(SpringRunner.class)
@SpringBootTest
public class GoodsRepositoryTest {
public void testAgg{
// 1 创建查询构建器(spring提供的)
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
String aggName = "popularBrand";
// 2 聚合
queryBuilder.addAggregation(AggregationBuilders.terms(CategoryAggName).field("brand"));
// 3 查询并返回带聚合结果
AggregatedPage<Goods> result = template.queryForPage(queryBuilder.build(), Goods.class);
// 4 解析聚合
Aggregations aggs = result.getAggregations();
// 5 获取指定名称的聚合
StringTerms terms = aggs.getName(aggName);
// 6 获取桶
List<StringTerms.Bucket> buckets = terms.getBuckets();
for(StringTerms.Bucket bucket:buckets){
bucket.getKeyAsString();
...
}
........
}
}
2. 搭建项目
用户访问我们的首页,一般都会直接搜索来寻找自己想要购买的商品。
而商品的数量非常多,而且分类繁杂。如果能正确的显示出用户想要的商品,并进行合理的过滤,尽快促成交易,是搜索系统要研究的核心。
面对这样复杂的搜索业务和数据量,一般我们都会使用全文检索技术: Elasticsearch。
2.1 引入依赖
<?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.page.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>
<!--springboot启动器的测试功能-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</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-amqp</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 配置
server:
port: 8083
spring:
application:
name: search-service
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.184.130:9300
jackson:
default-property-inclusion: non_null #排除返回结构中字段值为null的属性
rabbitmq:
host: 192.168.184.130
username: leyou
password: leyou
virtual-host: /leyou
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
registry-fetch-interval-seconds: 10
instance:
#lease-renewal-interval-in-seconds: 5 # 每隔5秒发送一次心跳
#lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
prefer-ip-address: true
ip-address: 127.0.0.1
2.3 启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class LySearchApplication {
public static void main(String[] args) {
SpringApplication.run(LySearchApplication.class, args);
}
}
3. 索引库数据格式分析
接下来,我们需要商品数据导入索引库,便于用户搜索。
那么问题来了,我们有SPU和SKU,到底如何保存到索引库?
3.1 以结果为导向
我们来看下搜索结果页:
可以看到,每一个搜索结果都有至少1个商品,当我们选择大图下方的小图,商品会跟着变化。
因此,搜索的结果是SPU,即多个SKU的集合。
既然搜索的结果是SPU,那么我们索引库中存储的应该也是SPU,但是却需要包含SKU的信息。
3.2 需要什么数据
由上图可以直观能看到的:图片、价格、标题、副标题(属于SKU数据,用来展示的);暗藏的数据:spu的id,sku的id
另外,页面还有过滤条件:
这些过滤条件也都需要存储到索引库中,包括:商品分类、品牌、可用来搜索的规格参数等
综上所述,我们需要的数据格式有:
spuId、SkuId、商品分类id、品牌id、图片、价格、商品的创建时间、sku信息集、可搜索的规格参数
3.3 最终的数据结构
我们创建一个类,封装要保存到索引库的数据,并设置映射属性:
@Data
@Document(indexName = "goods", type = "docs", shards = 1)
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 Set<Long> price;// 价格,对应到elasticsearch/json中是数组,一个spu有多个sku,就有多个价格
@Field(type = FieldType.keyword, index = false)
pri