elasticsearch
简介
https://www.elastic.co/cn/what-is/elasticsearch
全文搜索属于最常见的需求, 开源的 Elasticsearch 是目前全文搜索引擎的首选。
它可以快速地储存、 搜索和分析海量数据。 维基百科、 Stack Overflow、 Github 都采用它
Elastic 的底层是开源库 Lucene。 但是, 你没法直接用 Lucene, 必须自己写代码去调用它的接口。 Elastic 是 Lucene 的封装, 提供了 REST API 的操作接口, 开箱即用。
REST API: 天然的跨平台。
官方文档: https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文: https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文:
https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/
一、 基本概念
1.1、 Index(索引)
动词, 相当于 MySQL 中的 insert;
名词, 相当于 MySQL 中的 Database
1.2、 Type(类型)
在 Index(索引) 中, 可以定义一个或多个类型。
类似于 MySQL 中的 Table; 每一种类型的数据放在一起;
1.3、 Document(文档)
保存在某个索引(Index) 下, 某种类型(Type) 的一个数据(Document) , 文档是 JSON 格式的, Document 就像是 MySQL 中的某个 Table 里面的内容;
1.4、 倒排索引机制
可以参考这一篇博客:https://blog.csdn.net/fjxcsdn/article/details/103108690
二、 Docker 安装 Es
2.1、 下载镜像文件
- 存储和检索数据
docker pull elasticsearch:7.4.2 - 可视化检索数据
docker pull kibana:7.4.2
2.2、 创建实例
2.2.1、 ElasticSearch
- 创建文件
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
- 写配置文件
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
- 设置权限
chmod -R 777 /mydata/elasticsearch/ 保证权限
- 创建并启动docker容器
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
以后再外面装好插件重启即可;
特别注意:
-e ES_JAVA_OPTS=“-Xms64m -Xmx256m” \ 测试环境下, 设置 ES 的初始内存和最大内存, 否则导致过大启动不了 ES
2、 Kibana
- 编辑配置文件
vim /data/kibana/config/kibana.yml
#Kibana的映射端口
server.port: 5601
#网关地址
server.host: "0.0.0.0"
#Kibana实例对外展示的名称
server.name: "kibana-172.19.0.2"
#Elasticsearch的集群地址,也就是说所有的集群IP,之前查到的ip
elasticsearch.hosts: ["http://172.19.0.2:9200"]
#设置页面语言,中文使用zh-CN,英文使用en
i18n.locale: "zh-CN"
xpack.monitoring.ui.container.elasticsearch.enabled: true
启动
docker run -d -p 5601:5601 -v /data/kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml --network es-net --name kibana kibana:7.4.2
一定改为自己虚拟机的
三、 初步检索
3.1、 _cat
- 查看所有节点
GET /_cat/nodes - 查看es健康状况
GET /_cat/health - 查看主节点
GET /_cat/master - 查看索引
GET /_cat/indices
3. 2、 索引一个文档(保存)
保存一个数据, 保存在哪个索引的哪个类型下, 指定用哪个唯一标识
PUT customer/external/1
在 customer 索引下的 external 类型下保存 1 号数据为
PUT customer/external/1{
"name":"hollow word"
}
PUT 和 POST 都可以,
- POST 新增。 如果不指定 id, 会自动生成 id。 指定 id 就会修改这个数据, 并新增版本号
- PUT 可以新增可以修改。 PUT 必须指定 id; 由于 PUT 需要指定 id, 我们一般都用来做修改操作, 不指定 id 会报错。
第一次发put请求新增
第二次发put请求就是修改
post请求
3.3 查询一个文档
get /索引/类型/记录id
3.4 更新一个文档
- 方式一
POST customer/external/1/_update
{
"doc":{
"name": "John Doew"
}
}
- 方式二
POST customer/external/1
{
"name": "John Doe2"
}
- 方式三
PUT customer/external/1
{
"name": "John Doe"
}
不同: _update 操作会对比源文档数据, 如果相同不会有什么操作, 文档 version 不增加
适用场景:
- 对于大并发更新, 不带 update;
- 对于大并发查询偶尔更新, 带 update; 对比更新, 重新计算分配规则。
3.5 删除操作
- 删除文档
DELETE customer/external/1
- 删除索引
DELETE customer
3.6、 bulk 批量 API
jsonPOST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
语法格式:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
复杂实例:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123"} }
{ "doc" : {"title" : "My updated blog post"} }
bulk API 以此按顺序执行所有的 action(动作) 。 如果一个单个的动作因任何原因而失败,
它将继续处理它后面剩余的动作。 当 bulk API 返回时, 它将提供每个动作的状态(与发送的顺序相同) , 所以您可以检查是否一个指定的动作是不是失败了
3.7、 样本测试数据
我准备了一份顾客银行账户信息的虚构的 JSON 文档样本。 每个文档都有下列的
schema
(模式) :
{
"account_number": 0,
"balance": 16623,
"firstname": "Bradshaw",
"lastname": "Mckenzie",
"age": 29,
"gender": "F",
"address": "244 Columbus Place",
"employer": "Euron",
"email": "bradshawmckenzie@euron.com",
"city": "Hobucken",
"state": "CO"
}
https://raw.githubusercontent.com/elastic/elasticsearch/7.4/docs/src/test/resources/accounts.json
导入测试数据
POST bank/account/_bulk
四、 进阶检索
4.1、 SearchAPI
ES 支持两种基本方式检索 :
- 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
- 另一个是通过使用 REST request body 来发送它们(uri+请求体)1) 、 检索信息
4.1.1、 检索信息
- 一切检索从_search 开始
- GET bank/_search: 检索 bank 下所有信息, 包括 type 和 docs
- GET bank/_search?q=*&sort=account_number:asc 请求参数方式检索
响应结果解释:
字段 | 解释 |
---|---|
took | Elasticsearch 执行搜索的时间( 毫秒) |
time_out | 告诉我们搜索是否超时 |
shards | 告诉我们多少个分片被搜索了, 以及统计了成功/失败的搜索分片 |
hits | 搜索结果 |
hits.total | 搜索结果 |
hits.hits | 实际的搜索结果数组( 默认为前 10 的文档) |
sort - | 结果的排序 key( 键) ( 没有则按 score 排序) |
score | 相关性得分(全文检索用) |
max_score | 最高得分( 全文检索用) |
- url+请求体的形式进行检索
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
HTTP 客户端工具( POSTMAN) , get 请求不能携带请求体, 我们变为 post 也是一样的我们 POST 一个 JSON 风格的查询请求体到 _search API。
需要了解, 一旦搜索的结果被返回, Elasticsearch 就完成了这次请求, 并且不会维护任何服务端的资源或者结果的 cursor( 游标)
4.2、 Query DSL
4.2.1 、 基本语法格式
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL( domain-specific language 领域特定语言) 。 这个被称为 Query DSL。 该查询语言非常全面, 并且刚开始的时候感觉有点复杂,
真正学好它的方法是从一些基础的示例开始的。
一个查询语句的典型结构
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
如果是针对某个字段, 那么它的结构如下:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
示例模板
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
解释
- query 定义如何查询,
- match_all 查询类型【代表查询所有的所有】 , es 中可以在 query 中组合非常多的查询类型完成复杂查询
- 除了 query 参数之外, 我们也可以传递其它的参数以改变查询结果。 如 sort, size
- from+size 限定, 完成分页功能
- sort 排序, 多字段排序, 会在前序字段相等时后续字段内部排序, 否则以前序为准
4.2.2 返回部分字段
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"_source": ["age","balance"]
}
4.2.3 match【 匹配查询】
- 基本类型( 非字符串) , 精确匹配
GET bank/_search
{
"query": {
"match": {
"account_number": "20"
}
}
}
match 返回 account_number=20 的
- 字符串, 全文检索
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
}
}
最终查询出 address 中包含 mill 单词的所有记录,match 当搜索字符串类型的时候, 会进行全文检索, 并且每条记录有相关性得分。
- 字符串, 多个单词( 分词+全文检索)
GET bank/_search
{
"query": {
"match": {
"address": "mill road"
}
}
}
最终查询出 address 中包含 mill 或者 road 或者 mill road 的所有记录, 并给出相关性得分
4.2.4 、 match_phrase【 短语匹配】
将需要匹配的值当成一个整体单词( 不分词) 进行检索
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill road"
}
}
}
查出 address 中包含 mill road 的所有记录, 并给出相关性得分
4.2.5.5 、 multi_match【 多字段匹配】
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["state","address"]
}
}
}
state 或者 address 包含 mill
4.2.5.6、 bool【 复合查询】
bool 用来做复合查询:
复合语句可以合并 任何 其它查询语句, 包括复合语句, 了解这一点是很重要的。 这就意味着, 复合语句之间可以互相嵌套, 可以表达非常复杂的逻辑。
- must: 必须达到 must 列举的所有条件
get bank/_search
{
"query":{
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
]
}
}
}
查找address=mill同时gender=M的所有结果
- should: 应该达到 should 列举的条件, 如果达到会增加相关文档的评分, 并不会改变询的结果。 如果 query 中只有 should 且只有一种匹配规则, 那么 should 的条件就会被作为默认匹配条件而去改变查询结果
get bank/_search
{
"query":{
"bool": {
"must": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
],
"should": [
{
"match": {
"lastname": "wallace"
}
}
]
}
}
}
- must_not 必须不是指定的情况
GET bank/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"address": "mill"
}
},
{
"match": {
"gender": "M"
}
}
]
}
}
}
查询所有address不包含mill同时gender不包含M的所有结果集
4.2.5.7 filter【结果过滤】
并不是所有的查询都需要产生分数, 特别是那些仅用于 “filtering”(过滤) 的文档。 为了不计算分数 Elasticsearch 会自动检查场景并且优化查询的执行
GET bank/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gt": 10,
"lt": 100
}
}
}
}
}
}
查询age在10到100之间的所有结果
4.2.5.8 term
和 match 一样。 匹配某个属性的值。 全文检索字段用 match, 其他非 text 字段匹配用 term。
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"age": {
"value": 28
}
}
}
]
}
}
}
## 精确查询
GET bank/_search
{
"query": {
"match": {
"address.keyword": "789 Madison"
}
}
}
## 精确查询
GET bank/_search
{
"query": {
"match": {
"address": "789 Madison"
}
}
}
4.2.5.9 aggregations( 执行聚合)
聚合提供了从数据中分组和提取数据的能力。 最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。 在 Elasticsearch 中, 您有执行搜索返回 hits( 命中结果) , 并且同时返回聚合结果, 把一个响应中的所有 hits( 命中结果) 分隔开的能力。 这是非常强大且有效的,您可以执行查询和多个聚合, 并且在一次使用中得到各自的( 任何一个的) 返回结果, 使用一次简洁和简化的 API 来避免网络往返。
- 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄, 但不显示这些人的详情。
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"avgAge":{
"avg": {
"field": "age"
}
},
"aveBalance":{
"avg": {
"field": "balance"
}
}
}
}
- 按照年龄聚合, 并且请求这些年龄段的这些人的平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
- 复杂: 查出所有年龄分布, 并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄
段的总体平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"avgAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"ageBalannce": {
"avg": {
"field": "balance"
}
},
"ageGender":{
"terms": {
"field": "gender.keyword",
"size": 10
},
"aggs": {
"ageGenderBalance": {
"avg": {
"field": "balance"
}
}
}
}
}
}
}
}
4.3 Mapping 映射
4.3.1字段类型
字段类型 | Elasticsearch类型 | 说明 |
---|---|---|
string,varchar | keyword | 这是不可标记的文本字段,例如 CODE001 |
string,varchar,text | text | 这是要标记化的文本字段,例如 a nice text |
integer | integer | 这是一个整型(32位),例如 1、2、3 |
long | long | 这是一个长整型(64位) |
float | float | 这是一个浮点数(32位),例如 1.2 或 4.5 |
double | double | 这是一个 double 类型浮点数(64位) |
boolean | boolean | 这是一个布尔值:true 或 false |
date / datetime | date | 这是一个日期时间值 |
bytes / binary | binary | 这包含一些用于二进制数据的字节,例如文本或字节流 |
其他映射类型
4.3.2 映射
Mapping(映射)
Mapping 是用来定义一个文档( document) , 以及它所包含的属性( field) 是如何存储和索引的。 比如, 使用 mapping 来定义:
- 哪些字符串属性应该被看做全文本属性(full text fields) 。
- 哪些属性包含数字, 日期或者地理位置。
- 文档中的所有属性是否都能被索引(_all 配置) 。
- 日期的格式。
- 自定义映射规则来执行动态添加属性
查看映射信息
get 索引/_mapping
get bank/_mappinng
4.3.3 新版本改变
Es7 及以上移除了 type 的概念。
- 关系型数据库中两个数据表示是独立的, 即使他们里面有相同名称的列也不影响使用,但 ES 中不是这样的。 elasticsearch 是基于 Lucene 开发的搜索引擎, 而 ES 中不同 type下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name, 在 ES 同一个索引下其实被认为是同一个 filed,你必须在两个不同的 type 中定义相同的 filed 映射。 否则, 不同 type 中的相同字段名称就会在处理中出现冲突的情况, 导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
- Elasticsearch 7.x
URL 中的 type 参数为可选。 比如, 索引一个文档不再要求提供文档类型。 - Elasticsearch 8.x
不再支持 URL 中的 type 参数。
解决:- 将索引从多类型迁移到单类型, 每种类型文档一个独立索引
- 将已存在的索引下的类型数据, 全部迁移到指定位置即可。 详见数据迁移
4.3.3.1. 创建索引并指定映射
PUT /my_index
{
"mappings":{
"properties":{
"age":{"type":"integer"},
"name":{"type":"text"}
}
}
}
4.3.3.2. 添加新的映射
PUT /my_index/_mapping
{
"properties":{
"id":{
"type":"integer",
"index":"false"
}
}
}
解释:index:是否参与索引:true为是,false为否
4.3.3.3 更新映射
对于已经存在的映射字段, 我们不能更新。 更新必须创建新的索引进行数据迁移
先创建出 new_twitter 的正确映射。 然后使用如下方式进行数据迁移
POST _reindex [固定写法]
{
"source": {
"index": "twitter"
},
"dest": {
"index": "new_twitter"
}
}
实例将bank索引下account类型的数据迁移到network下面
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "network"
}
}
4.4 分词
一个 tokenizer( 分词器) 接收一个字符流, 将之分割为独立的 tokens( 词元, 通常是独立的单词) , 然后输出 tokens 流。
例如, whitespace tokenizer 遇到空白字符时分割文本。 它会将文本 "Quick brown fox!"分割为 [Quick, brown, fox!]。
该 tokenizer(分词器) 还负责记录各个 term(词条) 的顺序或 position 位置(用于phrase 短语和 word proximity 词近邻查询) , 以及 term(词条) 所代表的原始 word(单词) 的 start(起始) 和 end(结束) 的 character offsets(字符偏移量) (用于高亮显示搜索的内容) 。Elasticsearch 提供了很多内置的分词器, 可以用来构建 custom analyzers(自定义分词器) 。
4.4.1 、 安装 ik 分词器
注意: 不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2 对应 es 版本安装
- 进入 es 容器内部 plugins 目录
docker exec -it 容器 id /bin/bash
进入plugin目录 - 从https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2&page=7下载对应版本的jk分词器
- 将jk分词器的文件解析,并放在plugin目录下面
- 退出docker容器
- 重启elasticSearch
4.4.2 使用不同的分词器进行测试
POST _analyze
{
"text": "尚硅谷电商项目"
}
post _analyze
{
"analyzer":"ik_smart",
"text":"尚硅谷电商项目"
}
post _analyze
{
"analyzer":"ik_max_word",
"text":"尚硅谷电商项目"
}
能够看出不同的分词器, 分词有明显的区别, 所以以后定义一个索引不能再使用默认的 mapping 了, 要手工建立 mapping, 因为要选择分词器。
4.4.3 自定义词库
4.4.3.1 安装nginx
- 随便启动一个 nginx 实例, 只是为了复制出配置
docker run -p 80:80 --name nginx -d nginx:1.10
- 将容器内的配置文件拷贝到当前目录:
docker container cp nginx:/etc/nginx .
- 修改文件名称:
mv nginx conf
- 把这个 conf 移动到/mydata/nginx 下
mv conf nginx
- 终止原容器
docker stop nginx
- 执行命令删除原容器
docker rm $ContainerId
- 启动nginx
docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
4.4.3.2 自定义词库
- 创建词库对应的目录
在nginx下找到html目录并在下面创建es目录
具体路径为:/mydata/nginx/html/es - 在此目录下创建词库对应的文档
创建的词库如下图所示
4.4.3.3配置远程词库的地址
- 进入到ik词库的目录
cd /mydata/elasticsearch/plugins/ik/config/
- 编辑IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://47.93.21.100/es/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
原来的 xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
POST _analyze
{ "analyzer": "ik_max_word",
"text": "我是中国人"
}<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
- 重启es
- 测试
POST _analyze
{
"analyzer": "ik_smart",
"text": "尚硅谷谷粒商城"
}
结果(可以检索到自己的词库)
5.SpringBoot
5.1 创建module项目
新建一个gulimall-search的module
5.2 添加依赖
<?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.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.atguigu.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>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu.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>
<!--导入es elasticsearch-rest-high-level-client -->
<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-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5.3 yml
spring:
application:
name: gulimall_search
cloud:
nacos:
discovery:
server-addr: 127.0.0.1
5.4 配置es客户端
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 1.导入依赖
* 2. 编写配置 get容器中注入RestHighLevelClient
* 3. 参考API https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high.html
*/
@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient restHighLevelClient(){
RestClientBuilder builder = null;
builder = RestClient.builder(new HttpHost("47.93.21.100",9200,"http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
5.5 测试
package com.atguigu.gulimall.search;
import com.alibaba.fastjson.JSON;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.mysql.cj.QueryBindings;
import lombok.Data;
import lombok.ToString;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
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.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.aggregations.metrics.AvgAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTest {
@Autowired
private RestHighLevelClient client;
@Test
public void contestLoads(){
System.out.println(client);
}
/**
* 测试保存操作
* 更新也可以
* @throws IOException
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");//保存数据的id
// indexRequest.source("name","zhansgan","age",18,"gender","男");
User user = new User();
user.setName("zhansgan");
user.setAge(18);
user.setGender("男");
String s = JSON.toJSONString(user);
indexRequest.source(s, XContentType.JSON);
//执行操作
IndexResponse index = client.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//提取有用的相应数据
System.out.println(index);
}
/**
* 获取数据
* @throws IOException
*/
@Test
public void getData() throws IOException {
GetRequest getRequest = new GetRequest("users", "1");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(response);
}
@Test
public void searchData() throws IOException {
//1 创建检索请求
SearchRequest searchRequest = new SearchRequest();
//1.1 指定索引
searchRequest.indices("bank");
//1.2 指定DSL 检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//1.3 按照年龄值分布进行聚合
TermsAggregationBuilder ageAggg = AggregationBuilders.terms("AgeAgg").field("age").size(10);
searchSourceBuilder.aggregation(ageAggg);
//1.4 计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
searchSourceBuilder.aggregation(balanceAvg);
searchRequest.source(searchSourceBuilder);
System.out.println(searchRequest.toString());
System.out.println("===================");
//2 执行检索
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(response);
//3.1获取索引命中记录
SearchHits hits = response.getHits();
SearchHit[] searchHits = hits.getHits();
for(SearchHit hit :searchHits){
String string = hit.getSourceAsString();
Account account = JSON.parseObject(string, Account.class);
System.out.println(account);
}
System.out.println("-----------------------");
Aggregations aggregations = response.getAggregations();
System.out.println(aggregations);
System.out.println("**************");
Terms ageAgg = aggregations.get("ageAgg");
List<? extends Terms.Bucket> bucketList = ageAgg.getBuckets();
for(Terms.Bucket bucket:bucketList){
Object key = bucket.getKey();
long docCount = bucket.getDocCount();
System.out.println(key+"年龄有"+docCount+"人");
}
System.out.println("############");
Avg avgAge = aggregations.get("avgAge");
System.out.println(avgAge.getValue());
}
@Data
public class User{
private String name;
private String gender;
private Integer age;
}
@Data
@ToString
public static class Account{
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
}
6.谷粒商城项目es实战
6.1 mapping设置
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": "text"
},
"catelogId": {
"type": "long"
},
"catelogName": {
"type": "keyword"
},
"hasStock": {
"type": "boolean"
},
"hosStock": {
"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": "long"
}
}
}
}
6.2 写搜索条件
GET gulimall_product/_search
{
"from": 0,
"size": 16,
"query": {
"bool": {
"must": [
{
"match": {
"skuTitle": "Apple"
}
}
],
"filter": [
{
"term": {
"catalogId": 225
}
},
{
"term":{
"brandId":12
}
},
{
"nested": {
"query": {
"bool": {
"must": [
{
"term": {
"attrs.attrId":"15"
}
},
{
"terms": {
"attrs.attrValue": [
"以官网信息为准"
]
}
}
]
}
},
"path": "attrs"
}
},
{
"term": {
"hasStock":true
}
},
{
"range": {
"skuPrice": {
"from": null,
"to": null
}
}
}
]
}
},
"aggs": {
"brand_agg": {
"terms": {
"field": "brandId"
},
"aggs": {
"brand_name_agg": {
"terms": {
"field": "brandName"
}
},
"brand_img_agg": {
"terms": {
"field": "brandImg"
}
}
}
},
"catalog_agg": {
"terms": {
"field": "catalogId"
},
"aggs": {
"catalogg_name_agg": {
"terms": {
"field": "catelogName"
}
}
}
},
"attr_agg": {
"nested": {
"path": "attrs"
},
"aggs": {
"attrs_id_agg": {
"terms": {
"field": "attrs.attrId"
},
"aggs": {
"attr_name_agg": {
"terms": {
"field": "attrs.attrName"
}
},
"attr_value_agg": {
"terms": {
"field": "attrs.attrValue"
}
}
}
}
}
}
},
"highlight": {
"pre_tags": [
"<b style='color:red'>"
],
"post_tags": [
"</b>"
],
"fields": {
"skuTitle": {}
}
}
}
6.3 配置客户端
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 1.导入依赖
* 2. 编写配置 get容器中注入RestHighLevelClient
* 3. 参考API https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high.html
*/
@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient restHighLevelClient(){
RestClientBuilder builder = null;
builder = RestClient.builder(new HttpHost("47.93.21.100",9200,"http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
6.4 实体类
package com.atguigu.gulimall.search.vo;
import lombok.Data;
import java.util.List;
@Data
public class SearchParam {
private String keyword;//页面传递过来的全局匹配关键字
private Long catalogId;//三级分类id
/**
* sort=saleCount_asc/desc
* sort=skuPrice_asc/desc
* sort=hotScore_asc/desc
*/
private String sort;//排序条件
private Integer hasStock = 1;//是否只显示有货 0无 1有
private String skuPrice;//价格区间查询
private List<Long> brandId;//按照品牌进行查询,可以多选
private List<String> attrs;//按照属性进行筛选
private Integer pageNum = 1 ;//页码
}
package com.atguigu.gulimall.search.vo;
import com.atguigu.common.to.es.SkuEsModel;
import lombok.Data;
import java.util.List;
@Data
public class SearchResult {
//查询到的所有商品信息
private List<SkuEsModel> products;
private Integer pageNum;
private Long total;
private Integer totalPages;
private List<BrandVo> brands;//当前查询到的结果,所有涉及到的品牌
private List<AttrVo> attrs;//当前查询结果所涉及到的所有属性
private List<CatalogVo> catalogs;//当前查询结果所涉及到的所有分类
@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;
}
}
6.5 controller
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 1.导入依赖
* 2. 编写配置 get容器中注入RestHighLevelClient
* 3. 参考API https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high.html
*/
@Configuration
public class GulimallElasticSearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient restHighLevelClient(){
RestClientBuilder builder = null;
builder = RestClient.builder(new HttpHost("47.93.21.100",9200,"http"));
RestHighLevelClient client = new RestHighLevelClient(builder);
return client;
}
}
6.6 service
package com.atguigu.gulimall.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.to.es.SkuEsModel;
import com.atguigu.gulimall.search.config.GulimallElasticSearchConfig;
import com.atguigu.gulimall.search.constant.EsConstant;
import com.atguigu.gulimall.search.service.MallSearchService;
import com.atguigu.gulimall.search.vo.SearchParam;
import com.atguigu.gulimall.search.vo.SearchResult;
import com.mysql.cj.QueryBindings;
import org.apache.commons.lang.StringUtils;
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.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
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.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MallSearchServiceImpl implements MallSearchService {
@Autowired
private RestHighLevelClient client;
@Override
public SearchResult search(SearchParam searchParam) {
//1.动态构建出查询需要的DSL语句
SearchResult result = null;
//1.准备检索请求
SearchRequest searchRequest = buildSearchRequest(searchParam);
try {
//2.执行检索请求
SearchResponse response = client.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
//3.分析响应数据,解析成我们需要的格式
result = buildSearchResult(response,searchParam);
}catch (Exception e){
e.printStackTrace();
}
return result;
}
/**
* 构建索引请求
* @return
* * 模糊匹配 过滤(按照属性、分类、品牌、价格区间、库存),排序 分页 高亮 聚合分析
*/
private SearchRequest buildSearchRequest(SearchParam param){
SearchSourceBuilder builder = new SearchSourceBuilder();//构建DSL语句
//* 模糊匹配 过滤(按照属性、分类、品牌、价格区间、库存)
//1.构建bool query
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//1.1 must 模糊查询
if(StringUtils.isNotBlank(param.getKeyword())){
boolQuery.must(QueryBuilders.matchQuery("skuTitle",param.getKeyword()));
}
//1.2 bool filter 按照三级分类的id进行查询
if(param.getCatalogId() != null){
boolQuery.filter(QueryBuilders.termQuery("catalogId",param.getCatalogId()));
}
//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 attr:param.getAttrs()){
//attrs=1_5寸:8寸
BoolQueryBuilder nestBoolQuery = QueryBuilders.boolQuery();
String[] s = attr.split("_");
String attrId = s[0];
String[] attrValues = s[1].split(":");
nestBoolQuery.must(QueryBuilders.termQuery("attrs.attrId",attrId));
nestBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue",attrValues));
//每一个都必须生成一个nested查询
NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestBoolQuery, ScoreMode.None);
boolQuery.filter(nestedQuery);
}
}
//1.2 bool filter 按照是否有库存进行查询
boolQuery.filter(QueryBuilders.termQuery("hasStock",param.getHasStock() == 1));
//1.2 bool filter 按照价格区间进行查询
if(!StringUtils.isEmpty(param.getSkuPrice())){
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("skuPrice");
String[] s = param.getSkuPrice().split("-");
if(s.length==2){
rangeQuery.gte(s[0]).lte(s[1]);
}else if(s.length==1){
if(param.getSkuPrice().startsWith("-")){
rangeQuery.gte(s[0]);
}
if(param.getSkuPrice().endsWith("-")){
rangeQuery.lte(s[0]);
}
}
boolQuery.filter(rangeQuery);
}
builder.query(boolQuery);
// 排序 分页 高亮
// 2.1 排序
if(StringUtils.isNotBlank(param.getSort())){
String sort = param.getSort();
String[] split = sort.split("_");
SortOrder sortOrder = split[1].equalsIgnoreCase("asc")?SortOrder.ASC:SortOrder.DESC;;
builder.sort(split[0],sortOrder);
}
//2.2 分页
builder.from((param.getPageNum()-1)*EsConstant.PRODUCT_PAGESIZE);
builder.size(EsConstant.PRODUCT_PAGESIZE);
//2.3高亮
if(StringUtils.isNotBlank(param.getKeyword())){
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("skuTitle");
highlightBuilder.preTags("<b style='color:red'>");
highlightBuilder.postTags("</b>");
builder.highlighter(highlightBuilder);
}
/**
* 聚合分析
*/
//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(10));
brand_agg.subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(10));
builder.aggregation(brand_agg);
//2.分类聚合
TermsAggregationBuilder catalog_agg = AggregationBuilders.terms("catalog_agg").field("catalogId").size(20);
catalog_agg.subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catelogName").size(20));
builder.aggregation(catalog_agg);
//3.属性聚合 attr_agg
NestedAggregationBuilder attr_agg = AggregationBuilders.nested("attr_agg", "attrs");
//聚合出当前所有的attrId
AggregationBuilder attr_id_agg = AggregationBuilders.terms("attrs_id_agg").field("attrs.attrId").size(20);
attr_agg.subAggregation(attr_id_agg);
//聚合出当前所有attr_id对应的名字
AggregationBuilder attr_name_agg = AggregationBuilders.terms("attr_name_agg").field( "attrs.attrName").size(20);
//聚合出当前所有attr_id对应所有可能的属性值
AggregationBuilder attr_value_agg = AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(20);
attr_id_agg.subAggregation(attr_name_agg);
attr_id_agg.subAggregation(attr_value_agg);
builder.aggregation(attr_agg);
String dsl = builder.toString();
System.out.println(dsl);
SearchRequest searchRequest = new SearchRequest(new String[]{EsConstant.PRODUCT_INDEX},builder);
return searchRequest;
}
/**
* 返回构建结果
* @param response
* @return
*/
private SearchResult buildSearchResult(SearchResponse response,SearchParam searchParam) {
SearchResult result = new SearchResult();
// 1. 返回所有查询的商品
SearchHits hits = response.getHits();
ArrayList<SkuEsModel> esModels = new ArrayList<>();
if(hits.getHits() != null && hits.getHits().length>0){
for(SearchHit hit : hits.getHits()){
String source = hit.getSourceAsString();
SkuEsModel esModel = JSON.parseObject(source, SkuEsModel.class);
if(StringUtils.isNotBlank(searchParam.getKeyword())){
String skuTitle = hit.getHighlightFields().get("skuTitle").getFragments()[0].string();
esModel.setSkuTitle(skuTitle);
esModels.add(esModel);
}
}
}
result.setProducts(esModels);
//2, 返回所有查询到商品信息 所涉及到的属性信息
ArrayList<SearchResult.AttrVo> attrVos = new ArrayList<>();
ParsedNested attrAgg = response.getAggregations().get("attr_agg");
ParsedLongTerms attrsIdAgg = attrAgg.getAggregations().get("attrs_id_agg");
List<? extends Terms.Bucket> attrsIdAggBuckets = attrsIdAgg.getBuckets();
for(Terms.Bucket bucket:attrsIdAggBuckets){
SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
//属性的id
long attrId = bucket.getKeyAsNumber().longValue();
attrVo.setAttrId(attrId);
//属性的名字
ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attr_name_agg");
String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
attrVo.setAttrName(attrName);
//属性的值
ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attr_value_agg");
List<String> attrValues = attrValueAgg.getBuckets().stream().map(item -> {
String keyAsString = item.getKeyAsString();
return keyAsString;
}).collect(Collectors.toList());
attrVo.setAttrValue(attrValues);
attrVos.add(attrVo);
}
result.setAttrs(attrVos);
//3. 当前所有商品所涉及到的品牌信息
ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
ArrayList<SearchResult.BrandVo> brandVos = new ArrayList<>();
List<? extends Terms.Bucket> brandAggBuckets = brand_agg.getBuckets();
if(brandAggBuckets != null && brandAggBuckets.size()>0){
for(Terms.Bucket bucket:brandAggBuckets){
//品牌id
SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
String id = bucket.getKeyAsString();
brandVo.setBrandId(Long.parseLong(id));
//品牌图片
ParsedStringTerms brandImgAgg = bucket.getAggregations().get("brand_img_agg");
String brandImg = brandImgAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandImg(brandImg);
//品牌的名字
ParsedStringTerms brandNameAgg = bucket.getAggregations().get("brand_name_agg");
String brandName = brandNameAgg.getBuckets().get(0).getKeyAsString();
brandVo.setBrandName(brandName);
brandVos.add(brandVo);
}
}
result.setBrands(brandVos);
//4.当前所有商品所涉及到的分类信息
ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
ArrayList<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");
List<? extends Terms.Bucket> cataloggNameAggBuckets = catalog_name_agg.getBuckets();
if(cataloggNameAggBuckets != null && cataloggNameAggBuckets.size()> 0){
String catalog_name = cataloggNameAggBuckets.get(0).getKeyAsString();
catalogVo.setCatalogName(catalog_name);
}
catalogVos.add(catalogVo);
}
result.setCatalogs(catalogVos);
// 5. 分页信息 页码 总记录数
result.setPageNum(searchParam.getPageNum());
long total = hits.getTotalHits().value;
result.setTotal(total);
int totalPages = total%EsConstant.PRODUCT_PAGESIZE == 0? (int)total%EsConstant.PRODUCT_PAGESIZE :(int) (total%EsConstant.PRODUCT_PAGESIZE)+1;
result.setTotalPages(totalPages);
return result;
}
}