一、ElasticSearch 简介
1.什么是 ElasticSearch?
Elaticsearch,简称为es, es是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。es也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。
什么是 Lucene?
ES 是在之前的 Lucene 的基础上进行了封装,就像 Mybatis 对 jdbc 进行了封装一样,通过 Restful API,简化了对数据的各种操作
什么是全文检索引擎?
当我们在用 ES 搜索数据的时候,不管数据是什么样的数据,文本也好,word 文档也好,还是页面,都能够被搜索到
2.数据的分类
我们生活中数据总体分为两种:结构化数据与非结构化数据
结构化数据:指具有固定格式或有限长度的数据,如数据库、元数据等;
非结构化数据:指不定长或无固定格式的数据,如邮件、word 文档等磁盘上的文件;
结构化数据搜索
常见的结构化数据也就是数据库中的数据,在数据库中所搜很容易实现,通常使用 sql 进行搜索,而且很容易得到查询结果;
为什么数据库搜索很容易?
因为数据库中的数据是有规律的,有行有列,且数据格式、数据长度都是固定的。
非结构化数据搜索
顺序扫描法
所谓顺序扫描法,就是对磁盘上的所有文件进行顺序扫描,一个文件一个文件的扫描,每个文件都从头扫描到尾,来查询文件中是否有我们需要搜索的内容,这种查询方式相当的慢!!!
全文检索
首先我们介绍一下索引的概念:
将非结构化数据的中的一部分提取出来,重新组织成有一定结构的数据,然后对此有一定结构的数据进行检索,从而达到搜索相对较快的目的。这部分从非结构数据中提取出来然后重新组织的信息,我们就称之为索引;
例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。
这种先建立索引,再对索引进行搜索的过程就叫全文检索
虽然索引的创建非常耗时,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以消耗时间创建索引是值得的。
3.全文检索
我们可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开源的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
而 ES 正是对 Lucene 进行了封装,简化了操作
全文检索的应用场景
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如:百度、Google等搜索引擎、论坛站内搜索、电商网站站内搜索等。
全文检索的实现流程
1、绿色表示索引(建立索引)的过程,对原始内容进行索引建立索引库,索引的过程包括:
确定原始内容:我们要搜索的内容
采集文档
创建文档
分析文档
索引文档
2、红色表示搜索过程,从索引库中搜索内容,搜索包括:
用户通过搜索界面,创建查询(输入关键词)
执行搜索,从索引库中搜索
渲染搜索结果,返回给用户
总之:全文检索分为两个过程:创建索引(Indexing)和搜索索引(Search)
3.1.什么是索引?
文档的索引过程:将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。
那么索引中到底需要存放什么东西呢?
首先我们还是从顺序扫描说起
顺序扫描速度之所以慢,是因为我们不知道每个文件里大致包含哪些东西(关键词),用户输入关键词,我们需要遍历每一个文件,扫描每一个文件才能,返回具有该关键词的文件;
这时我们不妨反过来想,如果我们已经知道了每个文件大致包含了哪些内容(关键词),而且这些关键词都引用了相关的文件,就像 Map 一样,每个 key 都关联了一个或多个 value,这时用户再通过关键词搜索,我们就可以直接给出用户想要的文件;
之后讲到 正排索引和倒排索引 的时候会更清楚的了解
所以我们要建的索引就是每个文件中包含的大致内容,即关键词
3.2.创建文档对象
获取原始内容的目的就是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容;
这里的原始内容就是我们想要搜索的一切信息
这里我们可以将磁盘上的一个文件当成一个 document,Document 中包括了一些 Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容)。Document 是对原始内容的一个描述对象
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
每个文档都有一个唯一的编号,就是文档id。主要用来建立索引与文档之间的映射
3.3.分析文档
上面我们将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析
3.4.创建索引
非结构化数据中所存储的信息是每个文件包含哪些关键词,即已知文件,欲求关键词相对容易,也即是从文件到关键词的映射。【正排索引】
但是对于顺序扫描而言,是用户每次搜索,都要从头到尾遍历一遍,这样在数据量庞大时就会特别慢
而我们想搜索的信息是哪些文件包含此关键词,也即已知关键词,欲求文件,也即从关键词到文件的映射。【倒排索引】
两者恰恰相反。于是如果索引总能够保存从关键词到文件的映射,则会大大提高搜索速度。
强调:我们每一次增删改原始内容的时候,都要重新建立一次索引,当然不一定是立刻就建立索引,我们可以定时建立,每天或每12小时建立一次索引
正排索引:
倒排索引:
说明:
“单词ID”一栏记录了每个单词的单词编号;
第二栏是对应的单词;
第三栏即每个单词对应的倒排列表;
比如单词“谷歌”,其单词编号为1,倒排列表为{1,2,3,4,5},说明文档集合中每个文档都包含了这个单词。
强调:倒排列表中还可以存放更多信息来辅助搜索。比如说:我们可以在倒排列表中存储一个关键词在原始内容中出现的次数字段,如下
这样我们就可以通过在关键词出现的频率进行排序
同时我们也可以存储一个文件类型字段(页面,图片...)来辅助我们进行筛选
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
3.5.查询索引
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引库(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件)。
3.6.用户查询接口
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。
比如:
3.7.创建查询
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的 Field 文档域(比如说查询的是页面、图片、word...还是 ppt 等等)、查询关键字等,查询对象会生成具体的查询语法。
3.8.执行查询
搜索索引过程:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
3.9.渲染结果
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
4.Elasticsearch 与 Mysql 的区别
4.1.响应时间
MySQL
背景:当数据库中的文档数仅仅上万条时,关键词查询就比较慢了。如果一旦到企业级的数据,响应速度就会更加不可接受。
原因:在数据库做模糊查询时,如LIKE语句,它会遍历整张表,同时进行字符串匹配。
例如,在数据库查询“手机”时,数据库会在每一条记录去匹配“手机”这两字是否出现。实际上,并不是所有记录都包含“手机”,所以做了很多无用功。这个步骤并不高效,而且随着数据量的增大,消耗的资源和时间都会线性的增长。
Elasticsearch
提升:使用 ES 搜索服务后,这个问题被很好解决,TB 级数据在毫秒级就能返回检索结果。
原因:Elasticsearch 是基于倒排索引的,例子如下。
当搜索“手机”时,Elasticsearch 就会立即返回文档 F,G,H。这样就不用花多余的时间在其他文档上了,因此检索速度得到了数量级的提升。
4.2.分词
MySQL
背景:在做中文搜索时,组合词检索在数据库是很难完成的。
例如:当用户在搜索框输入“四川火锅”时,数据库通常只能把这四个字去进行全部匹配。可是在文本中,可能会出现“推荐四川好吃的火锅”,这时候就没有结果了。
原因:数据库并不支持分词。如果人工去开发分词功能,费时费精力。
Elasticsearch
提升:使用ES搜索服务后,就不用太过于关注分词了,因为 Elasticsearch 支持中文分词插件,很好地解决了问题。
原因:当用户使用 Elasticsearch 时进行搜索时,Elasticsearch 就自动帮他分好词了。
例如 输入“四川火锅”时,Elasticsearch 会自动做下面两件事
将“四川火锅”分词成“四川”和“火锅”
查找包含这两个词的文档
4.3.相关性
MySQL
背景:在用数据库做搜索时,结果经常会出现一系列不匹配的文档。比如说:
没有返回用户想要的文档
怎么才能把用户想要的文档排序在最前面?
原因:数据库并不支持相关性搜索。
例如:当用户搜索“咖啡厅”的时候,他很可能更想知道附近哪里可以喝咖啡,而不是怎么开咖啡厅。
Elasticsearch
提升:Elasticsearch 能很好地支持相关性评分。通过合理的优化,ES 搜索服务能够返回精准的结果,满足用户的需求。
原因:Elasticsearch 支持全文搜索和相关度评分。这样在返回结果就会根据分数由高到低排列。分数越高,意味着和查询语句越相关。
例如:当用户搜索“星巴克咖啡”,带有“星巴克咖啡”的信息就要比只包含“咖啡”的信息靠前。
总结
传统数据库在全文检索方面很鸡肋,海量数据下的查询很慢,对非结构化文本数据的不支持,ES 支持非结构化数据的存储和查询。
ES 支持分布式文档存储。
ES 是分布式实时搜索,并且响应时间比关系型数据库快。
ES 在分词方面比关系型好,能做到精确分词。
ES 对已有的数据,在数据匹配性方面比关系型数据库好
例如:搜索“星巴克咖啡”,ES 会优先返回带有“星巴克咖啡”的数据,不会优先返回带有“咖啡”的数据。
ElasticSearch 和 MySql 分工不同,MySQL 负责存储数据,ElasticSearch 负责搜索数据。
二、ElasticSearch 安装与启动
下面所有的工具包都在这里
提取码:syhn
1.Windows 版安装
ElasticSearch分为Linux和Window版本
ElasticSearch的官方地址: https://www.elastic.co/products/elasticsearch
说明:ElasticSearch 全版本支持 OpenJDK1.8,如果你的 JDK 不是 1.8 版本,或者不是 OpenJDK 的则需要注意版本兼容
当然我们下载的 ElasticSearch 中包含了一个 jdk ,如果你的 jdk 版本不兼容,可以直接将你 JAVA_HOME 变量改为 ElasticSearch 中的 jdk 目录即可(建议备份原理的 JAVA_HOME,因为你之前的项目都是用的原来的)
安装 ES 服务
Window 版的 ElasticSearch 的安装很简单,解压开即安装完毕,解压后的ElasticSearch的目录结构如下:
安装IK分词器插件:在plugin目录下创建ik文件夹,将 elasticsearch-analysis-ik-7.4.0.zip 内容解压到ik目录下,即可
2.启动 ES 服务
点击 ElasticSearch 下的 bin 目录下的 elasticsearch.bat 启动,控制台显示的日志信息如下:
注意:9300是 tcp 通讯端口,集群间和 TCPClient 都执行该端口,9200 是 http 协议的 RESTful 接口 。
我们通过浏览器访问:http://localhost:9200/,即可
注意事项一:ElasticSearch 是使用 java 开发的,且本版本的 es 需要的 jdk 版本要是 1.8 以上,所以安装 ElasticSearch 之前保证 JDK1.8+ 安装完毕,并正确的配置好 JDK 环境变量,否则启动 ElasticSearch 失败。
注意事项二:出现闪退,通过路径访问发现“空间不足”
修改 conf/jvm.options 文件的22行23行, Elasticsearch 启动的时候占用1个G的内存,可改成 512m:
-Xmx512m:设置 JVM 最大可用内存为 512M。
-Xms512m:设置 JVM 初始内存为 512m。此值可以设置与 -Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。
3.Postman 安装
省略...这里推荐给大家一个在线的 Postman
4.Kibana客户端(Windows版)
Kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 Elasticsearch 索引中的数据。
解压 kibana-7.4.0-windows-x86_64.zip,即可
进入config目录修改kibana.yml第2、28行,配置自身端口和连接的ES服务器地址。把配置打开即可,
server.port: 5601
elasticsearch.hosts: [" http://localhost:9200 "]
这里的hosts,填自己的 es 地址,安装在本地就配置 localhost
进入kibana的bin目录,双击kibana.bat启动
访问:http://localhost:5601,即可;
注:这里的界面是英文的,我们可以将 config 下的 yml 配置文件的最后一行 i18n 国际化改为 zh-CN,即可变为中文的;
5.Elasticsearch head 客户端
head插件是ES的一个可视化管理插件,用来监视ES的状态,并通过head客户端和ES服务进行交互,比如创建映射、创建索引等。
将 ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx 用压缩工具解压,打开 Chrome 扩展程序,点” 加载已解压的扩展程序”按钮,找到解压目录即可。
或者 直接将 ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx 拖到扩展程序窗口也可以
6.IK 分词器的使用
介绍
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始,IKAnalyzer已经推出 了3个大版本。最初,它是以开源项目Lucene为应用主体的,结合词典分词和文法分析算法的中文分词组件。新版本的IKAnalyzer3.0则发展为 面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。
下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases
特点
采用了特有的“正向迭代最细粒度切分算法“,具有60万字/秒的高速处理能力。
采用了多子处理器分析模式,支持:英文字母(IP地址、Email、URL)、数字(日期,常用中文数量词,罗马数字,科学计数法),中文词汇(姓名、地名处理)等分词处理。
对中英联合支持不是很好,在这方面的处理比较麻烦.需再做一次查询,同时是支持个人词条的优化的词典存储,更小的内存占用。
支持用户词典扩展定义。
针对Lucene全文检索优化的查询分析器IKQueryParser;采用歧义分析算法优化查询关键字的搜索排列组合,能极大的提高Lucene检索的命中率。
分词器的三种分词方式:standard、ik_max_word、ik_smart
standard:默认的分词方式,一个字一个词
ik_max_word:会将文本做最细粒度的拆分
ik_smart:做粗粒度的拆分
standard:
ik_max_word:
ik_smart:
三、ElasticSearch 相关概念(术语)
1.概述
先说 Elasticsearch 的文件存储,Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档,用 JSON 作为文档序列化的格式,比如下面这条用户数据;
文件存储:一个 ElasticSearch 就是一个用来进行文件存储的服务器
文档型数据库:ElasticSearch 是一个面向文档型数据库软件,一条数据就是一个文档,而在关系型数据库中,一条数据就是一行(关系型数据库中,有行列结构划分)
JSON:上面说的一条数据就是一个文档,其实就是一个 JSON 字符串,ElasticSearch 中存储的都是 JSON 格式的字符串
{"name":"jack","sex":"Male","age":25,"birthDate":"1990/05/01","about":"I love to go rock climbing","interests":["sports","music"]}
Elasticsearch 可以看成是一个数据库,只是和关系型数据库比起来数据格式和功能不一样而已
关系型数据库中的一个库 <==> ElasticSearch 中的一个索引(库)
关系型数据库中的一个表 <==> 就对应 ElasticSearch 中的一个类型。
例如:在关系型数据库中建立一张 User 表,就相当于在 ElasticSearch 中建立了一个 User 类型,类似于 Java 中的一个Class 类;
关系型数据库中的一行(一条数据) <==> ElasticSearch 中的一个文档。
例如关系型数据库中 User 表中的一条记录,就相当于 ElasticSearch 中的一条 JSON 串,类似于 Java 中的一个 Object 实例;
在数据库中的索引是一个 Tree 结构的树,而在 ElasticSearch 中的索引是一个倒排索引;
2.ElasticSearch 核心概念
索引(index):文档存储的地方,可以理解为索引【库】,一个数据库;
类型(type):在 ES6.0 之前有 类型的概念,之后就被抛弃了;
因为 ES 是全文检索,不关心文档的类型,只要包含关键词的都搜索出来,然后再根据相关性进行排序;
字段(field):相当于 MySQL 中的字段,在 ES 中可以理解为 JSON 中的键;
映射(mapping)
映射 是对文档中每个字段的类型进行定义,每一种数据类型都有对应的使用场景。
每个文档都有映射,但是在大多数使用场景中,我们并不需要显示的创建映射,因为ES中实现了动态映射。
我们在索引中写入一个下面的 JSON 文档,在动态映射的作用下,name 会映射成 text 类型,age 会映射成 long 类型。
{"name":"jack","age":18,}
自动判断的规则如下:
Elasticsearch中支持的类型如下:
string 类型:在 ElasticSearch 旧版本中使用较多,从 ElasticSearch 5.x 开始不再支持 string,由 text 和 keyword 类型替代。(已经废弃)
text 类型 :Email 内容、产品描述,应该使用 text 类型,text 类型可以被分词。
keyword 类型:比如email地址、主机名、状态码和标签。keyword 类型不支持分词。
示例:
文档(document)
文档在 ES 中相当于传统数据库中的行的概念,ES 中的数据都以 JSON 的形式来表示,在 MySQL 中插入一行数据和 ES 中插入一个 JSON 文档是一个意思。下面的 JSON 数据表示,一个包含3个字段的文档。
{"name":"jack","age":18,"gender":1}
一个文档不只有数据。它还包含了元数据(metadata)——关于文档的信息。三个必须的元数据节点是:
节点 | 说明 |
_index | 文档存储的地方 |
_type | 文档代表的对象的类 |
_id | 文档的唯一标识 |
四、ElasticSearch 客户端操作
实际开发中,主要有三种方式可以作为 elasticsearch 服务的客户端:
第一种,elasticsearch-head 插件
第二种,使用elasticsearch 提供的 Restful 接口直接访问
第三种,使用elasticsearch 提供的 API 进行访问
ElasticSearch 接口支持 Restful 风格的语法
1.Postman 演示(对索引库的操作)
命令 | 请求 |
创建索引 | PUT http://localhost:9200/blog 注:这里重复创建索引会报错 |
查看索引 | |
删除索引 | DELETE http://localhost:9200/blog |
关闭索引 | |
打开索引 | |
创建索引并进行映射 | PUT http://localhost:9200/blog 需要携带请求体 |
创建索引
PUT http://localhost:9200/blog 不携带请求体
响应体:
{
"acknowledged":true, // 表示创建索引库成功
"shards_acknowledged":true, // 表示分片页也成功了
"index":"blog" // 索引库的名称为 blog}
这时我们在 Kibana/可视化/索引管理 中就可以看见 blog 这个索引库了
在 elasticsearch-head 中也可以查看到索引库的基本信息
创建索引并进行映射
PUT localhost:9200/blog1
请求体
{"mappings":{"properties":{"id":{"type":"long", // 表示 id 是 long 类型的字段"store":true, // store: 表示 id 这个字段是否和索引库保存到同一个文件中"index":true // index: 表示这个字段是否支持索引},"title":{"type":"text","store":true,"index":true,"analyzer":"standard" // analyzer: 表示该字段支持什么样的分词操作,默认是 standard },"content":{"type":"text","store":true,"index":true,"analyzer":"standard"}}}}
2.Kibana 演示
1.操作索引
命令 | 请求 |
创建索引 | PUT person |
查询索引 | GET person |
删除索引 | DELETE person |
查询映射 | GET person/_mapping |
添加映射 | 需要携带请求体 |
添加映射
PUT person/_mapping
{"properties":{"name":{"type":"keyword"},"age":{"type":"integer"}}}
创建索引并添加映射
PUT person
{"mappings":{"properties":{"name":{"type":"keyword"},"age":{"type":"integer"}}}}
在索引库中添加字段
PUT person/_mapping
{"properties":{"address":{"type":"text"}}}
2.操作文档
添加/查询/修改/删除文档
// 先查询映射,根据映射添加文档
GET person
// 1.添加文档并指定 id
PUT person/_doc/1{
"name:"张三",
"age":20,
"address":"深圳宝安市"
}
// 查询我们刚刚添加的文档
GET person/_doc/1
// 添加文档不指定 id
POST person/_doc
{
"name": "阿一",
"age": 22,
"address": "河北省保定市"
}
// 不指定 id,系统会为我们自动生成一个 id
// 查询刚刚添加的文档
GET person/_doc/wchwIX0BkGnXZXJzYj7q
// 查询所有文档
GET person/_search
// 删除文档
// 注:这里的删除只是逻辑删除,只是将文档的状态标识为 delete,
// ElasticSearch 会在每次整理数据的时候,将状态为 delete 的文档物理删除
DELETE person/_doc/1
// 修改文档,根据 id,id 存在就是修改,不存在就是添加
PUT person/_doc/1
{
"name":"尚硅谷",
"age":12,
"address":"北京"
}
1.全文查询-match 查询
全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集
GET person/_search
{"query":{"match":{"address":"深圳保定市"}}}
查询结果
由于我们之前没有指定分词的模式,所以这里将 address 按每个字分为一个词条,即倒排索引为 “深,圳,市,宝,安,市,保,定”,所以当我们进行全文查询的时候,会将查询条件进行分词 “深,圳,保,定,市”,符合条件了会被查询到
"hits":[{"_index":"person", // 表示查询的哪个索引// 文档的类型,之前说过 ES6.0 之后就不提类型了,这里所有的文档都为_doc 类型"_type":"_doc",
"_id":"1","_score":2.354555, // 评分,ES 会自动进行相关度计算,给出一个评分"_source":{"name":"尚硅谷","age":12,"address":"深圳市宝安市"}},{"_index":"person","_type":"_doc","_id":"wchwIX0BkGnXZXJzYj7q","_score":2.1771858,"_source":{"name":"阿一","age":22,"address":"河北省保定市"}}]
查询全部文档,并分页
GET person/_search
{"query":{"match_all":{}},"from":0,"size":10}
2.词条查询-term 查询
词条查询不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配搜索
GET person/_search
{"query":{"term":{"address":{"value":"深圳南山区"}}}}
查询结果
这个结果和我们使用的分词器有关,我们之前没有指定分词模式,就默认是 standard,那么我们的倒排索引中每个字就是一个词,而 term 查询不会将查询条件进行分词,我们的倒排索引中又没有“深圳南山区”这个词条,就查询不出任何结果
{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}
3.关键字搜索
person 索引库中存储的数据
// 查询用户 name 为“阿一”的数据// 注:name 字段的类型为 keyword
GET person/_search/q=name:阿一 // 结果能正确查询// 查询用户 name 为“阿”的数据
GET person/_search/q=name:阿 // 结果为 null,说明类型为 keyword 不会进行分词// 查询 address 为“河北省”的数据
GET person/_search/q=address=河北省
// 这里的结果为 “河北省保定市”,“北京”// 因为 address 类型为 text,且默认分词为 standard
注:如果字段类型是 “text” 的会对查询条件进行分词操作,如果是 “keyword”或其他数字类型则不会进行分词操作
3.DSL 查询
就类似与数据库的 SQL 查询
// 查询年龄为 20 的数据
POST person/_search
{"query":{"match":{"age":20}}}// 查询年龄 大于等于 18 小于 25 的数据
POST person/_search
{"query":{"bool":{"filter":{"range":{"age":{"gte":18,"lt":25}}}}}}
4.高亮显示
我们在百度搜索的时候,我们查询的关键字会被高亮显示
实际上就是我们在用 ES 查询时,如果要求高亮显示,它在返回数据的时候就会将匹配的词条用 <em></em> 包裹;
// 我们查询 address 为“河北省”的数据,并让其高亮显示
GET person/_search
{"query":{"match":{"address":"河北省"}},"highlight":{"fields":{"address":{}}}}
我们可以看到由于我们使用默认的分词模式 standard,所以 ES 将匹配到的每一个字都用 <em></em> 包裹了
5.聚合(Group by)
在Elasticsearch中,支持聚合操作,类似SQL中的group by操作。
注意:如果我们对 text 类型的字段进行聚合会报错,这时我们就要在字段后面加上 keyword 属性
GET person/_search
{"aggs":{ // aggs: 聚合单词的缩写"all_interests":{ //这个单词我们随便定义的,就像 MySQL 里的取别名一样"terms":{"field":"age"// 如果要对 text字段进行聚合,就要如下写法"field":"address.keyword"}}}}
聚合结果
6.指定响应字段
在响应的数据中,如果我们不需要全部的字段,可以指定某些需要的字段进行返回
GET person/_doc/1?_source=id,name
等价于
GET person/_search
{"query":{"match":{"id":"1"}},"_source":["id","name"]}
7.判断文档是否存在
如果我们只需要判断文档是否存在,而不是查询文档内容,那么可以这样:
HEAD person/_doc/1
存在返回:200 - OK
不存在返回:404 – Not Found
8.批量操作
批量查询
POST person/_mget
{"ids":["1001","1003"]}
_bulk 操作
在Elasticsearch中,支持批量的插入、修改、删除操作,都是通过_bulk的api完成的。请求的格式如下:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
批量插入数据
POST _bulk
{"create":{"_index":"person","_id":3}} // 注意这一部分不能有任何换行{"name":"zs","age":18,"address":"zh-CN"} // 这一部分也不能有任何换行{"create":{"_index":"person","_id":4}}{"name":"ls","age":19,"address":"en"}
批量插入数据(index)
POST _bulk
{"index":{"_index":"person"}} // 注意这一部分不能有任何换行{"name":"ayi1","age":18,"address":"zh-CN"} // 这一部分也不能有任何换行{"create":{"_index":"person"}}{"name":"ayi2","age":19,"address":"en"}
注意我们在批量插入的时候 index 和 create 的区别
index 和 create 都会检查 _version 版本号。
index 插入时分两种情况:
没有指定 _version,那对于已经存在的 doc,_version 会递增,并对文档进行覆盖
指定 _version,如果与已经存在的文档 _version 不相等,则插入失败;如果相等则覆盖,_version 递增
create 插入时对于已经存在的文档,不会覆盖或创建新文档,而是会抛出异常
同时进行插入、修改、删除
POST _bulk
{"create":{"_index":"person","_id":5}}{"name":"ww","age":20,"address":"zh-CN"}{"update":{"_index":"person","_id":1}}{"doc":{"name":"update","age":99,"address":"zh"}}{"delete":{"_index":"person","_id":2}}
9.分页
和 MySQL 的 limit 一样,ES 也支持分页,接受 from 和 size 参数
size: 默认为10
from: 默认为0
请求
GET person/_search?size=2&from=1
查询全部文档,并分页
GET person/_search
{"query":{"match_all":{}},"from":0,"size":10}
10.terms 查询
和 term 作用类似,允许指定多个匹配条件,会将所有匹配返回
// 查询年龄是 18 或 19 岁的数据
GET person/_search
{"query":{"terms":{"age":[18,19]}}}
11.range 范围查询
range允许我们按照指定范围查找一批数据
gt: 大于
gte: 大于等于
lt: 小于
lte: 小于等于
// 查询年龄大于等于 18,小于等于 20 的数据
POST person/_search
{"query":{"range":{"age":{"gte":18,"lte":20}}}}
12.exists 查询
exists 查询可以用于查找文档中某个字段值是否为空
// 查询不为 null 的
GET person/_search
{"query":{"exists":{"field":"address"}}}// 查询字段为 null 的
GET person/_search
{"query":{"bool":{"must_not":[{"exists":{"field":"address"}}]}}}
五、ElasticSearch 集群
1.单点故障问题
单台服务器,往往都有最大的负载能力,超过了这个阈值,服务器的性能往往就会大大降低,甚至不可用。单点的 ES 也是一样,单点的 ES 服务器具有很多缺陷:
单台机器的存储容量有限
单台服务器容易出现单点故障,不能实现高可用
单台服务器的并发处理能力有限
因此,我们需要搭建 ES 的集群
集群中的节点数量没有限制,大于等于 2 个节点就可以看做是集群了,但是为了实现高可用,我们往往会搭建 3 个以上的节点
2.集群的相关概念
集群 cluster
一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是 “elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
节点 node
一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 ElasticSearch 集群中的哪些节点。
一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做 “elasticsearch” 的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做 “elasticsearch” 的集群中。
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做 “elasticsearch” 的集群。
分片和复制 shard & replicas
什么是分片?
ES 提供了将一个索引[库]划分成多份的能力,像这样的每一份就是一个分片
当我们在创建一个索引的时候,可以指定自己想要的分片数量。但是一旦我们创建了索引,就无法在修改分片数量
每一个分片本身也是一个功能完善的 “索引”,这个 “索引” 可以被放置到集群中的任何一个节点上。
为什么要分片?
分片前的缺点
一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间
单个节点处理搜索请求,响应太慢。
分片的作用
允许我们水平分割或扩展我们的内容容量
运行我们在分片,也就是多个节点之间进行分布式、并行操作,提高性能和吞吐量
至于一个分片怎样分布,多个分片上返回的结果怎么聚合回搜索请求,是完全由 ES 管理的,对于用户来说,完全是透明的。
什么是复制?
在一个网络/云环境中,某个分片/节点可能随时宕机,为了避免这种情况,就出现了复制。ES 允许你创建分片的一份或多份的拷贝,必要时这些拷贝就可以代替分片工作,这些拷贝就叫复制分片(副本)
分片和复制就类似 Redis 中的主机和从机之间的关系,主机宕机了,从机就会上位,代替主机正常工作;主机和从机也能同时进行并发操作,提高性能
复制的作用
在分片/节点失败的情况下,提供高可用。复制分片从不与主分片位于同一节点上,至于怎么实现完全是透明的
2.提高搜索量和吞吐量,搜索可以在所有复制分片上并行运行。
总之
每个索引可以有多个分片。
一个索引可以被复制 0 次或多次。
一旦复制了,每个索引就有了主分片和复制分片之分了
分片和复制的数量可以在创建索引的时候指定;之后可以动态改变复制的数量,但不能改变分片的数量
默认情况下:
ES6.x 中的每个索引被分片 5 个主分片和 1 个复制。这意味着,如果你的集群中有两个节点,你的索引将会有 5 个主分片和 5 个复制分片(相当于 1 个对 5 个主分片的完全拷贝),这样每个索引就一共有 10 个分片;
而在 ES7.x 中每个索引被分片 1 个主分片 和 1 个复制。同理,每个索引就一共有 2 个分片
3.集群搭建
先准备三台 ES 服务器
注:这三台服务器一定要是全新的,因为如果服务器中存在数据的话就无法成功搭建
修改每台服务器的配置
修改cluster\node*\config\elasticsearch.yml配置文件
node1 节点
#节点1的配置信息:
#集群名称,保证唯一,一个节点想要加入某个集群必须指定集群名称
cluster.name: my-elasticsearch
#默认为true。设置为 false 禁用磁盘分配决定器。
#这是啥意思呢?就是 ES 默认会帮我们管理磁盘,如果我们的磁盘占用达到高水位(磁盘剩余空间少了),ES就无法成功启动,或者直接宕机,所以我们要设置为 false
cluster.routing.allocation.disk.threshold_enabled: false
#当前节点名称,必须不一样
node.name: node-1
#必须为本机的ip地址;
#如果你是在本地搭建的三台服务器,就都写 localhost;如果分布在不同主机,就分别写自己所在主机的 ip(三台主机必须能相互通信)
network.host: 127.0.0.1
#服务端口号,在同一机器下必须不一样
http.port: 9201
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9301
#设置集群自动发现机器ip集合
#ES6.x
#discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#ES7.x
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1"]
node2 节点
#节点2的配置信息:
#集群名称,保证唯一,一个节点想要加入某个集群必须指定集群名称
cluster.name: my-elasticsearch
#默认为true。设置为 false 禁用磁盘分配决定器。
#这是啥意思呢?就是 ES 默认会帮我们管理磁盘,如果我们的磁盘占用达到高水位(磁盘剩余空间少了),ES就无法成功启动,或者直接宕机,所以我们要设置为 false
cluster.routing.allocation.disk.threshold_enabled: false
#当前节点名称,必须不一样
node.name: node-2
#必须为本机的ip地址;
#如果你是在本地搭建的三台服务器,就都写 localhost;如果分布在不同主机,就分别写自己所在主机的 ip(三台主机必须能相互通信)
network.host: 127.0.0.1
#服务端口号,在同一机器下必须不一样
http.port: 9202
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9302
#设置集群自动发现机器ip集合
#ES6.x
#discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#ES7.x
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1"]
node3 节点
#节点2的配置信息:
#集群名称,保证唯一,一个节点想要加入某个集群必须指定集群名称
cluster.name: my-elasticsearch
#默认为true。设置为 false 禁用磁盘分配决定器。
#这是啥意思呢?就是 ES 默认会帮我们管理磁盘,如果我们的磁盘占用达到高水位(磁盘剩余空间少了),ES就无法成功启动,或者直接宕机,所以我们要设置为 false
cluster.routing.allocation.disk.threshold_enabled: false
#当前节点名称,必须不一样
node.name: node-3
#必须为本机的ip地址;
#如果你是在本地搭建的三台服务器,就都写 localhost;如果分布在不同主机,就分别写自己所在主机的 ip(三台主机必须能相互通信)
network.host: 127.0.0.1
#服务端口号,在同一机器下必须不一样
http.port: 9203
#集群间通信端口号,在同一机器下必须不一样
transport.tcp.port: 9303
#设置集群自动发现机器ip集合
#ES6.x
#discovery.zen.ping.unicast.hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#ES7.x
discovery.seed_hosts: ["127.0.0.1:9301","127.0.0.1:9302","127.0.0.1:9303"]
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1"]
启动各个节点的服务器
注:启动服务器的时候最好一个一个启动,多个一起启动,可能从服务器在启动成功之后找不到主,就会上位,导致启动失败
内存不够的话可以在配置文件中修改一下内存占用,默认是 1 个g
4.集群测试
默认会有三个索引,前提是你启动了 Kibana。此外,我们发现每个索引的分片和复制都不在同一个节点上,且每个索引默认只有一个分片和一个复制
集群测试
创建索引及映射
# 请求方法:PUT
PUT person
{"mappings":{"properties":{"id":{"type":"long","index":true},"name":{"type":"keyword","index":false},"address":{"type":"text","analyzer":"ik_max_word"}}}}
添加文档
POST person/_doc/1{"id":1,"name":"阿一","address":"中国广西省福建市"}
使用 ElasticSearch-head 插件查看集群情况
或者使用命令查看
GET _cluster/health
服务器运行状态:
Green
所有的主分片和副本分片都已分配。你的集群是 100% 可用的。
Yellow
所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果 更多的 分片消失,你就会丢数据了。把 yellow 想象成一个需要及时调查的警告。
Red
至少一个主分片(以及它的全部副本)都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。
六、高级客户端
高级客户端:JAVA Rest API 官方文档
1.环境搭建
创建 elasticsearch-demo
引入依赖(需要的依赖都看官方文档)
注:可能会有依赖冲突的问题,从 maven 仓库里将依赖删了重下
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><!-- springboot 核心启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- springboot 单元测试启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!-- elasticsearch 坐标--><dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId><version>7.4.0</version></dependency><!-- elasticsearch 高级客户端 本身就有 elasticsearch依赖,如果报错就把下面的依赖打开--><!--<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.0</version>
</dependency>--><!-- json--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.4</version></dependency></dependencies>
创建 application.yml 配置文件
elasticsearch:host:127.0.0.1port:9200
创建启动类
package com.atguigu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublicclassElasticsearchDemoApplication {
publicstaticvoidmain(String[] args) {
SpringApplication.run(ElasticsearchDemoApplication.class, args);
}
}
创建配置类 ElasticSearchConfig
package com.atguigu.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration@ConfigurationProperties(prefix = "elasticsearch")publicclassElasticSearchConfig {
private String host;
privateint port;
public String getHost() {
return host;
}
publicvoidsetHost(String host) {
this.host = host;
}
publicintgetPort() {
return port;
}
publicvoidsetPort(int port) {
this.port = port;
}
@Beanpublic RestHighLevelClient client(){
returnnewRestHighLevelClient(RestClient.builder(newHttpHost(host,port,"http")));
}
}
测试
package com.atguigu;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* Description: elasticsearch-demo
* Created by dell on 2021/11/16 17:34
*/@RunWith(SpringRunner.class)@SpringBootTestpublicclassElasticSearchTest {
@Autowiredprivate RestHighLevelClient client;
@Testpublicvoidtest() {
System.out.println(client);
}
}
成功数据 elasticsearch 对象
2.索引操作
一下只抽了一部分常见的,更多 API 操作,直接看官方文档
创建索引
可能会遇到的异常:
java.lang.NoSuchMethodError
@TestpublicvoidaddIndex()throws IOException {
CreateIndexRequestrequest=newCreateIndexRequest("twitter");
// 是否对索引进行分片和复制/*request.settings(Settings.builder()
.put("index.number_of_shards", 3)
.put("index.number_of_replicas", 2)
);
// 创建索引的同时添加映射
。。。更多请看官方文档
*/
request.mapping(
"{\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"long\",\n" +
" \"index\": true\n" +
" },\n" +
" \"name\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": false\n" +
" },\n" +
" \"address\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
"}",XContentType.JSON);
// 以同步的方式创建索引CreateIndexResponseresponse= client.indices().create(request, RequestOptions.DEFAULT);
System.out.println("acknowledged = " + response.isAcknowledged()); // 是否创建成功
System.out.println("shardsAcknowledged = " + response.isShardsAcknowledged()); // 是否分片成功
}
查询索引
// 查询索引@TestpublicvoidGetIndex()throws IOException {
GetIndexRequestrequest=newGetIndexRequest("twitter");
GetIndexResponseresponse= client.indices().get(request, RequestOptions.DEFAULT);
Map<String, MappingMetaData> mappings = response.getMappings();
// key 为 _index,value 为 _mappingsfor (String key : mappings.keySet()){
System.out.println(key + ":" + mappings.get(key).getSourceAsMap());
}
}
删除索引
@TestpublicvoidDeleteIndex()throws IOException {
DeleteIndexRequestrequest=newDeleteIndexRequest("test");
AcknowledgedResponseresponse= client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println("acknowledged = " + response.isAcknowledged());
}
判断索引是否存在
@TestpublicvoidIndexExists()throws IOException {
GetIndexRequestrequest=newGetIndexRequest("twitter");
booleanexists= client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println("exists = " + exists);
}
3.文档操作
操作 | 请求 | 响应 |
添加文档 我们通常称添加一个文档为索引一个文档 | IndexRequest | IndexResponse |
修改文档 和添加文档一样,在添加文档的时候,如果 _id 存在就修改,否则就添加 或者使用 Update | IndexRequest UpdateRequest | IndexResponse UpdateResponse |
根据 _id 查询文档 | GetRequest | GetResponse |
根据 _id 删除文档 | DeleteRequest | DeleteResponse |
批量操作 | BulkRequest | BulkResponse |
Twitter 索引库的映射
注:我们在添加文档的时候,如果文档中有 id 属性,那么文档的元数据 _id 最好和文档的 id 一致;此外,如果我们在向一个不存在的索引库中添加文档的话,ES 会帮我们自动创建索引库并添加映射
添加文档(使用 map 作为数据源)
// 添加文档:使用 map 作为数据@TestpublicvoidaddDoc()throws IOException {
Mapdata=newHashMap();
data.put("id", 1);
data.put("name", "ayi");
data.put("address","中国北京");
IndexRequestrequest=newIndexRequest("twitter").id("1").source(data);
IndexResponseresponse= client.index(request, RequestOptions.DEFAULT);
// 打印响应结果
System.out.println(response.getId());
}
添加文档(使用对象作为数据源)
// 添加文档:使用对象作为数据源
@Test
public void addDoc2() throws IOException {
Person person = new Person("2","zs","中国上海");
String data = JSON.toJSONString(person);
IndexRequest request = new IndexRequest("twitter").id(person.getId()).source(data, XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}
修改文档:在添加文档的时候,如果 _id 已经存在就是修改文档,不存在则是添加文档
根据 _id 查询文档
// 根据 _id 查询文档
@Test
public void findDocById() throws IOException {
GetRequest request = new GetRequest("twitter","1");
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 获得对应数据的 json
System.out.println(response.getSourceAsString());
}
根据 _id 修改文档
// 根据 _id 删除文档
@Test
public void deleteDocById() throws IOException {
DeleteRequest request = new DeleteRequest("twitter","2");
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
System.out.println(response.getResult());
}
批量操作
// 批量操作@Testpublicvoidbulk()throws IOException {
BulkRequestbulkRequest=newBulkRequest();
// 1.删除 1 号文档DeleteRequestdeleteRequest=newDeleteRequest("twitter", "1");
bulkRequest.add(deleteRequest);
// 2.添加 6 号文档Personperson=newPerson("6", "ls", "中国深圳");
Stringdata= JSON.toJSONString(person);
IndexRequestindexRequest=newIndexRequest("twitter").id(person.getId()).source(data, XContentType.JSON);
bulkRequest.add(indexRequest);
// 3.修改 6 号文档
person = newPerson("6", "ww", "中国西藏");
data = JSON.toJSONString(person);
UpdateRequestupdateRequest=newUpdateRequest("twitter", "6").doc(data, XContentType.JSON);
bulkRequest.add(updateRequest);
// 响应并输出BulkResponseresponse= client.bulk(bulkRequest, RequestOptions.DEFAULT);
RestStatusstatus= response.status();
System.out.println("status = " + status);
}
批量将数据库中的数据导入到 ElasticSearch 中
思路:就是将数据库中的表数据全部查询出来,用遍历 list,生成 indexRequest,并添加到 bulkRequest 中,最后批量添加到 ES 中
4.查询操作
操作 | 请求 | 响应 |
1.查询全部文档,并分页
kibana 演示
GET twitter/_search
{"query":{"match_all":{}},"from":0,"size":10}
@TestpublicvoidmatchAll()throws IOException {
// 1.创建查询对象,指定索引名称SearchRequestsearchRequest=newSearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.查询条件MatchAllQueryBuilderqueryBuilder= QueryBuilders.matchAllQuery();
sourceBuilder.query(queryBuilder);
searchRequest.source(sourceBuilder);
// 分页
sourceBuilder.from(0);
sourceBuilder.size(10);
// 查询,获取结果SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
// 获取命中对象SearchHitshits= searchResponse.getHits();
// 获取总记录数longvalue= hits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
// 获取 Twitter 数据
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits){
StringsourceAsString= hit.getSourceAsString();
Personperson= JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
2.term 查询:不会对查询条件进行分词
kibana 演示
GET twitter/_search
{"query":{"term":{"address":{"value":"中国"}}}}
@TestpublicvoidtermSearch()throws IOException {
// 1.创建查询对象SearchRequestsearchRequest=newSearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.查询条件TermQueryBuildertermQuery= QueryBuilders.termQuery("address", "中国");
sourceBuilder.query(termQuery);
searchRequest.source(sourceBuilder);
// 4.获取响应数据SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
// 5.获取碰撞结果SearchHitssearchHits= searchResponse.getHits();
longvalue= searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
StringsourceAsString= hit.getSourceAsString();
Personperson= JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
3.match:词条分词查询
会对词条进行分词,对每个分词分别进行查询,最后求并集
Kibana 演示
GET twitter/_search
{"query":{"match":{"address":{"query":"查询条件","operator":"操作(and/or)"}}}}
// match 分词查询@TestpublicvoidmatchSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("twitter");
// 2.创建查询条件SearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件MatchQueryBuildermatchQuery= QueryBuilders.matchQuery("address", "中国北京");
// 4.设置查询操作
matchQuery.operator(Operator.OR); //求并集
sourceBuilder.query(matchQuery);
searchRequest.source(sourceBuilder);
// 5.获取响应SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
// 6.获取碰撞SearchHitssearchHits= searchResponse.getHits();
longvalue= searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
StringsourceAsString= hit.getSourceAsString();
Personperson= JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
4.模糊查询(wildcard/prefix)
wildcard查询: 会对查询条件进行分词。还可以使用通配符 ?(任意单个字符) 和 * (0个或多个字符)
prefix查询: 前缀查询
// wildcard 模糊查询
GET twitter/_search
{"query":{"wildcard":{"address":{ //字段名"value":"中?"}}}}//prefix 前缀查询
GET twitter/_search
{"query":{"prefix":{"address":{ //字段名"value":"中"}}}}
wildcard 模糊查询
// wildcard 模糊查询@TestpublicvoidwildcardSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件WildcardQueryBuilderwildcardQuery= QueryBuilders.wildcardQuery("address", "中?");
sourceBuilder.query(wildcardQuery);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
prefix 前缀查询
// prefix 前缀查询@TestpublicvoidprefixSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件PrefixQueryBuilderprefixQuery= QueryBuilders.prefixQuery("address", "中");
sourceBuilder.query(prefixQuery);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
5.range 范围查询
GET person/_search
{"query":{"range":{"age":{"gte":18,"lt":20}}}}
范围查询
// 范围查询@TestpublicvoidrangeSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件RangeQueryBuilderrangeQuery= QueryBuilders.rangeQuery("age");
rangeQuery.gte(18);
rangeQuery.lt(20);
sourceBuilder.query(rangeQuery);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
6. queryString 查询
queryString:
• 会对查询条件进行分词。
• 然后将分词后的查询条件和词条进行等值匹配
• 默认取并集(OR)
• 可以指定多个查询字段
GET person/_search
{"query":{"query_string":{"fields":["name","address"],"query":"河北 OR 李四"}}}
queryString 查询
// queryString 查询@TestpublicvoidquerySearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件QueryStringQueryBuilderstringQuery= QueryBuilders.queryStringQuery("河北李四");
stringQuery.field("name").field("address").defaultOperator(Operator.OR);
sourceBuilder.query(stringQuery);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
7.布尔查询
boolQuery:对多个查询条件连接。连接方式:
• must(and):条件必须成立
• must_not(not):条件必须不成立
• should(or):条件可以成立
• filter:条件必须成立,性能比must高。不会计算得分
// 查询 address != 中国西藏的文档,term 不进行分词
GET person/_search
{"query":{"bool":{"must_not":[{"term":{"address":{"value":"中国西藏"}}}]}}}// 查询在河北省保定市的叫“阿一”的人
GET person/_search
{"query":{"bool":{"must":[{"match":{"name":"阿一"}}],"filter":{"match":{"address":"河北省保定市"}}}}}
// 布尔查询;查询 河北省保定市的阿一@TestpublicvoidboolSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件BoolQueryBuilderboolQuery= QueryBuilders.boolQuery();
// 查询 name 为 “阿一”TermQueryBuildertermQuery= QueryBuilders.termQuery("name", "阿一");
boolQuery.must(termQuery);
// 查询 address 为 “河北省保定市”MatchQueryBuildermatchQuery= QueryBuilders.matchQuery("address", "河北省保定市");
boolQuery.filter(matchQuery);
// 查询 age 在 18~25 之间的人RangeQueryBuilderrangeQuery= QueryBuilders.rangeQuery("age");
rangeQuery.gte(18);
rangeQuery.lte(25);
boolQuery.filter(rangeQuery);
sourceBuilder.query(boolQuery);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
8.聚合查询
• 指标聚合:相当于MySQL的聚合函数。max、min、avg、sum等
• 桶聚合:相当于MySQL的 group by 操作。不要对text类型的数据进行分组,会失败。
// 查询 address 中包含 “北” 字的 最大年龄
GET person/_search
{"query":{ // 查询"wildcard":{"address":{"value":"*北*"}}},"aggs":{ //聚合,查询结果中年龄最大的"max_age":{"max":{"field":"age"}}}}// 桶聚合,
GET person/_search
{"query":{ // 查询"match_all":{}},"aggs":{"age_person":{ // 起别名,将来取数据的时候就是用别名取的"terms":{ //按 age 进行分组"field":"age","size":10 //多少条记录分一页}}}}
先查询,在聚合
// 聚合@TestpublicvoidaggSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件MatchAllQueryBuildermatchAllQuery= QueryBuilders.matchAllQuery();
sourceBuilder.query(matchAllQuery); // 先查询出结果,在聚合//聚合TermsAggregationBuilderaggregationBuilder= AggregationBuilders.terms("age_person").field("age").size(10);
sourceBuilder.aggregation(aggregationBuilder);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
// 获取查询结果
printResponse(searchResponse);
// 获取聚合结果,如果不明白怎么来的,可以自己 debug 看一下属性Aggregationsaggregations= searchResponse.getAggregations();
Map<String, Aggregation> aggregationMap = aggregations.asMap();
Termsperson= (Terms)aggregationMap.get("age_person");
List<? extendsTerms.Bucket> personBuckets = person.getBuckets();
for (Terms.Bucket bucket : personBuckets){
System.out.println(bucket.getKey());
}
}
9.高亮查询
高亮三要素:
• 高亮字段
• 前缀
• 后缀
// 高亮查询@TestpublicvoidhighlightSearch()throws IOException {
// 1.创建查询对象,指定索引库SearchRequestsearchRequest=newSearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilderSearchSourceBuildersourceBuilder=newSearchSourceBuilder();
// 3.创建查询条件:先是正常查询,最后再高亮MatchQueryBuildermatchQuery= QueryBuilders.matchQuery("address", "河北");
sourceBuilder.query(matchQuery);
// 高亮HighlightBuilderhighlightBuilder=newHighlightBuilder();
highlightBuilder.field("address");
highlightBuilder.preTags("<em>");
highlightBuilder.postTags("</em>");
sourceBuilder.highlighter(highlightBuilder);
searchRequest.source(sourceBuilder);
SearchResponsesearchResponse= client.search(searchRequest, RequestOptions.DEFAULT);
SearchHitssearchHits= searchResponse.getHits();
longvalue= searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
StringsourceAsString= hit.getSourceAsString();
// 获取查询结果,转为 javaBeanPersonperson= JSON.parseObject(sourceAsString, Person.class);
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightFieldhighlightField= highlightFields.get("address");
Text[] fragments = highlightField.fragments();
// 将查询结果替换成高亮
person.setAddress(fragments[0].toString());
System.out.println("person = " + person);
}
}
5.重建索引
随着业务需求的变更,索引的结果可能会发生改变;
ES 的索引一旦创建,只允许添加字段(映射),不允许修改/删除字段,因为改变字段,需要重新建立倒排索引,影响内部缓存结构,性能太低;
所以此时,我们就需要重新建立一个新的索引,并将原索引的数据导入到新索引中去;
我们就需要重复的查询,添加操作来实现重建索引
七、Spring Data Elasticsearch
Spring Data 的作用:简化了数据库的增删改查操作;
Spring Data Elasticsearch 的环境搭建,增删改查操作
Spring Data Elasticsearch 自定方法的命名规则
1.简介
Spring Data 是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架;
其目标是让数据访问变得更加快捷;
Spring Data 可以极大的简化 JPA(Elasticsearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了 CRUD 外,还包括如分页、排序等一些常用的功能。
Spring Data Elasticsearch 介绍
Spring Data ElasticSearch 基于 spring data API 简化 elasticsearch 操作,将原始操作 elasticsearch 的客户端 API 进行封装 。
Spring Data 为 Elasticsearch 项目提供集成搜索引擎。
2.Spring Data Elasticsearch 环境搭建
创建项目(elasticsearch-springdata-es)
添加 pom.xml 依赖
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.2.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><maven.compiler.encoding>UTF-8</maven.compiler.encoding><java.version>1.8</java.version></properties><dependencies><!-- elasticsearch 启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency><!-- 单元测试启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>repository.junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId></dependency></dependencies>
编写实体类
创建 Item 类对象
@Document(indexName = "item", shards = 1, replicas = 1)publicclassItem {
// 只有添加了 Field 注解的属性才会被添加为映射,// 这里 Id 没有添加 Field 注解,所以之后建索引的时候不会添加映射@Idprivate Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")private String title; //标题@Field(type = FieldType.Keyword)private String category;// 分类@Field(type = FieldType.Keyword)private String brand; // 品牌@Field(type = FieldType.Double)private Double price; // 价格@Field(index = false, type = FieldType.Keyword)// index = false 表示不参与索引,默认为 trueprivate String images; // 图片地址publicItem() {
}
publicItem(Long id, String title, String category, String brand, Double price, String images) {
this.id = id;
this.title = title;
this.category = category;
this.brand = brand;
this.price = price;
this.images = images;
}
// getters and setters and toString
配置 yml 配置文件
elasticsearch:host:127.0.0.1port:9200logging:level:com:atguigu:debug
创建 elasticsearch 的配置类
ElasticsearchRestTemplate 是 spring-data-elasticsearch 项目中的一个类,和其他 spring 项目中的 template 类似。
在新版的 spring-data-elasticsearch 中,ElasticsearhRestTemplate 代替了原来的 ElasticsearchTemplate。
原因是 ElasticsearchTemplate 基于 TransportClient,TransportClient 即将在 8.x 以后的版本中移除。所以,我们推荐使用 ElasticsearchRestTemplate。
ElasticsearchRestTemplate 基于 RestHighLevelClient 客户端的。需要自定义配置类,继承 AbstractElasticsearchConfiguration,并实现 elasticsearchClient() 抽象方法,创建 RestHighLevelClient 对象。
@Configuration@ConfigurationProperties(prefix = "elasticsearch")publicclassElasticsearchConfigextendsAbstractElasticsearchConfiguration {
private String host;
private Integer port;
// SpringBoot 会帮我们将创建好的 client 对象自定注入到 IOC 容器中// 这时我们就可以像之前那样使用原生的方法操作映射、文档了,// 当然我们也可以使用 Spring Data Elasticsearch 给我们提供的方法@Overridepublic RestHighLevelClient elasticsearchClient() {
returnnewRestHighLevelClient(RestClient.builder(newHttpHost(host, port)));
}
// getters and setters
}
测试
@RunWith(SpringRunner.class)@SpringBootTestpublicclassElasticsearchTest {
@Autowired
ElasticsearchRestTemplate restTemplate;
@TestpublicvoidtestCreate() {
restTemplate.createIndex(Item.class); // 创建索引
restTemplate.putMapping(Item.class); // 添加映射
}
}
3.增删改查
Spring Data 的强大之处,就在于你不用写任何 DAO 处理,自动根据方法名或类的信息进行 CRUD 操作。只要你定义一个接口,然后继承 XxxRepository 提供的一些子接口,就能具备各种基本的 CRUD 功能。
编写 ItemRepository 接口
// 不同添加注解,在父接口中已经有了publicinterfaceItemRepositoryextendsElasticsearchRepository<Item, Long> {
}
@Autowiredprivate ItemRepository itemRepository;
// 新增或更新@TestpublicvoidtestAdd() {
Itemitem=newItem(1L, "小米手机7", " 手机", "小米", 3499.00, "http://image.leyou.com/13123.jpg");
itemRepository.save(item); // id 不存在就添加,已存在就更新
}
批量新增
// 批量新增@TestpublicvoidtestAddList(){
List<Item> list = newArrayList<>();
list.add(newItem(2L, "坚果手机R1", " 手机", "锤子", 3699.00, "http://image.leyou.com/123.jpg"));
list.add(newItem(3L, "华为META10", " 手机", "华为", 4499.00, "http://image.leyou.com/3.jpg"));
itemRepository.saveAll(list);
}
删除操作
//删除操作@TestpublicvoidtestDelete() {
itemRepository.deleteById(1L);
}
根据 id 查询
// 根据 id 查询@TestpublicvoidtestFindById() {
// 因为我们这里使用的通用接口,不可能返回某个具体的类,所以就使用泛型
Optional<Item> optional = itemRepository.findById(1L);
// 这里的 get() 没有进行空值检测,如果没有查询到结果,就会抛 NoSuchElementException// 所以我们在实际开发时,要做异常处理
System.out.println(optional.get());
}
查询全部,并排序
// 查询全部,并按照价格降序排列@TestpublicvoidtestFind() {
Iterable<Item> items = itemRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(System.out::println);
}
4.自定义方法
Spring Data 的另一个强大功能,是根据方法名称自动实现功能。
比如:你的方法名叫做:findByTitle,那么它就知道你是根据title查询,然后自动帮你完成,无需写实现类。
当然,方法名称要符合一定的约定:
Keyword | Sample | Elasticsearch Query String |
And | findByNameAndPrice | {"bool" : {"must" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Or | findByNameOrPrice | {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"price" : "?"}} ]}} |
Is | findByName | {"bool" : {"must" : {"field" : {"name" : "?"}}}} |
Not | findByNameNot | {"bool" : {"must_not" : {"field" : {"name" : "?"}}}} |
Between(左闭右闭) | findByPriceBetween | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
LessThanEqual(小于等于) | findByPriceLessThan | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
GreaterThanEqual(大于等于) | findByPriceGreaterThan | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Before | findByPriceBefore | {"bool" : {"must" : {"range" : {"price" : {"from" : null,"to" : ?,"include_lower" : true,"include_upper" : true}}}}} |
After | findByPriceAfter | {"bool" : {"must" : {"range" : {"price" : {"from" : ?,"to" : null,"include_lower" : true,"include_upper" : true}}}}} |
Like | findByNameLike | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
StartingWith | findByNameStartingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "?*","analyze_wildcard" : true}}}}} |
EndingWith | findByNameEndingWith | {"bool" : {"must" : {"field" : {"name" : {"query" : "*?","analyze_wildcard" : true}}}}} |
Contains/Containing | findByNameContaining | {"bool" : {"must" : {"field" : {"name" : {"query" : "?","analyze_wildcard" : true}}}}} |
In | findByNameIn(Collectionnames) | {"bool" : {"must" : {"bool" : {"should" : [ {"field" : {"name" : "?"}}, {"field" : {"name" : "?"}} ]}}}} |
NotIn | findByNameNotIn(Collectionnames) | {"bool" : {"must_not" : {"bool" : {"should" : {"field" : {"name" : "?"}}}}}} |
Near | findByStoreNear | Not Supported Yet ! |
True | findByAvailableTrue | {"bool" : {"must" : {"field" : {"available" : true}}}} |
False | findByAvailableFalse | {"bool" : {"must" : {"field" : {"available" : false}}}} |
OrderBy | findByAvailableTrueOrderByNameDesc | {"sort" : [{ "name" : {"order" : "desc"} }],"bool" : {"must" : {"field" : {"available" : true}}}} |