全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。
image.png
为什么要使用ElasticSearch?
mysql虽然可以进行检索,但是其主要目的是为了对数据进行持久化与管理。当数据的量级很高,如果使用mysql进行检索,会导致数据库的压力相当大。因此需要专注于检索的ElasticSearch。
官方英文:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文(基于2.x):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(索引)

Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。
mysql中保存一条数据我们称为insert,在ElasticSearch中保存我们称为index
同样在mysql中存储数据之前需要创建一个database,那么在ElasticSearch中需要创建一个index
动词:相当于MySQL中的insert;
名词:相当于MySQL中的database;

1.2 Type(类型)

在 Index(索引)中,可以定义一个或多个类型。
类似类似于mysql中需要在数据库中创建一张或多张Table,而在ElasticSearch中对应的为Type
把一条数据存在某个index的某个type下,就相当于把数据存在某个database的某个table下。
在ElasticSearch6.0之后,Type类型被移除

1.3 Document(文档)

保存在某个 Index(索引)下,某种 Type(类型)的一个数据,Document(文档)是JSON格式的,Document 就像是 MySQL 中某个 Table 里面每一行的数据,字段就是Document里的属性
image.png

1.4 倒排索引

ElasticSearch在保存记录的时候会先进行整句拆分,然后根据单词维护一张索引表,记录了每个单词对应的记录。在进行检索的时候,会通过单词进行检索,然后计算相关性得分,从高到低排列。
image.png

二、ElasticSearch Docker安装

2.1 下载镜像文件

docker pull elasticsearch:7.4.2(存储和检索数据)
docker pull kibana:7.4.2(可视化检索数据,类似于SQLyog)

2.2 创建实例

2.2.1 ElasticSearch

创建挂载文件夹

  1. # -p表示创建多层目录 这里是为了将docker容器与宿主机挂载
  2. mkdir -p /mydata/elasticsearch/config # 创建配置文件目录
  3. mkdir -p /mydata/elasticsearch/data # 创建数据持久化目录
  4. # 配置任意机器可以访问 elasticsearch
  5. echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
  6. # 注意这里的权限设置 保证文件夹中文件可读可写
  7. chmod -R 777 /mydata/elasticsearch/

启动实例

  1. docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
  2. -e "discovery.type=single-node" \
  3. -e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
  4. -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
  5. -v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
  6. -v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
  7. -d elasticsearch:7.4.2
  • -p 9200:9200 -p 9300:9300:向外暴露两个端口,9200用于HTTP REST API请求,9300用于ES在分布式集群状态下 ES 之间的通信端口;
  • -e "discovery.type=single-node":设置es以单节点运行
  • -e ES_JAVA_OPTS="-Xms64m -Xmx512m":设置启动初始内存和最大内存,不设置可能会占用过大无法启动(虚拟机卡死)。
  • -v:挂载容器中的配置文件、数据文件、插件数据到本机的文件夹;
  • -d elasticsearch:7.6.2:指定要启动的镜像

注意:如果虚拟机的防火墙没有关闭,需要开启9200与9300的端口

  1. firewall-cmd --add-port=9200/tcp --permanent # 开启9200端口访问
  2. firewall-cmd --add-port=9300/tcp --permanent # 开启9300端口访问
  3. firewall-cmd --reload # 重启防火墙
  4. firewall-cmd --list-all # 查看防火墙当前状态
  5. # 设置ElasticSearch随docker启动 可选
  6. docker update elasticsearch --restart=always

启动后访问 虚拟机ip:9200,会显示如下信息:
image.png

2.2.2 kibana

  1. docker run --name kibana \
  2. -e ELASTICSEARCH_HOSTS=http://192.168.190.135:9200 \
  3. -p 5601:5601 \
  4. -d kibana:7.4.2

注意:这里 ELASTICSEARCH_HOSTS=http://**192.168.190.135**:9200ip地址填自己虚拟机的ip地址
同样,这里需要开启防火墙的5601端口。

  1. firewall-cmd --add-port=5601/tcp --permanent
  2. firewall-cmd --reload
  3. # 设置kibana随docker启动 可选
  4. docker update elasticsearch --restart=always

访问虚拟机ip:5601,出现以下界面则表示成功:
image.png

上面的方法是kibana通过虚拟机来访问es。涉及到docker容器之间的相互访问,也有不同方式,比如—link方式。创建容器的时候通过以下命令可以让容器在docker内访问,而不通过虚拟机

  1. docker run --name kibana --link elasticsearch -e ELASTICSEARCH_HOSTS=http://【elasticsearchIp】:9200 -p 5601:5601 -d kibana:7.4.2

这里elasticsearchIp是es容器在docker中的IPAddress,可以通过
docker inspect elasticsearch | grep IPAdress 查看。

三、初步检索

3.1 _cat

3.1.1 /_cat/nodes:查看所有节点

GET http://192.168.190.135:9200/_cat/nodes
image.png

3.1.2 /_cat/health:查看 es 健康状况

GET http://192.168.190.135:9200/_cat/health
image.png
这里的 1 1 3 3 0 0 0 0 是集群的分片信息,在集群的时候会讲

3.1.3 /_cat/master:查看主节点

GET http://192.168.190.135:9200/_cat/master
image.png

3.1.4 /_cat/indices:查看所有索引

类似于show databases; GET http://192.168.190.135:9200/_cat/indices
image.png

3.2 索引一个文档(保存一条记录)

索引一个文档即保存一条数据,保存在哪个索引(数据库)的哪个类型(表)下,指定用哪个唯一标识。

3.2.1 PUT 请求带Id保存(一般用于修改)

PUT customer/external/1;在 customer 索引下的 external 类型下保存 1 号数据为:{“name”:”John Doe”}
PUT http://192.168.190.135:9200/customer/external/1
image.png发送多次是一个更新操作。
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改操作,不指定 id 会报错

