前言
-
本文章适用于未接触ES或接触较少的中高级开发工程师,以较低的学习成本,快速学习ES并在生产中应用为核心目的
-
本文章主要以实战维度展开,在不影响数据安全以及基本的性能危机的前提下,不会过多的涉及深层次的底层原理(但也会涉及一些基本的原理,防止出现类似功能分辨不清导致系统性能受到极大的损耗的生产事故)
-
本文章借鉴了多个ES相关文档以及书籍《ES权威指南》,同时也进行了很多加工处理,例如: 横向DB对比、生涩知识点拆解为更通俗易懂的语言、以实战为主的知识点汇总、使用不当导致的风险点预知、涉及的编程思想等。希望能对大家在ES的学习上起到一定帮助。
环境
-
jdk - v1.8
-
ES - v7.x
目录
-
使用场景
-
存储结构
-
常用语句
-
API
-
索引
-
映射
-
更新
-
查询
-
API查询
-
DSL查询
使用场景
-
数据库ES、Mysql、Mongo等,核心是围绕CRUD。其中各DB的核心特性又取决于其存储结构。这里通过类比的方式来阐述ES的使用场景
-
Mysql核心存储结构是B+树,擅长范围查询(因B+树数据存储在叶子节点并用链表的方式相互关联,有利于做范围类查询操作)
-
Mongo核心存储结构是B树,擅长单数据查询(因B树任意节点都可以存储数据,这样在根据条件查询一条数据时,就会比mysql的随机IO次数平均值小)
-
ES核心存储结构是倒排索引(倒排索引原理后续会讲到),由于是属于重度空间换时间结构,所以在合理使用的前提下(这里特指结构化查询和过滤),范围、单数据等查询都具有不错的性能表现,但是相对的更新数据成本会更高。
-
综上所述,ES更适合写少读多的场景。并且对各种复杂查询都有良好的支持。
存储结构
-
ES使用的是倒排索引,和正排索引通过key查询value不同,倒排索引是通过value反向查询key。
-
例如,当进行关键词查询时,如果是正向索引需要扫描索引库中的所有文档,找到所有包含关键词的文档。很显然这样性能会成为问题。而倒排索引则是根据关键词去寻找对应文档。每个关键词都对应这一系列文档,这样只要建立了该关键词的索引,就可以避免全索引库扫描的方式来进行关键词查询。
-
那么ES又是如何生成索引的呢,举个例子,这里有两个文档,1: “小红明天去上学”,2: “小白明天不去上学”,这时ES在添加数据时,会进行分词从而得到“小红”、“小白”、“明天”、“明天”、“去”、“不去”、“上学”,“上学”各词组。然后我们会发现,有2个明天和2个上学。那么这个时候ES会将各词组当作索引项,对应的是该词组出现的文档,例如:
小红: 文档1
小白: 文档2
明天: 文档1、文档2
...
这样就生成了倒排索引,这种索引结构,在2例的场景下可以极大的提高搜索性能。
常用语句(基于kibana)
API
-
Java Api
-
基于HTTP协议,以JSON为数据交互格式的RESTful API
索引(index)
-
ES中的索引(index)类比关系型数据库中table的概念(5.0版本是以type对应关系型数据库 table的概念,7.0后取消了多type概念,最多一个,建立index时如果不设置会默认创建一个type且最多一个type)
-
建立索引: PUT /{index_name} (不可以有大写)
映射(mapping)
-
ES中的映射能力主要是为了给字段设定类型。虽然ES可以在索引有新字段的文档时会自动基于json类型规则生成映射,但是为了防止查询混乱的情况,我们一般会选择手动建立映射。例如: 当索引一个带有引号的数字”22”,这时他将会被映射为string类型。如果这时字段已经被映射为long类型,es将会尝试转换字符串为long,并在失败时会抛出异常。
-
建立映射语句:
PUT /{index_name}
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}
}
字段释义:
mappings, 映射
properties, 特性定义
type, 字段类型
PS: 此处还可设置分词器插件等相关属性
更新
添加文档: POST
_index、_type、_id三者唯一确定一个文档,所以想要保证文档是新加入的,最简单的方式是使用POST方法让ES自动生成唯一_id(type 7.0后默认为_doc)
POST /{index_name}/_doc
{
"studentName": "小明",
"age": 22
}
如果想自定义ID则可以使用_create关键字(只有在index+type+id均不存在时才会执行成功)
PUT /{index_name}/_doc/{id}/_create
{
"studentName": "小红",
"age": 22
}
-
更新文档:
全局更新: PUT, 在ES中,文档不能通常意义上的局部修改,只可以通过重建索引或者替换来实现文档更新,实现过程如下:
-
从旧文档中检索JSON
-
修改它
-
删除旧文档
-
索引新文档
PUT /{index_name}/_doc/{id}
{
"studentName": "小白",
"age": 12
}
局部更新: POST, 在使用层次ES提供了局部更新的能力,但是在ES内部中,依然遵循着上述的四个步骤,不过即便是这样,也一定程度上节约了网络IO和解决了并发问题(换句话说可以理解为ES将局部更新做了原子化处理)。因为如果没有这个能力,我们想要实现局部更新就不得不先将文档查询出来,然后将修改好的数据再通过PUT API请求发送到ES。
POST /user/_doc/1/_update
{
"doc": {
"name": "李四"
}
}
PS: doc为type值,由于7.0后取消了多type机制,所以当没有设置type时,这里默认为doc
查询
HTTP查询语句部分:
根据索引名称、类型和文档ID查询:
GET /{index_name}/_doc/{id}
1. index_name: 索引名称
2. type: 类型(7.0版本后与index_name为1对1关系)
3. id: 文档id
查询一个索引下的所有数据:
GET /{index_name}/_doc/_search
PS: _search:标记为查询操作的关键字,添加此关键字后,可进行更细粒度的查询操作。
条件查询
在请求中依旧使用 _search 关键字,然后将查询语句传递给参数 q= 。这样就可以得到所有field_name为field_value的结果.
GET /{index_name}/_doc/_search?q={field_name}:{field_value}
结构化查询和结构化过滤(DSL语句):
-
结构化查询:查询能力更强,具有相关性分析能力,整体查询原理更为动态,但是因为会实时计算相关性、分析等行为,无法进行缓存操作,导致性能相对较慢
-
结构化过滤:进行精确相等性匹配过滤然后存入内存,因为是完全基于索引并且使用了缓存,所以在大量级场景下具有稳定的性能。
-
使用建议:原则上来说,使用查询语句做全文本搜索或其他需要进行相关性评分的时候,剩下的全部用过滤语句
常用查询语句和过滤语句:
query: _search关键字中DSL语句最外层关键字,且_search下只能有query
GET /{index_name}/_doc/_search
{
"query":{
...
}
}
term - 过滤类语句:主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型)
GET /{index_name}/_doc/_search
{
"query":{
{
"term": {
"age": 26
}
}
}
}
terms - 过滤类语句: terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配:
GET /{index_name}/_doc/_search
{
"query": {
"terms": {
"age":[26, 33]
}
}
}
range - 过滤类语句: 允许我们按照指定范围查找一批数据
GET /{index_name}/_doc/_search
{
"query": {
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
}
PS:范围操作符包含:
gt: 大于
gte: 大于等于
lt: 小于
lte: 小于等于
exists 和 missing - 过滤类语句: 可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的 IS_NULL 条件
GET /{index_name}/_doc/_search
{
"query": {
"exists": {
"field": "name"
}
}
}
PS: 这两个过滤只是针对已经查出一批数据来,但是想区分出某个字段是否存在的时候使用。
bool - 过滤类语句: 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含以下操作符:
1. must:多个查询条件的完全匹配,相当于 and
2. must_not:多个查询条件的相反匹配,相当于 not 。
3. should:至少有一个查询条件匹配, 相当于 or 。
GET /{index_name}/_doc/_search
{
"query": {
"bool": {
"must": {"term": {"studentName": "张三"}},
"nust_not": {"term": {"age": 22}},
"should": [
{"term": {"hasMan": true}},
{"term": {"hasMonitor": false}}
]
}
}
}
match_all - 查询类语句: 可以查询到所有文档,是没有查询条件下的默认语句
GET /{index_name}/_doc/_search
{
"query": {
"match_all": {
}
}
}
match - 查询类语句: 匹配查询,全文(Fulltext)查询
不同于词条查询ES在处理全文查询时,会先对查询条件进行分析,然后根据分词插件做分词处理,最终以分词结果进行查询并返回查询结果。例如:
{
"query": {
"match": {
"msg": "Hello world"
}
}
}
如上所述, 在执行"hello world"查询条件时,会先进行分析器进行分词得到小写的hello和world,然后默认会执行字段“msg”的值,在hello和world中包含任意一个,则返回的逻辑。当然,这个是“or”逻辑就也可以设置为“and”逻辑。
PS: 此处包含有三种类型的匹配查询(默认为布尔,上述就是布尔匹配查询类型的运行逻辑,由于此处展开讲内容量较大,涉及较多机制设定、原理等问题,入门指南不做讲解,后续进阶系列会详细描述):
布尔
短语
短语前缀
PS:需要注意的是,如果是做精确查询,因为过滤类语句会进行缓存,所以最好使用过滤类语句。
multi_match - 查询类语句: 允许你做 match 查询的基础上同时搜索多个字段
{
"query": {
"multi_match": {
"query": "123",
"fields": [
"name",
"age"
]
}
}
}
bool - 查询类语句: bool 查询与 bool 过滤相似,用于合并多个查询子句。不同的是, bool 过滤可以直接给出是否匹配成功, 而 bool 查询 要计算每一个查询子句的 _score (相关性分值)。(此处摘自: ES权威指南)
1. must:查询指定文档一定要被包含。
2. must_not:查询指定文档一定不要被包含。
3. should:查询指定文档,有则可以为文档相关性加分。
4. 以下查询将会找到 title 字段中包含 "how to make millions",并且 "tag" 字段没有被标为 spam。如果有标识为 "starred",或者发布日期为2014年之前,那么这些匹配的文档将比同类网站等级高
{
"bool": {
"must": {
"match": {
"title": "how to make millions"
}
},
"must_not": {
"match": {
"tag": "spam"
}
},
"should": [
{
"match": {
"tag": "starred"
}
},
{
"range": {
"date": {
"gte": "2014-01-01"
}
}
}
]
}
}
PS: 如果 bool 查询下没有 must 子句,那至少应该有一个 should 子句。但是 如果有 must 子句,那么没 有 should 子句也可以进行查询。
查询与过滤条件的合并:
因为search API中只能包含query语句,所以我们需要用filtered来同时包含query和filter子句:
GET /{index_name}/_doc/_search
{
"query": {
"filtered": {
"query": {
"match": {
"email": "business opportunity"
}
},
"filter": {
"term": {
"folder": "inbox"
}
}
}
}
}
PS: 在混合使用时需要注意的是,我们很少用到的过滤语句中包含查询,保留这种用法只是为了语法的完整性。 只有在过滤中用到全文本匹配的时候才会使用这种结构。