Elaticsearch,简称为es, es是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。es也使用 Java 开发并使用 Lucene 作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API 来隐藏 Lucene 的复杂性,从而让全文搜索变得简单。
什么是 Lucene?
ES 是在之前的 Lucene 的基础上进行了封装,就像 Mybatis 对 jdbc 进行了封装一样,通过 Restful API,简化了对数据的各种操作
什么是全文检索引擎?
当我们在用 ES 搜索数据的时候,不管数据是什么样的数据,文本也好,word 文档也好,还是页面,都能够被搜索到
我们生活中数据总体分为两种:结构化数据与非结构化数据
- 结构化数据:指具有固定格式或有限长度的数据,如数据库、元数据等;
- 非结构化数据:指不定长或无固定格式的数据,如邮件、word 文档等磁盘上的文件;
结构化数据搜索
常见的结构化数据也就是数据库中的数据,在数据库中所搜很容易实现,通常使用 sql 进行搜索,而且很容易得到查询结果;
为什么数据库搜索很容易?
因为数据库中的数据是有规律的,有行有列,且数据格式、数据长度都是固定的。
非结构化数据搜索
所谓顺序扫描法,就是对磁盘上的所有文件进行顺序扫描,一个文件一个文件的扫描,每个文件都从头扫描到尾,来查询文件中是否有我们需要搜索的内容,这种查询方式相当的慢!!!
首先我们介绍一下索引的概念:
将非结构化数据的中的一部分提取出来,重新组织成有一定结构的数据,然后对此有一定结构的数据进行检索,从而达到搜索相对较快的目的。这部分从非结构数据中提取出来然后重新组织的信息,我们就称之为索引;
例如:字典。字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。
这种先建立索引,再对索引进行搜索的过程就叫全文检索
虽然索引的创建非常耗时,但是索引一旦创建就可以多次使用,全文检索主要处理的是查询,所以消耗时间创建索引是值得的。
我们可以使用 Lucene 实现全文检索。Lucene 是 apache 下的一个开源的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,部分文本分析引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。
而 ES 正是对 Lucene 进行了封装,简化了操作
全文检索的应用场景
对于数据量大、数据结构不固定的数据可采用全文检索方式搜索,比如:百度、Google等搜索引擎、论坛站内搜索、电商网站站内搜索等。
全文检索的实现流程
1、绿色表示索引(建立索引)的过程,对原始内容进行索引建立索引库,索引的过程包括:
2、红色表示搜索过程,从索引库中搜索内容,搜索包括:
总之:全文检索分为两个过程:创建索引(Indexing)和搜索索引(Search)
文档的索引过程:将用户要搜索的文档内容进行索引,索引存储在索引库(index)中。
那么索引中到底需要存放什么东西呢?
首先我们还是从顺序扫描说起
顺序扫描速度之所以慢,是因为我们不知道每个文件里大致包含哪些东西(关键词),用户输入关键词,我们需要遍历每一个文件,扫描每一个文件才能,返回具有该关键词的文件;
这时我们不妨反过来想,如果我们已经知道了每个文件大致包含了哪些内容(关键词),而且这些关键词都引用了相关的文件,就像 Map 一样,每个 key 都关联了一个或多个 value,这时用户再通过关键词搜索,我们就可以直接给出用户想要的文件;
之后讲到正排索引和倒排索引的时候会更清楚的了解
所以我们要建的索引就是每个文件中包含的大致内容,即关键词
获取原始内容的目的就是为了索引,在索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),域中存储内容;
这里的原始内容就是我们想要搜索的一切信息
这里我们可以将磁盘上的一个文件当成一个 document,Document 中包括了一些 Field(file_name文件名称、file_path文件路径、file_size文件大小、file_content文件内容)。Document 是对原始内容的一个描述对象
注意:每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field(域名和域值都相同)
每个文档都有一个唯一的编号,就是文档id。
上面我们将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析
非结构化数据中所存储的信息是每个文件包含哪些关键词,即已知文件,欲求关键词相对容易,也即是从文件到关键词的映射。【正排索引】
但是对于顺序扫描而言,是用户每次搜索,都要从头到尾遍历一遍,这样在数据量庞大时就会特别慢
而我们想搜索的信息是哪些文件包含此关键词,也即已知关键词,欲求文件,也即从关键词到文件的映射。【倒排索引】
两者恰恰相反。于是如果索引总能够保存从关键词到文件的映射,则会大大提高搜索速度。
强调:我们每一次增删改原始内容的时候,都要重新建立一次索引,当然不一定是立刻就建立索引,我们可以定时建立,每天或每12小时建立一次索引
正排索引:
倒排索引:
强调:倒排列表中还可以存放更多信息来辅助搜索。比如说:我们可以在倒排列表中存储一个关键词在原始内容中出现的次数字段,如下
这样我们就可以通过在关键词出现的频率进行排序
同时我们也可以存储一个文件类型字段(页面,图片…)来辅助我们进行筛选
倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它的规模较小,而文档集合较大。
查询索引也是搜索的过程。搜索就是用户输入关键字,从索引库(index)中进行搜索的过程。根据关键字搜索索引,根据索引找到对应的文档,从而找到要搜索的内容(这里指磁盘上的文件)。
全文检索系统提供用户搜索的界面供用户提交搜索的关键字,搜索完成展示搜索结果。
比如:
用户输入查询关键字执行搜索之前需要先构建一个查询对象,查询对象中可以指定查询要搜索的 Field 文档域(比如说查询的是页面、图片、word…还是 ppt 等等)、查询关键字等,查询对象会生成具体的查询语法。
搜索索引过程:根据查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表。
以一个友好的界面将查询结果展示给用户,用户根据搜索结果找自己想要的信息,为了帮助用户很快找到自己的结果,提供了很多展示的效果,比如搜索结果中将关键字高亮显示,百度提供的快照等。
背景:当数据库中的文档数仅仅上万条时,关键词查询就比较慢了。如果一旦到企业级的数据,响应速度就会更加不可接受。
原因:在数据库做模糊查询时,如LIKE语句,它会遍历整张表,同时进行字符串匹配。
例如,在数据库查询“手机”时,数据库会在每一条记录去匹配“手机”这两字是否出现。实际上,并不是所有记录都包含“手机”,所以做了很多无用功。这个步骤并不高效,而且随着数据量的增大,消耗的资源和时间都会线性的增长。
提升:使用 ES 搜索服务后,这个问题被很好解决,TB 级数据在毫秒级就能返回检索结果。
原因:Elasticsearch 是基于倒排索引的,例子如下。
当搜索“手机”时,Elasticsearch 就会立即返回文档 F,G,H。这样就不用花多余的时间在其他文档上了,因此检索速度得到了数量级的提升。
背景:在做中文搜索时,组合词检索在数据库是很难完成的。
例如:当用户在搜索框输入“四川火锅”时,数据库通常只能把这四个字去进行全部匹配。可是在文本中,可能会出现“推荐四川好吃的火锅”,这时候就没有结果了。
原因:数据库并不支持分词。如果人工去开发分词功能,费时费精力。
提升:使用ES搜索服务后,就不用太过于关注分词了,因为 Elasticsearch 支持中文分词插件,很好地解决了问题。
原因:当用户使用 Elasticsearch 时进行搜索时,Elasticsearch 就自动帮他分好词了。
例如 输入“四川火锅”时,Elasticsearch 会自动做下面两件事
- 将“四川火锅”分词成“四川”和“火锅”
- 查找包含这两个词的文档
背景:在用数据库做搜索时,结果经常会出现一系列不匹配的文档。比如说:
- 没有返回用户想要的文档
- 怎么才能把用户想要的文档排序在最前面?
原因:数据库并不支持相关性搜索。
例如:当用户搜索“咖啡厅”的时候,他很可能更想知道附近哪里可以喝咖啡,而不是怎么开咖啡厅。
提升:Elasticsearch 能很好地支持相关性评分。通过合理的优化,ES 搜索服务能够返回精准的结果,满足用户的需求。
原因:Elasticsearch 支持全文搜索和相关度评分。这样在返回结果就会根据分数由高到低排列。分数越高,意味着和查询语句越相关。
例如:当用户搜索“星巴克咖啡”,带有“星巴克咖啡”的信息就要比只包含“咖啡”的信息靠前。
下面所有的工具包都在这里
提取码:syhn
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目录下,即可
点击 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 重新分配内存。
省略…这里推荐给大家一个在线的 Postman
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,即可变为中文的;
head插件是ES的一个可视化管理插件,用来监视ES的状态,并通过head客户端和ES服务进行交互,比如创建映射、创建索引等。
将 ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx 用压缩工具解压,打开 Chrome 扩展程序,点” 加载已解压的扩展程序”按钮,找到解压目录即可。
或者 直接将 ElasticSearch-head-Chrome-0.1.5-Crx4Chrome.crx 拖到扩展程序窗口也可以
介绍
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:做粗粒度的拆分
先说 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 中的索引是一个倒排索引;
映射 是对文档中每个字段的类型进行定义,每一种数据类型都有对应的使用场景。
每个文档都有映射,但是在大多数使用场景中,我们并不需要显示的创建映射,因为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 类型不支持分词。
文档在 ES 中相当于传统数据库中的行的概念,ES 中的数据都以 JSON 的形式来表示,在 MySQL 中插入一行数据和 ES 中插入一个 JSON 文档是一个意思。下面的 JSON 数据表示,一个包含3个字段的文档。
{
"name":"jack",
"age":18,
"gender":1
}
一个文档不只有数据。它还包含了元数据(metadata)——关于文档的信息。三个必须的元数据节点是:
节点 | 说明 |
---|---|
_index | 文档存储的地方 |
_type | 文档代表的对象的类 |
_id | 文档的唯一标识 |
实际开发中,主要有三种方式可以作为 elasticsearch 服务的客户端:
ElasticSearch 接口支持 Restful 风格的语法
命令 | 请求 |
---|---|
创建索引 | PUT http://localhost:9200/blog 注:这里重复创建索引会报错 |
查看索引 | GET http://localhost:9200/blog |
删除索引 | DELETE http://localhost:9200/blog |
关闭索引 | POST http://localhost:9200/blog/_close |
打开索引 | POST http://localhost:9200/blog/_open |
创建索引并进行映射 | 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"
}
}
}
}
命令 | 请求 |
---|---|
创建索引 | 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"
}
}
}
// 先查询映射,根据映射添加文档
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":"北京"
}
全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集
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
}
词条查询不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配搜索
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" : [ ]
}
}
// 查询用户 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”或其他数字类型则不会进行分词操作
就类似与数据库的 SQL 查询
// 查询年龄为 20 的数据
POST person/_search
{
"query": {
"match": {
"age": 20
}
}
}
// 查询年龄 大于等于 18 小于 25 的数据
POST person/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gte": 18,
"lt": 25
}
}
}
}
}
}
我们在百度搜索的时候,我们查询的关键字会被高亮显示
实际上就是我们在用 ES 查询时,如果要求高亮显示,它在返回数据的时候就会将匹配的词条用 包裹;
// 我们查询 address 为“河北省”的数据,并让其高亮显示
GET person/_search
{
"query": {
"match": {
"address": "河北省"
}
},
"highlight": {
"fields": {
"address": {}
}
}
}
我们可以看到由于我们使用默认的分词模式 standard,所以 ES 将匹配到的每一个字都用 包裹了
在Elasticsearch中,支持聚合操作,类似SQL中的group by操作。
注意:如果我们对 text 类型的字段进行聚合会报错,这时我们就要在字段后面加上 keyword 属性
GET person/_search
{
"aggs": { // aggs: 聚合单词的缩写
"all_interests": { //这个单词我们随便定义的,就像 MySQL 里的取别名一样
"terms": {
"field": "age"
// 如果要对 text字段进行聚合,就要如下写法
"field": "address.keyword"
}
}
}
}
在响应的数据中,如果我们不需要全部的字段,可以指定某些需要的字段进行返回
GET person/_doc/1?_source=id,name
等价于
GET person/_search
{
"query": {
"match": {
"id": "1"
}
},
"_source": ["id","name"]
}
如果我们只需要判断文档是否存在,而不是查询文档内容,那么可以这样:
HEAD person/_doc/1
POST person/_mget
{
"ids" : [ "1001", "1003" ]
}
在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"}
POST _bulk
{"index": {"_index": "person"}} // 注意这一部分不能有任何换行
{"name": "ayi1", "age": 18, "address": "zh-CN"} // 这一部分也不能有任何换行
{"create": {"_index": "person"}}
{"name": "ayi2", "age": 19, "address": "en"}
注意我们在批量插入的时候 index 和 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}}
和 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
}
和 term 作用类似,允许指定多个匹配条件,会将所有匹配返回
// 查询年龄是 18 或 19 岁的数据
GET person/_search
{
"query": {
"terms": {
"age": [18, 19]
}
}
}
range允许我们按照指定范围查找一批数据
gt: 大于
gte: 大于等于
lt: 小于
lte: 小于等于
// 查询年龄大于等于 18,小于等于 20 的数据
POST person/_search
{
"query": {
"range": {
"age": {
"gte": 18,
"lte": 20
}
}
}
}
exists 查询可以用于查找文档中某个字段值是否为空
// 查询不为 null 的
GET person/_search
{
"query": {
"exists": {
"field": "address"
}
}
}
// 查询字段为 null 的
GET person/_search
{
"query": {
"bool": {
"must_not": [
{
"exists": {
"field": "address"
}
}
]
}
}
}
单台服务器,往往都有最大的负载能力,超过了这个阈值,服务器的性能往往就会大大降低,甚至不可用。单点的 ES 也是一样,单点的 ES 服务器具有很多缺陷:
- 单台机器的存储容量有限
- 单台服务器容易出现单点故障,不能实现高可用
- 单台服务器的并发处理能力有限
因此,我们需要搭建 ES 的集群
集群中的节点数量没有限制,大于等于 2 个节点就可以看做是集群了,但是为了实现高可用,我们往往会搭建 3 个以上的节点
一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是 “elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 ElasticSearch 集群中的哪些节点。
一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做 “elasticsearch” 的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做 “elasticsearch” 的集群中。
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做 “elasticsearch” 的集群。
什么是分片?
- ES 提供了将一个索引[库]划分成多份的能力,像这样的每一份就是一个分片
- 当我们在创建一个索引的时候,可以指定自己想要的分片数量。但是一旦我们创建了索引,就无法在修改分片数量
- 每一个分片本身也是一个功能完善的 “索引”,这个 “索引” 可以被放置到集群中的任何一个节点上。
为什么要分片?
- 分片前的缺点
- 一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间
- 单个节点处理搜索请求,响应太慢。
- 分片的作用
- 允许我们水平分割或扩展我们的内容容量
- 运行我们在分片,也就是多个节点之间进行分布式、并行操作,提高性能和吞吐量
至于一个分片怎样分布,多个分片上返回的结果怎么聚合回搜索请求,是完全由 ES 管理的,对于用户来说,完全是透明的。
什么是复制?
- 在一个网络/云环境中,某个分片/节点可能随时宕机,为了避免这种情况,就出现了复制。ES 允许你创建分片的一份或多份的拷贝,必要时这些拷贝就可以代替分片工作,这些拷贝就叫复制分片(副本)
- 分片和复制就类似 Redis 中的主机和从机之间的关系,主机宕机了,从机就会上位,代替主机正常工作;主机和从机也能同时进行并发操作,提高性能
复制的作用
- 在分片/节点失败的情况下,提供高可用。复制分片从不与主分片位于同一节点上,至于怎么实现完全是透明的
2.提高搜索量和吞吐量,搜索可以在所有复制分片上并行运行。
总之
- 每个索引可以有多个分片。
- 一个索引可以被复制 0 次或多次。
- 一旦复制了,每个索引就有了主分片和复制分片之分了
- 分片和复制的数量可以在创建索引的时候指定;之后可以动态改变复制的数量,但不能改变分片的数量
默认情况下:
- ES6.x 中的每个索引被分片 5 个主分片和 1 个复制。这意味着,如果你的集群中有两个节点,你的索引将会有 5 个主分片和 5 个复制分片(相当于 1 个对 5 个主分片的完全拷贝),这样每个索引就一共有 10 个分片;
- 而在 ES7.x 中每个索引被分片 1 个主分片 和 1 个复制。同理,每个索引就一共有 2 个分片
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”]
默认会有三个索引,前提是你启动了 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": "中国广西省福建市"
}
服务器运行状态:
高级客户端:JAVA Rest API 官方文档
引入依赖(需要的依赖都看官方文档)
注:可能会有依赖冲突的问题,从 maven 仓库里将依赖删了重下
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.4version>
dependency>
dependencies>
elasticsearch:
host: 127.0.0.1
port: 9200
package com.atguigu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ElasticsearchDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ElasticsearchDemoApplication.class, args);
}
}
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")
public class ElasticSearchConfig {
private String host;
private int port;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(new HttpHost(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)
@SpringBootTest
public class ElasticSearchTest {
@Autowired
private RestHighLevelClient client;
@Test
public void test() {
System.out.println(client);
}
}
成功数据 elasticsearch 对象
一下只抽了一部分常见的,更多 API 操作,直接看官方文档
@Test
public void addIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("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);
// 以同步的方式创建索引
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println("acknowledged = " + response.isAcknowledged()); // 是否创建成功
System.out.println("shardsAcknowledged = " + response.isShardsAcknowledged()); // 是否分片成功
}
// 查询索引
@Test
public void GetIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("twitter");
GetIndexResponse response = client.indices().get(request, RequestOptions.DEFAULT);
Map<String, MappingMetaData> mappings = response.getMappings();
// key 为 _index,value 为 _mappings
for (String key : mappings.keySet()){
System.out.println(key + ":" + mappings.get(key).getSourceAsMap());
}
}
@Test
public void DeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("test");
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println("acknowledged = " + response.isAcknowledged());
}
@Test
public void IndexExists() throws IOException {
GetIndexRequest request = new GetIndexRequest("twitter");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println("exists = " + exists);
}
操作 | 请求 | 响应 |
---|---|---|
添加文档 我们通常称添加一个文档为索引一个文档 |
IndexRequest | IndexResponse |
修改文档 和添加文档一样,在添加文档的时候,如果 _id 存在就修改,否则就添加 或者使用 Update |
IndexRequest UpdateRequest |
IndexResponse UpdateResponse |
根据 _id 查询文档 | GetRequest | GetResponse |
根据 _id 删除文档 | DeleteRequest | DeleteResponse |
批量操作 | BulkRequest | BulkResponse |
Twitter 索引库的映射
注:我们在添加文档的时候,如果文档中有 id 属性,那么文档的元数据 _id 最好和文档的 id 一致;此外,如果我们在向一个不存在的索引库中添加文档的话,ES 会帮我们自动创建索引库并添加映射
// 添加文档:使用 map 作为数据
@Test
public void addDoc() throws IOException {
Map data = new HashMap();
data.put("id", 1);
data.put("name", "ayi");
data.put("address","中国北京");
IndexRequest request = new IndexRequest("twitter").id("1").source(data);
IndexResponse response = 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 删除文档
@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());
}
// 批量操作
@Test
public void bulk() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
// 1.删除 1 号文档
DeleteRequest deleteRequest = new DeleteRequest("twitter", "1");
bulkRequest.add(deleteRequest);
// 2.添加 6 号文档
Person person = new Person("6", "ls", "中国深圳");
String data = JSON.toJSONString(person);
IndexRequest indexRequest = new IndexRequest("twitter").id(person.getId()).source(data, XContentType.JSON);
bulkRequest.add(indexRequest);
// 3.修改 6 号文档
person = new Person("6", "ww", "中国西藏");
data = JSON.toJSONString(person);
UpdateRequest updateRequest = new UpdateRequest("twitter", "6").doc(data, XContentType.JSON);
bulkRequest.add(updateRequest);
// 响应并输出
BulkResponse response = client.bulk(bulkRequest, RequestOptions.DEFAULT);
RestStatus status = response.status();
System.out.println("status = " + status);
}
操作 | 请求 | 响应 |
---|---|---|
kibana 演示
GET twitter/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10
}
@Test
public void matchAll() throws IOException {
// 1.创建查询对象,指定索引名称
SearchRequest searchRequest = new SearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.查询条件
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
sourceBuilder.query(queryBuilder);
searchRequest.source(sourceBuilder);
// 分页
sourceBuilder.from(0);
sourceBuilder.size(10);
// 查询,获取结果
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 获取命中对象
SearchHits hits = searchResponse.getHits();
// 获取总记录数
long value = hits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
// 获取 Twitter 数据
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits){
String sourceAsString = hit.getSourceAsString();
Person person = JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
kibana 演示
GET twitter/_search
{
"query": {
"term": {
"address": {
"value": "中国"
}
}
}
}
@Test
public void termSearch() throws IOException {
// 1.创建查询对象
SearchRequest searchRequest = new SearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("address", "中国");
sourceBuilder.query(termQuery);
searchRequest.source(sourceBuilder);
// 4.获取响应数据
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 5.获取碰撞结果
SearchHits searchHits = searchResponse.getHits();
long value = searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
Person person = JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
会对词条进行分词,对每个分词分别进行查询,最后求并集
Kibana 演示
GET twitter/_search
{
"query": {
"match": {
"address": {
"query": "查询条件",
"operator": "操作(and/or)"
}
}
}
}
// match 分词查询
@Test
public void matchSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("twitter");
// 2.创建查询条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "中国北京");
// 4.设置查询操作
matchQuery.operator(Operator.OR); //求并集
sourceBuilder.query(matchQuery);
searchRequest.source(sourceBuilder);
// 5.获取响应
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 6.获取碰撞
SearchHits searchHits = searchResponse.getHits();
long value = searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
Person person = JSON.parseObject(sourceAsString, Person.class);
System.out.println("person = " + person);
}
}
wildcard查询: 会对查询条件进行分词。还可以使用通配符 ?(任意单个字符) 和 * (0个或多个字符)
prefix查询: 前缀查询
// wildcard 模糊查询
GET twitter/_search
{
"query": {
"wildcard": {
"address": { //字段名
"value": "中?"
}
}
}
}
//prefix 前缀查询
GET twitter/_search
{
"query": {
"prefix": {
"address": { //字段名
"value": "中"
}
}
}
}
wildcard 模糊查询
// wildcard 模糊查询
@Test
public void wildcardSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
WildcardQueryBuilder wildcardQuery = QueryBuilders.wildcardQuery("address", "中?");
sourceBuilder.query(wildcardQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
prefix 前缀查询
// prefix 前缀查询
@Test
public void prefixSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("twitter");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
PrefixQueryBuilder prefixQuery = QueryBuilders.prefixQuery("address", "中");
sourceBuilder.query(prefixQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
GET person/_search
{
"query": {
"range": {
"age": {
"gte": 18,
"lt": 20
}
}
}
}
范围查询
// 范围查询
@Test
public void rangeSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("age");
rangeQuery.gte(18);
rangeQuery.lt(20);
sourceBuilder.query(rangeQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
queryString:
• 会对查询条件进行分词。
• 然后将分词后的查询条件和词条进行等值匹配
• 默认取并集(OR)
• 可以指定多个查询字段
GET person/_search
{
"query": {
"query_string": {
"fields": ["name","address"],
"query": "河北 OR 李四"
}
}
}
queryString 查询
// queryString 查询
@Test
public void querySearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
QueryStringQueryBuilder stringQuery = QueryBuilders.queryStringQuery("河北李四");
stringQuery.field("name").field("address").defaultOperator(Operator.OR);
sourceBuilder.query(stringQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
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": "河北省保定市"
}
}
}
}
}
// 布尔查询;查询 河北省保定市的阿一
@Test
public void boolSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 查询 name 为 “阿一”
TermQueryBuilder termQuery = QueryBuilders.termQuery("name", "阿一");
boolQuery.must(termQuery);
// 查询 address 为 “河北省保定市”
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "河北省保定市");
boolQuery.filter(matchQuery);
// 查询 age 在 18~25 之间的人
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("age");
rangeQuery.gte(18);
rangeQuery.lte(25);
boolQuery.filter(rangeQuery);
sourceBuilder.query(boolQuery);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
printResponse(searchResponse);
}
• 指标聚合:相当于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 //多少条记录分一页
}
}
}
}
先查询,在聚合
// 聚合
@Test
public void aggSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件
MatchAllQueryBuilder matchAllQuery = QueryBuilders.matchAllQuery();
sourceBuilder.query(matchAllQuery); // 先查询出结果,在聚合
//聚合
TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("age_person").field("age").size(10);
sourceBuilder.aggregation(aggregationBuilder);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 获取查询结果
printResponse(searchResponse);
// 获取聚合结果,如果不明白怎么来的,可以自己 debug 看一下属性
Aggregations aggregations = searchResponse.getAggregations();
Map<String, Aggregation> aggregationMap = aggregations.asMap();
Terms person = (Terms)aggregationMap.get("age_person");
List<? extends Terms.Bucket> personBuckets = person.getBuckets();
for (Terms.Bucket bucket : personBuckets){
System.out.println(bucket.getKey());
}
}
高亮三要素:
• 高亮字段
• 前缀
• 后缀
// 高亮查询
@Test
public void highlightSearch() throws IOException {
// 1.创建查询对象,指定索引库
SearchRequest searchRequest = new SearchRequest("person");
// 2.创建查询条件构建器 SearchSourceBuilder
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3.创建查询条件:先是正常查询,最后再高亮
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "河北");
sourceBuilder.query(matchQuery);
// 高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("address");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
sourceBuilder.highlighter(highlightBuilder);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits searchHits = searchResponse.getHits();
long value = searchHits.getTotalHits().value;
System.out.println("总记录数 value = " + value);
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits){
String sourceAsString = hit.getSourceAsString();
// 获取查询结果,转为 javaBean
Person person = JSON.parseObject(sourceAsString, Person.class);
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField highlightField = highlightFields.get("address");
Text[] fragments = highlightField.fragments();
// 将查询结果替换成高亮
person.setAddress(fragments[0].toString());
System.out.println("person = " + person);
}
}
- Spring Data 是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架;
- 其目标是让数据访问变得更加快捷;
- Spring Data 可以极大的简化 JPA(Elasticsearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了 CRUD 外,还包括如分页、排序等一些常用的功能。
Spring Data Elasticsearch 介绍
- Spring Data ElasticSearch 基于 spring data API 简化 elasticsearch 操作,将原始操作 elasticsearch 的客户端 API 进行封装 。
- Spring Data 为 Elasticsearch 项目提供集成搜索引擎。
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-elasticsearchartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>repository.junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
dependency>
dependencies>
创建 Item 类对象
@Document(indexName = "item", shards = 1, replicas = 1)
public class Item {
// 只有添加了 Field 注解的属性才会被添加为映射,
// 这里 Id 没有添加 Field 注解,所以之后建索引的时候不会添加映射
@Id
private 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 表示不参与索引,默认为 true
private String images; // 图片地址
public Item() {
}
public Item(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
elasticsearch:
host: 127.0.0.1
port: 9200
logging:
level:
com:
atguigu: debug
- 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")
public class ElasticsearchConfig extends AbstractElasticsearchConfiguration {
private String host;
private Integer port;
// SpringBoot 会帮我们将创建好的 client 对象自定注入到 IOC 容器中
// 这时我们就可以像之前那样使用原生的方法操作映射、文档了,
// 当然我们也可以使用 Spring Data Elasticsearch 给我们提供的方法
@Override
public RestHighLevelClient elasticsearchClient() {
return new RestHighLevelClient(RestClient.builder(new HttpHost(host, port)));
}
// getters and setters
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticsearchTest {
@Autowired
ElasticsearchRestTemplate restTemplate;
@Test
public void testCreate() {
restTemplate.createIndex(Item.class); // 创建索引
restTemplate.putMapping(Item.class); // 添加映射
}
}
Spring Data 的强大之处,就在于你不用写任何 DAO 处理,自动根据方法名或类的信息进行 CRUD 操作。只要你定义一个接口,然后继承 XxxRepository 提供的一些子接口,就能具备各种基本的 CRUD 功能。
// 不同添加注解,在父接口中已经有了
public interface ItemRepository extends ElasticsearchRepository<Item, Long> {
}
@Autowired
private ItemRepository itemRepository;
// 新增或更新
@Test
public void testAdd() {
Item item = new Item(1L, "小米手机7", " 手机", "小米", 3499.00, "http://image.leyou.com/13123.jpg");
itemRepository.save(item); // id 不存在就添加,已存在就更新
}
批量新增
// 批量新增
@Test
public void testAddList(){
List<Item> list = new ArrayList<>();
list.add(new Item(2L, "坚果手机R1", " 手机", "锤子", 3699.00, "http://image.leyou.com/123.jpg"));
list.add(new Item(3L, "华为META10", " 手机", "华为", 4499.00, "http://image.leyou.com/3.jpg"));
itemRepository.saveAll(list);
}
删除操作
//删除操作
@Test
public void testDelete() {
itemRepository.deleteById(1L);
}
根据 id 查询
// 根据 id 查询
@Test
public void testFindById() {
// 因为我们这里使用的通用接口,不可能返回某个具体的类,所以就使用泛型
Optional<Item> optional = itemRepository.findById(1L);
// 这里的 get() 没有进行空值检测,如果没有查询到结果,就会抛 NoSuchElementException
// 所以我们在实际开发时,要做异常处理
System.out.println(optional.get());
}
查询全部,并排序
// 查询全部,并按照价格降序排列
@Test
public void testFind() {
Iterable<Item> items = itemRepository.findAll(Sort.by(Sort.Direction.DESC, "price"));
items.forEach(System.out::println);
}
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}}}} |