3.2.2 POST保存

PUT 和 POST 都可以新增和修改。POST 新增。如果不指定 id,会自动生成 id指定 id,该id如果没有对应数据为新建操作,如果有对应数据则为update操作,并更新版本号。
image.png

3.3 查询文档(查询记录)

GET /index/type/id
image.png

  1. {
  2. "_index": "customer", # 在哪个索引(库)
  3. "_type": "external", # 在哪个类型(表)
  4. "_id": "1", # 文档id(记录)
  5. "_version": 2, # 版本号
  6. "_seq_no": 1, # 并发控制字段,每次更新都会+1,用来做乐观锁
  7. "_primary_term": 1, # 同上,主分片重新分配,如重启,就会变化
  8. "found": true,
  9. "_source": { # 数据
  10. "name": "John Doe"
  11. }
  12. }
  13. # 乐观锁更新时携带 ?if_seq_no=1&if_primary_term=1 携带数据要求_seq_no1_primary_term1的时候才进行更改,如果不是则不更改

image.png

3.4 更新文档——POST /_update

更新文档有很多种方式,前面说了两种PUT /index/type/id 和 POST /index/type/id
POST customer/external/1/_update 注意该方式下的请求体格式

  1. {
  2. "doc":{ "name": "John Doew"}
  3. }

post /_update更新会对比原来的数据,与原来一样就什么都不做,version、seq_no均不改变。
PUT customer/external/1POST customer/external/1则不会进行比较,对原数据进行覆盖。
那么什么时候用什么呢?

  • 对于大并发更新,不带 update;
  • 对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。

    3.5 删除文档&索引

    3.5.1 删除文档

    DELETE customer/external/1
    DELETE http://192.168.190.135:9200/customer/external/1
    image.png

    3.5.2 删除索引

    DELETE customer
    类似于drop database;
    DELETE http://192.168.190.135:9200/customer
    image.png

    3.6 bulk——批量操作api

    使用bulk进行批量操作必须发送POST请求
    语法格式: ```json {action:{metadata}}\n // 例如index保存记录,update更新 {request body }\n

{action:{metadata}}\n {request body }\n

  1. <a name="O9f4r"></a>
  2. ### 3.6.1 指定索引和类型的bulk批量操作
  3. `POST http://192.168.190.135/customer/external/_bulk`
  4. ```json
  5. {"index":{"_id":"1"}} // 这里index表示保存操作
  6. {"name":"John Doe"}
  7. {"index":{"_id":"2"}}
  8. {"name":"John Doe"}

这里无法使用postman进行测试,用kibana额Dev_tools中执行
image.png

3.6.2 对所有索引执行批量操作

POST /_bulk

  1. {"delete":{"_index":"website","_type":"blog","_id":"123"}}
  2. {"create":{"_index":"website","_type":"blog","_id":"123"}}
  3. {"title":"my first blog post"}
  4. {"index":{"_index":"website","_type":"blog"}}
  5. {"title":"my second blog post"}
  6. {"update":{"_index":"website","_type":"blog","_id":"123","retry_on_conflict":3}}
  7. {"doc":{"title":"my updated blog post"}}

image.png
bulk的批量操作,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。
bulk api以此按顺序执行所有的action(动作)。如果一个单个的动作因任何原因失败,它将继续处理它后面剩余的动作。当bulk api返回时,它将提供每个动作的状态(与发送的顺序相同),所以可以检查某一个指定的动作是否失败了。

3.7 样本测试数据

准备了一份顾客银行账户信息的虚构的JSON文档样本。每个文档都有下列的schema(模式)。

  1. {
  2. "account_number": 1,
  3. "balance": 39225,
  4. "firstname": "Amber",
  5. "lastname": "Duke",
  6. "age": 32,
  7. "gender": "M",
  8. "address": "880 Holmes Lane",
  9. "employer": "Pyrami",
  10. "email": "amberduke@pyrami.com",
  11. "city": "Brogan",
  12. "state": "IL"
  13. }

POST /bank/account/_bulk
JSON数据连接
image.png

四、进阶检索

https://www.elastic.co/guide/en/elasticsearch/reference/7.5/getting-started-search.html

4.1 SearchAPI

ES 支持两种基本方式检索 :

  • 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
  • 另一个是通过使用 REST request body 来发送它们(uri+请求体)

    4.1.1 uri+检索参数

    GET bank/_search?q=*&sort=account_number:asc ?后面表示检索条件
    bank:表示在bank索引下;
    _search:表示检索,固定写法;
    q=:表示查询所有;
    sort=account_number:asc:表示按照account_number:asc升序排列;
    image.png
    *Elasticsearch 默认会分页返回10条数据,不会一下返回所有数据。

    响应字段解释:

  • took:耗费时间,单位为ms;

  • time_out:请求是否超时;
  • _shards:搜索了多少分片,以及多少分片成功、失败或被跳过的细分(集群模式下)。
  • max_score:找到的最相关文档的分数
  • hits.total.value:找到了多少匹配的文档
  • hits.sort:文档的排序位置(不按相关性分数排序时)
  • hits._score:文档的相关性分数(使用时不适用match_all)

    4.1.2 uri+请求体

    1. GET /bank/_search
    2. {
    3. "query": {
    4. "match_all": {}
    5. },
    6. "sort": [
    7. {
    8. "account_number": "asc"
    9. }
    10. ]
    11. }
    12. # query 查询条件
    13. # sort 排序条件

    4.2 Query DSL

    Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特定语言)。这个被称为 Query DSL。

    4.2.1 基本语法格式

    1. {
    2. QUERY_NAME:{
    3. ARGUMENT:VALUE,
    4. ARGUMENT:VALUE,...
    5. }
    6. }
    如果是针对某一个字段的查询,那么它的结构如下:
    1. {
    2. QUERY_NAME: {
    3. FIELD_NAME: {
    4. ARGUMENT: VALUE,
    5. ARGUMENT: VALUE,...
    6. }
    7. }
    8. }
    4.1.2节中请求体就是一种DSL。
    示例:
    1. GET bank/_search
    2. {
    3. "query": {
    4. "match_all": {}
    5. },
    6. "sort": [
    7. {
    8. "balance": "desc"
    9. }
    10. ],
    11. "from": 0,
    12. "size": 5
    13. }
    14. // match_all 查询类型【代表查询所有的所有】,es中可以在query中组合非常多的查询类型完成复杂查询;
    15. // 除了 query 参数之外,我们也可以传递其它的参数以改变查询结果。如 sortsize
    16. // from+size 限定,完成分页功能;从第几条数据开始,每页有多少数据。类似于mysql limt
    17. // sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准;
    image.png

    4.2.2 返回部分字段

    "_source": ["field1","field2"]
    1. GET bank/_search
    2. {
    3. "query": {
    4. "match_all": {}
    5. },
    6. "from": 0,
    7. "size": 5,
    8. "sort": [
    9. {
    10. "account_number": {
    11. "order": "desc"
    12. }
    13. }
    14. ],
    15. "_source": ["balance","firstname"] // 只返回balancefirstname字段
    16. }
    image.png

    4.2.3 match【匹配查询】

    精确查找——非字符串类型

    1. GET bank/_search
    2. {
    3. "query": {
    4. "match": {
    5. "account_number": "20"
    6. }
    7. }
    8. }
    9. // 非字符串字段会精确匹配,查找匹配 account_number 20 的数据,非字符串类型推荐使用term
    image.png

    模糊查询——字符串类型

    ```json GET bank/_search { “query”: { “match”: {
    1. "address": "Kings" // 字符串字段会模糊查询
    } } }

