当前位置:   article > 正文

Elasticsearch分布式搜索引擎_es搜索

es搜索

一、初识elasticsearch

1、了解ES

1. ES的作用

  • elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容:

    • 在GitHub搜索代码:展示相关信息,并高亮显示相同部分

    • 在电商网站搜索商品:展示相关产品

    • 在百度搜索答案:展示相关信息,并高亮显示相同部分

    • 在打车软件搜索附近的车:显示最近车辆位置

2. ELK技术栈

  • elasticsearch 结合 kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:

  • elasticsearch 是 elastic stack 的核心(不可替代),负责存储、搜索、分析数据。

2、倒排索引

1. 正向索引

  • 根据id查询,正向索引查询速度非常快。

  • 但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:

    • 用户搜索数据,条件是title符合`"%手机%"

    • 逐行获取数据,比如id为1的数据

    • 判断数据中的title是否符合用户搜索条件

    • 如果符合则放入结果集,不符合则丢弃。回到步骤1

  • 逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

2. 倒排索引

  • elasticsearch采用倒排索引:

    • 文档(document):每条数据就是一个文档

    • 词条(term):文档按照语义分成的词语

  • 倒排索引的搜索流程如下(以搜索"华为手机"为例):

    • 用户输入条件"华为手机"进行搜索。

    • 对用户输入内容 分词,得到词条:华为手机

    • 拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3

    • 拿着文档id到正向索引中查找具体文档。

3. 正向和倒排

正向索引

  • 优点:

    • 可以给多个字段创建索引

    • 根据索引字段搜索、排序速度非常快

  • 缺点:

    • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引

  • 优点:

    • 根据词条搜索、模糊搜索时,速度非常快

  • 缺点:

    • 只能给词条创建索引,而不是字段

    • 无法根据字段做排序

3、ES的一些概念

1. 文档和字段

  • ES 是面向文档(Document)存储,可以是数据库中的一条商品数据,一个订单信息

  • 文档数据会被序列化为 json格式 后存储

  • Json文档中往往包含很多的字段(Field),类似于数据库中的列。

2. 索引和映射

  • 索引(Index),就是相同类型的文档的集合

  • 我们可以把索引当做是数据库中的表

  • 数据库的表有 约束信息,用来定义表的结构、字段的名称、类型等信息

  • 索引库中就有 映射(mapping),是索引中文档的字段约束信息,类似表的结构约束

3. mysql与ES

MySQLES说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD
  • mysql 与 ES 两者各自有自己的擅长支出:

    • Mysql:擅长事务类型操作,可以确保数据的安全和一致性

    • Elasticsearch:擅长海量数据的搜索、分析、计算

  • 因此在企业中,往往是两者结合使用:

    • 对安全性要求较高的写操作,使用mysql实现

    • 对查询性能要求较高的搜索需求,使用elasticsearch实现

    • 两者再基于某种方式,实现数据的同步,保证一致性

4、部署单点ES

1. 创建网络

  • 因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:

docker network create es-net

2. 加载镜像

docker pull elasticsearch:7.14.2

3. 运行

  1. docker run -d \
  2. --name es \
  3. -e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
  4. -e "discovery.type=single-node" \
  5. -v es-data:/usr/share/elasticsearch/data \
  6. -v es-plugins:/usr/share/elasticsearch/plugins \
  7. --privileged=true \
  8. --network es-net \
  9. -p 9200:9200 \
  10. -p 9300:9300 \
  11. elasticsearch:7.14.2

命令解释:

  • -d:后台运作

  • -e "cluster.name=es-docker-cluster":设置集群名称

  • -e "http.host=0.0.0.0":监听的地址,可以外网访问

  • -e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m":内存大小

  • -e "discovery.type=single-node":非集群模式

  • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录

  • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录

  • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录

  • --privileged:授予逻辑卷访问权

  • --network es-net :加入一个名为es-net的网络中

  • -p 9200:9200:端口映射配置

4. 浏览器查看

浏览器访问地址:http://192.168.116.129:9200 即可看到elasticsearch的响应结果

5、部署kibana

  • kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

1. 加载镜像

docker pull kibana:7.14.2

2. 部署

  1. docker run -d \
  2. --name kibana \
  3. -e ELASTICSEARCH_HOSTS=http://es:9200 \
  4. --network=es-net \
  5. -p 5601:5601 \
  6. kibana:7.14.2
  • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中

  • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch

  • -p 5601:5601:端口映射配置

3. 查看启动日志

kibana启动一般比较慢,需要多等待一会,可以通过以下命令,查看运行日志:

docker logs -f kibana

4. 浏览器查看

浏览器访问地址:http://192.168.116.129:5601

kibana 中提供了一个DevTools界面:可以编写 DSL 来操作 elasticsearch ,并且对 DSL 语句有自动补全功能。

6、安装IK分词器

1. 部署

  1. # 进入容器内部
  2. docker exec -it es /bin/bash
  3. # 在线下载并安装
  4. ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.14.2/elasticsearch-analysis-ik-7.14.2.zip
  5. # 退出
  6. exit
  7. # 重启容器
  8. docker restart es

2. 测试

IK分词器包含两种模式:

  • ik_smart:最少切分

  • ik_max_word:最细切分

  1. GET /_analyze
  2. {
  3. "analyzer": "ik_max_word",
  4. "text": "黑马程序员学习java太棒了"
  5. }

结果:

  1. {
  2. "tokens" : [
  3. {
  4. "token" : "黑马",
  5. "start_offset" : 0,
  6. "end_offset" : 2,
  7. "type" : "CN_WORD",
  8. "position" : 0
  9. },
  10. {
  11. "token" : "程序员",
  12. "start_offset" : 2,
  13. "end_offset" : 5,
  14. "type" : "CN_WORD",
  15. "position" : 1
  16. },
  17. {
  18. "token" : "程序",
  19. "start_offset" : 2,
  20. "end_offset" : 4,
  21. "type" : "CN_WORD",
  22. "position" : 2
  23. },
  24. {
  25. "token" : "员",
  26. "start_offset" : 4,
  27. "end_offset" : 5,
  28. "type" : "CN_CHAR",
  29. "position" : 3
  30. },
  31. {
  32. "token" : "学习",
  33. "start_offset" : 5,
  34. "end_offset" : 7,
  35. "type" : "CN_WORD",
  36. "position" : 4
  37. },
  38. {
  39. "token" : "java",
  40. "start_offset" : 7,
  41. "end_offset" : 11,
  42. "type" : "ENGLISH",
  43. "position" : 5
  44. },
  45. {
  46. "token" : "太棒了",
  47. "start_offset" : 11,
  48. "end_offset" : 14,
  49. "type" : "CN_WORD",
  50. "position" : 6
  51. },
  52. {
  53. "token" : "太棒",
  54. "start_offset" : 11,
  55. "end_offset" : 13,
  56. "type" : "CN_WORD",
  57. "position" : 7
  58. },
  59. {
  60. "token" : "了",
  61. "start_offset" : 13,
  62. "end_offset" : 14,
  63. "type" : "CN_CHAR",
  64. "position" : 8
  65. }
  66. ]
  67. }

3. 扩展词典

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。

1)打开IK分词器config目录:

2)在IKAnalyzer.cfg.xml配置文件内容添加:

  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">ext.dic</entry>
  7. </properties>

3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改

  • 注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑

  1. 传智播客
  2. 奥力给

4)重启elasticsearch

  1. docker restart es
  2. # 查看日志:日志中已经成功加载 ext.dic 配置文件
  3. docker logs -f es

4. 停用词典

在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。

IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。

1)IKAnalyzer.cfg.xml配置文件内容添加:

  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">ext.dic</entry>
  7. <!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典-->
  8. <entry key="ext_stopwords">stopword.dic</entry>
  9. </properties>

3)在 stopword.dic 添加停用词

尔尔

4)重启elasticsearch

  1. # 重启服务
  2. docker restart es
  3. docker restart kibana
  4. # 查看 日志
  5. docker logs -f es

日志中已经成功加载stopword.dic配置文件

5)测试效果:

  1. GET /_analyze
  2. {
  3. "analyzer": "ik_max_word",
  4. "text": "传智播客Java就业率超过95%,尔尔都点赞,奥力给!"
  5. }

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑

7、部署ES集群

部署 ES 集群可以直接使用docker-compose来完成,不过要求你的Linux虚拟机至少有4G的内存空间

首先编写一个docker-compose文件,内容如下:

  1. version: '2.2'
  2. services:
  3. es01:
  4. image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
  5. container_name: es01
  6. environment:
  7. - node.name=es01
  8. - cluster.name=es-docker-cluster
  9. - discovery.seed_hosts=es02,es03
  10. - cluster.initial_master_nodes=es01,es02,es03
  11. - bootstrap.memory_lock=true
  12. - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  13. ulimits:
  14. memlock:
  15. soft: -1
  16. hard: -1
  17. volumes:
  18. - data01:/usr/share/elasticsearch/data
  19. ports:
  20. - 9200:9200
  21. networks:
  22. - elastic
  23. es02:
  24. image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
  25. container_name: es02
  26. environment:
  27. - node.name=es02
  28. - cluster.name=es-docker-cluster
  29. - discovery.seed_hosts=es01,es03
  30. - cluster.initial_master_nodes=es01,es02,es03
  31. - bootstrap.memory_lock=true
  32. - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  33. ulimits:
  34. memlock:
  35. soft: -1
  36. hard: -1
  37. volumes:
  38. - data02:/usr/share/elasticsearch/data
  39. networks:
  40. - elastic
  41. es03:
  42. image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
  43. container_name: es03
  44. environment:
  45. - node.name=es03
  46. - cluster.name=es-docker-cluster
  47. - discovery.seed_hosts=es01,es02
  48. - cluster.initial_master_nodes=es01,es02,es03
  49. - bootstrap.memory_lock=true
  50. - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  51. ulimits:
  52. memlock:
  53. soft: -1
  54. hard: -1
  55. volumes:
  56. - data03:/usr/share/elasticsearch/data
  57. networks:
  58. - elastic
  59. volumes:
  60. data01:
  61. driver: local
  62. data02:
  63. driver: local
  64. data03:
  65. driver: local
  66. networks:
  67. elastic:
  68. driver: bridge

Run docker-compose to bring up the cluster:

docker-compose up

二、kibana操作

索引库就类似数据库表,mapping映射就类似表的结构,我们要向es中存储数据,必须先创建“库”和“表”。

1、mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

    • 数值:long、integer、short、byte、double、float

    • 布尔:boolean

    • 日期:date

    • 对象:object

  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段

  1. {
  2.     "age": 21,
  3.     "weight": 52.1,
  4.     "isMarried"false,
  5.     "info""黑马程序员Java讲师",
  6. "email""zy@itcast.cn",
  7. "score": [99.1, 99.5, 98.9],
  8.     "name": {
  9.         "firstName""云",
  10.         "lastName""赵"
  11.     }
  12. }

对应的每个字段映射(mapping):

  • age:类型为 integer;参与搜索,因此需要index为true;无需分词器

  • weight:类型为float;参与搜索,因此需要index为true;无需分词器

  • isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器

  • info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart

  • email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器

  • score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器

  • name:类型为object,需要定义多个子属性

    • name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

    • name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器

2、索引库的CRUD

1. 创建索引库和映射

语法

  • 请求方式:PUT

  • 请求路径:/索引库名,可以自定义

  • 请求参数:mapping映射

  1. PUT /索引库名称
  2. {
  3.   "mappings": {
  4.     "properties": {
  5.       "字段名":{
  6.         "type""text",
  7.         "analyzer""ik_smart"
  8.       },
  9.       "字段名2":{
  10.         "type""keyword",
  11.         "index""false"
  12.       },
  13.       "字段名3":{
  14.         "properties": {
  15.           "子字段": {
  16.             "type""keyword"
  17.           }
  18.         }
  19.       },
  20. // ...略
  21.     }
  22.   }
  23. }

示例

  1. # 创建索引库-名称 heima
  2. PUT /heima
  3. {
  4. # mapping映射
  5. "mappings": {
  6. # 具体字段
  7. "properties": {
  8. # info 字段
  9. "info": {
  10. # 可分词的文本
  11. "type": "text",
  12. # 分词器
  13. "analyzer": "ik_smart"
  14. },
  15. # email 字段
  16. "email": {
  17. # 精确值
  18. "type": "keyword",
  19. # 无需创建索引
  20. "index": false
  21. },
  22. # name 字段
  23. "name": {
  24. "type": "object",
  25. # 子属性
  26. "properties": {
  27. "firstName": {
  28. "type": "keyword"
  29. },
  30. "lastName": {
  31. "type": "keyword"
  32. }
  33. }
  34. }
  35. }
  36. }
  37. }

2. 查询索引库

语法

  • 请求方式:GET

  • 请求路径:/索引库名

  • 请求参数:无

GET /索引库名

示例

3. 修改索引库

倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping

虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。

语法

  1. PUT /索引库名/_mapping
  2. {
  3. "properties": {
  4. "新字段名":{
  5. "type""integer"
  6.     }
  7.   }
  8. }

示例

添加字段后:

4. 删除索引库

语法

  • 请求方式:DELETE

  • 请求路径:/索引库名

  • 请求参数:无

DELETE /索引库名

示例

3、文档操作

1. 新增文档

语法

  1. POST /索引库名/_doc/文档id
  2. {
  3. "字段1""值1",
  4. "字段2""值2",
  5. "字段3": {
  6. "子属性1""值3",
  7. "子属性2""值4"
  8. },
  9. // ...
  10. }

示例

  1. POST /heima/_doc/1
  2. {
  3. "info": "黑马程序员Java讲师",
  4. "email": "zy@itcast.cn",
  5. "name": {
  6. "firstName": "云",
  7. "lastName": "赵"
  8. }
  9. }

2. 查询文档

根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。

语法

GET /{索引库名称}/_doc/{id}

示例

GET /heima/_doc/1

3. 删除文档

删除使用DELETE请求,同样,需要根据id进行删除:

语法

DELETE /{索引库名}/_doc/id值

示例

  1. # 根据id删除数据
  2. DELETE /heima/_doc/1

4. 修改文档

4.1 全量修改

全量修改是覆盖原来的文档,其本质是:

  • 根据指定的 id 删除文档

  • 新增一个相同 id 的文档

注意:如果根据 id 删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。

语法

  1. PUT /{索引库名}/_doc/文档id
  2. {
  3. "字段1""值1",
  4. "字段2""值2",
  5. // ... 略
  6. }

示例

  1. PUT /heima/_doc/1
  2. {
  3. "info": "黑马程序员高级Java讲师",
  4. "email": "zhaoyun@itcast.cn",
  5. "name": {
  6. "firstName": "云",
  7. "lastName": "赵"
  8. }
  9. }

4.2 增量修改

增量修改是只修改指定id匹配的文档中的部分字段。

语法

  1. POST /{索引库名}/_update/文档id
  2. {
  3. "doc": {
  4. "字段名""新的值",
  5. }
  6. }

示例

  1. POST /heima/_update/1
  2. {
  3.   "doc": {
  4.     "email""ZhaoYun@itcast.cn"
  5.   }
  6. }

4、DSL查询文档

1. DSL Query的分类

Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。

    • match_all

  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。

    • match_query

    • multi_match_query

  • 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。

    • ids

    • range

    • term

  • 地理(geo)查询:根据经纬度查询。

    • geo_distance

    • geo_bounding_box

  • 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。

    • bool

    • function_score

2. 查询所有

  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "match_all": {
  5. }
  6.   }
  7. }

3.全文检索查询

  • 搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式

3.1 match_query
  1. // 单字段检索
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "match": {
  6. // 检索字段:检索内容
  7.       "FIELD""TEXT"
  8.     }
  9.   }
  10. }

3.2 multi_match_query
  1. // 多字段查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "multi_match": {
  6. // 检索内容
  7.       "query""TEXT",
  8. // 检索字段
  9.       "fields": ["FIELD1"" FIELD12"]
  10.     }
  11.   }
  12. }

4. 精准查询

4.1 term查询
  1. // term查询:根据词条精确查询
  2. GET /indexName/_search
  3. {
  4. "query": {
  5. "term": {
  6. // 查询字段
  7.       "FIELD": {
  8.         "value""VALUE"
  9.       }
  10.     }
  11.   }
  12. }

4.2 range查询
  1. // range查询:根据值的范围查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "range": {
  6. // 查询字段
  7.       "FIELD": {
  8.         "gte": 10, // 这里的gte代表大于等于,gt则代表大于
  9.         "lte": 20 // lte代表小于等于,lt则代表小于
  10.       }
  11.     }
  12.   }
  13. }

5. 地理坐标查询

5.1 矩形范围查询

  1. // geo_bounding_box查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "geo_bounding_box": {
  6.       "FIELD": {
  7.         "top_left": { // 左上点
  8.           "lat": 31.1,
  9.           "lon": 121.5
  10.         },
  11.         "bottom_right": { // 右下点
  12.           "lat": 30.9,
  13.           "lon": 121.7
  14.         }
  15.       }
  16.     }
  17.   }
  18. }

5.2 附近查询

  1. // geo_distance 查询
  2. GET /indexName/_search
  3. {
  4.   "query": {
  5.     "geo_distance": {
  6.       "distance""15km", // 半径
  7.       "FIELD""31.21,121.5" // 圆心
  8.     }
  9.   }
  10. }

6.复合查询

复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

  • fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名

  • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索

6.1 相关性算分

在elasticsearch中,早期使用的打分算法是TF-IDF算法,在后来的5.1版本升级中,改进为BM25算法:

TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:

6.2 算分函数查询

示例 :给“如家”这个品牌的酒店排名靠前一些:

  • 原始条件:不确定,可以任意变化

  • 过滤条件:brand = "如家"

  • 算分函数:可以简单粗暴,直接给固定的算分结果,weight

  • 运算模式:比如求和

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "function_score": {
  5. // 原始查询,可以是任意条件
  6.       "query": {....},
  7.        // 算分函数
  8. "functions": [ 
  9.         {
  10. // 过滤条件
  11.           "filter": { 
  12.             "term": {
  13. // 满足的条件,品牌必须是如家
  14.               "brand""如家"
  15.             }
  16.           },
  17.           "weight": 2 // 算分权重为2
  18.         }
  19.       ],
  20. "boost_mode": "sum" // 加权模式,求和
  21.     }
  22.   }
  23. }

原始函数:

算分函数查询:

6.3 布尔查询

布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:

  • must:必须匹配每个子查询,类似“与”

  • should:选择性匹配子查询,类似“或”

  • must_not:必须不匹配,不参与算分,类似“非”

  • filter:必须匹配,不参与算分

5、搜索结果处理

1. 排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索 结果排序 。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

1.1 普通字段排序
  1. GET /indexName/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "sort": [
  7.     {
  8. // 排序字段、排序方式ASC、DESC
  9.       "FIELD""desc"  
  10.     }
  11.   ]
  12. }

1.2 地理坐标排序
  1. GET /indexName/_search
  2. {
  3. "query": {
  4. "match_all": {}
  5. },
  6. "sort": [
  7. {
  8. "_geo_distance" : {
  9. // 文档中geo_point类型的字段名、目标坐标点
  10. "FIELD" : "纬度,经度",
  11. // 排序方式
  12. "order" : "asc",
  13. // 排序的距离单位
  14. "unit" : "km"
  15. }
  16. }
  17. ]
  18. }

2. 分页

2.1 基本的分页
  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match_all": {}
  5.   },
  6.   "from": 0, // 分页开始的位置,默认为0
  7.   "size": 10, // 期望获取的文档总数
  8.   "sort": [
  9.     {"price""asc"}
  10.   ]
  11. }

 

2.2 深度分页问题

针对深度分页,ES提供了两种解决方案,官方文档

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。

  • scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。

2.3 总结

分页查询的常见实现方案以及优缺点:

  • from + size

    • 优点:支持随机翻页

    • 缺点:深度分页问题,默认查询上限(from + size)是10000

    • 场景:百度、京东、谷歌、淘宝这样的随机翻页搜索

  • after search

    • 优点:没有查询上限(单次查询的size不超过10000)

    • 缺点:只能向后逐页查询,不支持随机翻页

    • 场景:没有随机翻页需求的搜索,例如手机向下滚动翻页

  • scroll

    • 优点:没有查询上限(单次查询的size不超过10000)

    • 缺点:会有额外内存消耗,并且搜索结果是非实时的

    • 场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。

3. 高亮

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match": {
  5. // 查询条件,高亮一定要使用全文检索查询
  6.       "FIELD""TEXT"
  7.     }
  8.   },
  9.   "highlight": {
  10.     "fields": {
  11. // 指定要高亮的字段
  12.       "FIELD": {
  13.         "pre_tags""<em>",  // 用来标记高亮字段的前置标签
  14.         "post_tags""</em>" // 用来标记高亮字段的后置标签
  15. "require_field_match": "false" // 查询字段 与 指定字段不匹配
  16.       }
  17.     }
  18.   }
  19. }

6、总结

  1. GET /hotel/_search
  2. {
  3.   "query": {
  4.     "match": { "name""如家" }
  5.   },
  6.   "from": 0, // 分页开始的位置
  7.   "size": 20, // 期望获取的文档总数
  8.   "sort": [ 
  9.     { "price""asc" }, // 普通排序
  10.     {
  11.       "_geo_distance" : { // 距离排序
  12.           "location" : "31.040699,121.618075"
  13.           "order" : "asc",
  14.           "unit" : "km"
  15.       }
  16.     }
  17.   ],
  18.   "highlight": {
  19.     "fields": { // 高亮字段
  20.       "name": {
  21.         "pre_tags""<em>",  // 用来标记高亮字段的前置标签
  22.         "post_tags""</em>" // 用来标记高亮字段的后置标签
  23.       }
  24.     }
  25.   }
  26. }

三、RestAPI

1、创建数据库

1. 创建 mysql 数据库

  1. CREATE TABLE `tb_hotel` (
  2.   `id` bigint(20NOT NULL COMMENT '酒店id',
  3.   `name` varchar(255NOT NULL COMMENT '酒店名称;例:7天酒店',
  4.   `address` varchar(255NOT NULL COMMENT '酒店地址;例:航头路',
  5.   `price` int(10NOT NULL COMMENT '酒店价格;例:329',
  6.   `score` int(2NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
  7.   `brand` varchar(32NOT NULL COMMENT '酒店品牌;例:如家',
  8.   `city` varchar(32NOT NULL COMMENT '所在城市;例:上海',
  9.   `star_name` varchar(16DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
  10.   `business` varchar(255DEFAULT NULL COMMENT '商圈;例:虹桥',
  11.   `latitude` varchar(32NOT NULL COMMENT '纬度;例:31.2497',
  12.   `longitude` varchar(32NOT NULL COMMENT '经度;例:120.3925',
  13.   `pic` varchar(255DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
  14.   PRIMARY KEY (`id`)
  15. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2、mapping映射分析

  • 字段名、字段数据类型,可以参考数据表结构的名称和类型

  • 是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索

  • 是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词

  • 分词器,我们可以统一使用ik_max_word

  • ES中支持两种地理坐标数据类型:

    • geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:"32.8752345, 120.2981576"

    • geo_shape:有多个geo_point组成的复杂几何图形。例如一条直线,"LINESTRING (-77.03653 38.897676, -77.009051 38.889939)"

  • 字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:

  1. "all": {
  2. "type""text",
  3. "analyzer""ik_max_word"
  4. },
  5. "brand": {
  6. "type""keyword",
  7. "copy_to""all"
  8. }
  1. PUT /hotel
  2. {
  3. "mappings": {
  4. "properties": {
  5. "id": {
  6. "type": "keyword"
  7. },
  8. "name":{
  9. "type": "text",
  10. "analyzer": "ik_max_word",
  11. "copy_to": "all"
  12. },
  13. "address":{
  14. "type": "keyword",
  15. "index": false
  16. },
  17. "price":{
  18. "type": "integer"
  19. },
  20. "score":{
  21. "type": "integer"
  22. },
  23. "brand":{
  24. "type": "keyword",
  25. "copy_to": "all"
  26. },
  27. "city":{
  28. "type": "keyword",
  29. "copy_to": "all"
  30. },
  31. "starName":{
  32. "type": "keyword"
  33. },
  34. "business":{
  35. "type": "keyword"
  36. },
  37. "location":{
  38. "type": "geo_point"
  39. },
  40. "pic":{
  41. "type": "keyword",
  42. "index": false
  43. },
  44. "all":{
  45. "type": "text",
  46. "analyzer": "ik_max_word"
  47. }
  48. }
  49. }
  50. }

2、创建项目

3、导入依赖

  1. <dependency>
  2. <groupId>org.elasticsearch.client</groupId>
  3. <artifactId>elasticsearch-rest-client</artifactId>
  4. <version>7.14.2</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.elasticsearch</groupId>
  8. <artifactId>elasticsearch</artifactId>
  9. <version>7.14.2</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.elasticsearch.client</groupId>
  13. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  14. <version>7.14.2</version>
  15. </dependency>

四、RestClient操作索引库

1、RestHighLevelClient

- 这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:

  1. @SpringBootTest
  2. public class _01初始化RestHighLevelClient {
  3. private RestHighLevelClient client;
  4. @BeforeEach
  5. void setUp() {
  6. this.client = new RestHighLevelClient(RestClient.builder(
  7. // https 会报错
  8. HttpHost.create("http://192.168.116.129:9200")
  9. ));
  10. }
  11. @AfterEach
  12. void tearDown() throws IOException {
  13. this.client.close();
  14. }
  15. }

2、创建索引库

  1. @SpringBootTest
  2. public class _02创建索引库 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void createHotelIndex() throws IOException {
  6. // 1.创建Request对象
  7. CreateIndexRequest request = new CreateIndexRequest("hotel");
  8. // 2.准备请求的参数:DSL语句
  9. request.source(MAPPING_TEMPLATE, XContentType.JSON);
  10. // 3.发送请求
  11. client.indices().create(request, RequestOptions.DEFAULT);
  12. }
  13. }
  14. public class HotelConstants {
  15. public static final String MAPPING_TEMPLATE = "{\n" +
  16. " \"mappings\": {\n" +
  17. " \"properties\": {\n" +
  18. " \"id\": {\n" +
  19. " \"type\": \"keyword\"\n" +
  20. " },\n" +
  21. " \"name\":{\n" +
  22. " \"type\": \"text\",\n" +
  23. " \"analyzer\": \"ik_max_word\",\n" +
  24. " \"copy_to\": \"all\"\n" +
  25. " },\n" +
  26. " \"address\":{\n" +
  27. " \"type\": \"keyword\",\n" +
  28. " \"index\": false\n" +
  29. " },\n" +
  30. " \"price\":{\n" +
  31. " \"type\": \"integer\"\n" +
  32. " },\n" +
  33. " \"score\":{\n" +
  34. " \"type\": \"integer\"\n" +
  35. " },\n" +
  36. " \"brand\":{\n" +
  37. " \"type\": \"keyword\",\n" +
  38. " \"copy_to\": \"all\"\n" +
  39. " },\n" +
  40. " \"city\":{\n" +
  41. " \"type\": \"keyword\",\n" +
  42. " \"copy_to\": \"all\"\n" +
  43. " },\n" +
  44. " \"starName\":{\n" +
  45. " \"type\": \"keyword\"\n" +
  46. " },\n" +
  47. " \"business\":{\n" +
  48. " \"type\": \"keyword\"\n" +
  49. " },\n" +
  50. " \"location\":{\n" +
  51. " \"type\": \"geo_point\"\n" +
  52. " },\n" +
  53. " \"pic\":{\n" +
  54. " \"type\": \"keyword\",\n" +
  55. " \"index\": false\n" +
  56. " },\n" +
  57. " \"all\":{\n" +
  58. " \"type\": \"text\",\n" +
  59. " \"analyzer\": \"ik_max_word\"\n" +
  60. " }\n" +
  61. " }\n" +
  62. " }\n" +
  63. "}";
  64. }

3、删除索引库

  1. @SpringBootTest
  2. public class _03删除索引库 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void deleteHotelIndex() throws IOException {
  6. // 1.创建Request对象
  7. DeleteIndexRequest request = new DeleteIndexRequest("hotel");
  8. // 2.发送请求
  9. client.indices().delete(request, RequestOptions.DEFAULT);
  10. }
  11. }

4、判断是否存在

  1. @SpringBootTest
  2. public class _04判断索引库是否存在 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void isExistHotelIndex() throws IOException {
  6. // 1.创建Request对象
  7. GetIndexRequest request = new GetIndexRequest("hotel");
  8. // 2.发送请求
  9. boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
  10. // 3.输出
  11. System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
  12. }
  13. }

五、RestClient操作文档

1、新增文档

  1. @SpringBootTest
  2. public class _05新增文档 {
  3. @Autowired
  4. private IHotelService hotelService;
  5. private RestHighLevelClient client;
  6. @Test
  7. void createDocument() throws IOException {
  8. // 1.根据id查询酒店数据
  9. Hotel hotel = hotelService.getById(61083L);
  10. // 2.转换为文档类型
  11. HotelDoc hotelDoc = new HotelDoc(hotel);
  12. // 3.将HotelDoc转json
  13. String json = JSON.toJSONString(hotelDoc);
  14. // 4.准备Request对象
  15. IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
  16. // 5.准备Json文档
  17. request.source(json, XContentType.JSON);
  18. // 6.发送请求
  19. client.index(request, RequestOptions.DEFAULT);
  20. }
  21. }

2、根据id查询文档

  1. @SpringBootTest
  2. public class _06根据id查询文档 {
  3. @Autowired
  4. private IHotelService hotelService;
  5. private RestHighLevelClient client;
  6. @Test
  7. void getDocumentById() throws IOException {
  8. // 1.准备Request
  9. GetRequest request = new GetRequest("hotel", "61083");
  10. // 2.发送请求,得到响应
  11. GetResponse response = client.get(request, RequestOptions.DEFAULT);
  12. // 3.解析响应结果
  13. String json = response.getSourceAsString();
  14. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  15. System.out.println(hotelDoc);
  16. }
  17. }

3、根据id修改文档

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改

  • 如果新增时,ID不存在,则新增

  1. @SpringBootTest
  2. public class _07根据id修改文档 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void updateDocumentById() throws IOException {
  6. // 1.准备Request
  7. UpdateRequest request = new UpdateRequest("hotel", "61083");
  8. // 2.准备请求参数
  9. request.doc(
  10. "price", "952",
  11. "starName", "四钻"
  12. );
  13. // 3.发送请求
  14. client.update(request, RequestOptions.DEFAULT);
  15. }
  16. }

4、根据id删除文档

  1. @SpringBootTest
  2. public class _08根据id删除文档 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void deleteDocumentById() throws IOException {
  6. // 1.准备Request
  7. DeleteRequest request = new DeleteRequest("hotel", "61083");
  8. // 2.发送请求
  9. client.delete(request, RequestOptions.DEFAULT);
  10. }
  11. }

5、批量导入文档

  1. @SpringBootTest
  2. public class _09批量导入文档 {
  3. @Autowired
  4. private IHotelService hotelService;
  5. private RestHighLevelClient client;
  6. @Test
  7. void BatchImportById() throws IOException {
  8. // 1.批量查询酒店数据
  9. List<Hotel> hotels = hotelService.list();
  10. // 2.创建Request
  11. BulkRequest request = new BulkRequest();
  12. // 3.准备参数,添加多个新增的Request
  13. for (Hotel hotel : hotels) {
  14. // 3.1 转换为文档类型HotelDoc
  15. HotelDoc hotelDoc = new HotelDoc(hotel);
  16. // 3.2 创建新增文档的Request对象
  17. IndexRequest tempHotel = new IndexRequest("hotel")
  18. .id(hotelDoc.getId().toString());
  19. // 3.3 获取json文档
  20. tempHotel.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
  21. request.add(tempHotel);
  22. }
  23. // 4.发送请求
  24. client.bulk(request, RequestOptions.DEFAULT);
  25. }
  26. }

六、RestClient查询文档

1、查询所有

  1. @SpringBootTest
  2. public class _10查询所有 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void matchAll() throws IOException {
  6. // 1.准备Request
  7. SearchRequest request = new SearchRequest("hotel");
  8. // 2.组织DSL参数
  9. request.source()
  10. .query(QueryBuilders.matchAllQuery());
  11. // 3.发送请求,得到响应结果
  12. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  13. handleResponse(response);
  14. }
  15. private void handleResponse(SearchResponse response) {
  16. // 4.解析结果
  17. SearchHits searchHits = response.getHits();
  18. // 4.1.查询的总条数
  19. long total = searchHits.getTotalHits().value;
  20. System.out.println("共搜索" + total + "条数据。");
  21. // 4.2.查询的结果数组
  22. SearchHit[] hits = searchHits.getHits();
  23. for (SearchHit hit : hits) {
  24. // 4.3.获取文档 source
  25. String json = hit.getSourceAsString();
  26. // 4.4 反序列化
  27. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  28. // 4.5.打印
  29. System.out.println(hotelDoc);
  30. }
  31. }
  32. }

2、全文检索查询

  1. @SpringBootTest
  2. public class _11全文检索查询 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void match() throws IOException {
  6. // 1.准备Request
  7. SearchRequest request = new SearchRequest("hotel");
  8. // 2.组织DSL参数
  9. request.source()
  10. .query(QueryBuilders.matchQuery("all", "如家"));
  11. // 3.发送请求,得到响应结果
  12. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  13. handleResponse(response);
  14. }
  15. }

3、精确查询

  1. @SpringBootTest
  2. public class _12精确查询 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void term() throws IOException {
  6. // 1.准备Request
  7. SearchRequest request = new SearchRequest("hotel");
  8. // 2.组织DSL参数
  9. request.source()
  10. .query(QueryBuilders.termQuery("city", "上海"));
  11. // 3.发送请求,得到响应结果
  12. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  13. // 4.解析响应
  14. handleResponse(response);
  15. }
  16. @Test
  17. void range() throws IOException {
  18. // 1.准备Request
  19. SearchRequest request = new SearchRequest("hotel");
  20. // 2.组织DSL参数
  21. request.source()
  22. .query(QueryBuilders.rangeQuery("price").gte(100).lte(500));
  23. // 3.发送请求,得到响应结果
  24. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  25. // 4.解析响应
  26. handleResponse(response);
  27. }
  28. }

4、复合查询

  1. @SpringBootTest
  2. public class _13复合查询 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void booleanQuery() throws IOException {
  6. // 1.准备Request
  7. SearchRequest request = new SearchRequest("hotel");
  8. // 2.组织DSL参数
  9. request.source()
  10. .query(QueryBuilders
  11. // 2.1 准备BooleanQuery
  12. .boolQuery()
  13. // 2.2 添加term
  14. .must(QueryBuilders.termQuery("city", "上海"))
  15. // 2.3 添加range
  16. .filter(QueryBuilders.rangeQuery("price").lte(250))
  17. );
  18. // 3.发送请求
  19. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  20. // 4.解析响应
  21. handleResponse(response);
  22. }
  23. }

5、排序和分页

  1. @SpringBootTest
  2. public class _14排序和分页 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void pageAndSort() throws IOException {
  6. // 页码,每页大小
  7. int page = 1, size = 5;
  8. // 1.准备Request
  9. SearchRequest request = new SearchRequest("hotel");
  10. // 2.准备DSL
  11. request.source()
  12. // 2.1 查询
  13. .query(QueryBuilders.matchAllQuery())
  14. // 2.2 排序 sort
  15. .sort("price", SortOrder.ASC)
  16. // 2.3 分页 from、size
  17. .from((page - 1) * size).size(5);
  18. // 3.发送请求
  19. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  20. // 4.解析响应
  21. handleResponse(response);
  22. }
  23. }

6、高亮

1. 高亮

2. 结果解析

  1. @SpringBootTest
  2. public class _15高亮 {
  3. private RestHighLevelClient client;
  4. @Test
  5. void highlighter() throws IOException {
  6. // 1.准备Request
  7. SearchRequest request = new SearchRequest("hotel");
  8. // 2.准备DSL
  9. request.source()
  10. // 2.1 查询
  11. .query(QueryBuilders.matchQuery("all", "如家"))
  12. // 2.2 高亮
  13. .highlighter(new HighlightBuilder()
  14. // 高亮内容
  15. .field("name")
  16. // 不匹配查询字段
  17. .requireFieldMatch(false));
  18. // 3.发送请求
  19. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  20. // 4.解析响应
  21. handleResponse(response);
  22. }
  23. private void handleResponse(SearchResponse response) {
  24. // 4.解析响应
  25. SearchHits searchHits = response.getHits();
  26. // 4.1.获取总条数
  27. long total = searchHits.getTotalHits().value;
  28. System.out.println("共搜索到" + total + "条数据");
  29. // 4.2.文档数组
  30. SearchHit[] hits = searchHits.getHits();
  31. // 4.3.遍历
  32. for (SearchHit hit : hits) {
  33. // 获取文档source
  34. String json = hit.getSourceAsString();
  35. // 反序列化
  36. HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
  37. // 获取高亮结果
  38. Map<String, HighlightField> highlightFields = hit.getHighlightFields();
  39. if (!CollectionUtils.isEmpty(highlightFields)) {
  40. // 根据字段名获取高亮结果
  41. HighlightField highlightField = highlightFields.get("name");
  42. if (highlightField != null) {
  43. // 获取高亮值
  44. String name = highlightField.getFragments()[0].string();
  45. // 覆盖非高亮结果
  46. hotelDoc.setName(name);
  47. }
  48. }
  49. System.out.println("hotelDoc = " + hotelDoc);
  50. }
  51. }
  52. }

七、数据聚合

1、聚合的分类

聚合常见的有三类:

  • 桶(Bucket)聚合:用来对文档做分组

    • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组

    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等

    • Avg:求平均值

    • Max:求最大值

    • Min:求最小值

    • Stats:同时求max、min、avg、sum等

  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

注意:参加聚合的字段必须是 keyword、日期、数值、布尔类型;一定不能是 text(可分词的文本)

2、DSL实现Bucket聚合

  • aggs代表聚合,与query同级,此时query的作用:

    • 限定聚合的的文档范围

  • 聚合必须的三要素:

    • 聚合名称

    • 聚合类型

    • 聚合字段

  • 聚合可配置属性有:

    • size:指定聚合结果数量

    • order:指定聚合结果排序方式

    • field:指定聚合字段

1. Bucket聚合

Bucket聚合:统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组,根据酒店品牌的名称做聚合。

  1. GET /hotel/_search
  2. {
  3.   "size": 0,  // 设置size为0,结果中不包含文档,只包含聚合结果
  4.   "aggs": { // 定义聚合
  5.     "brandAgg": { //给聚合起个名字
  6.       "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
  7.         "field""brand", // 参与聚合的字段
  8.         "size": 20 // 希望获取的聚合结果数量
  9.       }
  10.     }
  11.   }
  12. }

2. 聚合结果排序

我们可以指定order属性,自定义聚合的排序方式:

  1. GET /hotel/_search
  2. {
  3.   "size": 0, 
  4.   "aggs": {
  5.     "brandAgg": {
  6.       "terms": {
  7.         "field""brand",
  8.         "order": {
  9.           "_count""asc" // 按照_count升序排列
  10.         },
  11.         "size": 20
  12.       }
  13.     }
  14.   }
  15. }

3. 限定聚合范围

默认情况下,Bucket聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。

我们可以限定要聚合的文档范围,只要添加query条件即可:

  1. GET /hotel/_search
  2. {
  3. "query": {
  4. "range": {
  5. "price": {
  6. "lte": 200 // 只对200元以下的文档聚合
  7. }
  8. }
  9. },
  10. "size": 0,
  11. "aggs": {
  12. "brandAgg": {
  13. "terms": {
  14. "field": "brand",
  15. "size": 20
  16. }
  17. }
  18. }
  19. }

3、DSL实现Metrics聚合

1. Metrics聚合

我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。

这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。

语法如下:

  1. GET /hotel/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "brandAgg": {
  6. "terms": {
  7. "field": "brand",
  8. "size": 20
  9. },
  10. "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
  11. "score_stats": { // 聚合名称
  12. "stats": { // 聚合类型,这里stats可以计算min、max、avg等
  13. "field": "score" // 聚合字段,这里是score
  14. }
  15. }
  16. }
  17. }
  18. }
  19. }

2. 聚合结果排序

语法如下:

  1. GET /hotel/_search
  2. {
  3. "size": 0,
  4. "aggs": {
  5. "brandAgg": {
  6. "terms": {
  7. "field": "brand",
  8. "size": 20,
  9. "order": {
  10. "score_stats.avg": "desc" // 根据平均分排序
  11. }
  12. },
  13. "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
  14. "score_stats": { // 聚合名称
  15. "stats": { // 聚合类型,这里stats可以计算min、max、avg等
  16. "field": "score" // 聚合字段,这里是score
  17. }
  18. }
  19. }
  20. }
  21. }
  22. }

八、RestClient实现聚合

1、聚合查询

2、结果解析

  1. public class _01聚合查询 {
  2. private RestHighLevelClient client;
  3. @Test
  4. void aggregation() throws IOException {
  5. // 1.准备Request
  6. SearchRequest request = new SearchRequest("hotel");
  7. // 2.准备DSL
  8. String aggregationName = "brandAgg";
  9. request.source()
  10. // 2.1 设置size(0:清除文档数据)
  11. .size(0)
  12. // 2.2 聚合查询
  13. .aggregation(AggregationBuilders
  14. // 聚合名称
  15. .terms(aggregationName)
  16. // 查询字段
  17. .field("brand")
  18. // 查询数量
  19. .size(10)
  20. );
  21. // 3.发送请求
  22. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  23. // 4.解析响应
  24. handleResponse(response, aggregationName);
  25. }
  26. private void handleResponse(SearchResponse response, String aggregationName) {
  27. // 4.解析响应
  28. Aggregations aggregations = response.getAggregations();
  29. // 4.1.根据聚合名称获取聚合结果
  30. Terms aggregateTerms = aggregations.get(aggregationName);
  31. // 4.2.获取buckets
  32. List<? extends Terms.Bucket> buckets = aggregateTerms.getBuckets();
  33. // 4.3.遍历
  34. for (Terms.Bucket bucket : buckets) {
  35. // 获取key
  36. String key = bucket.getKeyAsString();
  37. System.out.println(key);
  38. }
  39. }
  40. @BeforeEach
  41. void setUp() {
  42. this.client = new RestHighLevelClient(RestClient.builder(
  43. HttpHost.create("http://192.168.116.129:9200")
  44. ));
  45. }
  46. @AfterEach
  47. void tearDown() throws IOException {
  48. this.client.close();
  49. }
  50. }

九、自动补全功能

1、拼音分词器

要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:GitHub - medcl/elasticsearch-analysis-pinyin: This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.

2、自定义分词器

默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。

elasticsearch中分词器(analyzer)的组成包含三部分:

  • character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符

  • tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart

  • tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等

文档分词时会依次由这三部分来处理文档:

声明自定义分词器的语法如下:

  1. PUT /test
  2. {
  3.   "settings": {
  4.     "analysis": {
  5.       "analyzer": { // 自定义分词器
  6.         "my_analyzer": {  // 分词器名称
  7.           "tokenizer""ik_max_word",
  8.           "filter""py"
  9.         }
  10.       },
  11.       "filter": { // 自定义tokenizer filter
  12.         "py": { // 过滤器名称
  13.           "type""pinyin", // 过滤器类型,这里是pinyin
  14. "keep_full_pinyin"false,
  15.           "keep_joined_full_pinyin"true,
  16.           "keep_original"true,
  17.           "limit_first_letter_length": 16,
  18.           "remove_duplicated_term"true,
  19.           "none_chinese_pinyin_tokenize"false
  20.         }
  21.       }
  22.     }
  23.   },
  24.   "mappings": {
  25.     "properties": {
  26.       "name": {
  27.         "type""text",
  28.         "analyzer""my_analyzer",
  29.         "search_analyzer""ik_smart"
  30.       }
  31.     }
  32.   }
  33. }

测试:

总结:

如何使用拼音分词器?

  • 下载pinyin分词器

  • 解压并放到elasticsearch的plugin目录

  • 重启即可

如何自定义分词器?

  • 创建索引库时,在settings中配置,可以包含三部分

  • character filter

  • tokenizer

  • filter

拼音分词器注意事项?

  • 为了避免搜索到同音字,搜索时不要使用拼音分词器

3、自动补全查询

elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:

  • 参与补全查询的字段必须是completion类型。

  • 字段的内容一般是用来补全的多个词条形成的数组。

比如,一个这样的索引库:

  1. // 创建索引库
  2. PUT test
  3. {
  4.   "mappings": {
  5.     "properties": {
  6.       "title":{
  7.         "type""completion"
  8.       }
  9.     }
  10.   }
  11. }

然后插入下面的数据:

  1. // 示例数据
  2. POST test/_doc
  3. {
  4.   "title": ["Sony""WH-1000XM3"]
  5. }
  6. POST test/_doc
  7. {
  8.   "title": ["SK-II""PITERA"]
  9. }
  10. POST test/_doc
  11. {
  12.   "title": ["Nintendo""switch"]
  13. }

查询的DSL语句如下:

  1. // 自动补全查询
  2. GET /test/_search
  3. {
  4.   "suggest": {
  5.     "title_suggest": {
  6.       "text""s", // 关键字
  7.       "completion": {
  8.         "field""title", // 补全查询的字段
  9.         "skip_duplicates"true, // 跳过重复的
  10.         "size": 10 // 获取前10条结果
  11.       }
  12.     }
  13.   }
  14. }

4、搜索框自动补全

现在,我们的hotel索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。

另外,我们需要添加一个字段,用来做自动补全,将brand、suggestion、city等都放进去,作为自动补全的提示。

因此,总结一下,我们需要做的事情包括:

  1. 修改hotel索引库结构,设置自定义拼音分词器

  2. 修改索引库的name、all字段,使用自定义分词器

  3. 索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器

  4. 给HotelDoc类添加suggestion字段,内容包含brand、business

  5. 重新导入数据到hotel库

1. 修改酒店映射结构

  1. // 酒店数据索引库
  2. PUT /hotel
  3. {
  4. "settings": {
  5. "analysis": {
  6. "analyzer": {
  7. "text_anlyzer": {
  8. "tokenizer": "ik_max_word",
  9. "filter": "py"
  10. },
  11. "completion_analyzer": {
  12. "tokenizer": "keyword",
  13. "filter": "py"
  14. }
  15. },
  16. "filter": {
  17. "py": {
  18. "type": "pinyin",
  19. "keep_full_pinyin": false,
  20. "keep_joined_full_pinyin": true,
  21. "keep_original": true,
  22. "limit_first_letter_length": 16,
  23. "remove_duplicated_term": true,
  24. "none_chinese_pinyin_tokenize": false
  25. }
  26. }
  27. }
  28. },
  29. "mappings": {
  30. "properties": {
  31. "id":{
  32. "type": "keyword"
  33. },
  34. "name":{
  35. "type": "text",
  36. "analyzer": "text_anlyzer",
  37. "search_analyzer": "ik_smart",
  38. "copy_to": "all"
  39. },
  40. "address":{
  41. "type": "keyword",
  42. "index": false
  43. },
  44. "price":{
  45. "type": "integer"
  46. },
  47. "score":{
  48. "type": "integer"
  49. },
  50. "brand":{
  51. "type": "keyword",
  52. "copy_to": "all"
  53. },
  54. "city":{
  55. "type": "keyword"
  56. },
  57. "starName":{
  58. "type": "keyword"
  59. },
  60. "business":{
  61. "type": "keyword",
  62. "copy_to": "all"
  63. },
  64. "location":{
  65. "type": "geo_point"
  66. },
  67. "pic":{
  68. "type": "keyword",
  69. "index": false
  70. },
  71. "all":{
  72. "type": "text",
  73. "analyzer": "text_anlyzer",
  74. "search_analyzer": "ik_smart"
  75. },
  76. "suggestion":{
  77. "type": "completion",
  78. "analyzer": "completion_analyzer"
  79. }
  80. }
  81. }
  82. }

2. 修改HotelDoc实体

HotelDoc中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求,最好是这些字段的数组。

因此我们在 HotelDoc中 添加一个 suggestion 字段,类型为List<String>,然后将brand、city、business等信息放到里面。

代码如下:

  1. @Data
  2. @NoArgsConstructor
  3. public class HotelDoc {
  4. private Long id;
  5. private String name;
  6. private String address;
  7. private Integer price;
  8. private Integer score;
  9. private String brand;
  10. private String city;
  11. private String starName;
  12. private String business;
  13. private String location;
  14. private String pic;
  15. private Object distance;
  16. private Boolean isAD;
  17. private List<String> suggestion;
  18. public HotelDoc(Hotel hotel) {
  19. this.id = hotel.getId();
  20. this.name = hotel.getName();
  21. this.address = hotel.getAddress();
  22. this.price = hotel.getPrice();
  23. this.score = hotel.getScore();
  24. this.brand = hotel.getBrand();
  25. this.city = hotel.getCity();
  26. this.starName = hotel.getStarName();
  27. this.business = hotel.getBusiness();
  28. this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
  29. this.pic = hotel.getPic();
  30. // 组装suggestion
  31. if(this.business.contains("/")){
  32. // business有多个值,需要切割
  33. String[] arr = this.business.split("/");
  34. // 添加元素
  35. this.suggestion = new ArrayList<>();
  36. this.suggestion.add(this.brand);
  37. Collections.addAll(this.suggestion, arr);
  38. }else {
  39. this.suggestion = Arrays.asList(this.brand, this.business);
  40. }
  41. }
  42. }

3. 重新导入

重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了suggestion:

4.自动补全查询的JavaAPI

之前我们学习了自动补全查询的DSL,而没有学习对应的JavaAPI,这里给出一个示例:

而自动补全的结果也比较特殊,解析的代码如下:

5.实现搜索框自动补全

查看前端页面,可以发现当我们在输入框键入时,前端会发起ajax请求:

返回值是补全词条的集合,类型为List<String>

1)在 HotelController中添加新接口,接收新的请求:

  1. @GetMapping("suggestion")
  2. public List<String> getSuggestions(@RequestParam("key") String prefix) {
  3. return hotelService.getSuggestions(prefix);
  4. }

2)在IhotelService中添加方法:

List<String> getSuggestions(String prefix);

3)在 HotelService中实现该方法:

  1. @Override
  2. public List<String> getSuggestions(String prefix) {
  3. try {
  4. // 1.准备Request
  5. SearchRequest request = new SearchRequest("hotel");
  6. // 2.准备DSL
  7. request.source().suggest(new SuggestBuilder().addSuggestion(
  8. "suggestions",
  9. SuggestBuilders.completionSuggestion("suggestion")
  10. .prefix(prefix)
  11. .skipDuplicates(true)
  12. .size(10)
  13. ));
  14. // 3.发起请求
  15. SearchResponse response = client.search(request, RequestOptions.DEFAULT);
  16. // 4.解析结果
  17. Suggest suggest = response.getSuggest();
  18. // 4.1.根据补全查询名称,获取补全结果
  19. CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
  20. // 4.2.获取options
  21. List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
  22. // 4.3.遍历
  23. List<String> list = new ArrayList<>(options.size());
  24. for (CompletionSuggestion.Entry.Option option : options) {
  25. String text = option.getText().toString();
  26. list.add(text);
  27. }
  28. return list;
  29. } catch (IOException e) {
  30. throw new RuntimeException(e);
  31. }
  32. }

十、数据同步

elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步

常见的数据同步方案有三种:

  • 同步调用

    • 优点:实现简单,粗暴

    • 缺点:业务耦合度高

  • 异步通知

    • 优点:低耦合,实现难度一般

    • 缺点:依赖mq的可靠性

  • 监听binlog

    • 优点:完全解除服务间耦合

    • 缺点:开启binlog增加数据库负担、实现复杂度高

1、同步调用

2、异步通知

3、监听binlog

4、利用MQ实现数据同步

1. 导入项目

导入课前资料提供的hotel-admin项目:

运行后,访问 http://localhost:8099

其中包含了酒店的CRUD功能:

2.声明交换机、队列

MQ结构如图:

1)引入依赖

在hotel-admin、hotel-demo中引入rabbitmq的依赖:

  1. <!--amqp-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-amqp</artifactId>
  5. </dependency>
2)声明队列交换机名称

在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts包下新建一个类MqConstants

  1. package cn.itcast.hotel.constatnts;
  2. public class MqConstants {
  3. /**
  4. * 交换机
  5. */
  6. public final static String HOTEL_EXCHANGE = "hotel.topic";
  7. /**
  8. * 监听新增和修改的队列
  9. */
  10. public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
  11. /**
  12. * 监听删除的队列
  13. */
  14. public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
  15. /**
  16. * 新增或修改的RoutingKey
  17. */
  18. public final static String HOTEL_INSERT_KEY = "hotel.insert";
  19. /**
  20. * 删除的RoutingKey
  21. */
  22. public final static String HOTEL_DELETE_KEY = "hotel.delete";
  23. }
3)声明队列交换机

在hotel-demo中,定义配置类,声明队列、交换机:

  1. @Configuration
  2. public class MqConfig {
  3. @Bean
  4. public TopicExchange topicExchange(){
  5. return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
  6. }
  7. @Bean
  8. public Queue insertQueue(){
  9. return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
  10. }
  11. @Bean
  12. public Queue deleteQueue(){
  13. return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
  14. }
  15. @Bean
  16. public Binding insertQueueBinding(){
  17. return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
  18. }
  19. @Bean
  20. public Binding deleteQueueBinding(){
  21. return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
  22. }
  23. }

3.发送MQ消息

在hotel-admin中的增、删、改业务中分别发送MQ消息:

4.接收MQ消息

hotel-demo接收到MQ消息要做的事情包括:

  • 新增消息:根据传递的hotel的id查询hotel信息,然后新增一条数据到索引库

  • 删除消息:根据传递的hotel的id删除索引库中的一条数据

1)首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、删除业务

  1. void deleteById(Long id);
  2. void insertById(Long id);

2)给hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中实现业务:

  1. @Override
  2. public void deleteById(Long id) {
  3. try {
  4. // 1.准备Request
  5. DeleteRequest request = new DeleteRequest("hotel", id.toString());
  6. // 2.发送请求
  7. client.delete(request, RequestOptions.DEFAULT);
  8. } catch (IOException e) {
  9. throw new RuntimeException(e);
  10. }
  11. }
  12. @Override
  13. public void insertById(Long id) {
  14. try {
  15. // 0.根据id查询酒店数据
  16. Hotel hotel = getById(id);
  17. // 转换为文档类型
  18. HotelDoc hotelDoc = new HotelDoc(hotel);
  19. // 1.准备Request对象
  20. IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
  21. // 2.准备Json文档
  22. request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
  23. // 3.发送请求
  24. client.index(request, RequestOptions.DEFAULT);
  25. } catch (IOException e) {
  26. throw new RuntimeException(e);
  27. }
  28. }

3)编写监听器

在hotel-demo中的cn.itcast.hotel.mq包新增一个类:

  1. package cn.itcast.hotel.mq;
  2.  ​
  3.  import cn.itcast.hotel.constants.MqConstants;
  4.  import cn.itcast.hotel.service.IHotelService;
  5.  import org.springframework.amqp.rabbit.annotation.RabbitListener;
  6.  import org.springframework.beans.factory.annotation.Autowired;
  7.  import org.springframework.stereotype.Component;
  8.  ​
  9.  @Component
  10.  public class HotelListener {
  11.  ​
  12.      @Autowired
  13.      private IHotelService hotelService;
  14.  ​
  15.      /**
  16.       * 监听酒店新增或修改的业务
  17.       * @param id 酒店id
  18.       */
  19.      @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
  20.      public void listenHotelInsertOrUpdate(Long id){
  21.          hotelService.insertById(id);
  22.     }
  23.  ​
  24.      /**
  25.       * 监听酒店删除的业务
  26.       * @param id 酒店id
  27.       */
  28.      @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
  29.      public void listenHotelDelete(Long id){
  30.          hotelService.deleteById(id);
  31.     }
  32.  }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/819935
推荐阅读
相关标签
  

闽ICP备14008679号