概述
ElasticSearch是什么?
Elasticsearch是一个基于Apache Lucene™的开源搜索引擎。无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
Elasticsearch与Solr的比较
- 二者都是基于Lucene开发;
- Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
- Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式;
- Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供;
- Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
- es 有完整的生态链(Elastic Stack = logstash + es +kibana + beats + ml + security + report) solr 目前还是单搜索引擎
- es查询性能要高于solr。所以在不断动态添加数据的时候,solr的检索效率会变的低下,而es则没有什么变化。所以现在大部分互联网公司转向了es,
安装教程
基本概念
- Relational DB -> Databases -> Tables -> Rows -> Columns
- Elasticsearch -> Indices -> Types -> Documents -> Fields
- 索引 index
Es的索引相当于mysql的一个database,需要注意它与关系型数据库的索引不一样。 - 类型 type
相当于mysql的表
6.0的版本以后不允许一个index下面有多个type - 文档Document
相当于一条数据,es只能存储json文档 - 属性 field
相当与一条数据的每个字段的值,对应着json文档的一个属性
元数据
- _index
代表document存放在哪个index中,index就是索引的名字。index名称必须是小写的,且不能以下划线’’,’-’,’+'开头。 - _type
代表document属于index中的哪个type(类别),就是type的名字。ES6.x版本中,一个index只能定义一个type。结构类似的document保存在一个index中。Type命名要求:字符大小写无要求,不能下划线开头,不能包含逗号。(ES低版本,5.x或更低版本。一般一个索引会划分若干type,逻辑上对index中的document进行细致的划分。在命名上,可以全大写或者全小写,不能下划线开头,不能包含逗号。) - _id
代表document的唯一标识。使用index、type和id可以定位唯一的一个document。id可以在新增document时手工指定,也可以由es自动创建。 - _source
一个doc的原生的json数据,不会被索引,用于获取提取字段值 - _version
代表的是document的版本。在ES中,为document定义了版本信息,document数据每次变化,代表一次版本的变更。版本变更可以避免数据错误问题(并发问题,乐观锁),同时提高ES的搜索效率。
第一次创建Document时,_version版本号为1,默认情况下,后续每次对Document执行修改或删除操作都会对_version数据自增1。
基础增删改查
新增
使用自定义id
格式 PUT /index/type/id
PUT /test/blog/1
{
"title": "My first blog entry",
"content": "Just trying this out special",
"date": "2015/01/01",
"todayCount": 3
}
注意,建议使用postman
如果使用es-head时需要先把elasticsearch-head目录下 _site/vendor.js中content-type的类型从
application/x-www-form-urlencoded改为application/json
使用自动生成的id
格式 POST /index/type
POST /test/blog
{
"title": "My third blog entry",
"content": "Just trying this out special",
"date": "2015/01/03",
"todayCount": 3
}
基本检索
根据id查询
格式 GET /index/type/id
GET /test/blog/1
查询所有
格式 GET /index/type/_search
GET /test/blog/_search
分页查询
和SQL使用 LIMIT M,N关键字返回只有一页的结果一样,Elasticsearch接受 from 和 size 参数:
size
: 结果数,默认 10 。相当于上边limit的N
from
: 跳过开始的结果数,默认 0 。相当于上边limit的M
由于两个默认值的存在,所以我们查询的时候最多返回10条数据。
GET /test/_search?size=5&from=0
轻量查询(字符串查询)
查询字符串搜索非常适用于通过命令行做即时查询。例如,查询在 blog
类型中 title
字段包含 blog
单词的所有文档:
GET /_all/blog/_search?q=title:blog
如果要再排除掉text中包含this的
GET /test/blog/_search?q=content:just -content:this
如果要加上text中包含one的
GET /test/blog/_search?q=content:just +content:one
+
前缀表示在前边的条件符合的情况下加上包含该条件的。类似地, -
前缀表示在前边条件的结果集中删去符合该条件的。
注意:+
,-
号之前要添加一个空格,否则无法识别。
判断是否存在
格式 HEAD /index/type/id
HEAD /test/blog/101
响应体是空的,根据响应码判断,200是存在,404是不存在
需要注意的是,es有写入延迟至少1s,所有有可能你查询的时候不存在,而稍后就存在了
更新
ES的更新其实是先删除,然后重新添加
例如现在 test blog 下存在id为6的文档,执行
PUT /test/blog/6
{
"title": "My blog entry",
"content": "Just trying this out special",
"date": "2015/01/06",
"todayCount": 6
}
响应体中会包含
“result”: "updated“
老的版本可能没有这个,应该都是返回
“created”: false
删除
DELETE /test/blog/6
与exist一样,响应体是空的,根据响应码判断,200是删除成功,404是不存在
多索引多类型查询
/_search
在所有索引的所有类型中搜索
/gb/_search
在索引 gb 的所有类型中搜索
/gb,us/_search
在索引 gb 和 us 的所有类型中搜索
/g*,u*/_search
在以 g 或 u 开头的索引的所有类型中搜索
/gb/user/_search
在索引 gb 的类型 user 中搜索
/gb,us/user,tweet/_search
在索引 gb 和 us 的类型为 user 和 tweet 中搜索
/_all/user,tweet/_search
在所有索引的 user 和 tweet 中搜索 search types user and tweet in all indices
当你搜索包含单一索引时,Elasticsearch转发搜索请求到这个索引的主分片或每个分片的复制分片上,然后聚集每个分片的
结果。搜索包含多个索引也是同样的方式——只不过或有更多的分片被关联。
分布式
副本与分片
查看索引的配置信息
GET /test/_settings
{
"test": {
"settings": {
"index": {
"creation_date": "1595212888934",
"number_of_shards": "1",
"number_of_replicas": "1",
"uuid": "p3UNJhupT2mJpRVv0O39Hw",
"version": {
"created": "7080099"
},
"provided_name": "test"
}
}
}
}
可以看到下面两个重要参数
number_of_shards:分片数量,类似于数据库里面分库分表,一经定义不可更改。主要响应写操作
number_of_replicas:副本数,用于备份分片的,和分片里面的数据保持一致,主要响应读操作,副本越多读取就越快。
分布式索引一定要注意分片数量不能更改,所以在创建的时候一定要预先估算好数据大小,如果调整了分片数那就要重建索引。
为什么分片数量不能修改:
因为ES采取的是分布式索引,假如我们有4篇文档id分别为:1,2,3,4。在数据库的分表中都知道有一个取模算法。用它来分配记录到对应的表中,其实es也采取的是这种思路。
所以13 % 2 ==> 1那么就会在第二个分片上。24 % 2 ==> 0那么就会在第一个分片上。
如果这时候你把分片数变了,很显然数据就不对了。所以分片数一旦变化需要重新全部建索引
主分片和副本分片如何交互
为了阐述意图, 我们假设有三个节点的集群。 它包含一个叫做 blogs 的索引并拥有两个主分片。 每个主分片有两个复制分片。 相同的分片不会放在同一个节点上, 所以我们的集群是这样的:
我们能够发送请求给集群中任意一个节点。 每个节点都有能力处理任意请求。 每个节点都知道任意文档所在的节点, 所以也
可以将请求转发到需要的节点。 下面的例子中, 我们将发送所有请求给 Node 1 , 这个节点我们将会称之为请求节点
(requesting node)
当我们发送请求, 最好的做法是循环通过所有节点请求, 这样可以平衡负载。
主分片和复制分片如何交互
提
数据查询过程
(1)客户端向Node1发送获取请求。
(2)节点使用文档的_id来哈希确定文档属于分片0,分片0的数据在三个节点上都有。 在这种情况下,它会根据负载均衡策略将请求转发到其中一个,比如Node2 。
(3)Node2将文档返回给Node1然后将文档返回给客户端。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。(数据不一致)
数据新增和更新
以下是在主副分片和任何副本分片上面 成功新建和删除文档所需要的步骤顺序:
(1)客户端向Node1发送新建或者删除请求。
(2)节点使用文档的_id确定文档属于分片0。请求会被转发到Node3因为分片 0 的主分片目前被分配在 Node3上。
(3)Node 3在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
集群中的分页
假设在一个有5个主分片的索引中搜索。当我们请求结果的第一页(结果1到10)时,每个分片产生自己最顶端10个结果然后返回它们给请求节点,它再排序这所有的50个结果以选出顶端的10个结果。
现在假设我们请求第1000页——结果10001到10010。这时为了保证排序的准确性,每个分片都必须产生顶端的10010个结果。然后请求节点排序这50050个结果并丢弃50040个!
你可以看到在分布式系统中,排序结果的花费随着分页的深入而成倍增长。这也是为什么一般来说网络搜索引擎中任何语句不能返回多于1000个结果的原因。
数据写入
- 1.分段存储
ES继承了Lucene的分段写入,已经写入到硬盘的段是不可修改的。这么做主要是因为更新索引性能消耗太大,且如果一个文件可读可写的话需要做读写锁的控制,效率会很低。分段写入的话,已经写入的段可以被查询检索,未写入完成的不可被查询检索。如果要修改数据的话需要先新增,然后删除。
- 2.延迟写入
索引信息写入需要首先写入到内存中,然后在达到时间限制或大小限制时再写入到硬盘上。默认是1s写入一次。由于内存中的段信息和硬盘上未写入完成的信息都是不可查询的,所以默认情况下至少需要1s之后才可以查询到新增的索引信息。
- 3.段合并
由于每次写入到硬盘都是大小不一的段,且不会很大。段太多的话会严重影响查询效率,所以需要对写入的段进行整理。ES会在后台自动定时整理段,在整理合并期不会影响查询。
并发控制
ES处理并发是利用_version字段,使用乐观锁机制
经典卖票问题。起始票有100张,然后第一个售票员卖出一张,剩余99张,第二个售票员应该修改为98张余票的,但是第二个售票员在第一个售票员改为99之前就读取了100这个数值,就会导致第二个售票员依然改为99,就会有问题。
如果更新的时候都带上version字段,这样就会只有第一个成功,其他会失败。
在新版本的es中已经弃用了version,改用if_seq_no和if_primary_term两个字段
-
if_seq_no从元数据中当前的_seq_no取值,每个文档新增和修改时这个值都会加1
-
if_primary_term从元数据中当前的_primary_term取值,该值是每个分片一个固定值。
倒排索引
当数据写入 ES 时,数据将会通过 分词 被切分为不同的 term,ES 将 term 与其对应的文档列表建立一种映射关系,这种结构就是 倒排索引。如下所示:
你的文本
T0 = it is what it is
T1 = what is it
T2 = it is a banana
生成的索引
a: {2}
banana: {2}
is: {0,1,2}
it: {0,1,2}
what : {0,1}
- 为了进一步提升索引的效率,ES 在 term 的基础上利用 term 的前缀或者后缀构建了 term index, 用于对 term 本身进行索引,ES 实际的索引结构如下图所示:
- 这样当我们去搜索某个关键词时,ES 首先根据它的前缀或者后缀迅速缩小关键词的在 term dictionary 中的范围,大大减少了磁盘IO的次数。
分词器
分词器顾名思义就是把文本分成一个一个的单词。在倒排索引里面提到的term其实就是分词器对文本分析产生的。
假如现在你有个字段文本是 我是中华人民共和国的公民
使用es的标准分词器
POST /_analyze
{
"analyzer": "standard",
"text" : "我是中华人民共和国的公民"
}
{
"tokens": [
{
"token": "我",
"start_offset": 0,
"end_offset": 1,
"type": "<IDEOGRAPHIC>",
"position": 0
},
{
"token": "是",
"start_offset": 1,
"end_offset": 2,
"type": "<IDEOGRAPHIC>",
"position": 1
},
{
"token": "中",
"start_offset": 2,
"end_offset": 3,
"type": "<IDEOGRAPHIC>",
"position": 2
},
{
"token": "华",
"start_offset": 3,
"end_offset": 4,
"type": "<IDEOGRAPHIC>",
"position": 3
},
{
"token": "人",
"start_offset": 4,
"end_offset": 5,
"type": "<IDEOGRAPHIC>",
"position": 4
},
{
"token": "民",
"start_offset": 5,
"end_offset": 6,
"type": "<IDEOGRAPHIC>",
"position": 5
},
{
"token": "共",
"start_offset": 6,
"end_offset": 7,
"type": "<IDEOGRAPHIC>",
"position": 6
},
{
"token": "和",
"start_offset": 7,
"end_offset": 8,
"type": "<IDEOGRAPHIC>",
"position": 7
},
{
"token": "国",
"start_offset": 8,
"end_offset": 9,
"type": "<IDEOGRAPHIC>",
"position": 8
},
{
"token": "的",
"start_offset": 9,
"end_offset": 10,
"type": "<IDEOGRAPHIC>",
"position": 9
},
{
"token": "公",
"start_offset": 10,
"end_offset": 11,
"type": "<IDEOGRAPHIC>",
"position": 10
},
{
"token": "民",
"start_offset": 11,
"end_offset": 12,
"type": "<IDEOGRAPHIC>",
"position": 11
}
]
}
使用ik分词器之ik_max_word
POST /_analyze
{
"analyzer": "ik_max_word",
"text" : "我是中华人民共和国的公民"
}
响应
{
"tokens": [
{
"token": "我",
"start_offset": 0,
"end_offset": 1,
"type": "CN_CHAR",
"position": 0
},
{
"token": "是",
"start_offset": 1,
"end_offset": 2,
"type": "CN_CHAR",
"position": 1
},
{
"token": "中华人民共和国",
"start_offset": 2,
"end_offset": 9,
"type": "CN_WORD",
"position": 2
},
{
"token": "中华人民",
"start_offset": 2,
"end_offset": 6,
"type": "CN_WORD",
"position": 3
},
{
"token": "中华",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 4
},
{
"token": "华人",
"start_offset": 3,
"end_offset": 5,
"type": "CN_WORD",
"position": 5
},
{
"token": "人民共和国",
"start_offset": 4,
"end_offset": 9,
"type": "CN_WORD",
"position": 6
},
{
"token": "人民",
"start_offset": 4,
"end_offset": 6,
"type": "CN_WORD",
"position": 7
},
{
"token": "共和国",
"start_offset": 6,
"end_offset": 9,
"type": "CN_WORD",
"position": 8
},
{
"token": "共和",
"start_offset": 6,
"end_offset": 8,
"type": "CN_WORD",
"position": 9
},
{
"token": "国",
"start_offset": 8,
"end_offset": 9,
"type": "CN_CHAR",
"position": 10
},
{
"token": "的",
"start_offset": 9,
"end_offset": 10,
"type": "CN_CHAR",
"position": 11
},
{
"token": "公民",
"start_offset": 10,
"end_offset": 12,
"type": "CN_WORD",
"position": 12
}
]
}
使用ik分词器之ik_smart
POST /_analyze
{
"analyzer": "ik_smart",
"text" : "我是中华人民共和国的公民"
}
响应
{
"tokens": [
{
"token": "我",
"start_offset": 0,
"end_offset": 1,
"type": "CN_CHAR",
"position": 0
},
{
"token": "是",
"start_offset": 1,
"end_offset": 2,
"type": "CN_CHAR",
"position": 1
},
{
"token": "中华人民共和国",
"start_offset": 2,
"end_offset": 9,
"type": "CN_WORD",
"position": 2
},
{
"token": "的",
"start_offset": 9,
"end_offset": 10,
"type": "CN_CHAR",
"position": 3
},
{
"token": "公民",
"start_offset": 10,
"end_offset": 12,
"type": "CN_WORD",
"position": 4
}
]
}
从上边我们可以看出来,es自带的standard分词器对中文支持并不友好,他只是将中文文本拆成一个一个的汉字,这显然没什么意义。而ik分词器就比较友好,可以把文本拆分成一些有类似公民
,中华
这样有意义的字段。
ik_max_word和ik_smart的区别
ik_max_word
会将文本做最细粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为“中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、大会堂、大会、会堂等词语。
ik_smart
会做最粗粒度的拆分,比如会将“中华人民共和国人民大会堂”拆分为中华人民共和国、人民大会堂。
两种分词器使用的最佳实践是:索引时用ik_max_word,在搜索时用ik_smart。
即:索引时最大化的将文章内容分词,搜索时更精确的搜索到想要的结果。
举个例子:
我是个用户,输入“华为手机”,我此时的想法是想搜索出“华为手机”的商品,而不是华为其它的商品,也就是商品信息中必须只有华为手机这个词。
此时使用ik_smart和ik_max_word都会将华为手机拆分为华为和手机两个词,那些只包括“华为”这个词的信息也被搜索出来了,我的目标是搜索只包含华为手机这个词的信息,这没有满足我的目标。
怎么解决呢?我们可以将华为手机添加到自定义词库,添加后两个分词器的效果为:
ik_max_word 的分词效果为:华为手机,华为,手机
ik_smart的分词效果:华为手机
因为华为手机是一个词,所以ik_smart不再细粒度分了。
此时,我们可以在索引时使用 ik_max_word,在搜索时用ik_smart。
映射
映射信息相当于mysql的表结构。它指定每个域(字段)的域类型(之所以叫做域类型,是因为es中称字段为域),每种域类型的索引方式等会有所不同。
在es中,索引和映射是一对一的关系,且不同索引下的映射互不影响。
简单域类型
Elasticsearch 支持如下简单域类型:
- 字符串:
string
,text
,keyword
。 string存在于5.0版本之前。5.0版本之后改为了 text和keyword类型。 - 整数 :
byte
,short
,integer
,long
- 浮点数:
float
,double
- 布尔型:
boolean
- 日期:
date
如果你添加一个新的类型的文档, Elasticsearch 会通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:
JSON type | 域 type |
---|---|
布尔型: true 或者 false | boolean |
整数: 123 | long |
浮点数: 123.45 | double |
字符串,有效日期: 2014-09-15 | date |
字符串: foo bar | string ,(5.0之前)text ,(5.0以及之后),且会添加一个keyword的索引方式 |
text
text类型用于索引全文文本,例如电子邮件的正文或产品的描述。 ES会对text字段进行分词然后建立倒排索引。其他的数字,日期类型则不会进行分词,会直接进行索引。但是text类型字段不用于排序。
keyword
用于索引结构化的数据的字段,比如 email 地址、主机名、状态码、邮政编码或者标签,通常用于过滤、排序、聚合。keyword 字段只能通过精确值来搜索。
自定义域映射
一般基本域数据类型已经够用,但字符串域经常需要自定义映射。自定义映射允许你执行下面的操作:
- 全文字符串域和精确值字符串域的区别
- 使用特定语言分析器
- 优化域以适应部分匹配
- 指定自定义数据格式
域映射的参数有很多,这里讲几个最重要的参数。具体可参考ElasticSearch 6.2 Mapping参数说明
type
指定域的域类型。一般除了text类型的只需要设置 type
即可。例如:
{
"count": {
"type": "long"
}
}
analyzer
指定在创建倒排索引时使用的分析器。默认, Elasticsearch 使用 standard
分析器, 也可以指定为你安装的ik分词器,比如ik_max_word
、ik_smart
{
"content": {
"type": "text",
"analyzer": "ik_max_word"
}
}
format
用来指定date域的时间格式,例如配置为"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
fields
可以对一个字段提供多种索引模式,使用text类型做全文检索,也可使用keyword类型做聚合和排序。
ignore_above
字段指如果该字段长度超过该设置将不再进行索引。
{
"content": {
"type": "text",
"fields": {
"key": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
search_analyzer
指定查询分词器 例如:ik_smart
{
"content": {
"type": "text",
"search_analyzer": "ik_max_word"
}
}
更新映射
当你首次创建一个索引的时候,可以指定类型的映射。你也可以使用 /index/_mapping
为新类型(或者为存在的类型更新映射)增加映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。因为如果你修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。
例如:新建一个test索引,查看它当前的映射为
GET /test/_mapping
{
"test": {
"mappings": {
"properties": {
"content": {
"type": "text",
"fields": {
"key": {
"type": "keyword",
"ignore_above": 128
}
}
},
"date": {
"type": "date",
"format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
},
"title": {
"type": "text",
"fields": {
"key": {
"type": "keyword",
"ignore_above": 128
},
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"todayCount": {
"type": "long"
}
}
}
}
}
如果我们要新增一个域字段为摘要(summary)域类型为text,使用ik_max_word分词器
PUT /test/_mapping
{
"properties": {
"summary": {
"type": "text",
"analyzer":"ik_max_word"
}
}
}
索引
创建索引
前面我们建立的索引是通过直接创建文档时es自动创建索引的。这种情况下索引会使用默认配置。如果我们要在索引创建时就设定好一些配置的话,就需要我们自己手动去创建索引。
索引的主要配置有
-
number_of_shards
每个索引的主分片数,默认值是
5
(从7.x版本开始默认为1
)。这个配置在索引创建后不能修改。 -
number_of_replicas
每个主分片的副本数,默认值是
1
。这个配置可以随时修改。创建索引的时候我们可以指定好映射信息。例如,创建一个4分片1副本的映射index1。
PUT /index1
{
"settings": {
"number_of_shards": "4",
"number_of_replicas": "1"
},
"mappings": {
"properties": {
"date": {
"format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis",
"type": "date"
},
"summary": {
"analyzer": "ik_max_word",
"type": "text"
},
"todayCount": {
"type": "long"
},
"title": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
},
"key": {
"ignore_above": 128,
"type": "keyword"
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 128,
"type": "keyword"
}
}
}
}
}
}
注意 在es的7.x版本,这样建立的索引会有一个默认的类型_doc
类型,如果想自定义类型,则需要在配置文件中配置 include_type_name: true ,但是不建议这么做,因为8.x版本中会删除该配置。建议以后不要用多类型检索,将索引名命名规范起来,只用多索引检索也可达到以往多类型多索引的效果。
删除索引
用以下的请求来 删除索引:
DELETE /my_index
你也可以这样删除多个索引:
DELETE /index_one,index_two
DELETE /index_*
你甚至可以这样删除全部索引:
DELETE /_all
DELETE /*
如果你想要避免意外的大量删除, 你可以在你的 elasticsearch.yml
做如下配置:
action.destructive_requires_name: true
这个设置使删除只限于特定名称指向的数据, 而不允许通过指定 _all
或通配符来删除指定索引
同一索引下多type弃用的原因
Lucene 没有文档类型的概念,Elasticsearch是将每个文档的类型名被存储在一个叫 _type
的元数据字段上。 当我们要检索某个类型的文档时, Elasticsearch 通过在 _type
字段上使用过滤器限制只返回这个类型的文档。
例如有一个索引有人类和电脑两个类型,人类有name、hairStyle、hobby三个属性,电脑有name、cpu、price三个属性,人不可能有cpu和price属性,电脑也不会有hairStyle和hobby。
在es上设置的mapping信息
{
"mappings": {
"person": {
"properties": {
"name": {
"type": "string"
},
"hairStyle": {
"type": "string"
},
"hobby": {
"type": "string"
}
}
},
"pc": {
"properties": {
"name": {
"type": "string"
},
"cpu": {
"type": "string"
},
"price": {
"type": "double"
}
}
}
}
}
实际上在lucene存储时的结构
{
"_type": {
"type": "string",
"index": "false"
},
"name": {
"type": "string"
},
"hairStyle": {
"type": "string"
},
"hobby": {
"type": "string"
},
"cpu": {
"type": "string"
},
"price": {
"type": "double"
}
}
如果你分别插入一条人和电脑的数据在Lucene中存储的将是
{
"_type": "person",
"name": "张三",
"hairStyle": "短发",
"hobby": "唱歌",
"cpu": "",
"price": ""
}
{
"_type": "pc",
"name": "win-001",
"hairStyle": "",
"hobby": "",
"cpu": "i9-9505",
"price": "9999.99"
}
这样就会造成大量的空值,造成资源浪费,且会导致搜索效率下降。所以es弃用了同一索引下多类型的用法。
请求体查询
先看下查询所有索引库(indices)中的所有文档:
GET /_search
{}
同理,多类型多索引
GET /test*/blog/_search
{}
请求体结构
请求体查询的结构基本如下:
{
"from": 1,
"size": 2,
"query": {
},
"sort": {
"title.key": {
"order": "asc"
}
},
"_source": [
"title",
"date",
"todayCount"
]
}
参数说明:
from :查询时跳过前多少条
size : 查询多少条数据
query:条件查询,里面可以根据自己的需求组合一些查询子句。后续再展开讲
sort :排序规则,支持多个字段排序,只需要把sort改成数组类型即可。注意,1. text类型字段不支持排序,但是前边也提到过,我们可以给text字段添加多种索引方式,添加一个keyword索引方式后就可以了。2.指定排序规则后返回的数据将不会有评分,将会返回_score=null
_source:指定返回文档的哪些字段。
查询子句
query可以由以下这些子句进行组合或单独使用。
match
如果你在一个全文字段(text
域类型)上使用 match
查询,在执行查询前,它将用正确的分析器去分析查询字符串
{ "match": { "title": "My first" }}
如果在一个精确值的字段上使用它,例如数字、日期、布尔字符串字段,那么它将会精确匹配给定的值:
{ "match": { "todayCount": 3 }}
{ "match": { "date": "2015/01/01" }}
multi_phrase
精确查询全文文本中是否包含某个字段(相当于sql中的 %str%)
{
"query": {
"match_phrase": {
"title": "third blog"
}
}
}
multi_match
multi_match
查询可以在多个字段上执行相同的 match
查询:
{
"multi_match": {
"query": "first out",
"fields": ["title","content"]
}
}
range
range
查询找出那些落在指定区间内的数字或者时间:
{
"range": {
"todayCount": {
"gte": 5,
"lt": 100
}
}
}
被允许的操作符如下:
-
gt
大于 -
gte
大于等于 -
lt
小于 -
lte
小于等于
term
term
查询被用于精确值匹配,这些精确值可能是数字、时间、布尔:
{ "term": { "age": 26 }}
{ "term": { "date": "2014-09-01" }}
{ "term": { "public": true }}
{ "term": { "tag": "full_text" }}
term
查询text
类型时将会根据传入的字符串去根据倒排索引中查询。例如
GET /test/_search
{
"query": {
"term": {
"title": "third"
}
}
}
查询结果
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 0.7549127,
"hits": [
{
"_index": "test",
"_type": "bolg",
"_id": "3",
"_score": 0.7549127,
"_source": {
"title": "My third blog entry",
"content": "Just trying this out special",
"date": "2015/01/03",
"todayCount": 3
}
},
{
"_index": "test",
"_type": "bolg",
"_id": "ZBUcMnYBTtNpnhXBmnoH",
"_score": 0.55654144,
"_source": {
"title": "My third blog entry 我的博客",
"content": "Just trying this out special",
"date": "2015/01/03",
"todayCount": 3
}
}
]
}
}
如果我们查询条件为
{
"query": {
"term": {
"title": "My"
}
}
}
查询结果
{
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 0,
"relation": "eq"
},
"max_score": null,
"hits": []
}
}
在我们的文档里面的title字段都是有My
这个字符串的,那么为什么这里查询会为空呢?
其实这个就是因为ik分析器在分析title字段时,将所有的英文单词都转为了小写的关键字去建立索引,然而我们在使用term子句查询时,es不会对我们的查询条件的字符串进行分析,而是直接去查询倒排索引,导致查询不到结果。
可以看下 ik分析My third blog entry 我的博客
这个字符串的结果。
GET /_analyze
{
"analyzer": "ik_max_word",
"text" : "My third blog entry 我的博客"
}
分析结果
{
"tokens": [
{
"token": "my",
"start_offset": 0,
"end_offset": 2,
"type": "ENGLISH",
"position": 0
},
{
"token": "third",
"start_offset": 3,
"end_offset": 8,
"type": "ENGLISH",
"position": 1
},
{
"token": "blog",
"start_offset": 9,
"end_offset": 13,
"type": "ENGLISH",
"position": 2
},
{
"token": "entry",
"start_offset": 14,
"end_offset": 19,
"type": "ENGLISH",
"position": 3
},
{
"token": "我",
"start_offset": 20,
"end_offset": 21,
"type": "CN_CHAR",
"position": 4
},
{
"token": "的",
"start_offset": 21,
"end_offset": 22,
"type": "CN_CHAR",
"position": 5
},
{
"token": "博客",
"start_offset": 22,
"end_offset": 24,
"type": "CN_WORD",
"position": 6
}
]
}
同样的道理,我们如果使用{“term”: {“title”: “的博客” }}这个条件也是查询不到结果的。如果我们想要查询出来的话应该使用match_phrase
子句。如果使用{“term”: {“title”: “我” }},或者{“term”: {“title”: “my” }}是可以的。
terms
terms
查询和 term
查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:
{
"query": {
"terms": {
"title": [
"first",
"second"
]
}
}
}
和 term
查询一样,terms
查询对于输入的文本不分析。
exists 和 missing
exists
查询和 missing
查询被用于查找那些指定字段中有值 (exists
) 或无值 (missing
) 的文档。相当于SQL中的 IS_NULL
(missing
) 和 NOT IS_NULL
(exists
)
{
"query": {
"exists": {
"field": "title"
}
}
}
注意:missing在5.x版本以后被取消了。但可以用must_not exists组合来替代missing。
bool查询
现实的查询需求从来都没有那么简单;它们需要在多个字段上查询多种多样的文本,并且根据一系列的标准来过滤。为了构建类似的高级查询,你需要一种能够将多查询组合成单一查询的查询方法。
你可以用 bool
查询来实现你的需求。这种查询将多查询组合在一起,成为用户自己想要的布尔查询。它接收以下参数:
-
must
文档 必须 匹配这些条件才能被查询出来。 -
must_not
文档 必须不 匹配这些条件才能被查询出来。 -
should
如果满足这些语句中的任意语句,将增加_score
,否则,无任何影响。它们主要用于修正每个文档的相关性得分。注意:如果没有
must
语句,那么至少需要能够匹配其中的一条should
语句。 -
filter
必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。
注:每一个子查询都独自地计算文档的相关性得分。一旦他们的得分被计算出来, bool
查询就将这些得分进行合并并且返回一个代表整个布尔操作的得分。
格式:
{
"query": {
"bool": {
"must": {//如果must中只有一个查询子句,这里用{}即可
"match": {
"title": "blog"
}
},
"should": [//如果包含多个子句,需要用数组形式,即[]
{
"match": {
"title": "third"
}
},
{
"range": {
"date": {
"gt": "2015/01/01"
}
}
}
],
"filter": {
"bool": {//可以嵌套bool查询
"must": [//同样可以用{}或[]
{
"match": {
"title": "my"
}
}
]
}
}
}
}
}
下面的查询用于查找 title
字段匹配 third blog
并且不包含 博客
的文档。那些在2015-01-01之后的文档,将比其他文档拥有更高的排名。
GET /test/_search
{
"query": {
"bool": {
"must": {
"match": {
"title": "third blog"
}
},
"must_not": {
"match": {
"title": "博客"
}
},
"should": [
{
"range": {
"date": {
"gt": "2015/01/01"
}
}
}
]
}
}
}
响应
{
"took": 7,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.515991,
"hits": [
{
"_index": "test",
"_type": "bolg",
"_id": "3",
"_score": 1.515991,
"_source": {
"title": "My third blog entry",
"content": "Just trying this out special",
"date": "2015/01/03",
"todayCount": 3
}
},
{
"_index": "test",
"_type": "bolg",
"_id": "1",
"_score": 1.0758171,
"_source": {
"title": "My first blog entry",
"content": "Just trying this out one",
"date": "2015/01/02",
"todayCount": 10
}
},
{
"_index": "test",
"_type": "bolg",
"_id": "2",
"_score": 0.0758171,
"_source": {
"title": "My second blog entry",
"content": "Just trying this out two",
"date": "2015/01/01",
"todayCount": 30
}
}
]
}
}
从上边的结果可以看出 包含third blog的文档比 first blog和second blog分值要高,是因为它的相关性更强。
first blog比second blog 分值高是因为first 的date满足了should里面的时间>2015/01/01的要求。
如果我们不想某个条件影响得分,可以用 filter
语句:
{
"query": {
"bool": {
"must": {
"match": {
"title": "third blog"
}
},
"must_not": {
"match": {
"title": "博客"
}
},
"should": [
{
"range": {
"date": {
"gt": "2015/01/01"
}
}
}
],
"filter": {
"range": {
"todayCount": {
"gt": 1
}
}
}
}
}
}
如果你需要通过多个条件过滤文档,且不能影响评分,则可以使用在filter语句中嵌套bool语句的方式
{
"query": {
"bool": {
"must": {
"match": {
"title": "third blog"
}
},
"must_not": {
"match": {
"title": "博客"
}
},
"should": [
{
"range": {
"date": {
"gt": "2015/01/01"
}
}
}
],
"filter": {
"bool": {
"must": [
{
"range": {
"todayCount": {
"gt": 1
}
}
},
{
"match": {
"title": "my"
}
}
],
"must_not": {
"match": {
"title": "地"
}
}
}
}
}
}
}
constant_score 查询
constant_score
查询将一个常量评分应用于所有匹配的文档。它被经常用于你只需要执行一个 filter 而没有其它查询(例如,不需要进行评分)的情况下。这样查询效率一般会更快。
{
"query": {
"constant_score": {
"filter": {
"term": {
"title": "blog"
}
}
}
}
}
查询与过滤
性能差异
过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来非常快。并且经常使用不评分查询(non-scoring queries),结果会被缓存到内存中以便快速读取,所以有各种各样的手段来优化查询结果。
相反,评分查询(scoring queries)不仅仅要找出匹配的文档,还要计算每个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。
在一般情况下,一个filter 会比一个评分的query性能更优异,并且每次都表现的很稳定。
如何选择查询与过滤
通常,使用查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。
聚合
桶
桶(Bucket)其实相当于sql中的分组,例如将车根据颜色分组,就会有黑色汽车桶,白色汽车桶等…
指标
指标(Metrics)相当于sql中的聚合函数,例如求平均值,最大值,最小值等
消息体格式
请求
{
"aggs": {
"自定义聚合名称(可以看作是我们sql中的别名)": {
"分组或指标(分组:terms,指标:avg,sum,max 等)": {
"field": "根据哪个字段分组,或计算哪个字段的avg,sum,max"
},
"aggs": {
"聚合1": {},
"聚合2": {}
}
}
}
}
响应
{
"aggregations": {
"自定义聚合名称": {
"buckets": [
{
"key": "分组的值,如果按照颜色分组,这里就会是 黑色,白色等",
"doc_count": "该分组下的文档数量",
"嵌套的自定义聚合名称": {
"value": "指标的值"
},
"嵌套的自定义聚合名称": {
"buckets": [
]
}
}
]
},
"自定义聚合名称": {
"value": "指标的值"
}
}
}
注意:分组或指标这一层,只能用一个指标或者terms,不能用多个指标或者terms + 指标。但可以使用指标+aggs或者 terms + aggs 。所以,返回体中value和buckets也不会同在同一层的聚合中同时出现。
分组
GET /cars/_search
{
"size" : 0,
"aggs" : {
"group_by_color" : {
"terms" : {
"field" : "color"
}
}
}
}
分组并计算平均值
GET /cars/_search
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
嵌套桶查询
{
"size": 0,
"aggs": {
"group_by_color": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
},
"group_by_make": {
"terms": {
"field": "make"
}
}
}
}
}
}
获取每组的前n个文档
{
"size": 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"top2": {
"top_hits": {
"size": 2,
"sort": {
"price":"desc"
}
}
}
}
}
}
}
区间统计 histogram
{
"size" : 0,
"aggs":{
"price_interval":{
"histogram":{
"field": "price",
"interval": 20000
},
"aggs":{
"interval_sum": {
"sum": {
"field" : "price"
}
}
}
}
}
}
interval 是我们设定的每个区间的范围大小。
返回值中的key是每个范围的起点值。
时间类型区间统计 date_histogram
{
"size": 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "month",
"format": "yyyy-MM-dd",
"min_doc_count": 2
}
}
}
}
interval指定区间范围,可填 year,quarter,month,week,day,hour,minute,second
format 日期格式化
min_doc_count 每个区间的最少文档数,小于该值,则不展示出来