《读书报告 – Elasticsearch入门 》
'
第四章 分布式文件存储
这章的主要内容是理解数据如何在分布式系统中存储。
4.1 路由文档到分片
创建一个新文档时,它是如何确定应该存储在分片1还是分片2上的呢?
这个过程不是随机的,因为将来要检索文档。事实上,它根据一个简单的算法决定:
shard = hash(routing) % number_of_primary_shards
routing
值是一个任意字符串,它默认是_id
但也可以自定义。这个routing
字符串通过哈希函数生成一个数字,然后除以主切片的数量得到一个余数(remainder),余数的范围永远是0
到number_of_primary_shards - 1
,这个数字就是特定文档所在的分片。
这也解释了为什么主分片的数量只能在创建索引时定义且不能修改:如果主分片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了。
所有的文档API(get
、index
、delete
、bulk
、update
、mget
)都接收一个routing
参数,它用来自定义文档到分片的映射。自定义路由值可以确保所有相关文档——例如属于同一个人的文档——被保存在同一分片上。
4.2 主分片和复制分片的交互
假设有三个节点的集群Node 1
、Node 2
、Node 3
。它包含一个叫做blogs
的索引并拥有两个主分片。每个主分片有两个复制分片。相同的分片不会放在同一个节点上。
我们能够发送请求给集群中任意一个节点。每个节点都有能力处理任意请求。每个节点都知道任意文档所在的节点,所以也可以将请求转发到需要的节点。下面的例子中,我们将发送所有请求给Node 1
,这个节点我们将会称之为请求节点(requesting node)
>
当我们发送请求,最好的做法是循环通过所有节点请求,这样可以平衡负载。
4.3 新建、索引和删除文档
新建、索引和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上。
下面是在主分片和复制分片上成功新建、索引或删除一个文档必要的顺序步骤:
- 客户端给
Node 1
发送新建、索引或删除请求。 - 节点使用文档的
_id
确定文档属于分片0
。它转发请求到Node 3
,分片0
位于这个节点上。 -
Node 3
在主分片上执行请求,如果成功,它转发请求到相应的位于Node 1
和Node 2
的复制节点上。当所有的复制节点报告成功,Node 3
报告成功到请求的节点,请求的节点再报告给客户端。
客户端接收到成功响应的时候,文档的修改已经被应用于主分片和所有的复制分片。这时修改就生效了。
- consistency
默认主分片在尝试写入时需要规定数量(quorum)或过半的分片(可以是主节点或复制节点)可用。这是防止数据被写入到错的网络分区。规定的数量计算公式如下:
int( (primary + number_of_replicas) / 2 ) + 1
consistency
允许的值为one
(只有一个主分片),all
(所有主分片和复制分片)或者默认的quorum
或过半分片。
注意number_of_replicas
是在索引中的设置,用来定义复制分片的数量,而不是现在活动的复制节点的数量。如果你定义了索引有3个复制节点,那规定数量是:
int( (primary + 3 replicas) / 2 ) + 1 = 3
但如果只有2个节点,那你的活动分片不够规定数量,也就不能索引或删除任何文档。
-timeout
当分片副本不足时会怎样?Elasticsearch会等待更多的分片出现。默认等待一分钟。如果需要,你可以设置timeout
参数让它终止的更早:100
表示100毫秒,30s
表示30秒。
新索引默认有
1
个复制分片,这意味着为了满足quorum
的要求需要两个活动的分片。当然,这个默认设置将阻止我们在单一节点集群中进行操作。为了避开这个问题,规定数量只有在number_of_replicas
大于一时才生效。
4.4 检索文档
文档能够从主分片或任意一个复制分片被检索。
下面是在主分片或复制分片上检索一个文档必要的顺序步骤:
- 客户端给
Node 1
发送get请求。 - 节点使用文档的
_id
确定文档属于分片0
。分片0
对应的复制分片在三个节点上都有。此时,它转发请求到Node 2
。 -
Node 2
返回document给Node 1
然后返回给客户端。
可能的情况是,一个被索引的文档已经存在于主分片上却还没来得及同步到复制分片上。这时复制分片会报告文档未找到,主分片会成功返回文档。一旦索引请求成功返回给用户,文档则在主分片和复制分片都是可用的。
4.5 局部更新文档
update
API 结合了之前提到的读和写的模式。
下面是执行局部更新必要的顺序步骤:
- 客户端给
Node 1
发送更新请求。 - 它转发请求到主分片所在节点
Node 3
。 -
Node 3
从主分片检索出文档,修改_source
字段的JSON,然后在主分片上重建索引。如果有其他进程修改了文档,它以retry_on_conflict
设置的次数重复步骤3,都未成功则放弃。 - 如果
Node 3
成功更新文档,它同时转发文档的新版本到Node 1
和Node 2
上的复制节点以重建索引。当所有复制节点报告成功,Node 3
返回成功给请求节点,然后返回给客户端。
update
API还接受《新建、索引和删除》章节提到的routing
、replication
、consistency
和timout
参数。
基于文档的复制
当主分片转发更改给复制分片时,并不是转发更新请求,而是转发整个文档的新版本。记住这些修改转发到复制节点是异步的,它们并不能保证到达的顺序与发送相同。如果Elasticsearch转发的仅仅是修改请求,修改的顺序可能是错误的,那得到的就是个损坏的文档。
4.6 多文档模式
mget
和bulk
API与单独的文档类似,差别是请求节点知道每个文档所在的分片。它把多文档请求拆成每个分片的对文档请求,然后转发每个参与的节点。
一旦接收到每个节点的应答,然后整理这些响应组合为一个单独的响应,最后返回给客户端。
-mget
通过一个mget
请求检索多个文档的顺序步骤:
- 客户端向
Node 1
发送mget
请求。 -
Node 1
为每个分片构建一个多条数据检索请求,然后转发到这些请求所需的主分片或复制分片上。当所有回复被接收,Node 1
构建响应并返回给客户端。
routing
参数可以被docs
中的每个文档设置。
-bulk
使用一个bulk
执行多个create
、index
、delete
和update
请求的顺序步骤:
- 客户端向
Node 1
发送bulk
请求。 -
Node 1
为每个分片构建批量请求,然后转发到这些请求所需的主分片上。 - 主分片一个接一个的按序执行操作。当一个操作执行完,主分片转发新文档(或者删除部分)给对应的复制节点,然后执行下一个操作。复制节点为报告所有操作完成,节点报告给请求节点,请求节点整理响应并返回给客户端。
bulk
API还可以在最上层使用replication
和consistency
参数,routing
参数则在每个请求的元数据中使用。
-bonus
“为什么bulk
API需要带换行符的奇怪格式,而不是像mget
API一样使用JSON数组?”
为了回答这个问题,我们需要简单的介绍一下背景:
批量中每个引用的文档属于不同的主分片,每个分片可能被分布于集群中的某个节点上。 操作(action)需要被转发到对应的分片和节点上。
如果每个单独的请求被包装到JSON数组中,那意味着我们需要:
- 解析JSON为数组(包括文档数据,可能非常大)
- 检查每个请求决定应该到哪个分片上
- 为每个分片创建一个请求的数组
- 序列化这些数组为内部传输格式
- 发送请求到每个分片
这可行,但需要大量的RAM来承载本质上相同的数据,还要创建更多的数据结构使得JVM花更多的时间执行垃圾回收。
取而代之的,Elasticsearch则是从网络缓冲区中一行一行的直接读取数据。它使用换行符识别和解析action/metadata行,以决定哪些分片来处理这个请求。
这些行请求直接转发到对应的分片上。这些没有冗余复制,没有多余的数据结构。整个请求过程使用最小的内存在进行。
第五章 搜索——基本的工具
Elasticsearch真正强大之处在于可以从混乱的数据中找出有意义的信息——从大数据到全面的信息。
Elasticsearch不只是存储(store)文档,也会索引(indexes)文档内容来使之可以被搜索。
每个文档里的字段都会被索引并被查询。而且在简单查询时,Elasticsearch可以使用所有的索引,以非常快的速度返回结果。
搜索(search)可以:
- 在类似于
gender
或者age
这样的字段上使用结构化查询,join_date
这样的字段上使用排序,就像SQL的结构化查询一样。 - 全文检索,可以使用所有字段来匹配关键字,然后按照关联性(relevance)排序返回结果。
- 或者结合以上两条。
很多搜索都是开箱即用的,为了充分挖掘Elasticsearch的潜力,你需要理解以下三个概念:
| 概念 | 解释
| ——————————- | —————————————– |
| 映射(Mapping) | 数据在每个字段中的解释说明 |
| 分析(Analysis) | 全文是如何处理的可以被搜索的 |
| 领域特定语言查询(Query DSL) | Elasticsearch使用的灵活的、强大的查询语言 |
5.1 空搜索
最基本的搜索API表单是空搜索(empty search),它没有指定任何的查询条件,只返回集群索引中的所有文档:
curl -XGET ‘ibd14:9200/_search?pretty’
- hits
响应中最重要的部分是hits
,它包含了total
字段来表示匹配到的文档总数,hits
数组还包含了匹配到的前10条数据。
hits
数组中的每个结果都包含_index
、_type
和文档的_id
字段,被加入到_source
字段中这意味着在搜索结果中我们将可以直接使用全部文档。
每个节点都有一个_score
字段,这是相关性得分(relevance score),它衡量了文档与查询的匹配程度。默认的,返回的结果中关联性最大的文档排在首位;这意味着,它是按照_score
降序排列的。这种情况下,我们没有指定任何查询,所以所有文档的相关性是一样的,因此所有结果的_score
都是取得一个中间值1
max_score
指的是所有文档匹配查询中_score
的最大值。
- took
took
告诉我们整个搜索请求花费的毫秒数。
- shards
_shards
节点告诉我们参与查询的分片数(total
字段),有多少是成功的(successful
字段),有多少的是失败的(failed
字段)。通常我们不希望分片失败,不过这个有可能发生。如果我们遭受一些重大的故障导致主分片和复制分片都故障,那这个分片的数据将无法响应给搜索请求。这种情况下,Elasticsearch将报告分片failed
,但仍将继续返回剩余分片上的结果。
- timeout
time_out
值告诉我们查询超时与否。一般的,搜索请求不会超时。如果响应速度比完整的结果更重要,你可以定义timeout
参数为10
或者10ms
(10毫秒),或者1s
(1秒)
curl -XGET ‘ibd14:9200/_search?timeout=10ms&pretty’
Elasticsearch将返回在请求超时前收集到的结果。
超时不是一个断路器(circuit breaker)(译者注:关于断路器的理解请看警告)。
警告
需要注意的是
timeout
不会停止执行查询,它仅仅告诉你目前顺利返回结果的节点然后关闭连接。在后台,其他分片可能依旧执行查询,尽管结果已经被发送。使用超时是因为对于你的业务需求(译者注:SLA,Service-Level Agreement服务等级协议,在此我翻译为业务需求)来说非常重要,而不是因为你想中断执行长时间运行的查询。
5.2 多索引和多类别
通过限制搜索的不同索引或类型,我们可以在集群中跨所有文档搜索。Elasticsearch转发搜索请求到集群中平行的主分片或每个分片的复制分片上,收集结果后选择顶部十个返回给我们。
通常,当然,你可能想搜索一个或几个自定的索引或类型,我们能通过定义URL中的索引或类型达到这个目的,像这样:
/_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
搜索一个索引有5个主分片和5个索引各有一个分片**事实上是一样的**。
5.3 分页
(空)搜索语句中,执行返回的结果只有10个文档在hits
数组中。如何看到其他文档?
和SQL使用LIMIT
关键字返回只有一页的结果一样,Elasticsearch接受from
和size
参数:
size
: 结果数,默认10
,指定每页返回的文档数
from
: 跳过开始的结果数,默认0
,从第几个文档开始
如果你想每页显示5个结果,页码从1到3,那请求如下:
GET /_search?size=5
curl -XGET ‘ibd14:9200/_search?size=5&pretty’
GET /_search?size=5&from=5
curl -XGET ‘ibd14:9200/_search?size=5&from=5&pretty’
GET /_search?size=5&from=10
curl -XGET ‘ibd14:9200/_search?size=5&from=10&pretty’
应该当心分页太深或者一次请求太多的结果。结果在返回前会被排序。但是记住一个搜索请求常常涉及多个分片。每个分片生成自己排好序的结果,它们接着需要集中起来排序以确保整体排序正确。
5.4 简易搜索
search
API有两种表单:一种是“简易版”的查询字符串(query string)将所有参数通过查询字符串定义,另一种版本使用JSON完整的表示请求体(request body),这种富搜索语言叫做结构化查询语句(DSL)
查询字符串搜索对于在命令行下运行点对点(ad hoc)查询特别有用。例如这个语句查询所有索引类型为blog
并在tags
字段中包含testing
字符的文档:
curl -XGET ‘ibd14:9200/_all/blog/_search?q=tags:testing&pretty’
查询所有索引类型为blog
并在tags
字段中包含testing
字符和views字段值为1的文档
curl -XGET ‘ibd14:9200/_all/blog/_search?q=tags:testing+views:1&pretty’
"+"
前缀表示语句匹配条件必须被满足。类似的"-"
前缀表示条件必须不被满足。所有条件如果没有+
或-
表示是可选的——匹配越多,相关的文档就越多。
返回包含"testing"
字符的所有文档的简单搜索
>
curl -XGET ‘ibd14:9200/_search?q=testing&pretty’
_all
字段
当你索引一个文档,Elasticsearch把所有字符串字段值连接起来放在一个大字符串中,它被索引为一个特殊的字段_all
。
_all
字段对于开始一个新应用时是一个有用的特性。之后,如果你定义字段来代替_all
字段,你的搜索结果将更加可控。当_all
字段不再使用,你可以停用它。
更复杂的语句
下一个搜索语句:
_all
field
* title
字段包含"first"
或"second"
* view
大于1
* _all
字段包含"entry"
或"pretty"
curl -XGET ‘ibd14:9200/_search?q=title:(first+second)+views>1+(entry+trying)&pretty’
从上面的例子,看到了简单(lite)查询字符串搜索惊人的强大。
然而,也看到简洁带来了隐晦和调试困难。而且它很脆弱——查询字符串中一个细小的语法错误,像-
、:
、/
或"
错位就会导致返回错误而不是结果。
第六章 映射与分析
映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型(string
, number
, booleans
, date
等)。
分析(analysis)机制用于进行全文文本(Full Text)的分词,以建立供搜索用的反向索引。
6.1 映射及分析
在已有的索引中有12个tweets,只有一个包含日期2014-09-15
,但是我们看看下面查询中的total
hits。
GET /_search?q=2014 # 12 个结果
GET /_search?q=2014-09-15 # 还是 12 个结果 !
GET /_search?q=date:2014-09-15 # 1 一个结果
GET /_search?q=date:2014 # 0 个结果 !
为什么全日期的查询返回所有的tweets,而针对date
字段进行年度查询却什么都不返回?
为什么我们的结果因查询_all
字段(译者注:默认所有字段中进行查询)或date
字段而变得不同?
让我们看看Elasticsearch在对website_weichao
索引中的blog
类型进行mapping(也称之为模式定义[注:此词有待重新定义(schema definition)])后是如何解读我们的文档结构:
{
"website_weichao" : {
"mappings" : {
"blog" : {
"properties" : {
"date" : {
"type" : "date",
"format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
},
"doc" : {
"properties" : {
"tags" : {
"type" : "string"
},
"views" : {
"type" : "long"
}
}
},
"script" : {
"type" : "string"
},
"tags" : {
"type" : "string"
},
"text" : {
"type" : "string"
},
"title" : {
"type" : "string"
},
"views" : {
"type" : "long"
}
}
}
}
Elasticsearch为对字段类型进行猜测,动态生成了字段和类型的映射关系。返回的信息显示了date
字段被识别为date
类型。_all
因为是默认字段所以没有在此显示,不过我们知道它是string
类型。
date
类型的字段和string
类型的字段的索引方式是不同的,因此导致查询结果的不同,这并不会让我们觉得惊讶。
6.2 确切值 vs 全文文本
Elasticsearch中的数据可以大致分为两种类型:
确切值 及 全文文本- 确切值是确定的,正如它的名字一样。比如一个date或用户ID,也可以包含更多的字符串比如username或email地址。确切值能够被准确地查询到。
- 全文文本常常被称为
非结构化数据
,其实是一种用词不当的称谓,实际上自然语言是高度结构化的。对于全文数据的查询来说,却有些微妙。我们不会去询问这篇文档是否匹配查询要求?
。我们会询问这篇文档和查询的匹配程度如何?
。换句话说,对于查询条件,这篇文档的相关性有多高?
为了方便在全文文本字段中进行这些类型的查询,Elasticsearch首先对文本分析(analyzes),然后使用结果建立一个倒排索引。
6.3 倒排索引
创建倒排索引,我们首先切分每个文档的content
字段为单独的单词,把所有的唯一词放入列表并排序。
使用相同的标准化规则处理查询字符串的content
字段。把这一过程叫做分词。
IMPORTANT
这很重要。你只可以找到确实存在于索引中的词,所以索引文本和查询字符串都要标准化为相同的形式。
这个标记化和标准化的过程叫做分词(analysis),这个在下节中我们讨论。
6.4 分析和分析器
分析(analysis)过程:
- 首先,标记化一个文本块为适用于倒排索引单独的词(term)
- 然后标准化这些词为标准形式,提高它们的“可搜索性”或“查全率”
这个工作是分析器(analyzer)完成的。
字符过滤器
首先字符串经过字符过滤器(character filter),它们的工作是在标记化前处理字符串。字符过滤器能够去除HTML标记,或者转换"&"
为"and"
。
分词器
分词器(tokenizer)的作用是标记化成独立的词。一个简单的分词器(tokenizer)可以根据空格或逗号将单词分开(译者注:这个在中文中不适用)。
标记过滤
最后,每个词都通过所有标记过滤(token filters),它可以修改词(如将"Quick"
转为小写),去掉词(例如停用词像"a"
、"and"
、"the"
等等),或者增加词(例如同义词像"jump"
和"leap"
)
Elasticsearch提供很多开箱即用的字符过滤器,分词器和标记过滤器。这些可以组合来创建自定义的分析器以应对不同的需求。
内建的分析器
不过,Elasticsearch还附带了一些预装的分析器,你可以直接使用它们。下面我们列出了最重要的几个分析器,来演示这个字符串分词后的表现差异:
"Set the shape to semi-transparent by calling set_trans(5)"
标准分析器
标准分析器是Elasticsearch默认使用的分析器。对于文本分析,它对于任何语言都是最佳选择(译者注:就是没啥特殊需求,对于任何一个国家的语言,这个分析器就够用了)。它根据Unicode Consortium的定义的单词边界(word boundaries)来切分文本,然后去掉大部分标点符号。最后,把所有词转为小写。产生的结果为:
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
简单分析器
简单分析器将非单个字母的文本切分,然后把每个词转为小写。产生的结果为:
set, the, shape, to, semi, transparent, by, calling, set, trans
空格分析器
空格分析器依据空格切分文本。它不转换小写。产生结果为:
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
语言分析器
特定语言分析器适用于很多语言。它们能够考虑到特定语言的特性。例如,english
分析器自带一套英语停用词库——像and
或the
这些与语义无关的通用词。这些词被移除后,因为语法规则的存在,英语单词的主体含义依旧能被理解(译者注:stem English words
这句不知道该如何翻译,查了字典,我理解的大概意思应该是将英语语句比作一株植物,去掉无用的枝叶,主干依旧存在,停用词好比枝叶,存在与否并不影响对这句话的理解。)。
english
分析器将会产生以下结果:
set, shape, semi, transpar, call, set_tran, 5
注意"transparent"
、"calling"
和"set_trans"
是如何转为词干的。
当分析器被使用
当我们索引(index)一个文档,全文字段会被分析为单独的词来创建倒排索引。不过,在全文字段搜索(search)时,查询字符串经过同样的分析流程处理,以确保这些词在索引中存在。
- 查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。
- 查询一个确切值(exact value)字段,查询将不分析查询字符串,但是你可以自己指定。
现在可以明白为什么《映射和分析》的开头会产生那种结果:
* date
字段包含一个确切值:单独的一个词"2014-09-15"
。
* _all
字段是一个全文字段,所以分析过程将日期转为三个词:"2014"
、"09"
和"15"
。
当我们在_all
字段查询2014
,它一个匹配到12条推文,因为这些推文都包含词2014
:
GET /_search?q=2014 # 12 results
当我们在_all
字段中查询2014-09-15
,首先分析查询字符串,产生匹配任一词2014
、09
或15
的查询语句,它依旧匹配12个推文,因为它们都包含词2014
。
GET /_search?q=2014-09-15 # 12 results !
当我们在date
字段中查询2014-09-15
,它查询一个确切的日期,然后只找到一条推文:
GET /_search?q=date:2014-09-15 # 1 result
当我们在date
字段中查询2014
,没有找到文档,因为没有文档包含那个确切的日期:
GET /_search?q=date:2014 # 0 results !
测试分析器
尤其当你是Elasticsearch新手时,对于如何分词以及存储到索引中理解起来比较困难。为了更好的理解如何进行,你可以使用analyze
API来查看文本是如何被分析的。在查询字符串参数中指定要使用的分析器,被分析的文本做为请求体:
curl -XGET ‘ibd14:9200/_analyze?analyzer=standard&text=Starting+to+get&pretty’
结果中每个节点在代表一个词:
{
"tokens" : [ {
"token" : "starting",
"start_offset" : 0,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 0
}, {
"token" : "to",
"start_offset" : 9,
"end_offset" : 11,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "get",
"start_offset" : 12,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 2
} ]
}
token
是一个实际被存储在索引中的词。position
指明词在原文本中是第几个出现的。start_offset
和end_offset
表示词在原文本中占据的位置。
analyze
API 对于理解Elasticsearch索引的内在细节是个非常有用的工具。
指定分析器
当Elasticsearch在你的文档中探测到一个新的字符串字段,它将自动设置它为全文string
字段并用standard
分析器分析。
你不可能总是想要这样做。也许你想使用一个更适合这个数据的语言分析器。或者,你只想把字符串字段当作一个普通的字段——不做任何分析,只存储确切值,就像字符串类型的用户ID或者内部状态字段或者标签。
为了达到这种效果,我们必须通过映射(mapping)人工设置这些字段。
6.5 映射
为了能够正确的区分精确查询和全文本搜索,Elasticsearch需要知道每个字段里面都包含了什么类型。这些类型和字段的信息存储(包含)在映射(mapping)中。
索引中每个文档都有一个类型(type)。每个类型拥有自己的映射(mapping)或者模式定义(schema definition)。一个映射定义了字段类型,每个字段的数据类型,以及字段被Elasticsearch处理的方式。映射还用于设置关联到类型上的元数据。
核心简单字段类型
Elasticsearch支持以下简单字段类型:
|类型 | 表示的数据类型 |
|—————-|————————————|
|String | string
|
|Whole number | byte
, short
, integer
, long
|
|Floating point | float
, double
|
|Boolean | boolean
|
|Date | date
|
当索引一个包含新字段的文档——一个之前没有的字段——Elasticsearch将使用动态映射猜测字段类型,这类型来自于JSON的基本数据类型,使用以下规则:
|JSON type | Field type |
|———————————–|————————|
|Boolean: true
or false
| "boolean"
|
|Whole number: 123
| "long"
|
|Floating point: 123.45
| "double"
|
|String, valid date: "2014-09-15"
| "date"
|
|String: "foo bar"
| "string"
|
查看映射
我们可以使用_mapping
后缀来查看Elasticsearch中的映射。在本章开始我们已经找到索引website_weichao
类型blog
中的映射:
>
GET /website_weichao/_mapping/blog
这展示给了我们字段的映射(叫做属性(properties)),这些映射是Elasticsearch在创建索引时动态生成的:
{
"website_weichao" : {
"mappings" : {
"blog" : {
"properties" : {
"date" : {
"type" : "date",
"format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis"
},
"doc" : {
"properties" : {
"tags" : {
"type" : "string"
},
"views" : {
"type" : "long"
}
}
},
"script" : {
"type" : "string"
},
"tags" : {
"type" : "string"
},
"text" : {
"type" : "string"
},
"title" : {
"type" : "string"
},
"views" : {
"type" : "long"
}
}
}
}
}
}
小提示
错误的映射,例如把`age`字段映射为`string`类型而不是`integer`类型,会造成查询结果混乱。 要检查映射类型,而不是假设它是正确的!
自定义字段映射
虽然大多数情况下基本数据类型已经能够满足,但你也会经常需要自定义一些特殊类型(fields),特别是字符串字段类型。
自定义类型可以使你完成一下几点:
区分全文(full text)字符串字段和准确字符串字段(译者注:就是分词与不分词,全文的一般要分词,准确的就不需要分词,比如『中国』这个词。全文会分成『中』和『国』,但作为一个国家标识的时候我们是不需要分词的,所以它就应该是一个准确的字符串字段)。
使用特定语言的分析器(译者注:例如中文、英文、阿拉伯语,不同文字的断字、断词方式的差异)
优化部分匹配字段
指定自定义日期格式(译者注:这个比较好理解,例如英文的
Feb,12,2016
和 中文的2016年2月12日
)以及更多
映射中最重要的字段参数是type
。除了string
类型的字段,你可能很少需要映射其他的type
:
{
"number_of_clicks": {
"type": "integer"
}
}
string
类型的字段,默认的,考虑到包含全文本,它们的值在索引前要经过分析器分析,并且在全文搜索此字段前要把查询语句做分析处理。
对于string
字段,两个最重要的映射参数是index
和analyer
。
index
index
参数控制字符串以何种方式被索引。它包含以下三个值当中的一个:
|值 |解释 |
|————–|————————————–|
|analyzed
|首先分析这个字符串,然后索引。换言之,以全文形式索引此字段。
|not_analyzed
|索引这个字段,使之可以被搜索,但是索引内容和指定值一样。不分析此字段。
|no
|不索引这个字段。这个字段不能为搜索到。|
string
类型字段默认值是analyzed
。如果我们想映射字段为确切值,我们需要设置它为not_analyzed
:
{
"tag": {
"type": "string",
"index": "not_analyzed"
}
}
其他简单类型(
long
、double
、date
等等)也接受index
参数,但相应的值只能是no
和not_analyzed
,它们的值不能被分析。
分析
对于analyzed
类型的字符串字段,使用analyzer
参数来指定哪一种分析器将在搜索和索引的时候使用。默认的,Elasticsearch使用standard
分析器,但是你可以通过指定一个内建的分析器来更改它,例如whitespace
、simple
或english
。
{
"tweet": {
"type": "string",
"analyzer": "english"
}
}
更新映射
你可以在第一次创建索引的时候指定映射的类型。此外,你也可以晚些时候为新类型添加映射(或者为已有的类型更新映射)。
重要
你可以向已有映射中**增加**字段,但你不能**修改**它。如果一个字段在映射中已经存在,这可能意味着那个字段的数据已经被索引。如果你改变了字段映射,那已经被索引的数据将错误并且不能被正确的搜索到。
我们可以更新一个映射来增加一个新字段,但是不能把已有字段的类型那个从analyzed
改到not_analyzed
。
为了演示两个指定的映射方法,让我们首先删除索引gb
:(我直接新建了索引’newinfo_weichao’)
DELETE /gb
然后创建一个新索引,指定tweet
字段的分析器为english
:
PUT /newinfo_weichao <1>
{
"mappings": {
"tweet" : {
"properties" : {
"tweet" : {
"type" : "string",
"analyzer": "english"
},
"date" : {
"type" : "date"
},
"name" : {
"type" : "string"
},
"user_id" : {
"type" : "long"
}
}
}
}
}
<1>
这将创建包含mappings
的索引,映射在请求体中指定。
接着,在tweet
的映射中增加一个新的not_analyzed
类型的文本字段,叫做tag
,使用_mapping
后缀:
PUT /gb/_mapping/tweet
{
"properties" : {
"tag" : {
"type" : "string",
"index": "not_analyzed"
}
}
}
注意到我们不再需要列出所有的已经存在的字段,因为我们没法修改他们。我们的新字段已经被合并至存在的那个映射中。
测试映射
你可以通过名字使用analyze
API测试字符串字段的映射。对比这两个请求的输出:
GET /newinfo_weichao1/_analyze?field=tweet&text=Black-cats <1>
GET /newinfo_weichao1/_analyze?field=tag&text=Black-cats <2>
<1>
<2>
我们想要分析的文本被放在请求体中。
<1>执行结果
{ "tokens" : [ { "token" : "black", "start_offset" : 0, "end_offset" : 5, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "cat", "start_offset" : 6, "end_offset" : 10, "type" : "<ALPHANUM>", "position" : 1 } ] }
<2>执行结果
{
“tokens” : [ {
“token” : “Black-cats”,
“start_offset” : 0,
“end_offset” : 10,
“type” : “word”,
“position” : 0
} ]
}
tweet
字段产生两个词,"black"
和"cat"
,tag
字段产生单独的一个词"Black-cats"
。换言之,我们的映射工作正常。
6.6 复合核心字段类型
除了之前提到的简单的标量类型,JSON还有null
值,数组和对象,所有这些Elasticsearch都支持:
多值字段
我们想让tag
字段包含多个字段,这非常有可能发生。我们可以索引一个标签数组来代替单一字符串:
{ "tag": [ "search", "nosql" ]}
对于数组不需要特殊的映射。任何一个字段可以包含零个、一个或多个值,同样对于全文字段将被分析并产生多个词。
这意味着数组中所有值必须为同一类型。如果你创建一个新字段,这个字段索引了一个数组,Elasticsearch将使用第一个值的类型来确定这个新字段的类型。
当你从Elasticsearch中取回一个文档,任何一个数组的顺序和你索引它们的顺序一致。你取回的
_source
字段的顺序同样与索引它们的顺序相同。数组是作为多值字段被索引的,它们没有顺序。在搜索阶段你不能指定“第一个值”或者“最后一个值”。倒不如把数组当作一个值集合(bag of values)
空字段
当然数组可以是空的。这等价于有零个值。事实上,Lucene没法存放null
值,所以一个null
值的字段被认为是空字段。
这四个字段将被识别为空字段而不被索引:
"empty_string": "",
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
多层对象
我们需要讨论的最后一个自然JSON数据类型是对象(object)——在其它语言中叫做hash、hashmap、dictionary 或者 associative array.
内部对象(inner objects)经常用于在另一个对象中嵌入一个实体或对象。例如,做为在tweet
文档中user_name
和user_id
的替代,我们可以这样写:
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
内部对象的映射
Elasticsearch 会动态的检测新对象的字段,并且映射它们为 object
类型,将每个字段加到 properties
字段下
{
"gb": {
"tweet": { <1>
"properties": {
"tweet": { "type": "string" },
"user": { <2>
"type": "object",
"properties": { "id": { "type": "string" }, "gender": { "type": "string" }, "age": { "type": "long" }, "name": { <3> "type": "object", "properties": { "full": { "type": "string" }, "first": { "type": "string" }, "last": { "type": "string" } } } } }
}
}
}
}
<1> 根对象.
<2><3> 内部对象.
对user
和name
字段的映射与tweet
类型自己很相似。事实上,type
映射只是object
映射的一种特殊类型,我们将 object
称为根对象。它与其他对象一模一样,除非它有一些特殊的顶层字段,比如 _source
, _all
等等。
内部对象是怎样被索引的
Lucene 并不了解内部对象。 一个 Lucene 文件包含一个键-值对应的扁平表单。 为了让 Elasticsearch 可以有效的索引内部对象,将文件转换为以下格式:
{
"tweet": [elasticsearch, flexible, very],
"user.id": [@johnsmith],
"user.gender": [male],
"user.age": [26],
"user.name.full": [john, smith],
"user.name.first": [john],
"user.name.last": [smith]
}
内部栏位可被归类至name,例如"first"
。 为了区别两个拥有相同名字的栏位,我们可以使用完整路径,例如"user.name.first"
或甚至类型
名称加上路径:"tweet.user.name.first"
。
注意: 在以上扁平化文件中,并没有栏位叫作
user
也没有栏位叫作user.name
。 Lucene 只索引阶层或简单的值,而不会索引复杂的资料结构。
对象-数组
内部对象数组
最后,一个包含内部对象的数组如何索引。 我们有个数组如下所示:
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
此文件会如我们以上所说的被扁平化,但其结果会像如此:
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
{age: 35}
与{name: Mary White}
之间的关联会消失,因每个多值的栏位会变成一个值集合,而非有序的阵列。
第七章 请求体查询
请求体查询(request body search
)API是对简单查询语句(lite)(一种有效的命令行adhoc查询)的有效补充。因为大多数的参数以JSON格式所容纳而非查询字符串,因此请求体查询是相当重要。
7.1 空查询
空查询将会返回索引中所有的文档。
GET /_search
{} <1>
<1> 这是一个空查询数据。
同字符串查询一样,你可以查询一个,多个或_all
索引(indices)或类型(types):
GET /index_2014*/type1,type2/_search
{}
使用from
及size
参数进行分页:
GET /_search
{
"from": 30,
"size": 10
}
7.2 结构化查询Query DSL(Query Domain Specific Language)
结构化查询是一种灵活的,多表现形式的查询语言。
Elasticsearch在一个简单的JSON接口中用结构化查询来展现Lucene绝大多数能力。它使得查询更加灵活,精准,易于阅读并且易于debug。
使用结构化查询,需要传递query
参数:
GET /_search
{
"query": YOUR_QUERY_HERE
}
空查询 - {}
- 在功能上等同于使用match_all
查询子句,正如其名字一样,匹配所有的文档:
GET /_search
{
"query": {
"match_all": {}
}
}
查询子句
一个查询子句一般使用这种结构:
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
或指向一个指定的字段:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
例如,可以使用match
查询子句用来找寻在tweet
字段中找寻包含elasticsearch
的成员:
{
"match": {
"tweet": "elasticsearch"
}
}
完整的查询请求是这样:
GET /_search
{
"query": {
"match": {
"tweet": "elasticsearch"
}
}
}
合并多子句
查询子句就像是搭积木一样,可以合并简单的子句为一个复杂的查询语句,比如:
叶子子句(leaf clauses)(比如
match
子句)用以在将查询字符串与一个字段(或多字段)进行比较-
复合子句(compound)用以合并其他的子句。例如,
bool
子句允许你合并其他的合法子句,must
,must_not
或者should
,如果可能的话:{ "bool": { "must": { "match": { "tweet": "elasticsearch" }}, "must_not": { "match": { "name": "mary" }}, "should": { "match": { "tweet": "full text" }} } }
复合子句能合并 任意其他查询子句,包括其他的复合子句。
这就意味着复合子句可以相互嵌套,从而实现非常复杂的逻辑。
以下实例查询在inbox中或未标记spam的邮件中找出包含"business opportunity"
的星标(starred)邮件:
{
"bool": {
"must": { "match": { "email": "business opportunity" }},
"should": [
{ "match": { "starred": true }},
{ "bool": {
"must": { "folder": "inbox" }},
"must_not": { "spam": true }}
}}
],
"minimum_should_match": 1
}
}
复合子句可以合并多种子句为一个单一的查询,无论是叶子子句还是其他的复合子句。
7.3 查询与过滤
前面讲到的是关于结构化查询语句,事实上有两种结构化语句可以使用:
结构化查询(Query DSL)和结构化过滤(Filter DSL)。
查询与过滤语句非常相似,但是它们由于使用目的不同而稍有差异。
一条过滤语句会询问每个文档的字段值是否包含着特定值:
created
的日期范围是否在2013
到2014
?status
字段中是否包含单词 “published” ?lat_lon
字段中的地理位置与目标点相距是否不超过10km ?
一条查询语句与过滤语句相似,但问法不同:
查询语句会询问每个文档的字段值与特定值的匹配程度如何?
查询语句的典型用法是为了找到文档:
查找与
full text search
这个词语最佳匹配的文档查找包含单词
run
,但是也包含runs
,running
,jog
或sprint
的文档同时包含着
quick
,brown
和fox
— 单词间离得越近,该文档的相关性越高标识着
lucene
,search
或java
— 标识词越多,该文档的相关性越高
一条查询语句会计算每个文档与查询语句的相关性,会给出一个相关性评分 _score
,并且按照相关性对匹配到的文档进行排序。
这种评分方式非常适用于一个没有完全配置结果的全文本搜索。
性能差异
使用过滤语句得到的结果集 – 一个简单的文档列表,快速匹配运算并存入内存是十分方便的,每个文档仅需要1个字节。这些缓存的过滤结果集与后续请求的结合使用是非常高效的。
查询语句不仅要查找相匹配的文档,还需要计算每个文档的相关性,所以一般来说查询语句要比过滤语句更耗时,并且查询结果也不可缓存。
倒排索引,使得一个只匹配少量文档的简单查询语句在百万级文档中的查询效率会与一条经过缓存的过滤语句旗鼓相当,甚至略占上风。
但是一般情况下,一条经过缓存的过滤查询要远胜一条查询语句的执行效率。
过滤语句的目的就是缩小匹配的文档结果集,所以需要仔细检查过滤条件。
什么情况下使用
原则上来说,使用查询语句做全文本搜索或其他需要进行相关性评分的时候,剩下的全部用过滤语句
7.4 最重要的查询
快速的介绍一下这些最常用到的查询过滤语句。
match_all
查询
使用match_all
可以查询到所有文档,是没有查询条件下的默认语句。
{
"match_all": {}
}
此查询常用于合并过滤条件。
比如说你需要检索所有的邮箱,所有的文档相关性都是相同的,所以得到的_score
为1
match
查询
match
查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。
如果你使用 match
查询一个全文本字段,它会在真正查询之前用分析器先分析match
一下查询字符:
{
"match": {
"tweet": "About Search"
}
}
如果用match
指定了一个确切值,在遇到数字,日期,布尔值或者not_analyzed
的字符串时,它将为你搜索你给定的值:
{ "match": { "age": 26 }}
{ "match": { "date": "2014-09-01" }}
{ "match": { "public": true }}
{ "match": { "tag": "full_text" }}
提示:
做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。
与《简单搜索》中介绍的字符查询不同,match
查询不可以用类似”+usid:2 +tweet:search”这样的语句。它只能就指定某个确切字段某个确切的值进行搜索,而你要做的就是为它指定正确的字段名以避免语法错误。
multi_match
查询
multi_match
查询允许你做match
查询的基础上同时搜索多个字段:
{
"multi_match": {
"query": "full text search",
"fields": [ "title", "body" ]
}
}
range
过滤
range
过滤允许我们按照指定范围查找一批数据:
{
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
范围操作符包含:
gt
:: 大于
gte
:: 大于等于
lt
:: 小于
lte
:: 小于等于
term
过滤
term
主要用于精确匹配一些值,比如数字,日期,布尔值或 not_analyzed
的字符串(未经分析的文本数据类型):
{ "term": { "age": 26 }}
{ "term": { "date": "2014-09-01" }}
{ "term": { "public": true }}
{ "term": { "tag": "full_text" }}
terms
过滤
terms
跟 term
有点类似,但 terms
允许指定多个匹配条件。
如果某个字段指定了多个值,那么文档需要一起去做匹配:
{
"terms": {
"tag": [ "search", "full_text", "nosql" ]
}
}
exists
和 missing
过滤
exists
和 missing
过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的IS_NULL
条件
{
"exists": {
"field": "title"
}
}
这两个过滤只是针对已经查出一批数据来,但是想区分出某个字段是否存在的时候使用。
7.5 查询与过滤条件的合并
实际生产中,没有哪个请求查询是简单的,往往需要将多请求查询和单个请求查询结合起来。
bool
过滤
bool
过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:
must
:: 多个查询条件的完全匹配,相当于 and
。
must_not
:: 多个查询条件的相反匹配,相当于 not
。
should
:: 至少有一个查询条件匹配, 相当于 or
。
filter
:: 依据条件筛选出满足或不满足条件的文档。
这些参数可以分别继承一个过滤条件或者一个过滤条件的数组:
{
"bool": {
"must": { "match": { "title": "how to make millions" }},
"must_not": { "match": { "tag": "spam" }},
"should": [
{ "match": { "tag": "starred" }},
{ "range": { "date": { "gte": "2014-01-01" }}}
]
}
}
以上查询将会找到 title
字段中包含 “how to make millions”,并且 “tag” 字段没有被标为 spam
。
如果有标识为 “starred” 或者发布日期为2014年之前,那么这些匹配的文档将比同类文档等级高。
提示:
如果bool
查询下没有must
子句,那至少应该有一个should
子句。但是
如果有must
子句,那么没有should
子句也可以进行查询。
bool
查询
bool
查询与 bool
过滤相似,用于合并多个查询子句。不同的是,bool
过滤可以直接给出是否匹配成功,
而bool
查询要计算每一个查询子句的 _score
(相关性分值)。
must
:: 查询指定文档一定要被包含。
must_not
:: 查询指定文档一定不要被包含。
should
:: 查询指定文档,有则可以为文档相关性加分。
7.6 验证查询
查询语句可以变得非常复杂,特别是与不同的分析器和字段映射相结合后,就会有些难度。
validate
API 可以验证一条查询语句是否合法。
GET /gb/tweet/_validate/query
{
"query": {
"tweet" : {
"match" : "really powerful"
}
}
}
从下面的返回信息得知,以上请求的这条语句是非法的:
{
"valid" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"failed" : 0
}
}
理解错误信息
想知道语句非法的具体错误信息,需要加上 explain
参数:
GET /gb/tweet/_validate/query?explain <1>
{
"query": {
"tweet" : {
"match" : "really powerful"
}
}
}
<1> explain
参数可以提供语句错误的更多详情。
很显然,我们把 query 语句的 match
与字段名位置弄反了:
{
"valid" : false,
"_shards" : { ... },
"explanations" : [ {
"index" : "gb",
"valid" : false,
"error" : "org.elasticsearch.index.query.QueryParsingException:
[gb] No query registered for [tweet]"
} ]
}
“`
理解查询语句
如果是合法语句的话,使用 explain
参数可以返回一个带有查询语句的可阅读描述,可以帮助了解查询语句在ES中是如何执行的:
GET /_validate/query?explain
{
"query": {
"match" : {
"tweet" : "really powerful"
}
}
}
explanation
会为每一个索引返回一段描述,因为每个索引会有不同的映射关系和分析器:
{
"valid" : true,
"_shards" : { ... },
"explanations" : [ {
"index" : "us",
"valid" : true,
"explanation" : "tweet:really tweet:powerful"
}, {
"index" : "gb",
"valid" : true,
"explanation" : "tweet:really tweet:power"
} ]
}
从返回的 explanation
你会看到 match
是如何为查询字符串 "really powerful"
进行查询的,
首先,它被拆分成两个独立的词分别在 tweet
字段中进行查询。
而且,在索引us
中这两个词为"really"
和"powerful"
,在索引gb
中被拆分成"really"
和 "power"
。
这是因为我们在索引gb
中使用了english
分析器。
7.7 总结
详细介绍了如何在项目中使用常见的查询语句。
想要完全掌握搜索和结构化查询,还需要在工作中花费大量的时间来理解ES的工作方式。
第八章 相关性排序
默认情况下,结果集会按照相关性进行排序 – 相关性越高,排名越靠前。
这一章我们会讲述相关性是什么以及它是如何计算的。
8.1 排序方式
为了使结果可以按照相关性进行排序,我们需要一个相关性的值。在ElasticSearch的查询结果中,相关性分值会用_score
字段来给出一个浮点型的数值,所以默认情况下,结果集以_score
进行倒序排列。
有时,即便如此,你还是没有一个有意义的相关性分值。比如,以下语句返回所有tweets中 user_id
是否
包含值 1
:
GET /_search
{
"query" : {
"filtered" : {
"filter" : {
"term" : {
"user_id" : 1
}
}
}
}
}
过滤语句与 _score
没有关系,但是有隐含的查询条件 match_all
为所有的文档的 _score
设值为 1
。也就相当于所有的文档相关性是相同的。
字段值排序
下面例子中,对结果集按照时间排序,将最新的文档排列靠前。使用 sort
参数进行排序:
GET /_search
{
"query" : {
"filtered" : {
"filter" : { "term" : { "user_id" : 1 }}
}
},
"sort": { "date": { "order": "desc" }}
}
你会发现这里有两个不同点:
"hits" : {
"total" : 6,
"max_score" : null, <1>
"hits" : [ {
"_index" : "us",
"_type" : "tweet",
"_id" : "14",
"_score" : null, <1>
"_source" : {
"date": "2014-09-24",