GET bank/_search { “query”: { “match”: { “address”: “mill lane” // 匹配包含mill或lane的数据 } } }

全文检索最终会按照评分进行排序,会对检索条件进行分词匹配

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1647360344356-367d0942-1082-4c58-a528-3471b94d23c9.png#clientId=u1c66e82d-61fd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=486&id=u5c6bdf41&name=image.png&originHeight=486&originWidth=1121&originalType=binary&ratio=1&rotation=0&showTitle=false&size=73702&status=done&style=none&taskId=u8a185d3a-951e-40c0-b39a-e0869d6756b&title=&width=1121)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1647360672602-4ae29f9b-f91f-41e9-82f7-82d760e43efa.png#clientId=u1c66e82d-61fd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=469&id=u330d62fb&name=image.png&originHeight=469&originWidth=973&originalType=binary&ratio=1&rotation=0&showTitle=false&size=69065&status=done&style=none&taskId=u22fe6486-adbb-4a6b-aaad-b6fafb9567a&title=&width=973)
  2. <a name="eQJ8f"></a>
  3. #### 精确匹配——字符串类型
  4. ```json
  5. GET bank/_search
  6. {
  7. "query": {
  8. "match": {
  9. "address.keyword": "288 Mill Street"
  10. }
  11. }
  12. }
  13. # 查找 address 为 288 Mill Street 的数据。
  14. # 这里的查找是精确查找,只有完全匹配时才会查找出存在的记录,
  15. # 如果想模糊查询应该使用match_phrase 短语匹配
  16. # 注意这里的address.keyword

4.2.4 match_phrase【短语匹配】

之前通过match匹配的时候,es会自动分词,只要目标字段包含了分词都会计算相关性得分返回。
而match_phrase不会进行分词,会把查询条件作为完整短语进行匹配。

  1. GET bank/_search
  2. {
  3. "query": {
  4. "match_phrase": { "address": "mill road"}
  5. }
  6. }
  7. # match_phrase:查出 address 中包含 mill road 的所有记录,并给出相关性得分
  8. # match_phrase是对"mill road"整体进行模糊匹配

image.png

4.2.5 multi_match【多字段匹配】

  1. GET bank/_search
  2. {
  3. "query": {
  4. "multi_match": {
  5. "query": "mill lopezo",
  6. "fields": ["city","address"]
  7. }
  8. }
  9. }
  10. # select * from xxx where `state` like mill or `address` like mill or `state` like lopezo or `address` like lopezo
  11. # state 或者 address 包含 mill

multi_match多字段匹配的时候也会自动进行分词。

4.2.6 bool【复合查询】

bool 用来做复合查询:复合语句可以合并,任何其他查询语句,包括符合语句。这也就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

  • must:必须达到must所列举的所有条件。
  • must_not,必须不匹配must_not所列举的所有条件。
  • should,应该满足should所列举的条件,如果达到会增加相关文档的评分,并不会改变查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会被作为默认匹配条件而去改变查询结果。
    1. GET bank/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "must": [
    6. {"match": {"gender": "F"}},
    7. {"match": {"address": "mill"}}
    8. ]
    9. }
    10. }
    11. }
    12. # select * from xxx where `gender` = F and `address` like mill;
    image.png
    1. GET bank/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "must": [
    6. {"match": {"gender": "M"}},
    7. {"match": {"address": "mill"}}
    8. ],
    9. "must_not": [
    10. {"match": {"age": "38"}}
    11. ]
    12. }
    13. }
    14. }
    15. # select * from xxx where `gender` = M and `address` like "mill" and age != 38;
    1. GET bank/_search
    2. {
    3. "query": {
    4. "bool": {
    5. "must": [
    6. {"match": {"gender": "M"}},
    7. {"match": {"address": "mill"}}
    8. ],
    9. "must_not": [
    10. {"match": {"age": "18"}}
    11. ],
    12. "should": [
    13. {"match": {"lastname": "Wallace"}}
    14. ]
    15. }
    16. }
    17. }

    4.2.7 filter【结果过滤】

    并不是所有的查询都需要产生分数,filter会对结果进行过滤,且不会计算相关性得分。(其作用和must相同,唯一区别就是不会计算相关性得分)
    must_not 就可以看做是一个filter,它影响文档是否包含在结果中,但是不影响文档的评分。 ```json GET bank/_search { “query”: { “bool”: {
    1. "must": [ // must会计算相关性得分
    2. {"range": {
    3. "age": {
    4. "gte": 18,
    5. "lte": 30
    6. }
    7. }}
    8. ]
    } } }

GET bank/_search { “query”: { “bool”: { “filter”: { // filter不会计算相关性得分 “range”: { “age”: { “gte”: 18, “lte”: 30 } } } } } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/22423156/1647398324861-86287bb7-cc9c-495b-8c8a-abb0e03e8f17.png#clientId=u1c66e82d-61fd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=494&id=u04a5b48c&name=image.png&originHeight=494&originWidth=1236&originalType=binary&ratio=1&rotation=0&showTitle=false&size=76903&status=done&style=none&taskId=u426a6abf-1dc6-41bb-bb05-138baa82cad&title=&width=1236)
  2. > Each `must`, `should`, and `must_not` element in a Boolean query is referred to as a query clause. How well a document meets the criteria in each `must` or `should` clause contributes to the documents _relevance score_. The higher the score, the better the document matches your search criteria. By default, Elasticsearch returns documents ranked by these relevance scores.
  3. > boolean查询中,`must`, `should` `must_not` 元素都被称为查询子句 文档是否符合每个`must``should`子句中的标准,决定了文档的"**相关性得分**"。得分越高,文档越符合您的搜索条件。 默认情况下,Elasticsearch 返回根据这些相关性得分排序的文档。
  4. >
  5. > The criteria in a `must_not` clause is treated as a _filter_. It affects whether or not the document is included in the results, but does not contribute to how documents are scored. You can also explicitly specify arbitrary filters to include or exclude documents based on structured data.
  6. > `"must_not"子句中的条件被视为"过滤器"。`它影响文档是否包含在结果中,但**不影响文档的评分方式**。还可以显式地指定任意过滤器来包含或排除基于结构化数据的文档。
  7. <a name="y1M8V"></a>
  8. ### 4.2.8 term【精确检索】
  9. match一样,匹配某个属性的值。全文检索字段用match,其他非text字段匹配用term
  10. > You can use the `term` query to find documents based on a **precise value **such as a price, a product ID, or a username.
  11. > Avoid using the term query for [text](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/text.html) fields.
  12. > 避免使用term查询文本字段。
  13. > By default, Elasticsearch changes the values of text fields as part of [analysis](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/analysis.html). This can make finding exact matches for text field values difficult.
  14. > 默认情况下,es通过analysis分词将文本字段分解为多个部分。这使得精确匹配文本字段变得困得。
  15. > To search `text` field values, use the `[match](https://www.elastic.co/guide/en/elasticsearch/reference/7.11/query-dsl-match-query.html)` query instead.
  16. > 查询文本字段值,使用match代替。
  17. ```json
  18. GET /_search
  19. {
  20. "query": {
  21. "term": {
  22. "age": 28
  23. }
  24. }
  25. }
  26. // 查找年龄为28的用户数据

4.2.9 aggregations【执行聚合】

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。在 Elasticsearch 中,您有执行搜索返回 hits(命中结果),并且同时返回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的 API 来避免网络往返。
https://www.elastic.co/guide/en/elasticsearch/reference/7.5/search-aggregations.html
语法

  1. "aggregations" : {
  2. "<aggregation_name>" : {
  3. "<aggregation_type>" : {
  4. <aggregation_body>
  5. }
  6. [,"meta" : { [<meta_data_body>] } ]?
  7. [,"aggregations" : { [<sub_aggregation>]+ } ]?
  8. }
  9. [,"<aggregation_name_2>" : { ... } ]*
  10. }

image.png

示例1

搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情。

  1. GET bank/_search
  2. {
  3. "query": {
  4. "match": {
  5. "address": "Mill"
  6. }
  7. },
  8. "aggs": {
  9. "ageAgg": {
  10. "terms": { # terms aggregation 表示查看有多少种不同类型
  11. "field": "age", # 分析query查询结果中,共有多少不同age类型
  12. "size": 10 # 只显示10
  13. }
  14. },
  15. "ageAvg": { # 分析query查询结果中的平均年龄
  16. "avg": {
  17. "field": "age"
  18. }
  19. },
  20. "balanceAvg": { # 分析query查询结果中的平均余额
  21. "avg": {
  22. "field": "balance"
  23. }
  24. }
  25. },
  26. "size": 0 # 不显示query到的数据
  27. }

image.png

示例2

按照年龄聚合,并且请求这些年龄段的这些人的平均薪资。

  1. GET bank/_search
  2. {
  3. "query": {
  4. "match_all": {}
  5. },
  6. "aggs": {
  7. "ageAgg": {
  8. "terms": {
  9. "field": "age",
  10. "size": 100
  11. },
  12. "aggs": { # 这是一个子聚合,对聚合结果进行分析
  13. "balanceAvg": {
  14. "avg": {
  15. "field": "balance"
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }

image.png

示例3

查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资

  1. GET bank/_search
  2. {
  3. "query": {
  4. "match_all": {}
  5. },
  6. "aggs": {
  7. "ageAgg": {
  8. "terms": {
  9. "field": "age",
  10. "size": 100
  11. },
  12. "aggs": {
  13. "genderAgg": {
  14. "terms": {
  15. "field": "gender.keyword", //"field": "gender.keyword" gendertxt没法聚合 必须加.keyword精确替代
  16. "size": 10
  17. },
  18. "aggs": {
  19. "balanceAvg": {
  20. "avg": {
  21. "field": "balance"
  22. }
  23. }
  24. }
  25. },
  26. "ageBalanceAvg": {
  27. "avg": {
  28. "field": "balance"
  29. }
  30. }
  31. }
  32. }
  33. }
  34. }

image.png

4.3 Mapping

https://www.elastic.co/guide/en/elasticsearch/reference/7.5/mapping.html

4.3.1 Mapping介绍

Maping是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。比如,使用 mapping 来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields)。
  • 哪些属性包含数字,日期或者地理位置。
  • 文档中的所有属性是否都能被索引(_all 配置)。
  • 日期的格式。
  • 自定义映射规则来执行动态添加属性。

查看 bank索引中的mapping 信息:
GET bank/_mapping

4.3.2 新版本改变

Es7 及以上移除了 type 的概念。关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。

  • 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 field,你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
  • 去掉 type 就是为了提高 ES 处理数据的效率。

Elasticsearch 7.x:URL 中的 type 参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.x:不再支持 URL 中的 type 参数。

4.3.3 创建映射

创建索引并指定属性的映射规则(相当于新建表并指定字段和字段类型)属性数据类型官方文档

  1. PUT /my_index
  2. {
  3. "mappings": {
  4. "properties": {
  5. "age": {"type": "integer"},
  6. "email": { "type": "keyword"}, # keyword表示不会进行分词检索类型
  7. "name": {"type": "text"}
  8. }
  9. }
  10. }
  11. //运行结果
  12. {
  13. "acknowledged" : true,
  14. "shards_acknowledged" : true,
  15. "index" : "my_index"
  16. }

4.3.4 给已有映射增加字段

https://www.elastic.co/guide/en/elasticsearch/reference/7.x/explicit-mapping.html#add-field-mapping
比如我们给上述mapping添加新的字段:

  1. PUT /my_index/_mapping
  2. {
  3. "properties": {
  4. "employee-id": {
  5. "type": "keyword",
  6. "index": false # index表示当前字段不被索引,即无法被检索到(默认是true
  7. }
  8. }
  9. }
  10. // https://www.elastic.co/guide/en/elasticsearch/reference/7.x/mapping-index.html

4.3.5 查看映射

  1. GET /my_index/_mapping # 查看整个索引的映射
  2. GET /my_index/_mapping/field/employee-id # 查看某一个字段的映射

4.3.6 更新已有映射与数据迁移

对于已经存在的映射字段,我们不能更新其类型。更新必须创建新的索引进行数据迁移。
如果您需要更改字段的映射,请创建一个具有正确映射的新索引,并将您的数据重新索引到该索引中。

https://www.elastic.co/guide/en/elasticsearch/reference/7.5/docs-reindex.html

迁移方式分为两种,一种是从type迁移到没有type的情况,另一种是没有type迁移到没有type的情况

  1. POST reindex [固定写法]
  2. {
  3. "source":{
  4. "index":"twitter",
  5. "twitter":"twitter"
  6. },
  7. "dest":{
  8. "index":"new_twitters"
  9. }
  10. }
  1. POST reindex [固定写法]
  2. {
  3. "source":{
  4. "index":"twitter"
  5. },
  6. "dest":{
  7. "index":"new_twitters"
  8. }
  9. }

4.3.7 数据迁移示例

我们创建一个无type的新索引 newbank 并更新其映射字段的类型,然后将包含type的索引bank中的数据迁移过来。
创建新索引 newbank 并修改字段类型:

  1. PUT /newbank
  2. {
  3. "mappings": {
  4. "properties": {
  5. "account_number": {
  6. "type": "long"
  7. },
  8. "address": {
  9. "type": "text"
  10. },
  11. "age": {
  12. "type": "integer"
  13. },
  14. "balance": {
  15. "type": "long"
  16. },
  17. "city": {
  18. "type": "keyword"
  19. },
  20. "email": {
  21. "type": "keyword"
  22. },
  23. "employer": {
  24. "type": "keyword"
  25. },
  26. "firstname": {
  27. "type": "text"
  28. },
  29. "gender": {
  30. "type": "keyword"
  31. },
  32. "lastname": {
  33. "type": "text",
  34. "fields": {
  35. "keyword": {
  36. "type": "keyword",
  37. "ignore_above": 256
  38. }
  39. }
  40. },
  41. "state": {
  42. "type": "keyword"
  43. }
  44. }
  45. }
  46. }

数据迁移:

  1. GET bank/_search # 查看bank索引的信息
  2. POST _reindex
  3. {
  4. "source": {
  5. "index": "bank",
  6. "type": "account"
  7. },
  8. "dest": {
  9. "index": "newbank"
  10. }
  11. }

image.png
迁移之后,_type变成了_doc
image.png

五、分词

一个 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(自定义分词器)。
使用标准分词器查看分词结果:
image.png
但是es中的分词器都是针对英文,如果是中文则需要安装其他的分词器。

5.1 安装ik分词器

注意:不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装 新版的内置了ik分词器?
根据自己的版本下载(视频里是7.4.2):https://github.com/medcl/elasticsearch-analysis-ik/releases

  • IK 分词器属于 Elasticsearch 的插件,所以 IK 分词器的安装目录是 Elasticsearch 的 plugins 目录,在我们使用Docker启动 Elasticsearch 时,已经将该目录挂载到主机的 /mydata/elasticsearch/plugins 目录。所以我们直接在虚拟机的挂载目录内安装就行,不需要进入到docker的es容器内。
  1. # cd到挂载的目录下
  2. cd /mydata/elasticsearch/plugins
  3. yum install wget
  4. # 下载ik分词器
  5. wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
  6. # 将下载好的压缩包解压到 /mydata/elasticsearch/plugins/ik 目录下
  7. # 可能需要下载unzip: yum isntall -y unzip
  8. unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
  9. # 删除下载的压缩包
  10. rm -f elasticsearch-analysis-ik-7.4.2.zip
  11. # 修改文件夹访问权限
  12. chmod -R 777 ik

查看安装的ik插件

  1. # 进入到es容器内部
  2. docker exec -it 容器id /bin/bash
  3. # 进入 es bin 目录
  4. cd /usr/share/elasticsearch/bin
  5. # 执行查看命令 显示ik表示安装成功
  6. elasticsearch-plugin list
  7. # 退出容器
  8. exit
  9. # 重启 Elasticsearch
  10. docker restart elasticsearch

这里执行elasticsearch-plugin list的时候一定要删除下载的ik安装包,不然会报错。

5.2 测试ik分词器

  1. POST _analyze
  2. {
  3. "tokenizer": "ik_smart",
  4. "text": "我是你爸爸"
  5. }

image.png对于默认词库中没有的词,不会拆分成词语的组合,可以通过自定义词库以及远程词库扩展词库。

5.3 自定义词库

这里我们通过在nginx放置词库,让ik分词器给nginx发送请求,由nginx返回最新的词库,合并新词库与原来的词库。(nginx默认访问html文件夹下的文件)Docker安装nginx

5.3.1 nginx中自定义分词文件

  1. # 在/mydata/nginx/html下创建es文件夹,并cd过去
  2. mkdir /mydata/nginx/html/es
  3. cd /mydata/nginx/html/es
  4. # 创建一个fenci.txt文本文件
  5. vim fenci.txt
  6. 尚硅谷
  7. 乔碧萝
  8. 蔡徐坤

访问 虚拟机IP/es/fenci.txt 即可读取到文件 (这里因为字符编码的问题乱码了,先不管他)

5.3.2 ik分词器配置自定义词库

cd /mydata/elasticsearch/plugins/ik/config cd到ik分词器的配置文件目录下,修改IKAnalyzer.cfg.xml文件。这里我们配置了nginx下的远程词库

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
  3. <properties>
  4. <comment>IK Analyzer 扩展配置</comment>
  5. <!--用户可以在这里配置自己的扩展字典 -->
  6. <entry key="ext_dict"></entry>
  7. <!--用户可以在这里配置自己的扩展停止词字典-->
  8. <entry key="ext_stopwords"></entry>
  9. <!--用户可以在这里配置远程扩展字典 -->
  10. <entry key="remote_ext_dict">http://192.168.190.135/es/fenci.txt</entry>
  11. <!--用户可以在这里配置远程扩展停止词字典-->
  12. <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
  13. </properties>

重启es:docker restart elasticsearch
测试分词效果:
image.png

Docker 安装nginx

这里介绍如何使用docker安装nginx,如需了解其他nginx的内容参看:nginx笔记
首先我们需要启动一个临时的nginx容器,目的是将其配置文件copy到我们虚拟机挂载的nginx配置目录中,然后再创建一个新的nginx容器挂载使用。

  1. # 虚拟机中创建nginx挂载文件夹
  2. mkdir -p /mydata/nginx
  3. # 启动一个临时的nginx容器 端口映射80 版本1.10
  4. docker run -p 80:80 --name nginx -d nginx:1.10
  5. # 将nginx容器中的nginx目录复制到本机的/mydata/nginx目录下
  6. docker container cp nginx:/etc/nginx /mydata/nginx
  7. # 修改复制出来的nginx文件夹名为conf
  8. mv nginx conf
  9. # 停止目前正在运行的nginx容器 也可以不停止直接删除 docker rm -rf nginx
  10. docker stop nginx
  11. # 删除nginx容器
  12. docker rm nginx
  1. docker run -p 80:80 --name nginx \
  2. -v /mydata/nginx/conf:/etc/nginx \
  3. -v /mydata/nginx/html:/usr/share/nginx/html \
  4. -v /mydata/nginx/logs:/var/log/nginx \
  5. -d nginx:1.10
  6. # nginx的所有静态页面资源,我们这里都映射到/mydata/nginx/html目录下
  7. # log日志相关信息,映射到/mydata/nginx/logs下
  8. # 设置nginx随docker启动
  9. docker update nginx --restart=always
  1. firewall-cmd --add-port=80/tcp --permanent
  2. firewall-cmd --reload

访问虚拟机ip,80端口不用加,默认就是80端口,会出现403 Forbidden,这是因为nginx的静态资源文件夹下没有文件可读取。在/mydata/nginx/html文件夹下创建一个index.html文件写入如下内容:
<h1>Gulimall-nginx</h1>。再次访问虚拟机ip:
image.png

六、项目整合ES

现在当我们在前端选中一些检索条件的时候,就需要给es发送请求来检索真正的数据;这些请求应该由Java程序来接收并发送给ES进行处理,最后将处理的结果返回给前端。

6.1 ElasticSearch-Rest-Client

Java操作ES有两种方式:

  1. 9300:TCP 通过操作ES的9300端口
  • spring-data-elasticsearch:transport-api.jar; 只整合到了es的6.3.8
    • springboot 版本不同, transport-api.jar 不同,不能适配 es 版本
    • 7.x 已经不建议使用9300端口,8 以后就要废弃9300端口
  1. 9200:HTTP 通过操作ES的9200端口,发送HTTP请求
  • JestClient:非官方,更新慢
  • RestTemplate/HttpClient:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
  • Elasticsearch-Rest-Client:官方 RestClient,封装了 ES 操作,API 层次分明,上手简单

最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

注:elasticsearch-rest-high-level-client为JAVA REST Client,这个在7.17弃用,2022年es8官网建议使用的是JAVA Client(从7.16版本开始),不建议使用JAVA API和JAVA REST Client https://www.elastic.co/guide/en/elasticsearch/client/index.html

6.2 创建es检索服务模块

创建gulimall-search用于提供检索服务
image.png

6.2.1 pom.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>gulimall</artifactId>
  7. <groupId>com.atguigu.gulimall</groupId>
  8. <version>0.0.1-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <description>es检索服务</description>
  12. <artifactId>gulimall-search</artifactId>
  13. <properties>
  14. <maven.compiler.source>8</maven.compiler.source>
  15. <maven.compiler.target>8</maven.compiler.target>
  16. </properties>
  17. <dependencies>
  18. <dependency>
  19. <groupId>com.atguigu.gulimall</groupId>
  20. <artifactId>gulimall-common</artifactId>
  21. <version>0.0.1-SNAPSHOT</version>
  22. <exclusions>
  23. <exclusion>
  24. <groupId>com.baomidou</groupId>
  25. <artifactId>mybatis-plus-boot-starter</artifactId>
  26. </exclusion>
  27. </exclusions>
  28. </dependency>
  29. <!--引入es的JAVA REST Client 高阶依赖-->
  30. <dependency>
  31. <groupId>org.elasticsearch.client</groupId>
  32. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  33. <version>7.4.2</version>
  34. </dependency>
  35. </dependencies>
  36. </project>

引入依赖后,发现存在版本不对应的问题,这是因为spring-boot-dependence(左)对es的版本做了管理
image.png
父pom更新es版本管理:
我这里跟视频不一样,我是直接在父工程pom里管理依赖的版本,这里我们添加上es的版本,覆盖掉原来spring-boot-dependence中es的版本。这里只给出了需要修改(添加)的地方

  1. <!-- 这里的属性会被子模块继承 -->
  2. <properties>
  3. ...
  4. <elasticsearch.version>7.4.2</elasticsearch.version>
  5. </properties>
  6. <!-- 子模块继承父模块之后,提供作用:锁定版本 + 子模块不用再写 version -->
  7. <dependencyManagement>
  8. <dependencies>
  9. <!-- 重写覆盖 spring-boot-dependencies 中的依赖版本 -->
  10. <dependency>
  11. <groupId>org.elasticsearch.client</groupId>
  12. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  13. <version>${elasticsearch.version}</version>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.elasticsearch</groupId>
  17. <artifactId>elasticsearch</artifactId>
  18. <version>${elasticsearch.version}</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.elasticsearch.client</groupId>
  22. <artifactId>elasticsearch-rest-client</artifactId>
  23. <version>${elasticsearch.version}</version>
  24. </dependency>
  25. <dependencies>
  26. ...
  27. <dependencyManagement>

6.2.2 主启动类

  1. package com.atguigu.gulimall.gulimallsearch;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  5. /**
  6. * @author mrlinxi
  7. * @create 2022-03-17 1:10
  8. */
  9. @SpringBootApplication
  10. @EnableDiscoveryClient
  11. public class GulimallSearchApplication {
  12. public static void main(String[] args) {
  13. SpringApplication.run(GulimallSearchApplication.class, args);
  14. }
  15. }

6.2.3 application.yml

  1. spring:
  2. cloud:
  3. nacos:
  4. discovery:
  5. server-addr: localhost:8848
  6. application:
  7. name: gulimall-search
  8. server:
  9. port: 12000

6.2.4 ES配置类

  1. /**
  2. * 1. 导入依赖
  3. * 2. 编写配置,给容器中注入一个RestHighLevelClient
  4. * 3. 参照API文档操作
  5. */
  6. @Configuration
  7. public class GulimallElasticSearchConfig {
  8. /**
  9. * 配置请求选项
  10. * 参考:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-low-usage-requests.html#java-rest-low-usage-request-options
  11. */
  12. public static final RequestOptions COMMON_OPTIONS;
  13. static {
  14. RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
  15. // builder.addHeader("Authorization", "Bearer " + TOKEN);
  16. // builder.setHttpAsyncResponseConsumerFactory(
  17. // new HttpAsyncResponseConsumerFactory
  18. // .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
  19. COMMON_OPTIONS = builder.build();
  20. }
  21. @Bean
  22. public RestHighLevelClient esRestClient() {
  23. // 集群模式下,需要指明每个es的ip跟端口号,有几个集群就new 几个HttpHost
  24. // RestHighLevelClient client = new RestHighLevelClient(
  25. // RestClient.builder(new HttpHost("192.168.190.135", 9200, "http"))
  26. // );
  27. // 把上面的拆分一下
  28. RestClientBuilder builder = null;
  29. builder = RestClient.builder(new HttpHost("192.168.190.135", 9200, "http"));
  30. RestHighLevelClient client = new RestHighLevelClient(builder);
  31. return client;
  32. }
  33. }

RestHighLevelClient 中的所有 API 都接受一个 RequestOptions,您可以使用它来自定义请求,而不会改变 Elasticsearch 执行请求的方式。比如可以在RequestOptions中指定使用哪个NodeSelector来控制哪个节点接收请求(集群下)。更多自定义配置详见low-level参考文档

The RequestOptions class holds parts of the request that should be shared between many requests in the same application. You can make a singleton instance and share it between all requests. RequestOptions 类包含在同application中的许多请求之间共享的部分。你可以创建一个单例并在所有请求间共享。

image.png
目前暂时用不到RequestOptions,所以在配置类中将一些配置先注释掉,等需要用到再进行配置。

6.3 测试

测试均在GulimallSearchApplicationTests类中完成

6.3.1 测试配置类注入

  1. //@RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class GulimallSearchApplicationTests {
  4. @Autowired
  5. private RestHighLevelClient restHighLevelClient;
  6. @Test
  7. public void contextLoads() {
  8. System.out.println(restHighLevelClient);
  9. }
  10. }
  11. 打印: org.elasticsearch.client.RestHighLevelClient@3686389

这里不需要加@RunWith注解,SpringBoot 2.x 之后只需要导入 @SpringBootTest 即可,低版本需要加。

6.3.2 测试Index API (存储数据)

参考:Index Api Document
提供文档源(要存储的数据)有很多方式:

  • String字符串
  • Map
  • XContentBuilder 对象
  • KV 键值对
  • 自定义类型对象转JSON字符串

文档源设置完成后,便需要执行提交到es,执行分为同步与异步执行,具体可以参考文档。

  1. /**
  2. * 测试给es中存储数据,更新也可以
  3. */
  4. @Test
  5. public void indexData() throws IOException {
  6. // 创建IndexRequest,并指定index名(在哪个索引下面存储数据)
  7. IndexRequest indexRequest = new IndexRequest("users");
  8. indexRequest.id("1"); // 设置id,必须为字符串的形式,不设置会自动生成
  9. // source方法用于设置文档数据,数据可以有多种格式
  10. // 1.json字符串 2.Map 3.XContentBuilder 4.KV键值对
  11. // kv键值对方式传输
  12. //indexRequest.source("username", "zhangsan", "age", 18, "gender", "男");
  13. // 将自定义对象转为json字符串传输
  14. User user = new User();
  15. user.setUserName("zhangsan");
  16. user.setGender("男");
  17. user.setAge(18);
  18. // 把对象转为json字符串
  19. String jsonString = JSON.toJSONString(user);// 需要导入com.alibaba.fastjson依赖
  20. indexRequest.source(jsonString, XContentType.JSON); // 这里需要指明内容类型
  21. // 执行保存操作,执行分为同步与异步的方式,这里使用同步执行
  22. // 只要是网络操作,必然会考虑到异常,index方法会抛出IOException
  23. IndexResponse indexResponse = restHighLevelClient.index(indexRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
  24. // 提取有用的响应数据
  25. System.out.println(indexResponse);
  26. }
  27. @Data
  28. class User {
  29. private String userName;
  30. private String gender;
  31. private Integer age;
  32. }

IndexResponse[index=users,type=_doc,id=1,version=1,result=created,seqNo=0,primaryTerm=1,shards={“total”:2,”successful”:1,”failed”:0}]

6.3.3 复杂检索测试

参考:Search API Document

  1. @Test
  2. public void searchData() throws IOException {
  3. // 1.创建检索请求
  4. SearchRequest searchRequest = new SearchRequest();
  5. // SearchRequest searchRequest = new SearchRequest("bank"); // 也可以创建的时候直接指定index
  6. searchRequest.indices("bank"); // 指定从哪个index检索
  7. // 指定DSL,检索条件
  8. // searchSourceBuilder 封装了全部检索条件
  9. SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  10. // 1.1 构造检索条件
  11. // searchSourceBuilder.query();
  12. // searchSourceBuilder.from();
  13. // searchSourceBuilder.size();
  14. // searchSourceBuilder.aggregation();
  15. // 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄
  16. searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
  17. // 1.2 按照年龄的值分布进行聚集
  18. TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
  19. searchSourceBuilder.aggregation(ageAgg);
  20. // 1.3 计算match条件得到结果的平均薪资
  21. AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
  22. searchSourceBuilder.aggregation(balanceAvg);
  23. System.out.println("检索条件" + searchSourceBuilder.toString());
  24. searchRequest.source(searchSourceBuilder);
  25. // 2.执行检索 同样分为异步与同步
  26. SearchResponse searchResponse = restHighLevelClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
  27. // 3.分析检索结果 searchResponse
  28. System.out.println(searchResponse.toString());
  29. // JSON.parseObject(searchResponse.toString(), Map.class);
  30. // 3.1 获取所有查到的文档
  31. SearchHits hits = searchResponse.getHits(); // 这是外面最大的hits,里面有个hits数组存储的命中文档
  32. SearchHit[] searchHits = hits.getHits();
  33. for (SearchHit searchHit : searchHits) {
  34. /**
  35. * "_index" : "bank",
  36. * "_type" : "account",
  37. * "_id" : "970",
  38. * "_score" : 5.4032025,
  39. * "_source"
  40. */
  41. // searchHit.getIndex();
  42. String string = searchHit.getSourceAsString(); // 结果映射成JSON字符串
  43. Account account = JSON.parseObject(string, Account.class); // 通过JSON.parseObject 把JSON字符串转为对象,Account如果是内部类必须是static的
  44. System.out.println("account:" + account);
  45. }
  46. // 3.2 获取这次检索到的分析信息
  47. Aggregations aggregations = searchResponse.getAggregations(); // 获取所有聚合结果
  48. // for (Aggregation aggregation : aggregations.asList()) {
  49. // System.out.println("当前聚合名称" + aggregation.getName());
  50. // }
  51. Terms ageAgg1 = aggregations.get("ageAgg");
  52. for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
  53. System.out.println("年龄:" + bucket.getKeyAsString() + "==>" + bucket.getDocCount());
  54. }
  55. Avg balanceAvg1 = aggregations.get("balanceAvg");
  56. System.out.println(balanceAvg1.getValue());
  57. }
  58. @Data
  59. @ToString
  60. static class Account {
  61. private int account_number;
  62. private int balance;
  63. private String firstname;
  64. private String lastname;
  65. private int age;
  66. private String gender;
  67. private String address;
  68. private String employer;
  69. private String email;
  70. private String city;
  71. private String state;
  72. }

image.png