简介
Elasticsearch
是一个实时分布式搜索和分析引擎,基于开源搜索引擎库 Apache Lucene™ 。但是,Lucene
只是一个库。想要使用它,你必须使用 Java
来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene
非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。Elasticsearch
也使用 Java
开发并使用 Lucene
作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API
来隐藏 Lucene
的复杂性,从而让全文搜索变得简单。
Elasticsearch
在分布式概念上做了很大程度上的透明化,在教程中你不需要知道任何关于分布式系统、分片、集群发现或者其他大量的分布式概念。
Elasticsearch
致力于隐藏分布式系统的复杂性。以下这些操作都是在底层自动完成的:
- 将文档分区到不同的容器或者分片(shards)中,它们可以存在于一个或多个节点中。
- 将分片均匀的分配到各个节点,对索引和搜索做负载均衡。
- 冗余每一个分片,防止硬件故障造成的数据丢失。
- 将集群中任意一个节点上的请求路由到相应数据所在的节点。
- 无论是增加节点,还是移除节点,分片都可以做到无缝的扩展和迁移。
Curl 请求
向 Elasticsearch
发出的请求的组成部分与其它普通的 HTTP
请求是一样的:
curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT>/<PATH>?<QUERY_STRING>' -d '<BODY>'
-
VERB HTTP方法:GET, POST, PUT, HEAD, DELETE
-
PROTOCOL http或者https协议(只有在Elasticsearch前面有https代理的时候可用)
-
HOST Elasticsearch集群中的任何一个节点的主机名,如果是在本地的节点,那么就叫localhost
-
PORT Elasticsearch HTTP服务所在的端口,默认为9200
PATH API路径(例如_count将返回集群中文档的数量),PATH可以包含多个组件,例如_cluster/stats或者_nodes/stats/jvm -
QUERY_STRING 一些可选的查询请求参数,例如?pretty参数将使请求返回更加美观易读的JSON数据
-
BODY 一个JSON格式的请求主体(如果请求需要的话)
例如为了计算集群中的文档数量,我们可以这样做:
curl -XGET 'http://localhost:9200/_count?pretty' -d '
{
"query": {
"match_all": {}
}
}
'
Elasticsearch
返回一个类似200 OK的HTTP状态码和JSON格式的响应主体(除了HEAD请求)。上面的请求会得到如下的JSON格式的响应主体:
{
"count" : 0,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
}
}
文档
Elasticsearch
是面向文档(document oriented)的,这意味着它可以存储整个对象或文档(document)。然而它不仅仅是存储,还会索引(index)每个文档的内容使之可以被搜索。在 Elasticsearch
中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields
Elasticsearch
集群可以包含多个索引(indices)(数据库),每一个索引可以包含多个类型(types)(表),每一个类型包含多个文档(documents)(行),然后每个文档包含多个字段(Fields)(列)。
ELasticsearch
使用JSON,作为文档序列化格式,例如
{
"email": "john@smith.com",
"first_name": "John",
"last_name": "Smith",
"info": {
"bio": "Eco-warrior and defender of the weak",
"age": 25,
"interests": [ "dolphins", "whales" ]
},
"join_date": "2014/05/01"
}
默认情况下,文档中的所有字段都会被索引(拥有一个倒排索引)
为每个员工的文档(document)建立索引,每个文档包含了相应员工的所有信息。
- 每个文档的类型为employee。
- employee类型归属于索引megacorp。
- megacorp索引存储在Elasticsearch集群中。
PUT /megacorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
搜索
这对于 Elasticsearch
来说非常简单。我们只要执行 HTTP GET
请求并指出文档的“地址”——索引、类型和ID既可。根据这三部分信息,我们就可以返回原始JSON文档:
GET /megacorp/employee/1
响应的内容中包含一些文档的元信息,John Smith的原始JSON文档包含在_source字段中。
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
}
简单搜索
GET请求非常简单,如搜索全部员工
GET /megacorp/employee/_search
可以看到,依然使用megacorp索引和employee类型,但是在结尾使用关键字 _search
来取代原来的文档ID。响应内容的 hits
数组中包含了我们所有的三个文档。默认情况下搜索会返回前10个结果。
DSL 查询
查询字符串搜索便于通过命令行完成特定搜索,但是它也有局限性(参阅简单搜索章节)。Elasticsearch提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。
DSL(Domain Specific Language特定领域语言)以 JSON
请求体的形式出现。我们可以这样表示之前关于“Smith”的查询:
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"last_name" : "Smith"
}
}
}
这会返回与之前查询相同的结果。你可以看到有些东西改变了,我们不再使用查询字符串(query string)做为参数,而是使用请求体代替。这个请求体使用JSON表示,其中使用了match语句(查询类型之一,具体我们以后会学到)。
更复杂的搜索
我们让搜索稍微再变的复杂一些。我们依旧想要找到姓氏为“Smith”的员工,但是我们只想得到年龄大于30岁的员工。我们的语句将添加过滤器(filter),它使得我们高效率的执行一个结构化搜索:
GET /megacorp/employee/_search
{
"query" : {
"filtered" : {
"filter" : {
"range" : {
"age" : { "gt" : 30 } <1>
}
},
"query" : {
"match" : {
"last_name" : "smith" <2>
}
}
}
}
}
这部分查询属于 区间过滤器(range filter) ,它用于查找所有年龄大于30岁的数据—— gt
为"greater than"的缩写。
这部分查询与之前的 match
语句(query) 一致。
全文搜索
GET /megacorp/employee/_search
{
"query" : {
"match" : {
"about" : "rock climbing"
}
}
}
默认情况下,Elasticsearch
根据结果相关性评分来对结果集进行排序,所谓的「结果相关性评分」就是文档与查询条件的匹配程度。
短语搜索
有时候想要确切的匹配若干个单词或者短语(phrases)。例如我们想要查询同时包含"rock"和"climbing"(并且是相邻的)的员工记录。
要做到这个,我们只要将 match
查询变更为 match_phrase
查询即可:
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
}
}
高亮
在Elasticsearch中高亮片段是非常容易的。
让我们在之前的语句上增加 highlight
参数:
GET /megacorp/employee/_search
{
"query" : {
"match_phrase" : {
"about" : "rock climbing"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
当我们运行这个语句时,会命中与之前相同的结果,但是在返回结果中会有一个新的部分叫做 highlight
,这里包含了来自about字段中的文本,并且用 <em></em>
来标识匹配到的单词。
聚合
Elasticsearch有一个功能叫做聚合(aggregations),它允许你在数据上生成复杂的分析统计。它很像SQL中的 GROUP BY
,但是功能更强大。
举个例子,让我们找到所有职员中最大的共同点(兴趣爱好)是什么:
GET /megacorp/employee/_search
{
"aggs": {
"all_interests": {
"terms": { "field": "interests" }
}
}
}
暂时先忽略语法只看查询结果:
{
...
"hits": { ... },
"aggregations": {
"all_interests": {
"buckets": [
{
"key": "music",
"doc_count": 2
},
{
"key": "forestry",
"doc_count": 1
},
{
"key": "sports",
"doc_count": 1
}
]
}
}
}
我们可以看到两个职员对音乐有兴趣,一个喜欢林学,一个喜欢运动。这些数据并没有被预先计算好,它们是实时的从匹配查询语句的文档中动态计算生成的。如果我们想知道所有姓"Smith"的人最大的共同点(兴趣爱好),我们只需要增加合适的语句既可:
GET /megacorp/employee/_search
{
"query": {
"match": {
"last_name": "smith"
}
},
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
}
}
}
all_interests
聚合已经变成只包含和查询语句相匹配的文档了:
{
...
"all_interests": {
"buckets": [
{
"key": "music",
"doc_count": 2
},
{
"key": "sports",
"doc_count": 1
}
]
}
}
聚合也允许分级汇总。例如,让我们统计每种兴趣下职员的平均年龄:
GET /megacorp/employee/_search
{
"aggs" : {
"all_interests" : {
"terms" : { "field" : "interests" },
"aggs" : {
"avg_age" : {
"avg" : { "field" : "age" }
}
}
}
}
}
{
...
"all_interests": {
"buckets": [
{
"key": "music",
"doc_count": 2,
"avg_age": {
"value": 28.5
}
},
{
"key": "forestry",
"doc_count": 1,
"avg_age": {
"value": 35
}
},
{
"key": "sports",
"doc_count": 1,
"avg_age": {
"value": 25
}
}
]
}
}
该聚合结果比之前的聚合结果要更加丰富。我们依然得到了兴趣以及数量(指具有该兴趣的员工人数)的列表,但是现在每个兴趣额外拥有 avg_age
字段来显示具有该兴趣员工的平均年龄。
集群
Elasticsearch用于构建高可用和可扩展的系统。扩展的方式可以是购买更好的服务器(纵向扩展(vertical scale or scaling up))或者购买更多的服务器(横向扩展(horizontal scale or scaling out))。
Elasticsearch虽然能从更强大的硬件中获得更好的性能,但是纵向扩展有它的局限性。真正的扩展应该是横向的,它通过增加节点来均摊负载和增加可靠性。
空集群
一个节点(node)就是一个Elasticsearch实例,而一个集群(cluster)由一个或多个节点组成,它们具有相同的cluster.name,它们协同工作,分享数据和负载。当加入新的节点或者删除一个节点时,集群就会感知到并平衡数据。
集群中一个节点会被选举为主节点(master),它将临时管理集群级别的一些变更,例如新建或删除索引、增加或移除节点等。主节点不参与文档级别的变更或搜索,这意味着在流量增长的时候,该主节点不会成为集群的瓶颈。任何节点都可以成为主节点。
集群健康
status字段提供一个综合的指标来表示集群的的服务状况。三种颜色各自的含义:
颜色 | 意义 |
---|---|
green | 所有主要分片和复制分片都可用 |
yellow | 所有主要分片可用,但不是所有复制分片都可用 |
red | 不是所有的主要分片都可用 |
添加索引
为了将数据添加到Elasticsearch,我们需要索引(index)——一个存储关联数据的地方。实际上,索引只是一个用来指向一个或多个分片(shards)的“逻辑命名空间(logical namespace)”.
一个分片(shard)是一个最小级别“工作单元(worker unit)”,它只是保存了索引中所有数据的一部分。我们的文档存储在分片中,并且在分片中被索引,但是我们的应用程序不会直接与它们通信,取而代之的是,直接与索引通信。分片是Elasticsearch在集群中分发数据的关键。把分片想象成数据的容器。文档存储在分片中,然后分片分配到你集群中的节点上。当你的集群扩容或缩小,Elasticsearch将会自动在你的节点间迁移分片,以使集群保持平衡。
分片可以是主分片(primary shard)或者是复制分片(replica shard)。你索引中的每个文档属于一个单独的主分片,所以主分片的数量决定了索引最多能存储多少数据。
理论上主分片能存储的数据大小是没有限制的,限制取决于你实际的使用情况。分片的最大容量完全取决于你的使用状况:硬件存储的大小、文档的大小和复杂度、如何索引和查询你的文档,以及你期望的响应时间。复制分片只是主分片的一个副本,它可以防止硬件故障导致的数据丢失,同时可以提供读请求,比如搜索或者从别的shard取回文档。
当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。默认情况下,一个索引被分配5个主分片。
故障转移
在单一节点上运行意味着有单点故障的风险——没有数据备份。幸运的是,要防止单点故障,我们唯一需要做的就是启动另一个节点。
应对故障
如果杀掉的节点是一个主节点,而一个集群必须要有一个主节点才能使其功能正常。因此,集群做的第一件事就是各节点选举了一个新的主节点:Node 2。
主分片1和2在我们杀掉Node 1时已经丢失,我们的索引在丢失主分片时不能正常工作。如果此时我们检查集群健康,我们将看到状态red:不是所有主分片都可用!
幸运的是丢失的两个主分片的完整拷贝存在于其他节点上,所以新主节点做的第一件事是把这些在Node 2和Node 3上的复制分片升级为主分片,这时集群健康回到yellow状态。如果我们重启Node 1,集群将能够重新分配丢失的复制分片。如果Node 1依旧有旧分片的拷贝,它将会尝试再利用它们,它只会从主分片上复制在故障期间有数据变更的那一部分。
数据
文档
通常,我们可以认为对象(object)和文档(document)是等价相通的。不过,他们还是有所差别:对象(Object)是一个JSON结构体——类似于哈希、hashmap、字典或者关联数组;对象(Object)中还可能包含其他对象(Object)。 在Elasticsearch中,文档(document)这个术语有着特殊含义。它特指最顶层结构或者根对象(root object)序列化成的JSON数据(以唯一ID标识并存储于Elasticsearch中)。
文档元数据
一个文档不只有数据。它还包含了元数据(metadata)——关于文档的信息。三个必须的元数据节点是:
节点 | 说明 |
---|---|
_index | 文档存储的地方 |
_type | 文档代表的对象的类 |
_id | 文档的唯一标识 |
事实上,我们的数据被存储和索引在分片(shards)中,索引只是一个把一个或多个分片分组在一起的逻辑空间。然而,这只是一些内部细节——我们的程序完全不用关心分片。对于我们的程序而言,文档存储在索引(index)中。
索引
自动生成的ID有22个字符长,URL-safe, Base64-encoded string universally unique identifiers
获取
检索文档的一部分
通常,GET请求将返回文档的全部,存储在_source参数中。但是可能你感兴趣的字段只是title。请求个别字段可以使用_source参数。多个字段可以使用逗号分隔:
GET /website/blog/123?_source=title,text
存在
如果你想做的只是检查文档是否存在——你对内容完全不感兴趣——使用HEAD方法来代替GET。HEAD请求不会返回响应体,只有HTTP头:
curl -i -XHEAD http://localhost:9200/website/blog/123
更新
文档在Elasticsearch中是不可变的——我们不能修改他们。如果需要更新已存在的文档,
PUT /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
在内部,Elasticsearch已经标记旧文档为删除并添加了一个完整的新文档。旧版本文档不会立即消失,但你也不能去访问它。Elasticsearch会在你继续索引更多数据时清理被删除的文档。使用update API,我们可以使用一个请求来实现局部更新,例如增加数量的操作。
删除
删除文档的语法模式与之前基本一致,只不过要使用DELETE方法:
DELETE /website/blog/123
尽管文档不存在——"found"的值是false——_version依旧增加了。这是内部记录的一部分,它确保在多节点间不同操作可以有正确的顺序。
版本控制
悲观并发控制(Pessimistic concurrency control)
这在关系型数据库中被广泛的使用,假设冲突的更改经常发生,为了解决冲突我们把访问区块化。典型的例子是在读一行数据前锁定这行,然后确保只有加锁的那个线程可以修改这行数据。
乐观并发控制(Optimistic concurrency control):
被Elasticsearch使用,假设冲突不经常发生,也不区块化访问,然而,如果在读写过程中数据发生了变化,更新操作将失败。这时候由程序决定在失败后如何解决冲突。实际情况中,可以重新尝试更新,刷新数据(重新读取)或者直接反馈给用户。
乐观并发控制
Elasticsearch是分布式的。当文档被创建、更新或删除,文档的新版本会被复制到集群的其它节点。Elasticsearch即是同步的又是异步的,意思是这些复制请求都是平行发送的,并无序(out of sequence)的到达目的地。这就需要一种方法确保老版本的文档永远不会覆盖新的版本。
MGet
如果你需要从Elasticsearch中检索多个文档,相对于一个一个的检索,更快的方式是在一个请求中使用multi-get或者mget API。
mget API参数是一个docs数组,数组的每个节点定义一个文档的_index、_type、_id元数据。如果你只想检索一个或几个确定的字段,也可以定义一个_source参数:
POST /_mget
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}
{
"docs" : [
{
"_index" : "website",
"_id" : "2",
"_type" : "blog",
"found" : true,
"_source" : {
"text" : "This is a piece of cake...",
"title" : "My first external blog entry"
},
"_version" : 10
},
{
"_index" : "website",
"_id" : "1",
"_type" : "pageviews",
"found" : true,
"_version" : 2,
"_source" : {
"views" : 2
}
}
]
}
如果你想检索的文档在同一个_index中(甚至在同一个_type中),你就可以在URL中定义一个默认的/_index或者/_index/_type。
你依旧可以在单独的请求中使用这些值:
POST /website/blog/_mget
{
"docs" : [
{ "_id" : 2 },
{ "_type" : "pageviews", "_id" : 1 }
]
}
同样的,type
相同时也可以省略。
批量
更新时的批量操作
就像mget允许我们一次性检索多个文档一样,bulk API允许我们使用单一请求来实现多个文档的create、index、update或delete。这对索引类似于日志活动这样的数据流非常有用,它们可以以成百上千的数据为一个批次按序进行索引。
bulk请求体如下,它有一点不同寻常:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
...
这种格式类似于用"\n"符号连接起来的一行一行的JSON文档流(stream)。两个重要的点需要注意:
每行必须以"\n"符号结尾,包括最后一行。这些都是作为每行有效的分离而做的标记。
每一行的数据不能包含未被转义的换行符,它们会干扰分析——这意味着JSON不能被美化打印。
action/metadata这一行定义了文档行为(what action)发生在哪个文档(which document)之上。
行为(action)必须是以下几种:
| 行为 | 解释 |
| create | 当文档不存在时创建之 |
| index | 创建新文档或替换已有文档 |
| update | 局部更新文档 |
| delete | 删除一个文档 |
{
"delete":
{
"_index": "website",
"_type": "blog",
"_id": "123"
}
}
请求体(request body)由文档的_source组成——文档所包含的一些字段以及其值。
POST /_bulk
{
"delete":
{
"_index": "website",
"_type": "blog",
"_id": "123"
}
}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }
Elasticsearch响应包含一个items数组,它罗列了每一个请求的结果,结果的顺序与我们请求的顺序相同:
{
"took": 4,
"errors": false,
"items": [
{ "delete": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 2,
"status": 200,
"found": true
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 3,
"status": 201
}},
{ "create": {
"_index": "website",
"_type": "blog",
"_id": "EiwfApScQiiy7TIKFxRCTw",
"_version": 1,
"status": 201
}},
{ "update": {
"_index": "website",
"_type": "blog",
"_id": "123",
"_version": 4,
"status": 200
}}
]
}}
每个子请求都被独立的执行,所以一个子请求的错误并不影响其它请求。整个批量请求需要被加载到接受我们请求节点的内存里,所以请求越大,给其它请求可用的内存就越小。有一个最佳的bulk请求大小。超过这个大小,性能不再提升而且可能降低。
你可能在同一个index下的同一个type里批量索引日志数据。为每个文档指定相同的元数据是多余的。就像mget API,bulk请求也可以在URL中使用/_index或/_index/_type:
你依旧可以覆盖元数据行的_index和_type,在没有覆盖时它会使用URL中的值作为默认值:
POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}
{ "title": "Overriding the default type" }
分布式文档存储
路由
文档分配至哪个分片,
shard = hash(routing) % number_of_primary_shards
routing值是一个任意字符串,它默认是_id但也可以自定义。这也解释了为什么主分片的数量只能在创建索引时定义且不能修改:如果主分片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了。所有的文档API(get、index、delete、bulk、update、mget)都接收一个routing参数,它用来自定义文档到分片的映射。自定义路由值可以确保所有相关文档——例如属于同一个人的文档——被保存在同一分片上。
新建、索引和删除
新建、索引和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上:
- 客户端给Node 1发送新建、索引或删除请求。
- 节点使用文档的_id确定文档属于分片0。它转发请求到Node 3分片0位于这个节点上。
- Node 3在主分片上执行请求,如果成功,它转发请求到相应的位于Node 1和Node 2的复制节点上。当所有的复制节点报告成功,Node 3报告成功到请求的节点,请求的节点再报告给客户端。
默认主分片在尝试写入时需要规定数量(quorum)或过半的分片(可以是主节点或复制节点)可用。这是防止数据被写入到错的网络分区。规定的数量计算公式如下:
int( (primary + number_of_replicas) / 2 ) + 1
consistency允许的值为one(只有一个主分片),all(所有主分片和复制分片)或者默认的quorum或过半分片。
注意number_of_replicas是在索引中的的设置,用来定义复制分片的数量,而不是现在活动的复制节点的数量。如果你定义了索引有3个复制节点,那规定数量是:
int( (primary + 3 replicas) / 2 ) + 1 = 3
检索
文档能够从主分片或任意一个复制分片被检索。
- 客户端给Node 1发送get请求。
- 节点使用文档的_id确定文档属于分片0。分片0对应的复制分片在三个节点上都有。此时,它转发请求到Node 2。
- Node 2返回文档(document)给Node 1,然后返回给客户端。
局部更新文档
- 客户端给Node 1发送更新请求。
- 它转发请求到主分片所在节点Node 3。
- Node 3从主分片检索出文档,修改_source字段的JSON,然后在主分片上重建索引。如果有其他进程修改了文档,它以retry_on_conflict设置的次数重复步骤3,都未成功则放弃。
- 如果Node 3成功更新文档,它同时转发文档的新版本到Node 1和Node 2上的复制节点以重建索引。当所有复制节点报告成功,Node 3返回成功给请求节点,然后返回给客户端。
批量请求
mget
- 客户端向Node 1发送mget请求。
- Node 1为每个分片构建一个多条数据检索请求,然后转发到这些请求所需的主分片或复制分片上。当所有回复被接收,Node 1构建响应并返回给客户端。
- routing 参数可以被docs中的每个文档设置。
bulk
- 客户端向Node 1发送bulk请求。
- Node 1为每个分片构建批量请求,然后转发到这些请求所需的主分片上。
- 主分片一个接一个的按序执行操作。当一个操作执行完,主分片转发新文档(或者删除部分)给对应的复制节点,然后执行下一个操作。一旦所有复制节点报告所有操作已成功完成,节点就报告success给请求节点,后者(请求节点)整理响应并返回给客户端。
bulk API还可以在最上层使用replication和consistency参数,routing参数则在每个请求的元数据中使用。
批量格式
批量中每个引用的文档属于不同的主分片,每个分片可能被分布于集群中的某个节点上。这意味着批量中的每个操作(action)需要被转发到对应的分片和节点上。
如果每个单独的请求被包装到JSON数组中,那意味着我们需要:
- 解析JSON为数组(包括文档数据,可能非常大)
- 检查每个请求决定应该到哪个分片上
- 为每个分片创建一个请求的数组
- 序列化这些数组为内部传输格式
- 发送请求到每个分片
这可行,但需要大量的RAM来承载本质上相同的数据,还要创建更多的数据结构使得JVM花更多的时间执行垃圾回收。
取而代之的,Elasticsearch则是从网络缓冲区中一行一行的直接读取数据。它使用换行符识别和解析action/metadata行,以决定哪些分片来处理这个请求。
这些行请求直接转发到对应的分片上。这些没有冗余复制,没有多余的数据结构。整个请求过程使用最小的内存在进行。
搜索
搜索(search)可以:
- 在类似于gender或者age这样的字段上使用结构化查询,join_date这样的字段上使用排序,就像SQL的结构化查询一样。
- 全文检索,可以使用所有字段来匹配关键字,然后按照关联性(relevance)排序返回结果。
- 或者结合以上两条。
概念 | 解释 |
---|---|
映射(Mapping) | 数据在每个字段中的解释说明 |
分析(Analysis) | 全文是如何处理的可以被搜索的 |
领域特定语言查询(Query DSL) | Elasticsearch使用的灵活的、强大的查询语言 |
空查询
最基本的搜索API表单是空搜索(empty search),它没有指定任何的查询条件,只返回集群索引中的所有文档:
GET /_search
响应内容(为了编辑简洁)类似于这样:
{
"hits" : {
"total" : 14,
"hits" : [
{
"_index": "us",
"_type": "tweet",
"_id": "7",
"_score": 1,
"_source": {
"date": "2014-09-17",
"name": "John Smith",
"tweet": "The Query DSL is really powerful and flexible",
"user_id": 2
}
},
... 9 RESULTS REMOVED ...
],
"max_score" : 1
},
"took" : 4,
"_shards" : {
"failed" : 0,
"successful" : 10,
"total" : 10
},
"timed_out" : false
}
hits
响应中最重要的部分是hits,它包含了total字段来表示匹配到的文档总数,hits数组还包含了匹配到的前10条数据。hits数组中的每个结果都包含_index、_type和文档的_id字段,被加入到_source字段中这意味着在搜索结果中我们将可以直接使用全部文档。这不像其他搜索引擎只返回文档ID,需要你单独去获取文档。
每个节点都有一个_score字段,这是相关性得分(relevance score),它衡量了文档与查询的匹配程度。默认的,返回的结果中关联性最大的文档排在首位;这意味着,它是按照_score降序排列的。这种情况下,我们没有指定任何查询,所以所有文档的相关性是一样的,因此所有结果的_score都是取得一个中间值1
max_score指的是所有文档匹配查询中_score的最大值。
took
took告诉我们整个搜索请求花费的毫秒数。
shards
_shards节点告诉我们参与查询的分片数(total字段),有多少是成功的(successful字段),有多少的是失败的(failed字段)。通常我们不希望分片失败,不过这个有可能发生。如果我们遭受一些重大的故障导致主分片和复制分片都故障,那这个分片的数据将无法响应给搜索请求。这种情况下,Elasticsearch将报告分片failed,但仍将继续返回剩余分片上的结果。
多索引和多类型
你注意到空搜索的结果中不同类型的文档——user和tweet——来自于不同的索引——us和gb。
通过限制搜索的不同索引或类型,我们可以在集群中跨所有文档搜索。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
分页
Elasticsearch接受from和size参数:
size: 结果数,默认10
from: 跳过开始的结果数,默认0
如果你想每页显示5个结果,页码从1到3,那请求如下:
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
应该当心分页太深或者一次请求太多的结果。结果在返回前会被排序。但是记住一个搜索请求常常涉及多个分片。每个分片生成自己排好序的结果,它们接着需要集中起来排序以确保整体排序正确。
查询字符串
search API有两种表单:一种是“简易版”的查询字符串(query string)将所有参数通过查询字符串定义,另一种版本使用JSON完整的表示请求体(request body),这种富搜索语言叫做结构化查询语句(DSL)
查询字符串搜索对于在命令行下运行点对点(ad hoc)查询特别有用。例如这个语句查询所有类型为tweet并在tweet字段中包含elasticsearch字符的文档:
GET /_all/tweet/_search?q=tweet:elasticsearch
下一个语句查找name字段中包含"john"和tweet字段包含"mary"的结果。实际的查询只需要:
+name:john +tweet:mary
但是百分比编码(percent encoding)(译者注:就是url编码)需要将查询字符串参数变得更加神秘:
"+“前缀表示语句匹配条件必须被满足。类似的”-"前缀表示条件必须不被满足。所有条件如果没有+或-表示是可选的——匹配越多,相关的文档就越多。
_all字段
返回包含"mary"字符的所有文档的简单搜索:
GET /_search?q=mary
在前一个例子中,我们搜索tweet或name字段中包含某个字符的结果。然而,这个语句返回的结果在三个不同的字段中包含"mary":
用户的名字是“Mary”
“Mary”发的六个推文
针对“@mary”的一个推文
Elasticsearch是如何设法找到三个不同字段的结果的?
当你索引一个文档,Elasticsearch把所有字符串字段值连接起来放在一个大字符串中,它被索引为一个特殊的字段_all。例如,当索引这个文档:
{
“tweet”: “However did I manage before Elasticsearch?”,
“date”: “2014-09-14”,
“name”: “Mary Jones”,
“user_id”: 1
}
这好比我们增加了一个叫做_all的额外字段值:
“However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1”
若没有指定字段,查询字符串搜索(即q=xxx)使用_all字段搜索。
更复杂的语句
下一个搜索推特的语句:
_all field
name字段包含"mary"或"john"
date晚于2014-09-10
_all字段包含"aggregations"或"geo"
+name:(mary john) +date:>2014-09-10 +(aggregations geo)
编码后的查询字符串变得不太容易阅读:
?q=%2Bname%3A(mary+john)+%2Bdate%3A%3E2014-09-10+%2B(aggregations+geo)
就像你上面看到的例子,简单(lite)查询字符串搜索惊人的强大。它的查询语法,会在《查询字符串语法》章节阐述。参考文档允许我们简洁明快的表示复杂的查询。这对于命令行下一次性查询或者开发模式下非常有用。
映射与分析
映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型(string, number, booleans, date等)。
分析(analysis)机制用于进行全文文本(Full Text)的分词,以建立供搜索用的反向索引。
倒排索引
为了方便在全文文本字段中进行这些类型的查询,Elasticsearch首先对文本分析(analyzes),然后使用结果建立一个倒排索引。
分析
分析(analysis)是这样一个过程:
首先,标记化一个文本块为适用于倒排索引单独的词(term)
然后标准化这些词为标准形式,提高它们的“可搜索性”或“查全率”
这个工作是分析器(analyzer)完成的。一个分析器(analyzer)只是一个包装用于将三个功能放到一个包里:
字符过滤器
首先字符串经过字符过滤器(character filter),它们的工作是在标记化前处理字符串。字符过滤器能够去除HTML标记,或者转换"&“为"and”。
分词器
下一步,分词器(tokenizer)被标记化成独立的词。一个简单的分词器(tokenizer)可以根据空格或逗号将单词分开(译者注:这个在中文中不适用)。
标记过滤
最后,每个词都通过所有标记过滤(token filters),它可以修改词(例如将"Quick"转为小写),去掉词(例如停用词像"a"、“and”、“the"等等),或者增加词(例如同义词像"jump"和"leap”)
当我们索引(index)一个文档,全文字段会被分析为单独的词来创建倒排索引。不过,当我们在全文字段搜索(search)时,我们要让查询字符串经过同样的分析流程处理,以确保这些词在索引中存在。
- 当查询全文(full text)字段,查询将使用相同的分析器来分析查询字符串,以产生正确的词列表。
- 当查询一个确切值(exact value)字段,查询将不分析查询字符串,但是可以自己指定。
GET /_analyze?analyzer=standard&text=Text to analyze
结果中每个节点在代表一个词:
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
token是一个实际被存储在索引中的词。position指明词在原文本中是第几个出现的。start_offset和end_offset表示词在原文本中占据的位置。
映射
Elasticsearch支持以下简单字段类型:
类型 | 表示的数据类型 |
---|---|
String | string |
Whole number | byte, short, integer, long |
Floating point | float, double |
Boolean | boolean |
Date | date |
查看映射
我们可以使用_mapping后缀来查看Elasticsearch中的映射。在本章开始我们已经找到索引gb类型tweet中的映射:
GET /gb/_mapping/tweet
这展示给了我们字段的映射(叫做属性(properties)),这些映射是Elasticsearch在创建索引时动态生成的:
{
"gb": {
"mappings": {
"tweet": {
"properties": {
"date": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"name": {
"type": "string"
},
"tweet": {
"type": "string"
},
"user_id": {
"type": "long"
}
}
}
}
}
}
自定义字段映射
string类型的字段,默认的,考虑到包含全文本,它们的值在索引前要经过分析器分析,并且在全文搜索此字段前要把查询语句做分析处理。
对于string字段,两个最重要的映射参数是index和analyer。
index参数控制字符串以何种方式被索引。它包含以下三个值当中的一个:
值 | 解释 |
---|---|
analyzed | 首先分析这个字符串,然后索引。换言之,以全文形式索引此字段。 |
not_analyzed | 索引这个字段,使之可以被搜索,但是索引内容和指定值一样。不分析此字段。 |
no | 不索引这个字段。这个字段不能为搜索到。 |
{
"tag": {
"type": "string",
"index": "not_analyzed"
}
},
{
"tweet": {
"type": "string",
"analyzer": "english"
}
}
更新映射
PUT /gb
{
"mappings": {
"tweet" : {
"properties" : {
"tweet" : {
"type" : "string",
"analyzer": "english"
},
"date" : {
"type" : "date"
},
"name" : {
"type" : "string"
},
"user_id" : {
"type" : "long"
}
}
}
}
}
在tweet的映射中增加一个新的not_analyzed类型的文本字段,叫做tag,使用_mapping后缀:
PUT /gb/_mapping/tweet
{
"properties" : {
"tag" : {
"type" : "string",
"index": "not_analyzed"
}
}
}
复合类型
多值字段
任何一个字段可以包含零个、一个或多个值,同样对于全文字段将被分析并产生多个词。
{ "tag": [ "search", "nosql" ]}
言外之意,这意味着数组中所有值必须为同一类型。你不能把日期和字符窜混合。如果你创建一个新字段,这个字段索引了一个数组,Elasticsearch将使用第一个值的类型来确定这个新字段的类型。
空字段
这四个字段将被识别为空字段而不被索引:
"empty_string": "",
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
多层对象
{
"tweet": "Elasticsearch is very flexible",
"user": {
"id": "@johnsmith",
"gender": "male",
"age": 26,
"name": {
"full": "John Smith",
"first": "John",
"last": "Smith"
}
}
}
{
"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> 内部对象.
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]
}
结构化查询
结构化查询
一个查询子句一般使用这种结构:
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
或指向一个指定的字段:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
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" }}
}
}
查询与过滤
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” ]
}
}
range 过滤
range过滤允许我们按照指定范围查找一批数据:
{
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
范围操作符包含:
- gt :: 大于
- gte:: 大于等于
- lt :: 小于
- lte:: 小于等于
exists 和 missing 过滤
exists 和 missing 过滤可以用于查找文档中是否包含指定字段或没有某个字段
{
"exists": {
"field": "title"
}
}
bool 过滤
bool 过滤可以用来合并多个过滤条件查询结果的布尔逻辑,它包含一下操作符:
- must :: 多个查询条件的完全匹配,相当于 and。
-must_not :: 多个查询条件的相反匹配,相当于 not。 - should :: 至少有一个查询条件匹配, 相当于 or。
这些参数可以分别继承一个过滤条件或者一个过滤条件的数组:
{
"bool": {
"must": { "term": { "folder": "inbox" }},
"must_not": { "term": { "tag": "spam" }},
"should": [
{ "term": { "starred": true }},
{ "term": { "unread": true }}
]
}
}
match_all 查询
使用match_all 可以查询到所有文档,是没有查询条件下的默认语句。
{
"match_all": {}
}
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" }}
做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。
multi_match 查询
{
"multi_match": {
"query": "full text search",
"fields": [ "title", "body" ]
}
}
bool 查询
bool 查询与 bool 过滤相似,用于合并多个查询子句。不同的是,bool 过滤可以直接给出是否匹配成功, 而bool 查询要计算每一个查询子句的 _score (相关性分值)。
- must:: 查询指定文档一定要被包含。
- must_not:: 查询指定文档一定不要被包含。
- should:: 查询指定文档,有则可以为文档相关性加分。
{
"bool": {
"must": { "match": { "title": "how to make millions" }},
"must_not": { "match": { "tag": "spam" }},
"should": [
{ "match": { "tag": "starred" }},
{ "range": { "date": { "gte": "2014-01-01" }}}
]
}
}
使用过滤语句得到的结果集 – 一个简单的文档列表,快速匹配运算并存入内存是十分方便的, 每个文档仅需要1个字节。这些缓存的过滤结果集与后续请求的结合使用是非常高效的。
查询语句不仅要查找相匹配的文档,还需要计算每个文档的相关性,所以一般来说查询语句要比 过滤语句更耗时,并且查询结果也不可缓存。
过滤查询
search API中只能包含 query 语句
GET /_search
{
"query": {
"filtered": {
"query": { "match": { "email": "business opportunity" }},
"filter": { "term": { "folder": "inbox" }}
}
}
}
验证查询
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
排序
排序方式
在ElasticSearch的查询结果中, 相关性分值会用_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",
...
},
"sort" : [ 1411516800000 ] <2>
},
...
}
<1> _score 字段没有经过计算,因为它没有用作排序。
<2> date 字段被转为毫秒当作排序依据。
字段值默认以顺序排列,而 _score 默认以倒序排列。
多级排序
如果我们想要合并一个查询语句,并且展示所有匹配的结果集使用第一排序是date,第二排序是 _score:
GET /_search
{
"query" : {
"filtered" : {
"query": { "match": { "tweet": "manage text search" }},
"filter" : { "term" : { "user_id" : 2 }}
}
},
"sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
}
排序是很重要的。结果集会先用第一排序字段来排序,当用用作第一字段排序的值相同的时候, 然后再用第二字段对第一排序值相同的文档进行排序,以此类推。
多值字段排序
被分析器(analyser)处理过的字符称为analyzed field(译者注:即已被分词并排序的字段,所有写入ES中的字段默认圴会被analyzed), analyzed字符串字段同时也是多值字段,在这些字段上排序往往得不到你想要的值。 比如你分析一个字符 “fine old art”,它最终会得到三个值。例如我们想要按照第一个词首字母排序, 如果第一个单词相同的话,再用第二个词的首字母排序,以此类推,可惜 ElasticSearch 在进行排序时 是得不到这些信息的。
当然你可以使用 min 和 max 模式来排(默认使用的是 min 模式)但它是依据art 或者 old排序, 而不是我们所期望的那样。
为了使一个string字段可以进行排序,它必须只包含一个词:即完整的not_analyzed字符串(译者注:未经分析器分词并排序的原字符串)。 当然我们需要对字段进行全文本搜索的时候还必须使用被 analyzed 标记的字段。
在 _source 下相同的字符串上排序两次会造成不必要的资源浪费。 而我们想要的是同一个字段中同时包含这两种索引方式,我们只需要改变索引(index)的mapping即可。 方法是在所有核心字段类型上,使用通用参数 fields对mapping进行修改。 比如,我们原有mapping如下:
"tweet": {
"type": "string",
"analyzer": "english"
}
改变后的多值字段mapping如下:
"tweet": { <1>
"type": "string",
"analyzer": "english",
"fields": {
"raw": { <2>
"type": "string",
"index": "not_analyzed"
}
}
}
<1> tweet 字段用于全文本的 analyzed 索引方式不变。
<2> 新增的 tweet.raw 子字段索引方式是 not_analyzed。
现在,在给数据重建索引后,我们既可以使用 tweet 字段进行全文本搜索,也可以用tweet.raw字段进行排序:
GET /_search
{
"query": {
"match": {
"tweet": "elasticsearch"
}
},
"sort": "tweet.raw"
}
警告: 对 analyzed 字段进行强制排序会消耗大量内存。
相关性简介
查询语句会为每个文档添加一个 _score 字段。评分的计算方式取决于不同的查询类型 – 不同的查询语句用于不同的目的:fuzzy 查询会计算与关键词的拼写相似程度,terms查询会计算 找到的内容与关键词组成部分匹配的百分比。而文本相似度为 tf-idf。此外,长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段。
字段数据
为了提高排序效率,ElasticSearch 会将所有字段的值加载到内存中,这就叫做"数据字段"。
重要: ElasticSearch将所有字段数据加载到内存中并不是匹配到的那部分数据。 而是索引下所有文档中的值,包括所有类型。
分布式搜索
查询阶段
在初始化查询阶段(query phase),查询被向索引中的每个分片副本(原本或副本)广播。每个分片在本地执行搜索并且建立了匹配document的优先队列(priority queue)。
优先队列
一个优先队列(priority queue is)只是一个存有前n个(top-n)匹配document的有序列表。这个优先队列的大小由分页参数from和size决定。
当一个搜索请求被发送到一个节点Node,这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集。这个结果集会被返回给客户端。
第一步是向索引里的每个节点的分片副本广播请求。就像document的GET请求一样,搜索请求可以被每个分片的原本或任意副本处理。这就是更多的副本(当结合更多的硬件时)如何提高搜索的吞吐量的方法。对于后续请求,协调节点会轮询所有的分片副本以分摊负载。
每一个分片在本地执行查询和建立一个长度为from+size的有序优先队列——这个长度意味着它自己的结果数量就足够满足全局的请求要求。分片返回一个轻量级的结果列表给协调节点。只包含documentID值和排序需要用到的值,例如_score。
协调节点将这些分片级的结果合并到自己的有序优先队列里。这个就代表了最终的全局有序结果集。到这里,查询阶段结束。
取回阶段
查询阶段辨别出那些满足搜索请求的document,但我们仍然需要取回那些document本身。这就是取回阶段的工作。
搜索选项
preference(偏爱)
preference参数允许你控制使用哪个分片或节点来处理搜索请求。她接受如下一些参数 _primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz和_shards:2,3。这些参数在文档搜索偏好(search preference)里有详细描述。
然而通常最有用的值是一些随机字符串,它们可以避免结果震荡问题(the bouncing results problem)。
结果震荡(Bouncing Results)
想像一下,你正在按照timestamp字段来对你的结果排序,并且有两个document有相同的timestamp。由于搜索请求是在所有有效的分片副本间轮询的,这两个document可能在原始分片里是一种顺序,在副本分片里是另一种顺序。
这就是被称为结果震荡(bouncing results)的问题:用户每次刷新页面,结果顺序会发生变化。避免这个问题方法是对于同一个用户总是使用同一个分片。方法就是使用一个随机字符串例如用户的会话ID(session ID)来设置preference参数。
timeout(超时)
通常,协调节点会等待接收所有分片的回答。如果有一个节点遇到问题,它会拖慢整个搜索请求。
timeout参数告诉协调节点最多等待多久,就可以放弃等待而将已有结果返回。返回部分结果总比什么都没有好。
routing(路由选择)
在路由值那节里,我们解释了如何在建立索引时提供一个自定义的routing参数来保证所有相关的document(如属于单个用户的document)被存放在一个单独的分片中。在搜索时,你可以指定一个或多个routing 值来限制只搜索那些分片而不是搜索index里的全部分片:
GET /_search?routing=user_1,user2
search_type(搜索类型)
虽然query_then_fetch是默认的搜索类型,但也可以根据特定目的指定其它的搜索类型,例如:
GET /_search?search_type=count
count(计数)
count(计数)搜索类型只有一个query(查询)的阶段。当不需要搜索结果只需要知道满足查询的document的数量时,可以使用这个查询类型。
query_and_fetch(查询并且取回)
query_and_fetch(查询并且取回)搜索类型将查询和取回阶段合并成一个步骤。这是一个内部优化选项,当搜索请求的目标只是一个分片时可以使用,例如指定了routing(路由选择)值时。虽然你可以手动选择使用这个搜索类型,但是这么做基本上不会有什么效果。
dfs_query_then_fetch 和 dfs_query_and_fetch
dfs搜索类型有一个预查询的阶段,它会从全部相关的分片里取回项目频数来计算全局的项目频数。
scan(扫描)
scan(扫描)搜索类型是和scroll(滚屏)API连在一起使用的,可以高效地取回巨大数量的结果。
扫描和滚屏
scan(扫描)搜索类型是和scroll(滚屏)API一起使用来从Elasticsearch里高效地取回巨大数量的结果而不需要付出深分页的代价。
scroll(滚屏)
一个滚屏搜索允许我们做一个初始阶段搜索并且持续批量从Elasticsearch里拉取结果直到没有结果剩下。这有点像传统数据库里的cursors(游标)。
滚屏搜索会及时制作快照。这个快照不会包含任何在初始阶段搜索请求后对index做的修改。它通过将旧的数据文件保存在手边,所以可以保护index的样子看起来像搜索开始时的样子。
scan(扫描)
深度分页代价最高的部分是对结果的全局排序,但如果禁用排序,就能以很低的代价获得全部返回结果。为达成这个目的,可以采用scan(扫描)搜索模式。扫描模式让Elasticsearch不排序,只要分片里还有结果可以返回,就返回一批结果。
为了使用scan-and-scroll(扫描和滚屏),需要执行一个搜索请求,将search_type 设置成scan,并且传递一个scroll参数来告诉Elasticsearch滚屏应该持续多长时间。
索引管理
创建删除
此外,可以通过在 config/elasticsearch.yml 中添加下面的配置来防止自动创建索引。
action.auto_create_index: false
使用以下的请求来删除索引:
DELETE /my_index
你也可以用下面的方式删除多个索引
DELETE /index_one,index_two
DELETE /index_*
你甚至可以删除所有索引
DELETE /_all
配置分析器
第三个重要的索引设置是 analysis 部分,用来配置已存在的分析器或创建自定义分析器来定制化你的索引。
在【分析器介绍】中,我们介绍了一些内置的分析器,用于将全文字符串转换为适合搜索的倒排索引。
standard 分析器是用于全文字段的默认分析器,对于大部分西方语系来说是一个不错的选择。它考虑了以下几点:
- standard 分词器,在词层级上分割输入的文本。
- standard 标记过滤器,被设计用来整理分词器触发的所有标记(但是目前什么都没做)。
- lowercase 标记过滤器,将所有标记转换为小写。
- stop 标记过滤器,删除所有可能会造成搜索歧义的停用词,如 a,the,and,is。
默认情况下,停用词过滤器是被禁用的。如需启用它,你可以通过创建一个基于 standard 分析器的自定义分析器,并且设置 stopwords 参数。可以提供一个停用词列表,或者使用一个特定语言的预定停用词列表。
PUT /spanish_docs
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_spanish_"
}
}
}
}
}
自定义分析器
字符过滤器
字符过滤器是让字符串在被分词前变得更加“整洁”。例如,如果我们的文本是 HTML 格式,它可能会包含一些我们不想被索引的 HTML 标签,诸如
<p> 和 <div>
我们可以使用 html_strip 字符过滤器 来删除所有的 HTML 标签,并且将 HTML 实体转换成对应的 Unicode 字符,比如将 Á 转成 Á。
一个分析器可能包含零到多个字符过滤器。
分词器
一个分析器 必须 包含一个分词器。分词器将字符串分割成单独的词(terms)或标记(tokens)。standard 分析器使用 standard 分词器将字符串分割成单独的字词,删除大部分标点符号,但是现存的其他分词器会有不同的行为特征。
例如,keyword 分词器输出和它接收到的相同的字符串,不做任何分词处理。[whitespace 分词器]只通过空格来分割文本。[pattern 分词器]可以通过正则表达式来分割文本。
标记过滤器
分词结果的 标记流 会根据各自的情况,传递给特定的标记过滤器。
标记过滤器可能修改,添加或删除标记。我们已经提过 lowercase 和 stop 标记过滤器,但是 Elasticsearch 中有更多的选择。stemmer 标记过滤器将单词转化为他们的根形态(root form)。ascii_folding 标记过滤器会删除变音符号,比如从 très 转为 tres。 ngram 和 edge_ngram 可以让标记更适合特殊匹配情况或自动完成。
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": { ... custom character filters ... },
"tokenizer": { ... custom tokenizers ... },
"filter": { ... custom token filters ... },
"analyzer": { ... custom analyzers ... }
}
}
}
可如下配置
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "]
}
}
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ "the", "a" ]
}
}
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [ "html_strip", "&_to_and" ],
"tokenizer": "standard",
"filter": [ "lowercase", "my_stopwords" ]
}
}
根对象
映射的最高一层被称为 根对象,它可能包含下面几项:
- 一个 properties 节点,列出了文档中可能包含的每个字段的映射
- 多个元数据字段,每一个都以下划线开头,例如 _type, _id 和 _source
- 设置项,控制如何动态处理新的字段,例如 analyzer, dynamic_date_formats 和 dynamic_templates。
- 其他设置,可以同时应用在根对象和其他 object 类型的字段上,例如 enabled, dynamic 和 include_in_all
_source 字段
默认情况下,Elasticsearch 用 JSON 字符串来表示文档主体保存在 _source 字段中。像其他保存的字段一样,_source 字段也会在写入硬盘前压缩。
这几乎始终是需要的功能,因为:
-
搜索结果中能得到完整的文档 —— 不需要额外去别的数据源中查询文档
-
如果缺少 _source 字段,部分 更新 请求不会起作用
-
当你的映射有变化,而且你需要重新索引数据时,你可以直接在 Elasticsearch 中操作而不需要重新从别的数据源中取回数据。
-
你可以从 _source 中通过 get 或 search 请求取回部分字段,而不是整个文档。
-
这样更容易排查错误,因为你可以准确的看到每个文档中包含的内容,而不是只能从一堆 ID 中猜测他们的内容。
_all 字段
_all 字段:一个所有其他字段值的特殊字符串字段。
_id 字段
文档唯一标识由四个元数据字段组成:
- _id:文档的字符串 ID
- _type:文档的类型名
- _index:文档所在的索引
- _uid:_type 和 _id 连接成的 type#id
默认情况下,_uid 是被保存(可取回)和索引(可搜索)的。- _type 字段被索引但是没有保存,_id 和 _index 字段则既没有索引也没有储存,它们并不是真实存在的。
Elasticsearch 使用 _uid 字段来追溯 _id。虽然你可以修改这些字段的 index 和 store 设置,但是基本上不需要这么做。
动态映射
当 Elasticsearch 处理一个位置的字段时,它通过【动态映射】来确定字段的数据类型且自动将该字段加到类型映射中。
dynamic 设置来控制这些行为,它接受下面几个选项:
- true:自动添加字段(默认)
- false:忽略字段
- strict:当遇到未知字段时抛出异常
动态模板
使用 dynamic_templates,你可以完全控制新字段的映射,你设置可以通过字段名或数据类型应用一个完全不同的映射。
每个模板都有一个名字用于描述这个模板的用途,一个 mapping 字段用于指明这个映射怎么使用,和至少一个参数(例如 match)来定义这个模板适用于哪个字段。
模板按照顺序来检测,第一个匹配的模板会被启用。例如,我们给 string 类型字段定义两个模板:
- es: 字段名以 _es 结尾需要使用 spanish 分析器。
- en: 所有其他字段使用 english 分析器。
我们将 es 模板放在第一位,因为它比匹配所有字符串的 en 模板更特殊一点
PUT /my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{ "es": {
"match": "*_es", <1>
"match_mapping_type": "string",
"mapping": {
"type": "string",
"analyzer": "spanish"
}
}},
{ "en": {
"match": "*", <2>
"match_mapping_type": "string",
"mapping": {
"type": "string",
"analyzer": "english"
}
}}
]
}}}
默认映射
通常,一个索引中的所有类型具有共享的字段和设置。用 default 映射来指定公用设置会更加方便,而不是每次创建新的类型时重复操作。_default 映射像新类型的模板。所有在 default 映射 之后 的类型将包含所有的默认设置,除非在自己的类型映射中明确覆盖这些配置。
PUT /my_index
{
"mappings": {
"_default_": {
"_all": { "enabled": false }
},
"blog": {
"_all": { "enabled": true }
}
}
}
default 映射也是定义索引级别的动态模板的好地方。
重建索引
索引 别名 就像一个快捷方式或软连接,可以指向一个或多个索引,也可以给任何需要索引名的 API 使用。别名带给我们极大的灵活性,允许我们做到:
- 在一个运行的集群上无缝的从一个索引切换到另一个
- 给多个索引分类(例如,last_three_months)
- 给索引的一个子集创建 视图
GET /*/_alias/my_index
或哪些别名指向这个索引:
GET /my_index_v1/_alias/*
两者都将返回下列值:
{
"my_index_v1" : {
"aliases" : {
"my_index" : { }
}
}
}
别名可以指向多个索引,所以我们需要在新索引中添加别名的同时从旧索引中删除它。这个操作需要原子化,所以我们需要用 _aliases 操作:
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}
深入分片
写入磁盘的倒排索引是不可变的,它有如下好处:
不需要锁。如果从来不需要更新一个索引,就不必担心多个程序同时尝试修改。
一旦索引被读入文件系统的缓存(译者:在内存),它就一直在那儿,因为不会改变。只要文件系统缓存有足够的空间,大部分的读会直接访问内存而不是磁盘。这有助于性能提升。
在索引的声明周期内,所有的其他缓存都可用。它们不需要在每次数据变化了都重建,因为数据不会变。
写入单个大的倒排索引,可以压缩数据,较少磁盘IO和需要缓存索引的内存大小。
当然,不可变的索引有它的缺点,首先是它不可变!你不能改变它。如果想要搜索一个新文档,必须重见整个索引。这不仅严重限制了一个索引所能装下的数据,还有一个索引可以被更新的频次。
动态索引
Elasticsearch底层依赖的Lucene,引入了per-segment search的概念。一个段(segment)是有完整功能的倒排索引,但是现在Lucene中的索引指的是段的集合,再加上提交点(commit point,包括所有段的文件)
一个per-segment search如下工作:
-
新的文档首先写入内存区的索引缓存。
-
不时,这些buffer被提交:
- 一个新的段——额外的倒排索引——写入磁盘。
- 新的提交点写入磁盘,包括新段的名称。
-
磁盘是fsync’ed(文件同步)——所有写操作等待文件系统缓存同步到磁盘,确保它们可以被物理写入。
新段被打开,它包含的文档可以被检索 -
内存的缓存被清除,等待接受新的文档。
段是不可变的,所以文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。相反,每一个提交点包括一个.del文件,包含了段上已经被删除的文档。
当一个文档被删除,它实际上只是在.del文件中被标记为删除,依然可以匹配查询,但是最终返回之前会被从结果中删除。
文档的更新操作是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。也许该文档的不同版本都会匹配一个查询,但是更老版本会从结果中删除。
近实时搜索
因为per-segment search机制,索引和搜索一个文档之间是有延迟的。新的文档会在几分钟内可以搜索,但是这依然不够快。
磁盘是瓶颈。提交一个新的段到磁盘需要fsync操作,确保段被物理地写入磁盘,即时电源失效也不会丢失数据。但是fsync是昂贵的,它不能在每个文档被索引的时就触发。
所以需要一种更轻量级的方式使新的文档可以被搜索,这意味这移除fsync。
位于Elasticsearch和磁盘间的是文件系统缓存。如前所说,在内存索引缓存中的文档(图1)被写入新的段(图2),但是新的段首先写入文件系统缓存,这代价很低,之后会被同步到磁盘,这个代价很大。但是一旦一个文件被缓存,它也可以被打开和读取,就像其他文件一样。
Lucene允许新段写入打开,好让它们包括的文档可搜索,而不用执行一次全量提交。这是比提交更轻量的过程,可以经常操作,而不会影响性能。
refeash API
在Elesticsearch中,这种写入打开一个新段的轻量级过程,叫做refresh。默认情况下,每个分片每秒自动刷新一次。这就是为什么说Elasticsearch是近实时的搜索了:文档的改动不会立即被搜索,但是会在一秒内可见。
这会困扰新用户:他们索引了个文档,尝试搜索它,但是搜不到。解决办法就是执行一次手动刷新,通过API:
POST /_refresh <1>
POST /blogs/_refresh <2>
<1> refresh所有索引
<2> 只refresh 索引blogs
虽然刷新比提交更轻量,但是它依然有消耗。人工刷新在测试写的时有用,但是不要在生产环境中每写一次就执行刷新,这会影响性能。相反,你的应用需要意识到ES近实时搜索的本质,并且容忍它。
不是所有的用户都需要每秒刷新一次。也许你使用ES索引百万日志文件,你更想要优化索引的速度,而不是进实时搜索。你可以通过修改配置项refresh_interval减少刷新的频率:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s" <1>
}
}
<1> 每30s refresh一次my_logs
refresh_interval可以在存在的索引上动态更新。你在创建大索引的时候可以关闭自动刷新,在要使用索引的时候再打开它。
PUT /my_logs/_settings
{ "refresh_interval": -1 } <1>
PUT /my_logs/_settings
{ "refresh_interval": "1s" } <2>
<1> 禁用所有自动refresh
<2> 每秒自动refresh
持久化变更
没用fsync同步文件系统缓存到磁盘,我们不能确保电源失效,甚至正常退出应用后,数据的安全。为了ES的可靠性,需要确保变更持久化到磁盘。
我们说过一次全提交同步段到磁盘,写提交点,这会列出所有的已知的段。在重启,或重新打开索引时,ES使用这次提交点决定哪些段属于当前的分片。
当我们通过每秒的刷新获得近实时的搜索,我们依然需要定时地执行全提交确保能从失败中恢复。但是提交之间的文档怎么办?我们也不想丢失它们。
ES增加了事务日志(translog),来记录每次操作。有了事务日志,过程现在如下:
- 当一个文档被索引,它被加入到内存缓存,同时加到事务日志。
- refresh使得分片的进入如下图描述的状态。每秒分片都进行refeash:
- 内存缓冲区的文档写入到段中,但没有fsync。
- 段被打开,使得新的文档可以搜索。
- 缓存被清除
- 随着更多的文档加入到缓存区,写入日志,这个过程会继续。
- 不时地,比如日志很大了,新的日志会创建,会进行一次全提交:
- 内存缓存区的所有文档会写入到新段中。
- 清除缓存
- 一个提交点写入硬盘
- 文件系统缓存通过fsync操作flush到硬盘
- 事务日志被清除
在ES中,进行一次提交并删除事务日志的操作叫做 flush。分片每30分钟,或事务日志过大会进行一次flush操作。
flush API可用来进行一次手动flush:
POST /blogs/_flush <1>
POST /_flush?wait_for_ongoing <2>
合并段
通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了。有太多的段是一个问题。每个段消费文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。
ES通过后台合并段解决这个问题。小段被合并成大段,再合并成更大的段。
这是旧的文档从文件系统删除的时候。旧的段不会再复制到更大的新段中。
这个过程你不必做什么。当你在索引和搜索时ES会自动处理。这个过程如图:两个提交的段和一个未提交的段合并为了一个更大的段所示:
- 索引过程中,refresh会创建新的段,并打开它。
- 合并过程会在后台选择一些小的段合并成大的段,这个过程不会中断索引和搜索。
- 下图描述了合并后的操作:
- 新的段flush到了硬盘。
- 新的提交点写入新的段,排除旧的段。
- 新的段打开供搜索。
- 旧的段被删除。
结构化搜索
Elasticsearch 在内部会通过一些操作来执行一次过滤:
-
查找匹配文档。
term 过滤器在倒排索引中查找词 XHDK-A-1293-#fJ3,然后返回包含那个词的文档列表。在这个例子中,只有文档 1 有我们想要的词。
-
创建字节集
然后过滤器将创建一个 字节集 —— 一个由 1 和 0 组成的数组 —— 描述哪些文档包含这个词。匹配的文档得到 1 字节,在我们的例子中,字节集将是 [1,0,0,0]
-
缓存字节集
最后,字节集被储存在内存中,以使我们能用它来跳过步骤 1 和 2。这大大的提升了性能,让过滤变得非常的快。
当执行 filtered 查询时,filter 会比 query 早执行。结果字节集会被传给 query 来跳过已经被排除的文档。这种过滤器提升性能的方式,查询更少的文档意味着更快的速度。
组合过滤
bool 过滤器可嵌套
多值查询
GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"terms" : { <1>
"price" : [20, 30]
}
}
}
}
}
term 和 terms 是包含操作,而不是相等操作。
范围
range 过滤器,数字,日期,字符串
null
exists 过滤器,这个过滤器将返回任何包含这个字段的文档
missing 过滤器,相反
缓存
缓存的字节集很“聪明”:他们会增量更新。你索引中添加了新的文档,只有这些新文档需要被添加到已存的字节集中,而不是一遍遍重新计算整个缓存的过滤器。过滤器和整个系统的其他部分一样是实时的,你不需要关心缓存的过期时间。
独立的过滤缓存
每个过滤器都被独立计算和缓存,而不管它们在哪里使用。如果两个不同的查询使用相同的过滤器,则会使用相同的字节集。同样,如果一个查询在多处使用同样的过滤器,只有一个字节集会被计算和重用。
过滤顺序
在 bool 条件中过滤器的顺序对性能有很大的影响。更详细的过滤条件应该被放置在其他过滤器之前,以便在更早的排除更多的文档。
全文搜索
全文检索最重要的两个方面是:
- 相关度(Relevance)
根据文档与查询的相关程度对结果集进行排序的能力。相关度可以使用TF/IDF、地理位置相近程度、模糊相似度或其他算法计算。 - 分析(Analysis)
将一段文本转换为一组唯一的、标准化了的标记(token),用以(a)创建倒排索引,(b)查询倒排索引。
文本查询可以分为两大类:
- 基于短语(Term-based)的查询:
像term或fuzzy一类的查询是低级查询,它们没有分析阶段。这些查询在单一的短语上执行。例如对单词’Foo’的term查询会在倒排索引里精确地查找’Foo’这个词,并对每个包含这个单词的文档计算TF/IDF相关度’_score’。
牢记term查询只在倒排查询里精确地查找特定短语,而不会匹配短语的其它变形,如foo或FOO。不管短语怎样被加入索引,都只匹配倒排索引里的准确值。如果你在一个设置了’not_analyzed’的字段为’[“Foo”, “Bar”]‘建索引,或者在一个用’whitespace’解析器解析的字段为’Foo Bar’建索引,都会在倒排索引里加入两个索引’Foo’和’Bar’。
- 全文(Full-text)检索
- match和query_string这样的查询是高级查询,它们会对字段进行分析:
- 如果检索一个’date’或’integer’字段,它们会把查询语句作为日期或者整数格式数据。
- 如果检索一个准确值(‘not_analyzed’)字符串字段,它们会把整个查询语句作为一个短语。
如果检索一个全文(‘analyzed’)字段,查询会先用适当的解析器解析查询语句,产生需要查询的短语列表。然后对列表中的每个短语执行低级查询,合并查询结果,得到最终的文档相关度。
匹配查询
Elasticsearch通过下面的步骤执行match查询:
-
检查field类型
title字段是一个字符串(analyzed),所以该查询字符串也需要被分析(analyzed)
-
分析查询字符串
查询词QUICK!经过标准分析器的分析后变成单词quick。因为我们只有一个查询词,因此match查询可以以一种低级别term查询的方式执行。
-
找到匹配的文档
term查询在倒排索引中搜索quick,并且返回包含该词的文档。在这个例子中,返回的文档是1,2,3。
-
为每个文档打分
term查询综合考虑词频(每篇文档title字段包含quick的次数)、逆文档频率(在全部文档中title字段包含quick的次数)、包含quick的字段长度(长度越短越相关)来计算每篇文档的相关性得分_score。
多词查询
匹配包含任意个数查询关键字的文档可能会得到一些看似不相关的结果,这是一种霰弹策略(shotgun approach)。然而我们可能想得到包含所有查询关键字的文档。换句话说,我们想得到的是匹配’brown AND dog’的文档,而非’brown OR dog’。
match查询接受一个’operator’参数,默认值为or。如果要求所有查询关键字都匹配,可以更改参数值为and:
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": { <1>
"query": "BROWN DOG!",
"operator": "and"
}
}
}
}
match查询有’minimum_should_match’参数,参数值表示被视为相关的文档必须匹配的关键词个数。参数值可以设为整数,也可以设置为百分数。因为不能提前确定用户输入的查询关键词个数,使用百分数也很合理。bool 查询也是这样的。
提高查询得分
当然,bool查询并不仅仅是组合多个简单的一个词的match查询。他可以组合任何其他查询,包括bool查询。bool查询通常会通过组合几个不同查询的得分为每个文档调整相关性得分。
匹配的should子句越多,文档的相关性就越强。到目前为止一切都很好。但是如果我们想给包含“Lucene”一词的文档比较高的得分,甚至给包含“Elasticsearch”一词更高的得分要怎么做呢?
我们可以在任何查询子句中指定一个boost值来控制相对权重,默认值为1。一个大于1的boost值可以提高查询子句的相对权重。因此我们可以像下面一样重写之前的查询:
GET /_search
{
"query": {
"bool": {
"must": {
"match": { (1)
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": {
"content": {
"query": "Elasticsearch",
"boost": 3 (2)
}
}},
{ "match": {
"content": {
"query": "Lucene",
"boost": 2 (3)
}
}}
]
}
}
}
分析控制
Elasticsearch查找分析器的顺序如下:
- 在映射文件中指定字段的analyzer,或者
- 在文档的_analyzer字段上指定分析器,或者
- 在映射文件中指定类型的默认分析器analyzer
- 在索引映射文件中设置默认分析器default
- 在节点级别设置默认分析器default
- standard分析器
查找索引的时候,Elasticsearch查找分析器的顺序稍微有点不一样:
- 在查询参数中指定analyzer,或者
- 在映射文件中指定字段的analyzer,或者
- 在映射文件中指定类型的默认分析器analyzer
- 在索引映射文件中设置默认分析器default
- 在节点级别设置默认分析器default
- standard分析器
有时候,在创建索引与查询索引的时候使用不同的分析器也是有意义的。举个例子:在创建索引的时候想要索引同义词 (比如, 出现quick的时候,我们也索引 fast, rapid, 和 speedy)。但是在查询索引的时候,我们不需要查询所有的同义词,我们只要查询用户输入的一个单词就可以了,它可以是quick, fast, rapid, 或者 speedy。
为了满足这种差异,Elasticsearch也支持index_analyzer 和 search_analyzer 参数,并且分析器被命名为default_index和default_search。把这些额外的参数考虑进去,Elasticsearch查找所有的分析器的顺序实际上像下面的样子:
- 在映射文件中指定字段的index_analyzer,或者
- 在映射文件中指定字段的analyzer,或者
- 在文档的_analyzer字段上指定分析器,或者
- 在映射文件中指定类型的创建索引的默认分析器index_analyzer
- 在映射文件中指定类型的默认分析器analyzer
- 在索引映射文件中设置创建索引的默认分析器default_index
- 在索引映射文件中设置默认分析器default
- 在节点级别设置创建索引的默认分析器default_index
- 在节点级别设置默认分析器default
- standard分析器
以及查询索引的时候:
- 在查询参数中指定analyzer,或者
- 在映射文件中指定字段的search_analyzer,或者
- 在映射文件中指定字段的analyzer,或者
- 在映射文件中指定类型的查询索引的默认分析器analyzer
- 在索引映射文件中设置查询索引的默认分析 default_search
- 在索引映射文件中设置默认分析器default_search
- 在节点级别设置查询索引的默认分析器default_search
- 在节点级别设置默认分析器default
- standard 分析器
关联失效
由于性能问题,Elasticsearch不通过索引中所有的文档计算IDF。每个分片会为分片中所有的文档计算一个本地的IDF取而代之。
在请求中添加?search_type=dfs_query_then_fetch。dfs就是Distributed Frequency Search,并且它会告诉Elasticsearch检查每一个分片的本地IDF为了计算整个索引的全局IDF。
多字段搜索
布尔查询执行每个匹配查询,把他们的得分加在一起,然后乘以匹配子句的数量,并且除以子句的总数。每个同级的子句权重是相同的。在前面的查询中,包含翻译者的布尔查询占用总得分的三分之一。如果我们把翻译者的子句放在和标题与作者同级的目录中,我们会把标题与作者的作用减少到四分之一。
- 运行should子句中的两个查询
- 相加查询返回的分值
- 将相加得到的分值乘以匹配的查询子句的数量
- 除以总的查询子句的数量
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
文档1在两个字段中都包含了brown,因此两个match查询都匹配成功并拥有了一个分值。文档2在body字段中包含了brown以及fox,但是在title字段中没有出现任何搜索的单词。因此对body字段查询得到的高分加上对title字段查询得到的零分,然后在乘以匹配的查询子句数量1,最后除以总的查询子句数量2,导致整体分值比文档1的低。
相比使用bool查询,我们可以使用dis_max查询(Disjuction Max Query)。Disjuction的意思"OR"(而Conjunction的意思是"AND"),因此Disjuction Max Query的意思就是返回匹配了任何查询的文档,并且分值是产生了最佳匹配的查询所对应的分值:
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
tie_breaker参数会让dis_max查询的行为更像是dis_max和bool的一种折中。它会通过下面的方式改变分值计算过程:
- 取得最佳匹配查询子句的_score。
- 将其它每个匹配的子句的分值乘以tie_breaker。
- 将以上得到的分值进行累加并规范化。
通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。
{
"dis_max": {
"queries": [
{
"match": {
"title": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
{
"match": {
"body": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
],
"tie_breaker": 0.3
}
}
可以通过multi_match简单地重写如下:
{
"multi_match": {
"query": "Quick brown fox",
"type": "best_fields", <1>
"fields": [ "title", "body" ],
"tie_breaker": 0.3,
"minimum_should_match": "30%" <2>
}
}
个别字段可以通过caret语法()进行加权:仅需要在字段名后添加^boost,其中的boost是一个浮点数:
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ] <1>
}
}
同样的,还存在 most_fields
ES通过字段映射中的copy_to参数向我们提供了这一功能:
PUT /my_index
{
"mappings": {
"person": {
"properties": {
"first_name": {
"type": "string",
"copy_to": "full_name" <1>
},
"last_name": {
"type": "string",
"copy_to": "full_name" <1>
},
"full_name": {
"type": "string"
}
}
}
}
}
cross_fields 使用以词条为中心(Term-centric)进行匹配。
它通过混合(Blending)字段的倒排文档频度来解决词条频度的问题:
+blended(“peter”, fields: [first_name, last_name])
+blended(“smith”, fields: [first_name, last_name])
换言之,它会查找词条smith在first_name和last_name字段中的IDF值,然后使用两者中较小的作为两个字段最终的IDF值。因为smith是一个常见的姓氏,意味着它也会被当做一个常见的名字。