ES基础入门

ElasticSearch是一个高可用的、开源的、全文检索引擎。它能够存储,搜索和实时快速的分析大量数据。html

应用场景:java

电商网站中的商品搜索;
结合Logstash收集日志和业务数据,注入到ElasticSearch中,再进行搜索;

安装报错:node

  1. $./bin/elasticsearch
  2. Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x0000000085330000, 2060255232, 0) failed; error=’Cannot allocate memory’ (errno=12)
  3. #
  4. # There is insufficient memory for the Java Runtime Environment to continue.
  5. # Native memory allocation (mmap) failed to map 2060255232 bytes for committing reserved memory.
  6. # An error report file with more information is saved as:
  7. # /data/elasticsearch-5.2.2/hs_err_pid26945.log

解决方案:调小启动内存git

  1. # vi /usr/local/elasticsearch/config/jvm.options
  2. #-Xms2g
  3. #-Xmx2g
  4. -Xms256m
  5. -Xmx64m

上面设置的分配的内存的最大值为256MB和最小值64mb,您能够根据本身的机器状况设置内存大小。github

在应用程序中对象不多只是一个简单的键和值的列表。一般,它们拥有更复杂的数据结构,可能包括日期、地理信息、其余对象或者数组等。web

也许有一天你想把这些对象存储在数据库中。使用关系型数据库的行和列存储,这至关因而把一个表现力丰富的对象挤压到一个很是大的电子表格中:你必须将这个对象扁平化来适应表结构–一般一个字段>对应一列–并且又不得不在每次查询时从新构造对象。正则表达式

Elasticsearch 是 面向文档 的,意味着它存储整个对象或 文档。Elasticsearch 不只存储文档,并且 索引 每一个文档的内容使之能够被检索。在 Elasticsearch 中,你 对文档进行索引、检索、排序和过滤–而不是对行列数据。这是一种彻底不一样的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的缘由。算法

Elasticsearch: 权威指南

https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.htmlsql

1、什么是 ElasticSearch

ElasticSearch是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并做为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,可以达到实时搜索,稳定,可靠,快速,安装使用方便。数据库

1.1 基础概念

索引:含有相同属性的文档集合

类型:索引能够定义一个或多个类型,文档必须属于一个类型

文档:能够被索引的基础数据单位

分片:每一个索引都有多个分片,每一个分片都是 Lucene 索引

备份:拷贝一份分片就完成分片的备份

形象比喻:

百货大楼里有各式各样的商品,例如书籍、笔、水果等。书籍能够根据内容划分红不一样种类,如科技类、教育类、悬疑推理等。悬疑推理类的小说中比较有名气的有《福尔摩斯探案集》、《白夜行》等。

百货大楼 –> ElasticSearch 数据库

书籍 –> 索引

悬疑推理 –> 类型

白夜行 –> 文档

1.2 应用场景

  • 海量数据分析引擎
  • 站内搜索引擎
  • 数据仓库

你也许已经注意到 索引 这个词在 Elasticsearch 语境中包含多重意思, 因此有必要作一点儿说明:

索引(名词):

如前所述,一个 索引 相似于传统关系数据库中的一个 数据库 ,是一个存储关系型文档的地方。 索引 (index) 的复数词为 indices 或 indexes 。

索引(动词):

索引一个文档 就是存储一个文档到一个 索引 (名词)中以便它能够被检索和查询到。这很是相似于 SQL 语句中的 INSERT 关键词,除了文档已存在时新文档会替换旧文档状况以外。

倒排索引:

关系型数据库经过增长一个 索引 好比一个 B树(B-tree)索引 到指定的列上,以便提高数据检索速度。Elasticsearch 和 Lucene 使用了一个叫作 倒排索引 的结构来达到相同的目的。

  • 默认的,一个文档中的每个属性都是 被索引 的(有一个倒排索引)和可搜索的。一个没有倒排索引的属性是不能被搜索到的

2、安装和配置

本次测试使用一台 ip 为 192.168.2.41 的虚拟机(Centos7),建议使用 7.x 版本,笔者以前使用 6.x 启动服务时报出各类错误

2.1 依赖环境

JDK 和 NodeJS

2.2 下载

登录 elasticSearch 官网下载文件。

2.3 安装

tar -zxvf elasticsearch-5.6.1.tar.gz -C /usr

cd elasticsearch-5.6.1

2.4 启动

启动以前,请先查看“踩坑提醒”。

1)启动服务:

bin/elasticsearch 或 bin/elasticsearch -d # -d 表示后台启动
  • 1

踩坑提醒 1:

由于 Elasticsearch 能够执行脚本文件,为了安全性,默认不容许经过 root 用户启动服务。咱们须要新建立用户名和用户组启动服务

#增长 es 组
groupadd es

#增长 es 用户并附加到 es 组
useradd es -g es -p es

#给目录权限
chown -R es:es elasticsearch-5.6.1

#使用es用户
su es

踩坑提醒 2:

默认状况下,Elasticsearch 只容许本机访问,若是须要远程访问,须要修改其配置文件

vim config/elasticsearch.yml

# 去掉 network.host 前边的注释,将它的值改为0.0.0.0
network.host: 0.0.0.0

踩坑提醒 3:

启动报错:max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

命令行键入:

ulimit -n 65536

踩坑提醒 4:

启动报错:max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

vim /etc/sysctl.conf

vm.max_map_count=262144

保存,并执行 sysctl -p。

在启动过程当中,Centos 环境下可能还会报错,具体解决方案请参照文章末尾提供的资料

2)经过浏览器访问 http://192.168.2.41:9200 ,当出现以下内容说明启动成功:

{
  "name" : "OwUwJe-",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "vanzxnpaRumdRKiYic3f5A",
  "version" : {
    "number" : "5.6.1",
    "build_hash" : "667b497",
    "build_date" : "2017-09-14T19:22:05.189Z",
    "build_snapshot" : false,
    "lucene_version" : "6.6.1"
  },
  "tagline" : "You Know, for Search"
}

2.5 安装插件

访问 http://192.168.2.41:9200 查看内容显示效果不友好,所以,咱们须要安装一个名为 elasticsearch-head 的插件,让内容显示效果比较温馨。安装以前须要先安装node。

登录 GitHub 网站,搜索 mobz/elasticsearch-head ,将其下载到本地。

wget https://github.com/mobz/elasticsearch-head/archive/master.zip

unzip master.zip

cd elasticsearch-head-master

npm install

npm run start

 

经过上述命令的操做,咱们已经安装好 elasticsearch-head 插件。经过浏览器访问 http://192.168.2.41:9100,以下图:

这里写图片描述

图中咱们发现 elasticsearch-head 插件和 Elasticsearch 服务并无创建链接,因此咱们还须要修改 Elasticsearch 的配置文件:

cd elasticsearch-5.6.1

vim config/elasticsearch.yml

# 在文件末尾添加 2 段配置

http.cors.enabled: true
http.cors.allow-origin: "*"

保存文件后,分别起来 2 个程序:

cd elasticsearch-5.6.1

# 后台启动 elasticSearch 服务
bin/elasticsearch -d

cd elasticsearch-head-master

npm run start

经过浏览器访问 http://192.168.2.41:9100,以下图: 

经过插件建立索引 

查看索引基本状况 
这里写图片描述

该插件能直接对 Elasticsearch 的数据进行增删改查,所以存在安全性的问题。建议生产环境下不要使用该插件!

3、简单使用

不管咱们写什么样的程序,目的都是同样的:以某种方式组织数据服务咱们的目的 。可是数据不只仅由随机位和字节组成。咱们创建数据元素之间的关系以便于表示实体,或者现实世界中存在的 事物 。

一个 对象 是基于特定语言的内存的数据结构。 为了经过网络发送或者存储它,咱们须要将它表示成某种标准的格式。 JSON 是一种以人可读的文本表示对象的方法。 它已经变成 NoSQL 世界交换数据的事实标准。当一个对象被序列化成为 JSON,它被称为一个 JSON 文档 。

Elastcisearch 是分布式的 文档 存储。它能存储和检索复杂的数据结构–序列化成为JSON文档–以 实时 的方式。 换句话说,一旦一个文档被存储在 Elasticsearch 中,它就是能够被集群中的任意节点检索到。

在 Elasticsearch 中, 每一个字段的全部数据 都是 默认被索引的 。 即每一个字段都有为了快速检索设置的专用倒排索引。

文档

一般状况下,咱们使用的术语 对象 和 文档 是能够互相替换的。不过,有一个区别: 一个对象仅仅是相似于 hash 、 hashmap 、字典或者关联数组的 JSON 对象,对象中也能够嵌套其余的对象。 对象可能包含了另一些对象。在 Elasticsearch 中,术语 文档 有着特定的含义。它是指最顶层或者根对象, 这个根对象被序列化成 JSON 并存储到 Elasticsearch 中,指定了惟一 ID。

一个文档不只仅包含它的数据 ,也包含 元数据 —— 有关 文档的信息。 三个必须的元数据元素以下:

  • _index

    文档在哪存放 。一个 索引 应该是因共同的特性被分组到一块儿的文档集合。 索引名必须小写,不能如下划线开头,不能包含逗号。

  • _type 
    文档表示的对象类别

  • _id 
    文档惟一标识

Elasticsearch 支持 RESTFUL 风格 API,其 API 基本格式以下:

http://<ip>:<port>/<索引>/<类型>/<文档id>

3.1 建立/删除索引

为了方便测试,咱们使用 POSTMAN 工具进行接口的请求。

http://39.106.195.92:9200/megacorp/employee/1

若是咱们不但愿建立索引的时候覆盖以前存在的索引,怎么搞?

请记住, _index 、 _type 和 _id 的组合能够惟一标识一个文档。因此,确保建立一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成惟一 _id :

http://39.106.195.92:9200/megacorp/employee

然而,咱们已经有了本身的id的话, 然而,若是已经有本身的 _id ,那么咱们必须告诉 Elasticsearch ,只有在相同的 _index 、 _type 和 _id 不存在时才接受咱们的索引请求。这里有两种方式,他们作的实际是相同的事情。使用哪一种,取决于哪一种使用起来更方便。

第一种方法使用 op_type 查询 -字符串参数:

http://39.106.195.92:9200/megacorp/employee/1?op_type=create 

http://39.106.195.92:9200/megacorp/employee/1/_create

若是建立新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码。

另外一方面,若是具备相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch 将会返回 409 Conflict 响应码,以及以下的错误信息:

{
    "error": {
        "root_cause": [
            {
                "type": "version_conflict_engine_exception",
                "reason": "[employee][1]: version conflict, document already exists (current version [4])",
                "index_uuid": "lo-6VdTHQWKAYVuMu3djQg",
                "shard": "3",
                "index": "megacorp"
            }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[employee][1]: version conflict, document already exists (current version [4])",
        "index_uuid": "lo-6VdTHQWKAYVuMu3djQg",
        "shard": "3",
        "index": "megacorp"
    },
    "status": 409
}

 

删除一个索引,须要使用 DELETE 请求。执行:

http://39.106.195.92:9200/megacorp/employee/3

 

返回结果:

{
    "found": true,
    "_index": "megacorp",
    "_type": "employee",
    "_id": "3",
    "_version": 2,     //+1
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    }
}

 

3.2 插入数据

插入指定 ID 的数据,须要使用 PUT 请求。以下图: 
这里写图片描述

megacorp:索引名称;employee索引类型;1表明数据ID,请求体是JSON数据,包含一些信息 
也能够不指定 ID ,Elasticsearch 能够帮咱们自动生成 ID 。须要使用 POST 请求。以下图: 
这里写图片描述

3.3 查找数据(检索)

一、GET方式简单搜索

查询指定ID的数据,使用 GET 请求。

查询id是1的雇员信息,执行:

[GET] http://39.106.195.92:9200/megacorp/employee/1

返回结果:

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 2,
    "found": true,    //表示文档被找到
    "_source": {
        "first_name": "Douglas",
        "last_name": "Fir",
        "age": 35,
        "about": "I like to build cabinets",
        "interests": [
            "forestry"
        ]
    }
}

 

查询文档的一部分(只查询名字)

[GET] http://39.106.195.92:9200/megacorp/employee/1?_source=first_name,last_name

查询全部信息,不指定id,使用_search,执行:

[GET] http://39.106.195.92:9200/megacorp/employee/_search

按条件查询,仍然在请求路径中使用 _search 端点,并将查询自己赋值给参数 q= ,执行:

[GET] http://39.106.195.92:9200/megacorp/employee/_search?q=last_name:Smith

查询文档是否存在,把GET方式换位HEAD方式就能够了,HEAD 请求没有返回体,只返回一个 HTTP 请求报头: 存在返回200,否在返回404。

二、POST方式搜索

GET条件查询的方式虽然方便,但有很大的局限性,ES提供了POST的方式查询语言–查询表达式,它支持构建更加复杂和健壮的查询。 POST方式的搜索url都相同,只是body不一样,

执行:

[POST] http://39.106.195.92:9200/megacorp/employee/_search

请求参数:使用 JSON 构造,并使用了一个 match 查询(属于查询类型之一,这里match不能匹配多个字段。匹配多个字段要使用multi_match )

查找 last_name= “Smith”:

{
    "query" : {
        "match" : {
            "last_name" : "Smith"   //只能写一个
        }
    }
}

 

返回结果:

{
    "took": 5,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 1,
        "max_score": 0.2876821,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "2",
                "_score": 0.2876821,
                "_source": {
                    "first_name": "Jane",
                    "last_name": "Smith",
                    "age": 32,
                    "about": "I like to collect rock albums",
                    "interests": [
                        "music"
                    ]
                }
            }
        ]
    }
}

 

三、复杂的搜索

使用过滤器filter搜索姓氏为 Smith 的雇员 且年龄大于30,使用的是range过滤器,其中 gt 表示_大于 :

{
    "query" : {
        "bool": {
            "must": {
                "match" : {
                    "last_name" : "smith" 
                }
            },
            "filter": {
                "range" : {
                    "age" : { "gt" : 30 } 
                }
            }
        }
    }
}

 

四、全文搜索

使用match全文搜索,Elasticsearch 默认按照相关性得分排序,即每一个文档跟查询的匹配程度

{
    "query" : {
        "match" : {
            "about" : "rock climbing"
        }
    }
}

 

五、短语搜索

使用match_phrase短语搜索(准确搜索)

{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    }
}

 

六、高亮搜索

{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    },
    "highlight": {
        "fields" : {
            "about" : {}
        }
    }
}

 

七、分析

Elasticsearch 有一个功能叫聚合(aggregations),容许咱们基于数据生成一些精细的分析结果。聚合与 SQL 中的 GROUP BY 相似但更强大。

挖掘出雇员中最受欢迎的兴趣爱好

{
  "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
            }
         ]
      }
   }
}

 

八、mget批量查询

[POST] http://39.106.195.92:9200/_mget

{
   "docs" : [
      {
         "_index" : "megacorp",
         "_type" :  "employee",
         "_id" :    2
      },
      {
         "_index" : "website",    //没有索引的话,会报错搜索不到该索引的内容
         "_type" :  "employee",
         "_id" :    1,
         "_source": "age"
      }
   ]
}

 

3.3 修改数据

更新整个文档:在 Elasticsearch 中文档是 不可改变 的,不能修改它们。 相反,若是想要更新现有的文档,须要 重建索引 或者进行替换, 咱们能够使用相同的 index API 进行实现 。

[PUT] http://39.106.195.92:9200/megacorp/employee/1

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 3,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "created": false    //这里变成了false,表示是更新
}

部分更新,也能够使用 POST 请求,且 URL 须要添加 _update,它只是与现有的文档进行合并

执行:

[POST] http://39.106.195.92:9200/megacorp/employee/1/_update

请求参数(修改年龄为88,也能够增长参数):

{
    "doc": {
        "age": "88"
    }
}

 

 

返回结果:

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 4,
    "result": "updated",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    }
}

 

使用脚本部分更新文档

脚本能够在 update API中用来改变 _source 的字段内容, 它在更新脚本中称为 ctx._source 。 例如,咱们能够使用脚原本增长博客文章中 views 的数量:

[POST] http://39.106.195.92:9200/megacorp/employee/1/_update

{
   "script" : "ctx._source.age+=1"
}

 

在数据库领域中,有两种方法一般被用来确保并发更新时变动不会丢失:

  • 悲观并发控制 
    这种方法被关系型数据库普遍使用,它假定有变动冲突可能发生,所以阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据以前先将其锁住,确保只有放置锁的线程可以对这行数据进行修改。
  • 乐观并发控制 
    Elasticsearch 中使用的这种方法假定冲突是不可能发生的,而且不会阻塞正在尝试的操做。 然而,若是源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,能够重试更新、使用新的数据、或者将相关状况报告给用户。

    lasticsearch 是分布式的。当文档建立、更新或删除时, 新版本的文档必须复制到集群中的其余节点。Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,而且到达目的地时也许 顺序是乱的 。 Elasticsearch 须要一种方法确保文档的旧版本不会覆盖新的版本。

    当咱们以前讨论 index , GET 和 delete 请求时,咱们指出每一个文档都有一个 _version (版本)号,当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变动以正确顺序获得执行。若是旧版本的文档在新版本以后到达,它能够被简单的忽略。

    咱们能够利用 _version 号来确保 应用中相互冲突的变动不会致使数据丢失。咱们经过指定想要修改文档的 version 号来达到这个目的。 若是该版本不是当前版本号,咱们的请求将会失败。

代价较小的批量操做

与 mget 能够使咱们一次取回多个文档一样的方式, bulk API 容许在单个步骤中进行屡次 create 、 index 、 update 或 delete 请求。 若是你须要索引一个数据流好比日志事件,它能够排队和索引数百或数千批次。

bulk 与其余的请求体格式稍有不一样,以下所示:

{ action: { metadata }}\n
{ request body        }\n
{ action: { metadata }}\n
{ request body        }\n
...

 

这种格式相似一个有效的单行 JSON 文档 流 ,它经过换行符(\n)链接到一块儿。注意两个要点:

  • 每行必定要以换行符(\n)结尾, 包括最后一行 。这些换行符被用做一个标记,能够有效分隔行。
  • 这些行不能包含未转义的换行符,由于他们将会对解析形成干扰。这意味着这个 JSON 不 能使用 pretty 参数打印。

3.4 删除数据

修改数据,须要使用 DELETE 请求。

执行:

[DELETE] http://192.168.2.41:9200/fruit/apple/1

返回结果:

{
    "found": true,
    "_index": "fruit",
    "_type": "apple",
    "_id": "1",
    "_version": 8,
    "result": "deleted",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    }
}

 

3.5 集群内原理

ElasticSearch 的主旨是随时可用和按需扩容

一个运行中的 Elasticsearch 实例称为一个 节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会从新平均分布全部的数据。

当一个节点被选举成为 主 节点时, 它将负责管理集群范围内的全部变动,例如增长、删除索引,或者增长、删除节点等。 而主节点并不须要涉及到文档级别的变动和搜索等操做,因此当集群只拥有一个主节点的状况下,即便流量的增长它也不会成为瓶颈。 任何节点均可以成为主节点。咱们的示例集群就只有一个节点,因此它同时也成为了主节点。

做为用户,咱们能够将请求发送到 集群中的任何节点 ,包括主节点。 每一个节点都知道任意文档所处的位置,而且可以将咱们的请求直接转发到存储咱们所需文档的节点。 不管咱们将请求发送到哪一个节点,它都能负责从各个包含咱们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。

3.5.1 集群健康

Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是 集群健康 , 它在 status 字段中展现为 green 、 yellow 或者 red 。

3.5.2 添加索引

咱们往 Elasticsearch 添加数据时须要用到 索引 —— 保存相关数据的地方。 索引其实是指向一个或者多个物理 分片 的 逻辑命名空间 。

一个 分片 是一个底层的 工做单元 ,它仅保存了 所有数据中的一部分。 一个分片就是一个 Lucene 的实例,以及它自己就是一个完整的搜索引擎。 咱们的文档被存储和索引到分片内,可是应用程序是直接与索引而不是与分片进行交互。

Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。

一个分片能够是 主 分片或者 副本 分片。 索引内任意一个文档都归属于一个主分片,因此主分片的数目决定着索引可以保存的最大数据量。

4、 分布式文档存储

路由一个文档到一个分片中

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪一个分片中呢?当咱们建立文档时,它如何决定这个文档应当被存储在分片 1 仍是分片 2 中呢?

首先这确定不会是随机的,不然未来要获取文档的时候咱们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也能够设置成一个自定义的值。 routing 经过 hash 函数生成一个数字,而后这个数字再除以 number_of_primary_shards (主分片的数量)后获得 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是咱们所寻求的文档所在分片的位置。

这就解释了为何咱们要在建立索引的时候就肯定好主分片的数量 而且永远不会改变这个数量:由于若是数量变化了,那么全部以前路由的值都会无效,文档也再也找不到了。

全部的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫作 routing 的路由参数 ,经过这个参数咱们能够自定义文档到分片的映射。一个自定义的路由参数能够用来确保全部相关的文档——例如全部属于同一个用户的文档——都被存储到同一个分片中。

5、 搜索

搜索(search) 能够作到:

  • 在相似于 gender 或者 age 这样的字段 上使用结构化查询,join_date 这样的字段上使用排序,就像SQL的结构化查询同样。
  • 全文检索,找出全部匹配关键字的文档并按照相关性(relevance) 排序后返回结果。
  • 以上两者兼而有之。

不少搜索都是开箱即用的,为了充分挖掘 Elasticsearch 的潜力,你须要理解如下三个概念:

  • 映射(Mapping)

    描述数据在每一个字段内如何存储

  • 分析(Analysis)

    全文是如何处理使之能够被搜索的

  • 领域特定查询语言(Query DSL)

    Elasticsearch 中强大灵活的查询语言

空搜索

GET /_search      //简单地返回集群中全部索引下的全部文档

然而,常常的状况下,你 想在一个或多个特殊的索引而且在一个或者多个特殊的类型中进行搜索。咱们能够经过在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 类型

当在单一的索引下进行搜索的时候,Elasticsearch 转发请求到索引的每一个分片中,能够是主分片也能够是副本分片,而后从每一个分片中收集结果。多索引搜索刚好也是用相同的方式工做的–只是会涉及到更多的分片。

分页

在以前的 空搜索 中说明了集群中有 14 个文档匹配了(empty)query 。 可是在 hits 数组中只有 10 个文档。如何才能看到其余的文档?

和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,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

考虑到分页过深以及一次请求太多结果的状况,结果集在返回以前先进行排序。 但请记住一个请求常常跨越多个分片,每一个分片都产生本身的排序结果,这些结果须要进行集中排序以保证总体顺序是正确的。

轻量 搜索

有两种形式的 搜索 API:一种是 “轻量的” 查询字符串 版本,要求在查询字符串中传递全部的 参数,另外一种是更完整的 请求体 版本,要求使用 JSON 格式和更丰富的查询表达式做为搜索语言。

查询字符串搜索很是适用于经过命令行作即席查询。例如,查询在 tweet 类型中 tweet 字段包含 elasticsearch 单词的全部文档:

GET /_all/tweet/_search?q=tweet:elasticsearch

 

下一个查询在 name 字段中包含 john 而且在 tweet 字段中包含 mary 的文档。实际的查询就是这样

+name:john +tweet:mary

 

可是查询字符串参数所须要的 百分比编码 (译者注:URL编码)实际上更加难懂:

GET /_search?q=%2Bname%3Ajohn+%2Btweet%3Amary

 

  • 前缀表示必须与查询条件匹配。相似地, - 前缀表示必定不与查询条件匹配。没有 + 或者 - 的全部其余条件都是可选的——匹配的越多,文档就越相关。

    _all字段

这个简单搜索返回包含 mary 的全部文档:

GET /_search?q=mary

 

以前的例子中,咱们在 tweet 和 name 字段中搜索内容。然而,这个查询的结果在三个地方提到了 mary :

  • 有一个用户叫作 Mary
  • 6条微博发自 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"

 

除非设置特定字段,不然查询字符串就使用 _all 字段进行搜索。

在刚开始开发一个应用时,_all 字段是一个很实用的特性。以后,你会发现若是搜索时用指定字段来代替 _all 字段,将会更好控制搜索结果。当 _all 字段再也不有用的时候,能够将它置为失效,正如在 元数据: _all 字段 中所解释的。

更复杂的查询

下面的查询针对tweents类型,并使用如下的条件:

  • 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)

 

从以前的例子中能够看出,这种 轻量 的查询字符串搜索效果仍是挺让人惊喜的。 它的查询语法在相关参考文档中有详细解释,以便简洁的表达很复杂的查询。对于经过命令作一次性查询,或者是在开发阶段,都很是方便。

但同时也能够看到,这种精简让调试更加晦涩和困难。并且很脆弱,一些查询字符串中很小的语法错误,像 - , : , / 或者 ” 不匹配等,将会返回错误而不是搜索结果。

最后,查询字符串搜索容许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

由于这些缘由,不推荐直接向用户暴露查询字符串搜索功能,除非对于集群和数据来讲很是信任他们。

相反,咱们常常在生产环境中更多地使用功能全面的 request body 查询API,除了能完成以上全部功能,还有一些附加功能。但在到达那个阶段以前,首先须要了解数据在 Elasticsearch 中是如何被索引的。

6、映射和分析

精确值 VS 全文

Elasticsearch 中的数据能够归纳的分为两类:精确值和全文。

精确值 如它们听起来那样精确。例如日期或者用户 ID,但字符串也能够表示精确值,例如用户名或邮箱地址。对于精确值来说,Foo 和 foo 是不一样的,2014 和 2014-09-15 也是不一样的。

另外一方面,全文 是指文本数据(一般以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。

全文一般是指非结构化的数据,但这里有一个误解:天然语言是高度结构化的。问题在于天然语言的规则是复杂的,致使计算机难以正确解析。例如,考虑这条语句:

May is fun but June bores me.

它指的是月份仍是人?

精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示:

WHERE name    = "John Smith"
  AND user_id = 2
  AND date    > "2014-09-15"

查询全文数据要微妙的多。咱们问的不仅是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?

咱们不多对全文类型的域作精确匹配。相反,咱们但愿在文本类型的域中搜索。不只如此,咱们还但愿搜索可以理解咱们的 意图 :

  • 搜索 UK ,会返回包含 United Kindom 的文档。
  • 搜索 jump ,会匹配 jumped , jumps , jumping ,甚至是 leap 。
  • 搜索 johnny walker 会匹配 Johnnie Walker , johnnie depp 应该匹配 Johnny Depp 。
  • fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。

为了促进这类在全文域中的查询,Elasticsearch 首先 分析 文档,以后根据结果建立 倒排索引 。

倒排索引

Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中全部不重复词的列表构成,对于其中每一个词,有一个包含它的文档列表。

例如,假设咱们有两个文档,每一个文档的 content 域包含以下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了建立倒排索引,咱们首先将每一个文档的 content 域拆分红单独的词(咱们称它为 词条 或 tokens ),建立一个包含全部不重复词条的排序列表,而后列出每一个词条出如今哪一个文档。结果以下所示:

Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

 

如今,若是咱们想搜索 quick brown ,咱们只须要查找包含每一个词条的文档:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

 

两个文档都匹配,可是第一个文档比第二个匹配度更高。若是咱们使用仅计算匹配词条数量的简单 类似性算法 ,那么,咱们能够说,对于咱们查询的相关性来说,第一个文档比第二个文档更佳。

可是,咱们目前的倒排索引有一些问题:

  • Quick 和 quick 以独立的词条出现,然而用户可能认为它们是相同的词。
  • fox 和 foxes 很是类似, 就像 dog 和 dogs ;他们有相同的词根。
  • jumped 和 leap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。

使用前面的索引搜索 +Quick +fox 不会获得任何匹配文档。(记住,+ 前缀代表这个词必须存在。)只有同时出现 Quick 和 fox 的文档才知足这个查询条件,可是第一个文档包含 quick fox ,第二个文档包含 Quick foxes 。

咱们的用户能够合理的指望两个文档与查询匹配。咱们能够作的更好。

若是咱们将词条规范为标准模式,那么咱们能够找到与用户搜索的词条不彻底一致,但具备足够相关性的文档。例如:

  • Quick 能够小写化为 quick 。
  • foxes 能够 词干提取 –变为词根的格式– 为 fox 。相似的, dogs 能够为提取为 dog 。
  • jumped 和 leap 是同义词,能够索引为相同的单词 jump 。

如今索引看上去像这样:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
dog     |   X   |  X
fox     |   X   |  X
in      |       |  X
jump    |   X   |  X
lazy    |   X   |  X
over    |   X   |  X
quick   |   X   |  X
summer  |       |  X
the     |   X   |  X
------------------------

 

这还远远不够。咱们搜索 +Quick +fox 仍然 会失败,由于在咱们的索引中,已经没有 Quick 了。可是,若是咱们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 +quick +fox ,这样两个文档都会匹配!

这很是重要。你只能搜索在索引中出现的词条,因此索引文本和查询字符串必须标准化为相同的格式。

分词和标准化的过程称为 分析 。

分析与分析器

分析 包含下面的过程:

  • 首先,将一块文本分红适合于倒排索引的独立的 词条 ,
  • 以后,将这些词条统一化为标准格式以提升它们的“可搜索性”,或者 recall

分析器执行上面的工做。 分析器 其实是将三个功能封装到了一个包里:

  • 字符过滤器 
    首先,字符串按顺序经过每一个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器能够用来去掉HTML,或者将 & 转化成 and

  • 分词器 
    其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分红词条。

  • Token 过滤器 
    最后,词条按顺序经过每一个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增长词条(例如,像 jump 和 leap 这种同义词)。

Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。 这些能够组合起来造成自定义的分析器以用于不一样的目的。

内置分析器

可是, Elasticsearch还附带了能够直接使用的预包装的分析器。 接下来咱们会列出最重要的分析器。为了证实它们的差别,咱们看看每一个分析器会从下面的字符串获得哪些词条:

"Set the shape to semi-transparent by calling set_trans(5)"

- 标准分析器 
标准分析器是Elasticsearch默认使用的分析器。它是分析 
各类语言文本最经常使用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生 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) 
- 语言分析器 
特定语言分析器可用于 不少语言。它们能够考虑指定语言的特色。例如, 英语 分析器附带了一组英语无用词(经常使用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 因为理解英语语法的规则,这个分词器能够提取英语单词的 词干 。 英语 分词器会产生下面的词条:set, shape, semi, transpar, call, set_tran, 5注意看 transparent、 calling 和 set_trans 已经变为词根格式。

何时使用分析器

当咱们 索引 一个文档,它的全文域被分析成词条以用来建立倒排索引。 可是,当咱们在全文域 搜索 的时候,咱们须要将查询字符串经过 相同的分析过程 ,以保证咱们搜索的词条格式与索引中的词条格式一致。

全文查询,理解每一个域是如何定义的,所以它们能够作 正确的事:

  • 当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
  • 当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。

如今你能够理解以前的查询为何返回那样的结果:

  • 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 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:

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 指明字符在原始字符串中的位置。

每一个分析器的 type 值都不同,能够忽略它们。它们在Elasticsearch中的惟一做用在于keep_types token 过滤器。

analyze API 是一个有用的工具,它有助于咱们理解Elasticsearch索引内部发生了什么,随着深刻,咱们会进一步讨论它。

指定分析器

当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。

你不但愿老是这样。可能你想使用一个不一样的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域–不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。

要作到这一点,咱们必须手动指定这些域的映射。

映射

为了可以将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 须要知道每一个域中数据的类型。这个信息包含在映射中。

索引中每一个文档都有 类型 。每种类型都有它本身的 映射 ,或者 模式定义 。映射定义了类型中的域,每一个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。

核心简单域类型

Elasticsearch 支持 以下简单域类型:

  • 字符串: string
  • 整数 : 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

这意味着若是你经过引号( “123” )索引一个数字,它会被映射为 string 类型,而不是 long 。可是,若是这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,若是没法转化,则抛出一个异常。

查看映射

经过 /_mapping ,咱们能够查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射 。好比取得索引 gb 中类型 tweet 的映射:

GET /gb/_mapping/tweet

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"
               }
            }
         }
      }
   }
}

错误的映射,例如 将 age 域映射为 string 类型,而不是 integer ,会致使查询出现使人困惑的结果。

自定义域映射

尽管在不少状况下基本域数据类型 已经够用,但你常常须要为单独域自定义映射 ,特别是字符串域。自定义映射容许你执行下面的操做:

  • 全文字符串域和精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 还有更多

域最重要的属性是 type 。对于不是 string 的域,你通常只须要设置 type :

{
    "number_of_clicks": {
        "type": "integer"
    }
}

默认, string 类型域会被认为包含全文。就是说,它们的值在索引前,会经过 一个分析器,针对于这个域的查询在搜索前也会通过一个分析器。

string 域映射的两个最重要 属性是 index 和 analyzer 。

index

index 属性控制怎样索引字符串。它能够是下面三个值:

  • analyzed 
    首先分析字符串,而后索引它。换句话说,以全文索引这个域。
  • not_analyzed 
    索引这个域,因此它可以被搜索,但索引的是精确值。不会对它进行分析。
  • no 
    不索引这个域。这个域不会被搜索到。

string 域 index 属性默认是 analyzed 。若是咱们想映射这个字段为一个精确值,咱们须要设置它为 not_analyzed :

{
    "tag": {
        "type":     "string",
        "index":    "not_analyzed"
    }
}

其余简单类型(例如 long , double , date 等)也接受 index 参数,但有意义的值只有 no 和 not_analyzed , 由于它们永远不会被分析。

analyzer

对于 analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard 分析器, 但你能够指定一个内置的分析器替代它,例如 whitespace 、 simple 和 english:

{
    "tweet": {
        "type":     "string",
        "analyzer": "english"
    }
}

更新映射

当你首次 建立一个索引的时候,能够指定类型的映射。你也能够使用 /_mapping 为新类型(或者为存在的类型更新映射)增长映射。

尽管你能够 增长_ 一个存在的映射,你不能 _修改 存在的域映射。若是一个域的映射已经存在,那么该域的数据可能已经被索引。若是你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。

咱们能够更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed 改成 not_analyzed 。

为了描述指定映射的两种方式,咱们先删除 gd 索引:

DELETE /gb

而后建立一个新索引,指定 tweet 域使用 english 分析器:

PUT /gb 
{
  "mappings": {
    "tweet" : {
      "properties" : {
        "tweet" : {
          "type" :    "string",
          "analyzer": "english"
        },
        "date" : {
          "type" :   "date"
        },
        "name" : {
          "type" :   "string"
        },
        "user_id" : {
          "type" :   "long"
        }
      }
    }
  }
}

经过消息体中指定的 mappings 建立了索引。

稍后,咱们决定在 tweet 映射增长一个新的名为 tag 的 not_analyzed 的文本域,使用 _mapping :

PUT /gb/_mapping/tweet
{
  "properties" : {
    "tag" : {
      "type" :    "string",
      "index":    "not_analyzed"
    }
  }
}

注意,咱们不须要再次列出全部已存在的域,由于不管如何咱们都没法改变它们。新域已经被合并到存在的映射中。

测试映射

你能够使用 analyze API 测试字符串域的映射。比较下面两个请求的输出:

GET /gb/_analyze
{
  "field": "tweet",
  "text": "Black-cats" 
}

GET /gb/_analyze
{
  "field": "tag",
  "text": "Black-cats" 
}

消息体里面传输咱们想要分析的文本。

tweet 域产生两个词条 black 和 cat , tag 域产生单独的词条 Black-cats 。换句话说,咱们的映射正常工做。

复杂核心域类型

除了咱们提到的简单标量数据类型, JSON 还有 null 值,数组,和对象,这些 Elasticsearch 都是支持的。

多值域

颇有可能,咱们但愿 tag 域 包含多个标签。咱们能够以数组的形式索引标签:

{ "tag": [ "search", "nosql" ]}

对于数组,没有特殊的映射需求。任何域均可以包含0、1或者多个值,就像全文域分析获得多个词条。

这暗示 数组中全部的值必须是相同数据类型的 。你不能将日期和字符串混在一块儿。若是你经过索引数组来建立新的域,Elasticsearch 会用数组中第一个值的数据类型做为这个域的 类型 。

当你从 Elasticsearch 获得一个文档,每一个数组的顺序和你当初索引文档时同样。你获得的 _source 域,包含与你索引的如出一辙的 JSON 文档。

可是,数组是以多值域 索引的—能够搜索,可是无序的。 在搜索的时候,你不能指定 “第一个” 或者 “最后一个”。 更确切的说,把数组想象成 装在袋子里的值 。

空域

固然,数组能够为空。 这至关于存在零值。 事实上,在 Lucene 中是不能存储 null 值的,因此咱们认为存在 null 值的域为空域。

下面三种域被认为是空的,它们将不会被索引:

"null_value":               null,
"empty_array":              [],
"array_with_null_value":    [ null ]

多层级对象

咱们讨论的最后一个 JSON 原生数据类是 对象 – 在其余语言中称为哈希,哈希 map,字典或者关联数组。

内部对象 常常用于 嵌入一个实体或对象到其它对象中。例如,与其在 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 会动态 监测新的对象域并映射它们为 对象 ,在 properties 属性下列出内部域:

{
  "gb": {
    "tweet": { 
      "properties": {
        "tweet":            { "type": "string" },
        "user": { 
          "type":             "object",
          "properties": {
            "id":           { "type": "string" },
            "gender":       { "type": "string" },
            "age":          { "type": "long"   },
            "name":   { 
              "type":         "object",
              "properties": {
                "full":     { "type": "string" },
                "first":    { "type": "string" },
                "last":     { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}

user 和 name 域的映射结构与 tweet 类型的相同。事实上, type 映射只是一种特殊的 对象 映射,咱们称之为 根对象 。除了它有一些文档元数据的特殊顶级域,例如 _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]
}

内部域 能够经过名称引用(例如, first )。为了区分同名的两个域,咱们能够使用全 路径 (例如, user.name.first ) 或 type 名加路径( tweet.user.name.first )。

在前面简单扁平的文档中,没有 user 和 user.name 域。Lucene 索引只有标量和简单值,没有复杂数据结构。

内部对象数组

最后,考虑包含 内部对象的数组是如何被索引的。 假设咱们有个 followers 数组:

{
    "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} 之间的相关性已经丢失了,由于每一个多值域只是一包无序的值,而不是有序数组。这足以让咱们问,“有一个26岁的追随者?”

可是咱们不能获得一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?”

相关内部对象被称为 nested 对象,能够回答上面的查询。

7、请求体查询

1. 空查询

让咱们以 最简单的 search API 的形式开启咱们的旅程,空查询将返回全部索引库(indices)中的全部文档:

GET /_search
{}

只用一个查询字符串,你就能够在一个、多个或者 _all 索引库(indices)和一个、多个或者全部types中查询:

GET /index_2014*/type1,type2/_search
{}

同时你能够使用 from 和 size 参数来分页:

GET /_search
{
  "from": 30,
  "size": 10
}

2. 查询表达式

查询表达式(Query DSL)是一种很是灵活又富有表现力的 查询语言。 Elasticsearch 使用它能够以简单的 JSON 接口来展示 Lucene 功能的绝大部分。在你的应用中,你应该用它来编写你的查询语句。它能够使你的查询语句更灵活、更精确、易读和易调试。

要使用这种查询表达式,只需将查询语句传递给 query 参数:

GET /_search
{
    "query": YOUR_QUERY_HERE
}

空查询(empty search) —{}— 在功能上等价于使用 match_all 查询, 正如其名字同样,匹配全部文档:

GET /_search
{
    "query": {
        "match_all": {}
    }
}

查询语句的结构

一个查询语句 的典型结构:

{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}

若是是针对某个字段,那么它的结构以下:

{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}

举个例子,你能够使用 match 查询语句 来查询 tweet 字段中包含 elasticsearch 的 tweet:

{
    "match": {
        "tweet": "elasticsearch"
    }
}

完整的查询请求以下:

GET /_search
{
    "query": {
        "match": {
            "tweet": "elasticsearch"
        }
    }
}

合并查询语句

查询语句(Query clauses) 就像一些简单的组合块 ,这些组合块能够彼此之间合并组成更复杂的查询。这些语句能够是以下形式:

  • 叶子语句(Leaf clauses) (就像 match 语句) 被用于将查询字符串和一个字段(或者多个字段)对比。
  • 复合(Compound) 语句 主要用于 合并其它查询语句。 好比,一个 bool 语句 容许在你须要的时候组合其它语句,不管是 must 匹配、 must_not 匹配仍是 should 匹配,同时它能够包含不评分的过滤器(filters):


    “bool”: { 
    “must”: { “match”: { “tweet”: “elasticsearch” }}, 
    “must_not”: { “match”: { “name”: “mary” }}, 
    “should”: { “match”: { “tweet”: “full text” }}, 
    “filter”: { “range”: { “age” : { “gt” : 30 }} } 

    }

一条复合语句能够合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间能够互相嵌套,能够表达很是复杂的逻辑。

例如,如下查询是为了找出信件正文包含 business opportunity 的星标邮件,或者在收件箱正文包含 business opportunity 的非垃圾邮件:

{
    "bool": {
        "must": { "match":   { "email": "business opportunity" }},
        "should": [
            { "match":       { "starred": true }},
            { "bool": {
                "must":      { "match": { "folder": "inbox" }},
                "must_not":  { "match": { "spam": true }}
            }}
        ],
        "minimum_should_match": 1
    }
}

到目前为止,你没必要太在乎这个例子的细节,咱们会在后面详细解释。最重要的是你要理解到,一条复合语句能够将多条语句 — 叶子语句和其它复合语句 — 合并成一个单一的查询语句。

3. 查询与过滤

Elasticsearch 使用的查询语言(DSL) 拥有一套查询组件,这些组件能够以无限组合的方式进行搭配。这套组件能够在如下两种状况下使用:过滤状况(filtering context)和查询状况(query context)。

当使用于 过滤状况 时,查询被设置成一个“不评分”或者“过滤”查询。即,这个查询只是简单的问一个问题:“这篇文档是否匹配?”。回答也是很是的简单,yes 或者 no ,两者必居其一。

  • created 时间是否在 2013 与 2014 这个区间?
  • status 字段是否包含 published 这个单词?
  • lat_lon 字段表示的位置是否在指定点的 10km 范围内?

当使用于 查询状况 时,查询就变成了一个“评分”的查询。和不评分的查询相似,也要去判断这个文档是否匹配,同时它还须要判断这个文档匹配的有 多好(匹配程度如何)。 此查询的典型用法是用于查找如下文档:

  • 查找与 full text search 这个词语最佳匹配的文档
  • 包含 run 这个词,也能匹配 runs 、 running 、 jog 或者 sprint
  • 包含 quick 、 brown 和 fox 这几个词 — 词之间离的越近,文档相关性越高
  • 标有 lucene 、 search 或者 java 标签 — 标签越多,相关性越高

一个评分查询计算每个文档与此查询的 相关程度,同时将这个相关程度分配给表示相关性的字段 _score,而且按照相关性对匹配到的文档进行排序。这种相关性的概念是很是适合全文搜索的状况,由于全文搜索几乎没有彻底 “正确” 的答案。

自 Elasticsearch 问世以来,查询与过滤(queries and filters)就独自成为 Elasticsearch 的组件。但从 Elasticsearch 2.0 开始,过滤(filters)已经从技术上被排除了,同时全部的查询(queries)拥有变成不评分查询的能力。

然而,为了明确和简单,咱们用 “filter” 这个词表示不评分、只过滤状况下的查询。你能够把 “filter” 、 “filtering query” 和 “non-scoring query” 这几个词视为相同的。

类似的,若是单独地不加任何修饰词地使用 “query” 这个词,咱们指的是 “scoring query” 。

性能差别

过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来很是快。考虑到至少有一个过滤查询(filtering query)的结果是 “稀少的”(不多匹配的文档),而且常用不评分查询(non-scoring queries),结果会被缓存到内存中以便快速读取,因此有各类各样的手段来优化查询结果。

相反,评分查询(scoring queries)不只仅要找出 匹配的文档,还要计算每一个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。

多亏倒排索引(inverted index),一个简单的评分查询在匹配少许文档时可能与一个涵盖百万文档的filter表现的同样好,甚至会更好。可是在通常状况下,一个filter 会比一个评分的query性能更优异,而且每次都表现的很稳定。

过滤(filtering)的目标是减小那些须要经过评分查询(scoring queries)进行检查的文档。

如何选择查询与过滤

一般的规则是,使用 查询(query)语句来进行 全文 搜索或者其它任何须要影响 相关性得分 的搜索。除此之外的状况都使用过滤(filters)。

4. 最重要的查询

虽然 Elasticsearch 自带了不少的查询,但常常用到的也就那么几个。

match_all 查询

match_all 查询简单的 匹配全部文档。在没有指定查询方式时,它是默认的查询:

{ "match_all": {}}

它常常与 filter 结合使用–例如,检索收件箱里的全部邮件。全部邮件被认为具备相同的相关性,因此都将得到分值为 1 的中性 _score。

match 查询

不管你在任何字段上进行的是全文搜索仍是精确查询,match 查询是你可用的标准查询。

若是你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串:

{ "match": { "tweet": "About Search" }}

若是在一个精确值的字段上使用它, 例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值:

{ "match": { "age":    26           }}
{ "match": { "date":   "2014-09-01" }}
{ "match": { "public": true         }}
{ "match": { "tag":    "full_text"  }}

对于精确值的查询,你可能须要使用 filter 语句来取代 query,由于 filter 将会被缓存。接下来,咱们将看到一些关于 filter 的例子。

不像咱们在 轻量 搜索 章节介绍的字符串查询(query-string search), match 查询不使用相似 +user_id: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"  }}

term 查询对于输入的文本不 分析 ,因此它将给定的值进行精确查询。

terms 查询

terms 查询和 term 查询同样,但它容许你指定多值进行匹配。若是这个字段包含了指定值中的任何一个值,那么这个文档知足条件:

{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}

和 term 查询同样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值(包括在大小写、重音、空格等方面的差别)。

exists 查询和 missing 查询

exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具备共性:

{
    "exists":   {
        "field":    "title"
    }
}

这些查询常常用于某个字段有值的状况和某个字段缺值的状况。

  1. 组合多查询

现实的查询需求历来都没有那么简单;它们须要在多个字段上查询多种多样的文本,而且根据一系列的标准来过滤。为了构建相似的高级查询,你须要一种可以将多查询组合成单一查询的查询方法。

你能够用 bool 查询来实现你的需求。这种查询将多查询组合在一块儿,成为用户本身想要的布尔查询。它接收如下参数:

  • must 
    文档 必须 匹配这些条件才能被包含进来。
  • must_not 
    文档 必须不 匹配这些条件才能被包含进来。
  • should 
    若是知足这些语句中的任意语句,将增长 _score ,不然,无任何影响。它们主要用于修正每一个文档的相关性得分。
  • filter 
    必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。

因为这是咱们看到的第一个包含多个查询的查询,因此有必要讨论一下相关性得分是如何组合的。每个子查询都独自地计算文档的相关性得分。一旦他们的得分被计算出来, bool 查询就将这些得分进行合并而且返回一个表明整个布尔操做的得分。

下面的查询用于查找 title 字段匹配 how to make millions 而且不被标识为 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" }}}
        ]
    }
}

若是没有 must 语句,那么至少须要可以匹配其中的一条 should 语句。但,若是存在至少一条 must 语句,则对 should 语句的匹配没有要求。

增长带过滤器(filtering)的查询

若是咱们不想由于文档的时间而影响得分,能够用 filter 语句来重写前面的例子:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "range": { "date": { "gte": "2014-01-01" }} 
        }
    }
}

range 查询已经从 should 语句中移到 filter 语句

经过将 range 查询移到 filter 语句中,咱们将它转成不评分的查询,将再也不影响文档的相关性排名。因为它如今是一个不评分的查询,能够使用各类对 filter 查询有效的优化手段来提高性能。

全部查询均可以借鉴这种方式。将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的 filter 了。

若是你须要经过多个不一样的标准来过滤你的文档,bool 查询自己也能够被用作不评分的查询。简单地将它放置到 filter 语句中并在内部构建布尔逻辑:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "bool": { 
              "must": [
                  { "range": { "date": { "gte": "2014-01-01" }}},
                  { "range": { "price": { "lte": 29.99 }}}
              ],
              "must_not": [
                  { "term": { "category": "ebooks" }}
              ]
          }
        }
    }
}

将 bool 查询包裹在 filter 语句中,咱们能够在过滤标准中增长布尔逻辑

经过混合布尔查询,咱们能够在咱们的查询请求中灵活地编写 scoring 和 filtering 查询逻辑。

constant_score 查询

尽管没有 bool 查询使用这么频繁,constant_score 查询也是你工具箱里有用的查询工具。它将一个不变的常量评分应用于全部匹配的文档。它被常常用于你只须要执行一个 filter 而没有其它查询(例如,评分查询)的状况下。

能够使用它来取代只有 filter 语句的 bool 查询。在性能上是彻底相同的,但对于提升查询简洁性和清晰度有很大帮助。

{
    "constant_score":   {
        "filter": {
            "term": { "category": "ebooks" } 
        }
    }
}

term 查询被放置在 constant_score 中,转成不评分的 filter。这种方式能够用来取代只有 filter 语句的 bool 查询。

6. 验证查询

查询能够变得很是的复杂,尤为 和不一样的分析器与不一样的字段映射结合时,理解起来就有点困难了。不过 validate-query API 能够用来验证查询是否合法。

GET /gb/tweet/_validate/query
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

以上 validate 请求的应答告诉咱们这个查询是不合法的:

{
  "valid" :         false,
  "_shards" : {
    "total" :       1,
    "successful" :  1,
    "failed" :      0
  }
}

理解错误信息

为了找出 查询不合法的缘由,能够将 explain 参数 加到查询字符串中:

GET /gb/tweet/_validate/query?explain 
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

explain 参数能够提供更多关于查询不合法的信息。

很明显,咱们将查询类型(match)与字段名称 (tweet)搞混了:

{
  "valid" :     false,
  "_shards" :   { ... },
  "explanations" : [ {
    "index" :   "gb",
    "valid" :   false,
    "error" :   "org.elasticsearch.index.query.QueryParsingException:
                 [gb] No query registered for [tweet]"
  } ]
}

理解查询语句

对于合法查询,使用 explain 参数将返回可读的描述,这对准确理解 Elasticsearch 是如何解析你的 query 是很是有用的:

GET /_validate/query?explain
{
   "query": {
      "match" : {
         "tweet" : "really powerful"
      }
   }
}

咱们查询的每个 index 都会返回对应的 explanation ,由于每个 index 都有本身的映射和分析器:

{
  "valid" :         true,
  "_shards" :       { ... },
  "explanations" : [ {
    "index" :       "us",
    "valid" :       true,
    "explanation" : "tweet:really tweet:powerful"
  }, {
    "index" :       "gb",
    "valid" :       true,
    "explanation" : "tweet:realli tweet:power"
  } ]
}

从 explanation 中能够看出,匹配 really powerful 的 match 查询被重写为两个针对 tweet 字段的 single-term 查询,一个single-term查询对应查询字符串分出来的一个term。

固然,对于索引 us ,这两个 term 分别是 really 和 powerful ,而对于索引 gb ,term 则分别是 realli 和 power 。之因此出现这个状况,是因为咱们将索引 gb 中 tweet 字段的分析器修改成 english 分析器。

8、排序与相关性

默认状况下,返回的结果是按照 相关性 进行排序的——最相关的文档排在最前。 在本章的后面部分,咱们会解释 相关性 意味着什么以及它是如何计算的, 不过让咱们首先看看 sort 参数以及如何使用它。

  1. 排序

为了按照相关性来排序,须要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一个浮点数进行表示,并在搜索结果中经过 _score 参数返回, 默认排序是 _score 降序。

有时,相关性评分对你来讲并无意义。例如,下面的查询返回全部 user_id 字段包含 1 的结果:

GET /_search
{
    "query" : {
        "bool" : {
            "filter" : {
                "term" : {
                    "user_id" : 1
                }
            }
        }
    }
}

这里没有一个有意义的分数:由于咱们使用的是 filter (过滤),这代表咱们只但愿获取匹配 user_id: 1 的文档,并无试图肯定这些文档的相关性。 实际上文档将按照随机顺序返回,而且每一个文档都会评为零分。

若是评分为零对你形成了困扰,你能够使用 constant_score 查询进行替代:

GET /_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "term" : {
                    "user_id" : 1
                }
            }
        }
    }
}

这将让全部文档应用一个恒定分数(默认为 1 )。它将执行与前述查询相同的查询,而且全部的文档将像以前同样随机返回,这些文档只是有了一个分数而不是零分。

按照字段的值排序

在这个案例中,经过时间来对 tweets 进行排序是有意义的,最新的 tweets 排在最前。 咱们能够使用 sort 参数进行实现:

GET /_search
{
    "query" : {
        "bool" : {
            "filter" : { "term" : { "user_id" : 1 }}
        }
    },
    "sort": { "date": { "order": "desc" }}
}

你会注意到结果中的两个不一样点:

"hits" : {
    "total" :           6,
    "max_score" :       null, 
    "hits" : [ {
        "_index" :      "us",
        "_type" :       "tweet",
        "_id" :         "14",
        "_score" :      null, 
        "_source" :     {
             "date":    "2014-09-24",
             ...
        },
        "sort" :        [ 1411516800000 ] 
    },
    ...
}


    _score 不被计算, 由于它并无用于排序。                
    date 字段的值表示为自 epoch (January 1, 1970 00:00:00 UTC)以来的毫秒数,经过 sort 字段的值进行返回。

首先咱们在每一个结果中有一个新的名为 sort 的元素,它包含了咱们用于排序的值。 在这个案例中,咱们按照 date 进行排序,在内部被索引为 自 epoch 以来的毫秒数 。 long 类型数 1411516800000 等价于日期字符串 2014-09-24 00:00:00 UTC 。

其次 _score 和 max_score 字段都是 null 。 计算 _score 的花销巨大,一般仅用于排序; 咱们并不根据相关性排序,因此记录 _score 是没有意义的。若是不管如何你都要计算 _score , 你能够将 track_scores 参数设置为 true 。

一个简便方法是, 你能够 指定一个字段用来排序:

"sort": "number_of_children"

字段将会默认升序排序 ,而按照 _score 的值进行降序排序。

多级排序

假定咱们想要结合使用 date 和 _score 进行查询,而且匹配的结果首先按照日期排序,而后按照相关性排序:

GET /_search
{
    "query" : {
        "bool" : {
            "must":   { "match": { "tweet": "manage text search" }},
            "filter" : { "term" : { "user_id" : 2 }}
        }
    },
    "sort": [
        { "date":   { "order": "desc" }},
        { "_score": { "order": "desc" }}
    ]
}

排序条件的顺序是很重要的。结果首先按第一个条件排序,仅当结果集的第一个 sort 值彻底相同时才会按照第二个条件进行排序,以此类推。

多级排序并不必定包含 _score 。你能够根据一些不一样的字段进行排序, 如地理距离或是脚本计算的特定值。

Query-string 搜索 也支持自定义排序,能够在查询字符串中使用 sort 参数:

GET /_search?sort=date:desc&sort=_score&q=search

多值字段的排序

一种情形是字段有多个值的排序, 须要记住这些值并无固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪一个进行排序呢?

对于数字或日期,你能够将多值字段减为单值,这能够经过使用 min 、 max 、 avg 或是 sum 排序模式 。 例如你能够按照每一个 date 字段中的最先日期进行排序,经过如下方法:

"sort": {
    "dates": {
        "order": "asc",
        "mode":  "min"
    }
}

2. 字符串排序与多字段

被解析的字符串字段也是多值字段, 可是不多会按照你想要的方式进行排序。若是你想分析一个字符串,如 fine old art , 这包含 3 项。咱们极可能想要按第一项的字母排序,而后按第二项的字母排序,诸如此类,可是 Elasticsearch 在排序过程当中没有这样的信息。

你能够使用 min 和 max 排序模式(默认是 min ),可是这会致使排序以 art 或是 old ,任何一个都不是所但愿的。

为了以字符串字段进行排序,这个字段应仅包含一项: 整个 not_analyzed 字符串。 可是咱们仍须要 analyzed 字段,这样才能以全文进行查询

一个简单的方法是用两种方式对同一个字符串进行索引,这将在文档中包括两个字段: analyzed 用于搜索, not_analyzed 用于排序

可是保存相同的字符串两次在 _source 字段是浪费空间的。 咱们真正想要作的是传递一个 单字段 可是却用两种方式索引它。全部的 _core_field 类型 (strings, numbers, Booleans, dates) 接收一个 fields 参数

该参数容许你转化一个简单的映射如:

"tweet": {
    "type":     "string",
    "analyzer": "english"
}

为一个多字段映射如:

"tweet": { 
    "type":     "string",
    "analyzer": "english",
    "fields": {
        "raw": { 
            "type":  "string",
            "index": "not_analyzed"
        }
    }
}


    tweet 主字段与以前的同样: 是一个 analyzed 全文字段。
    新的 tweet.raw 子字段是 not_analyzed.

如今,至少只要咱们从新索引了咱们的数据,使用 tweet 字段用于搜索,tweet.raw 字段用于排序:

GET /_search
{
    "query": {
        "match": {
            "tweet": "elasticsearch"
        }
    },
    "sort": "tweet.raw"
}

以全文 analyzed 字段排序会消耗大量的内存。

3. 什么是相关性?

咱们曾经讲过,默认状况下,返回结果是按相关性倒序排列的。 可是什么是相关性? 相关性如何计算?

每一个文档都有相关性评分,用一个正浮点数字段 _score 来表示 。 _score 的评分越高,相关性越高。

查询语句会为每一个文档生成一个 _score 字段。评分的计算方式取决于查询类型 不一样的查询语句用于不一样的目的: fuzzy 查询会计算与关键词的拼写类似程度,terms 查询会计算 找到的内容与关键词组成部分匹配的百分比,可是一般咱们说的 relevance 是咱们用来计算全文本字段的值相对于全文本检索词类似程度的算法。

Elasticsearch 的类似度算法 被定义为检索词频率/反向文档频率, TF/IDF ,包括如下内容:

  • 检索词频率 
    检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。
  • 反向文档频率 
    每一个检索词在索引中出现的频率?频率越高,相关性越低。检索词出如今多数文档中会比出如今少数文档中的权重更低。
  • 字段长度准则 
    字段的长度是多少?长度越长,相关性越低。 检索词出如今一个短的 title 要比一样的词出如今一个长的 content 字段权重更大。

单个查询能够联合使用 TF/IDF 和其余方式,好比短语查询中检索词的距离或模糊查询里的检索词类似度。

相关性并不仅是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。

若是多条查询子句被合并为一条复合查询语句 ,好比 bool 查询,则每一个查询子句计算得出的评分会被合并到总的相关性评分中。

理解评分标准

当调试一条复杂的查询语句时, 想要理解 _score 到底是如何计算是比较困难的。Elasticsearch 在 每一个查询语句中都有一个 explain 参数,将 explain 设为 true 就能够获得更详细的信息。

GET /_search?explain 
{
   "query"   : { "match" : { "tweet" : "honeymoon" }}
}

explain 参数可让返回结果添加一个 _score 评分的得来依据。

增长一个 explain 参数会为每一个匹配到的文档产生一大堆额外内容,可是花时间去理解它是颇有意义的。 若是如今看不明白也不要紧 — 等你须要的时候再来回顾这一节就行。下面咱们来一点点的了解这块知识点。

首先,咱们看一下普通查询返回的元数据:

{
    "_index" :      "us",
    "_type" :       "tweet",
    "_id" :         "12",
    "_score" :      0.076713204,
    "_source" :     { ... trimmed ... },

这里加入了该文档来自于哪一个节点哪一个分片上的信息,这对咱们是比较有帮助的,由于词频率和 文档频率是在每一个分片中计算出来的,而不是每一个索引中:

"_shard" :      1,
    "_node" :       "mzIVYCsqSWCG_M_ZffSs9Q",

而后它提供了 _explanation 。每一个 入口都包含一个 description 、 value 、 details 字段,它分别告诉你计算的类型、计算结果和任何咱们须要的计算细节。

"_explanation": { 
   "description": "weight(tweet:honeymoon in 0)
                  [PerFieldSimilarity], result of:",
   "value":       0.076713204,
   "details": [
      {
         "description": "fieldWeight in 0, product of:",
         "value":       0.076713204,
         "details": [
            {  
               "description": "tf(freq=1.0), with freq of:",
               "value":       1,
               "details": [
                  {
                     "description": "termFreq=1.0",
                     "value":       1
                  }
               ]
            },
            { 
               "description": "idf(docFreq=1, maxDocs=1)",
               "value":       0.30685282
            },
            { 
               "description": "fieldNorm(doc=0)",
               "value":        0.25,
            }
         ]
      }
   ]
}


    honeymoon 相关性评分计算的总结
    检索词频率               
    反向文档频率              
    字段长度准则

输出 explain 结果代价是十分昂贵的,它只能用做调试工具 。千万不要用于生产环境。

第一部分是关于计算的总结。告诉了咱们 honeymoon 在 tweet 字段中的检索词频率/反向文档频率或 TF/IDF, (这里的文档 0 是一个内部的 ID,跟咱们没有关系,能够忽略。)

而后它提供了权重是如何计算的细节:

检索词频率:

检索词 `honeymoon` 在这个文档的 `tweet` 字段中的出现次数。

反向文档频率:

检索词 `honeymoon` 在索引上全部文档的 `tweet` 字段中出现的次数。

字段长度准则:

在这个文档中, `tweet` 字段内容的长度 -- 内容越长,值越小。

复杂的查询语句解释也很是复杂,可是包含的内容与上面例子大体相同。 经过这段信息咱们能够了解搜索结果是如何产生的。

JSON 形式的 explain 描述是难以阅读的, 可是转成 YAML 会好不少,只须要在参数中加上 format=yaml 。

理解文档是如何被匹配到的

当 explain 选项加到某一文档上时, explain api 会帮助你理解为什么这个文档会被匹配,更重要的是,一个文档为什么没有被匹配。

请求路径为 /index/type/id/_explain ,以下所示:

GET /us/tweet/12/_explain
{
   "query" : {
      "bool" : {
         "filter" : { "term" :  { "user_id" : 2           }},
         "must" :  { "match" : { "tweet" :   "honeymoon" }}
      }
   }
}

不仅是咱们以前看到的充分解释 ,咱们如今有了一个 description 元素,它将告诉咱们:

"failure to match filter: cache(user_id:[2 TO 2])"

也就是说咱们的 user_id 过滤子句使该文档不能匹配到。

  1. Doc Values 介绍

本章的最后一个话题是关于 Elasticsearch 内部的一些运行状况。在这里咱们先不介绍新的知识点,因此咱们应该意识到,Doc Values 是咱们须要反复提到的一个重要话题。

当你对一个字段进行排序时,Elasticsearch 须要访问每一个匹配到的文档获得相关的值。倒排索引的检索性能是很是快的,可是在字段值排序时却不是理想的结构。

  • 在搜索的时候,咱们能经过搜索关键词快速获得结果集。
  • 当排序的时候,咱们须要倒排索引里面某个字段值的集合。换句话说,咱们须要 转置 倒排索引。

转置 结构在其余系统中常常被称做 列存储 。实质上,它将全部单字段的值存储在单数据列中,这使得对其进行操做是十分高效的,例如排序。

在 Elasticsearch 中,Doc Values 就是一种列式存储结构,默认状况下每一个字段的 Doc Values 都是激活的,Doc Values 是在索引时建立的,当字段索引时,Elasticsearch 为了可以快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 Doc Values。

Elasticsearch 中的 Doc Values 常被应用到如下场景:

  • 对一个字段进行排序
  • 对一个字段进行聚合
  • 某些过滤,好比地理位置过滤
  • 某些与字段相关的脚本计算

由于文档值被序列化到磁盘,咱们能够依靠操做系统的帮助来快速访问。当 working set 远小于节点的可用内存,系统会自动将全部的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操做系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。

咱们稍后会深刻讨论 Doc Values。如今全部你须要知道的是排序发生在索引时创建的平行数据结构中。

9、执行分布式检索

在继续以前,咱们将绕道讨论一下在分布式环境中搜索是怎么执行的。 这比咱们在 分布式文档存储 章节讨论的基本的 增-删-改-查 (CRUD)请求要复杂一些。

内容提示

你能够根据兴趣阅读本章内容。你并不须要为了使用 Elasticsearch 而理解和记住全部的细节。

这章的阅读目的只为初步了解下工做原理,以便未来须要时能够及时找到这些知识, 可是不要被细节所困扰。

一个 CRUD 操做只对单个文档进行处理,文档的惟一性由 _index, _type, 和 routing values (一般默认是该文档的 _id )的组合来肯定。 这表示咱们确切的知道集群中哪一个分片含有此文档。

搜索须要一种更加复杂的执行模型由于咱们不知道查询会命中哪些文档: 这些文档有可能在集群的任何分片上。 一个搜索请求必须询问咱们关注的索引(index or indices)的全部分片的某个副原本肯定它们是否含有任何匹配的文档。

可是找到全部的匹配文档仅仅完成事情的一半。 在 search 接口返回一个 page 结果以前,多分片中的结果必须组合成单个排序列表。 为此,搜索被执行成一个两阶段过程,咱们称之为 query then fetch 。

  1. 查询阶段

在初始 查询阶段 时, 查询会广播到索引中每个分片拷贝(主分片或者副本分片)。 每一个分片在本地执行搜索并构建一个匹配文档的 优先队列。

优先队列

一个 优先队列 仅仅是一个存有 top-n 匹配文档的有序列表。优先队列的大小取决于分页参数 from 和 size 。例如,以下搜索请求将须要足够大的优先队列来放入100条文档。

GET /_search
{
    "from": 90,
    "size": 10
}

这个查询阶段的过程如图 图 14 “查询过程分布式搜索” 所示。

图 14. 查询过程分布式搜索

查询阶段包含如下三个步骤:

  1. 客户端发送一个 search 请求到 Node 3 , Node 3 会建立一个大小为 from + size 的空优先队列。
  2. Node 3 将查询请求转发到索引的每一个主分片或副本分片中。每一个分片在本地执行查询并添加结果到大小为 from + size 的本地有序优先队列中。
  3. 每一个分片返回各自优先队列中全部文档的 ID 和排序值给协调节点,也就是 Node 3 ,它合并这些值到本身的优先队列中来产生一个全局排序后的结果列表。

当一个搜索请求被发送到某个节点时,这个节点就变成了协调节点。 这个节点的任务是广播查询请求到全部相关分片并将它们的响应整合成全局排序后的结果集合,这个结果集合会返回给客户端。

第一步是广播请求到索引中每个节点的分片拷贝。就像 document GET requests 所描述的, 查询请求能够被某个主分片或某个副本分片处理, 这就是为何更多的副本(当结合更多的硬件)可以增长搜索吞吐率。 协调节点将在以后的请求中轮询全部的分片拷贝来分摊负载。

每一个分片在本地执行查询请求而且建立一个长度为 from + size 的优先队列—也就是说,每一个分片建立的结果集足够大,都可以知足全局的搜索请求。 分片返回一个轻量级的结果列表到协调节点,它仅包含文档 ID 集合以及任何排序须要用到的值,例如 _score 。

协调节点将这些分片级的结果合并到本身的有序优先队列里,它表明了全局排序结果集合。至此查询过程结束。

一个索引能够由一个或几个主分片组成, 因此一个针对单个索引的搜索请求须要可以把来自多个分片的结果组合起来。 针对 multiple 或者 all 索引的搜索工做方式也是彻底一致的–仅仅是包含了更多的分片而已。

  1. 取回阶段

查询阶段标识哪些文档知足 搜索请求,可是咱们仍然须要取回这些文档。这是取回阶段的任务, 正如 图 15 “分布式搜索的取回阶段” 所展现的。

图 15. 分布式搜索的取回阶段

分布式阶段由如下步骤构成:

  1. 协调节点辨别出哪些文档须要被取回并向相关的分片提交多个 GET 请求。
  2. 每一个分片加载并 丰富 文档,若是有须要的话,接着返回文档给协调节点。
  3. 一旦全部的文档都被取回了,协调节点返回结果给客户端。

协调节点首先决定哪些文档 确实 须要被取回。例如,若是咱们的查询指定了 { “from”: 90, “size”: 10 } ,最初的90个结果会被丢弃,只有从第91个开始的10个结果须要被取回。这些文档可能来自和最初搜索请求有关的一个、多个甚至所有分片。

协调节点给持有相关文档的每一个分片建立一个 multi-get request ,并发送请求给一样处理查询阶段的分片副本。

分片加载文档体– _source 字段–若是有须要,用元数据和 search snippet highlighting 丰富结果文档。 一旦协调节点接收到全部的结果文档,它就组装这些结果为单个响应返回给客户端。

深分页(Deep Pagination)

先查后取的过程支持用 from 和 size 参数分页,可是这是 有限制的 。 要记住须要传递信息给协调节点的每一个分片必须先建立一个 from + size 长度的队列,协调节点须要根据 number_of_shards * (from + size) 排序文档,来找到被包含在 size 里的文档。

取决于你的文档的大小,分片的数量和你使用的硬件,给 10,000 到 50,000 的结果文档深分页( 1,000 到 5,000 页)是彻底可行的。可是使用足够大的 from 值,排序过程可能会变得很是沉重,使用大量的CPU、内存和带宽。由于这个缘由,咱们强烈建议你不要使用深分页。

实际上, “深分页” 不多符合人的行为。当2到3页过去之后,人会中止翻页,而且改变搜索标准。会不知疲倦地一页一页的获取网页直到你的服务崩溃的罪魁祸首通常是机器人或者web spider。

若是你 确实 须要从你的集群取回大量的文档,你能够经过用 scroll 查询禁用排序使这个取回行为更有效率,咱们会在 later in this chapter 进行讨论。

  1. 搜索选项

有几个 查询参数能够影响搜索过程。

偏好

偏好这个参数 preference 容许 用来控制由哪些分片或节点来处理搜索请求。 它接受像 _primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, 和 _shards:2,3 这样的值, 这些值在 search preference 文档页面被详细解释。

可是最有用的值是某些随机字符串,它能够避免 bouncing results 问题。

Bouncing Results

想象一下有两个文档有一样值的时间戳字段,搜索结果用 timestamp 字段来排序。 因为搜索请求是在全部有效的分片副本间轮询的,那就有可能发生主分片处理请求时,这两个文档是一种顺序, 而副本分片处理请求时又是另外一种顺序。

这就是所谓的 bouncing results 问题: 每次用户刷新页面,搜索结果表现是不一样的顺序。 让同一个用户始终使用同一个分片,这样能够避免这种问题, 能够设置 preference 参数为一个特定的任意值好比用户会话ID来解决。

超时问题

一般分片处理完它全部的数据后再把结果返回给协同节点,协同节点把收到的全部结果合并为最终结果。

这意味着花费的时间是最慢分片的处理时间加结果合并的时间。若是有一个节点有问题,就会致使全部的响应缓慢。

参数 timeout 告诉 分片容许处理数据的最大时间。若是没有足够的时间处理全部数据,这个分片的结果能够是部分的,甚至是空数据。

搜索的返回结果会用属性 timed_out 标明分片是否返回的是部分结果:

...
    "timed_out":     true,  
    ...

这个搜索请求超时了。

超时仍然是一个最有效的操做,知道这一点很重要; 极可能查询会超过设定的超时时间。这种行为有两个缘由:

  1. 超时检查是基于每文档作的。 可是某些查询类型有大量的工做在文档评估以前须要完成。 这种 “setup” 阶段并不考虑超时设置,因此太长的创建时间会致使超过超时时间的总体延迟。
  2. 由于时间检查是基于每一个文档的,一次长时间查询在单个文档上执行而且在下个文档被评估以前不会超时。 这也意味着差的脚本(好比带无限循环的脚本)将会永远执行下去。

路由

在 路由一个文档到一个分片中 中, 咱们解释过如何定制参数 routing ,它可以在索引时提供来确保相关的文档,好比属于某个用户的文档被存储在某个分片上。 在搜索的时候,不用搜索索引的全部分片,而是经过指定几个 routing 值来限定只搜索几个相关的分片:

GET /_search?routing=user_1,user2

这个技术在设计大规模搜索系统时就会派上用场,咱们在 扩容设计 中详细讨论它。

搜索类型

缺省的搜索类型是 query_then_fetch 。 在某些状况下,你可能想明确设置 search_type 为 dfs_query_then_fetch 来改善相关性精确度:

GET /_search?search_type=dfs_query_then_fetch

搜索类型 dfs_query_then_fetch 有预查询阶段,这个阶段能够从全部相关分片获取词频来计算全局词频。 咱们在 被破坏的相关度! 会再讨论它。

  1. 游标查询 Scroll

scroll 查询 能够用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。

游标查询容许咱们 先作查询初始化,而后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。

游标查询会取某个时间点的快照数据。 查询初始化以后索引上的任何变化会被它忽略。 它经过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 同样。

深度分页的代价根源是结果集全局排序,若是去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。

启用游标查询能够经过在查询的时候设置参数 scroll 的值为咱们指望的游标查询的过时时间。 游标查询的过时时间会在每次作查询的时候刷新,因此这个时间只须要足够处理当前批的结果就能够了,而不是处理查询结果的全部文档的所需时间。 这个过时时间的参数很重要,由于保持这个游标查询窗口须要消耗资源,因此咱们指望若是再也不须要维护这种资源就该早点儿释放掉。 设置这个超时可以让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

GET /old_index/_search?scroll=1m 
{
    "query": { "match_all": {}},
    "sort" : ["_doc"], 
    "size":  1000
}


    保持游标查询窗口一分钟。       
    关键字 _doc 是最有效的排序顺序。

这个查询的返回结果包括一个字段 _scroll_id, 它是一个base64编码的长字符串 (((“scroll_id”))) 。 如今咱们能传递字段 _scroll_id 到 _search/scroll 查询接口获取下一批结果:

GET /_search/scroll
{
    "scroll": "1m", 
    "scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}


    注意再次设置游标查询过时时间为一分钟。

这个游标查询返回的下一批结果。 尽管咱们指定字段 size 的值为1000,咱们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 做用于单个分片,因此每一个批次实际返回的文档数量最大为 size * number_of_primary_shards 。

注意游标查询每次返回一个新字段 _scroll_id。每次咱们作下一次游标查询, 咱们必须把前一次查询返回的字段 _scroll_id 传递进去。 当没有更多的结果返回的时候,咱们就处理完全部匹配的文档了。

提示:某些官方的 Elasticsearch 客户端好比 Python 客户端 和 Perl 客户端 提供了这个功能易用的封装。

10、索引管理

咱们已经看到 Elasticsearch 让开发一个新的应用变得简单,不须要任何预先计划或设置。 不过,要不了多久你就会开始想要优化索引和搜索过程,以便更好地适合您的特定用例。 这些定制几乎围绕着索引和类型的方方面面,在本章,咱们将介绍管理索引和类型映射的 API 以及一些最重要的设置。

  1. 建立一个索引

到目前为止, 咱们已经经过索引一篇文档建立了一个新的索引 。这个索引采用的是默认的配置,新的字段经过动态映射的方式被添加到类型映射。如今咱们须要对这个创建索引的过程作更多的控制:咱们想要确保这个索引有数量适中的主分片,而且在咱们索引任何数据 以前 ,分析器和映射已经被创建好。

为了达到这个目的,咱们须要手动建立索引,在请求体里面传入设置或类型映射,以下所示:

PUT /my_index
{
    "settings": { ... any settings ... },
    "mappings": {
        "type_one": { ... any mappings ... },
        "type_two": { ... any mappings ... },
        ...
    }
}

若是你想禁止自动建立索引,你 能够经过在 config/elasticsearch.yml 的每一个节点下添加下面的配置:

action.auto_create_index: false

咱们会在以后讨论你怎么用 索引模板 来预配置开启自动建立索引。这在索引日志数据的时候尤为有用:你将日志数据索引在一个以日期结尾命名的索引上,子夜时分,一个预配置的新索引将会自动进行建立。

  1. 删除一个索引

用如下的请求来 删除索引:

DELETE /my_index

你也能够这样删除多个索引:

DELETE /index_one,index_two
DELETE /index_*

你甚至能够这样删除 所有 索引:

DELETE /_all
DELETE /*

对一些人来讲,可以用单个命令来删除全部数据可能会致使可怕的后果。若是你想要避免意外的大量删除, 你能够在你的 elasticsearch.yml 作以下配置:

action.destructive_requires_name: true

这个设置使删除只限于特定名称指向的数据, 而不容许经过指定 _all 或通配符来删除指定索引库。你一样能够经过 Cluster State API 动态的更新这个设置。

  1. 索引设置

你能够经过修改配置来自定义索引行为,详细配置参照 索引模块

Elasticsearch 提供了优化好的默认配置。 除非你理解这些配置的做用而且知道为何要去修改,不然不要随意修改。

下面是两个最重要的设置:

  • number_of_shards 
    每一个索引的主分片数,默认值是 5 。这个配置在索引建立后不能修改。

  • number_of_replicas 
    每一个主分片的副本数,默认值是 1 。对于活动的索引库,这个配置能够随时修改。

例如,咱们能够建立只有 一个主分片,没有副本的小索引:

PUT /my_temp_index
{
    "settings": {
        "number_of_shards" :   1,
        "number_of_replicas" : 0
    }
}

而后,咱们能够用 update-index-settings API 动态修改副本数:

PUT /my_temp_index/_settings
{
    "number_of_replicas": 1
}
  1. 配置分析器

第三个重要的索引设置是 analysis 部分, 用来配置已存在的分析器或针对你的索引建立新的自定义分析器。

在 分析与分析器 ,咱们介绍了一些内置的 分析器,用于将全文字符串转换为适合搜索的倒排索引。

standard 分析器是用于全文字段的默认分析器, 对于大部分西方语系来讲是一个不错的选择。 它包括了如下几点:

  • standard 分词器,经过单词边界分割输入的文本。
  • standard 语汇单元过滤器,目的是整理分词器触发的语汇单元(可是目前什么都没作)。
  • lowercase 语汇单元过滤器,转换全部的语汇单元为小写。
  • stop 语汇单元过滤器,删除停用词–对搜索相关性影响不大的经常使用词,如 a , the , and , is 。

默认状况下,停用词过滤器是被禁用的。如需启用它,你能够经过建立一个基于 standard 分析器的自定义分析器并设置 stopwords 参数。 能够给分析器提供一个停用词列表,或者告知使用一个基于特定语言的预约义停用词列表。

在下面的例子中,咱们建立了一个新的分析器,叫作 es_std , 并使用预约义的 西班牙语停用词列表:

PUT /spanish_docs
{
    "settings": {
        "analysis": {
            "analyzer": {
                "es_std": {
                    "type":      "standard",
                    "stopwords": "_spanish_"
                }
            }
        }
    }
}

es_std 分析器不是全局的–它仅仅存在于咱们定义的 spanish_docs 索引中。 为了使用 analyze API来对它进行测试,咱们必须使用特定的索引名:

GET /spanish_docs/_analyze?analyzer=es_std
El veloz zorro marrón

简化的结果显示西班牙语停用词 El 已被正确的移除:

{
  "tokens" : [
    { "token" :    "veloz",   "position" : 2 },
    { "token" :    "zorro",   "position" : 3 },
    { "token" :    "marrón",  "position" : 4 }
  ]
}
  1. 自定义分析器

虽然Elasticsearch带有一些现成的分析器,然而在分析器上Elasticsearch真正的强大之处在于,你能够经过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来建立自定义的分析器。

在 分析与分析器 咱们说过,一个 分析器 就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行:

  • 字符过滤器

    字符过滤器 用来 整理 一个还没有被分词的字符串。例如,若是咱们的文本是HTML格式的,它会包含像

    或者

    这样的HTML标签,这些标签是咱们不想索引的。咱们能够使用 html清除 字符过滤器 来移除掉全部的HTML标签,而且像把 Á 转换为相对应的Unicode字符 Á 这样,转换HTML实体。一个分析器可能有0个或者多个字符过滤器。

     

  • 分词器

    一个分析器 必须 有一个惟一的分词器。 分词器把字符串分解成单个词条或者词汇单元。 标准 分析器里使用的 标准 分词器 把一个字符串根据单词边界分解成单个词条,而且移除掉大部分的标点符号,然而还有其余不一样行为的分词器存在。例如, 关键词 分词器 完整地输出 接收到的一样的字符串,并不作任何分词。 空格 分词器 只根据空格分割文本 。 正则 分词器 根据匹配正则表达式来分割文本 。

  • 词单元过滤器

    通过分词,做为结果的 词单元流 会按照指定的顺序经过指定的词单元过滤器 。词单元过滤器能够修改、添加或者移除词单元。咱们已经提到过 lowercase 和 stop 词过滤器 ,可是在 Elasticsearch 里面还有不少可供选择的词单元过滤器。 词干过滤器 把单词 遏制 为 词干。 ascii_folding 过滤器移除变音符,把一个像 “très” 这样的词转换为 “tres” 。 ngram 和 edge_ngram 词单元过滤器 能够产生 适合用于部分匹配或者自动补全的词单元。

在 深刻搜索,咱们讨论了在哪里使用,以及怎样使用分词器和过滤器。可是首先,咱们须要解释一下怎样建立自定义的分析器。

建立一个自定义分析器

和咱们以前配置 es_std 分析器同样,咱们能够在 analysis 下的相应位置设置字符过滤器、分词器和词单元过滤器:

PUT /my_index
{
    "settings": {
        "analysis": {
            "char_filter": { ... custom character filters ... },
            "tokenizer":   { ...    custom tokenizers     ... },
            "filter":      { ...   custom token filters   ... },
            "analyzer":    { ...    custom analyzers      ... }
        }
    }
}

做为示范,让咱们一块儿来建立一个自定义分析器吧,这个分析器能够作到下面的这些事:

  1. 使用 html清除 字符过滤器移除HTML部分。
  2. 使用一个自定义的 映射 字符过滤器把 & 替换为 ” and ” :

    “char_filter”: { 
    “&_to_and”: { 
    “type”: “mapping”, 
    “mappings”: [ “&=> and “] 

    }

  3. 使用 标准 分词器分词。

  4. 小写词条,使用 小写 词过滤器处理。
  5. 使用自定义 中止 词过滤器移除自定义的中止词列表中包含的词:

    “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" ]
    }
}

汇总起来,完整的 建立索引 请求 看起来应该像这样:

PUT /my_index
{
    "settings": {
        "analysis": {
            "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" ]
            }}
}}}

索引被建立之后,使用 analyze API 来 测试这个新的分析器:

GET /my_index/_analyze?analyzer=my_analyzer
The quick & brown fox

下面的缩略结果展现出咱们的分析器正在正确地运行:

{
  "tokens" : [
      { "token" :   "quick",    "position" : 2 },
      { "token" :   "and",      "position" : 3 },
      { "token" :   "brown",    "position" : 4 },
      { "token" :   "fox",      "position" : 5 }
    ]
}

这个分析器如今是没有多大用处的,除非咱们告诉 Elasticsearch在哪里用上它。咱们能够像下面这样把这个分析器应用在一个 string 字段上:

PUT /my_index/_mapping/my_type
{
    "properties": {
        "title": {
            "type":      "string",
            "analyzer":  "my_analyzer"
        }
    }
}
  1. 类型和映射

类型 在 Elasticsearch 中表示一类类似的文档。 类型由 名称 —好比 user 或 blogpost —和 映射 组成。

映射, 就像数据库中的 schema ,描述了文档可能具备的字段或 属性 、 每一个字段的数据类型—好比 string, integer 或 date —以及Lucene是如何索引和存储这些字段的。

类型能够很好的抽象划分类似但不相同的数据。但因为 Lucene 的处理方式,类型的使用有些限制。

Lucene 如何处理文档

在 Lucene 中,一个文档由一组简单的键值对组成。 每一个字段均可以有多个值,但至少要有一个值。 相似的,一个字符串能够经过分析过程转化为多个值。Lucene 不关心这些值是字符串、数字或日期–全部的值都被当作 不透明字节 。

当咱们在 Lucene 中索引一个文档时,每一个字段的值都被添加到相关字段的倒排索引中。你也能够将未处理的原始数据 存储 起来,以便这些原始数据在以后也能够被检索到。

类型是如何实现的

Elasticsearch 类型是 以 Lucene 处理文档的这个方式为基础来实现的。一个索引能够有多个类型,这些类型的文档能够存储在相同的索引中。

Lucene 没有文档类型的概念,每一个文档的类型名被存储在一个叫 _type 的元数据字段上。 当咱们要检索某个类型的文档时, Elasticsearch 经过在 _type 字段上使用过滤器限制只返回这个类型的文档。

Lucene 也没有映射的概念。 映射是 Elasticsearch 将复杂 JSON 文档 映射 成 Lucene 须要的扁平化数据的方式。

例如,在 user 类型中, name 字段的映射能够声明这个字段是 string 类型,而且它的值被索引到名叫 name 的倒排索引以前,须要经过 whitespace 分词器分析:

"name": {
    "type":     "string",
    "analyzer": "whitespace"
}

避免类型陷阱

这致使了一个有趣的思想实验: 若是有两个不一样的类型,每一个类型都有同名的字段,但映射不一样(例如:一个是字符串一个是数字),将会出现什么状况?

简单回答是,Elasticsearch 不会容许你定义这个映射。当你配置这个映射时,将会出现异常。

详细回答是,每一个 Lucene 索引中的全部字段都包含一个单一的、扁平的模式。一个特定字段能够映射成 string 类型也能够是 number 类型,可是不能二者兼具。由于类型是 Elasticsearch 添加的 优于 Lucene 的额外机制(以元数据 _type 字段的形式),在 Elasticsearch 中的全部类型最终都共享相同的映射。

以 data 索引中两种类型的映射为例:

{
   "data": {
      "mappings": {
         "people": {
            "properties": {
               "name": {
                  "type": "string",
               },
               "address": {
                  "type": "string"
               }
            }
         },
         "transactions": {
            "properties": {
               "timestamp": {
                  "type": "date",
                  "format": "strict_date_optional_time"
               },
               "message": {
                  "type": "string"
               }
            }
         }
      }
   }
}

每一个类型定义两个字段 (分别是 “name”/”address” 和 “timestamp”/”message” )。它们看起来是相互独立的,但在后台 Lucene 将建立一个映射,如:

{
   "data": {
      "mappings": {
        "_type": {
          "type": "string",
          "index": "not_analyzed"
        },
        "name": {
          "type": "string"
        }
        "address": {
          "type": "string"
        }
        "timestamp": {
          "type": "long"
        }
        "message": {
          "type": "string"
        }
      }
   }
}

注: 这不是真实有效的映射语法,只是用于演示

对于整个索引,映射在本质上被 扁平化 成一个单一的、全局的模式。这就是为何两个类型不能定义冲突的字段:当映射被扁平化时,Lucene 不知道如何去处理。

类型结论

那么,这个讨论的结论是什么?技术上讲,多个类型能够在相同的索引中存在,只要它们的字段不冲突(要么由于字段是互为独占模式,要么由于它们共享相同的字段)。

重要的一点是: 类型能够很好的区分同一个集合中的不一样细分。在不一样的细分中数据的总体模式是相同的(或类似的)。

类型不适合 彻底不一样类型的数据 。若是两个类型的字段集是互不相同的,这就意味着索引中将有一半的数据是空的(字段将是 稀疏的 ),最终将致使性能问题。在这种状况下,最好是使用两个单独的索引。

总结:

  • 正确: 将 kitchen 和 lawn-care 类型放在 products 索引中, 由于这两种类型基本上是相同的模式
  • 错误: 将 products 和 logs 类型放在 data 索引中, 由于这两种类型互不相同。应该将它们放在不一样的索引中。

    1. 根对象

映射的最高一层被称为 根对象 ,它可能包含下面几项:

  • 一个 properties 节点,列出了文档中可能包含的每一个字段的映射
  • 各类元数据字段,它们都以一个下划线开头,例如 _type 、 _id 和 _source
  • 设置项,控制如何动态处理新的字段,例如 analyzer 、 dynamic_date_formats 和 dynamic_templates
  • 其余设置,能够同时应用在根对象和其余 object 类型的字段上,例如 enabled 、 dynamic 和 include_in_all

属性

咱们已经在 核心简单域类型 和 复杂核心域类型 章节中介绍过文档字段和属性的三个 最重要的设置:

  • type 
    字段的数据类型,例如 string 或 date
  • index 
    字段是否应当被当成全文来搜索( analyzed ),或被当成一个准确的值( not_analyzed ),仍是彻底不可被搜索( no )
  • analyzer 
    肯定在索引和搜索时全文字段使用的 analyzer

咱们将在本书的后续部分讨论其余字段类型,例如 ip 、 geo_point 和 geo_shape 。

元数据: _source 字段

默认地,Elasticsearch 在 _source 字段存储表明文档体的JSON字符串。和全部被存储的字段同样, _source 字段在被写入磁盘以前先会被压缩。

这个字段的存储几乎老是咱们想要的,由于它意味着下面的这些:

  • 搜索结果包括了整个可用的文档——不须要额外的从另外一个的数据仓库来取文档。
  • 若是没有 _source 字段,部分 update 请求不会生效。
  • 当你的映射改变时,你须要从新索引你的数据,有了_source字段你能够直接从Elasticsearch这样作,而没必要从另外一个(一般是速度更慢的)数据仓库取回你的全部文档。
  • 当你不须要看到整个文档时,单个字段能够从 _source 字段提取和经过 get 或者 search 请求返回。
  • 调试查询语句更加简单,由于你能够直接看到每一个文档包括什么,而不是从一列id猜想它们的内容。

然而,存储 _source 字段的确要使用磁盘空间。若是上面的缘由对你来讲没有一个是重要的,你能够用下面的映射禁用 _source 字段:

PUT /my_index
{
    "mappings": {
        "my_type": {
            "_source": {
                "enabled":  false
            }
        }
    }
}

在一个搜索请求里,你能够经过在请求体中指定 _source 参数,来达到只获取特定的字段的效果:

GET /_search
{
    "query":   { "match_all": {}},
    "_source": [ "title", "created" ]
}

这些字段的值会从 _source 字段被提取和返回,而不是返回整个 _source 。

Stored Fields 被存储字段

为了以后的检索,除了索引一个字段的值,你 还能够选择 存储 原始字段值。有 Lucene 使用背景的用户使用被存储字段来选择他们想要在搜索结果里面返回的字段。事实上, _source 字段就是一个被存储的字段。

在Elasticsearch中,对文档的个别字段设置存储的作法一般不是最优的。整个文档已经被存储为 _source 字段。使用 _source 参数提取你须要的字段老是更好的。

元数据: _all 字段

在 轻量 搜索 中,咱们介绍了 _all 字段:一个把其它字段值 看成一个大字符串来索引的特殊字段。 query_string 查询子句(搜索 ?q=john )在没有指定字段时默认使用 _all 字段。

_all 字段在新应用的探索阶段,当你还不清楚文档的最终结构时是比较有用的。你能够使用这个字段来作任何查询,而且有很大可能找到须要的文档:

GET /_search
{
    "match": {
        "_all": "john smith marketing"
    }
}

随着应用的发展,搜索需求变得更加明确,你会发现本身愈来愈少使用 _all 字段。 _all 字段是搜索的应急之策。经过查询指定字段,你的查询更加灵活、强大,你也能够对相关性最高的搜索结果进行更细粒度的控制。

relevance algorithm 考虑的一个最重要的原则是字段的长度:字段越短越重要。 在较短的 title 字段中出现的短语可能比在较长的 content 字段中出现的短语更加剧要。字段长度的区别在 _all 字段中不会出现。

若是你再也不须要 _all 字段,你能够经过下面的映射来禁用:

PUT /my_index/_mapping/my_type
{
    "my_type": {
        "_all": { "enabled": false }
    }
}

经过 include_in_all 设置来逐个控制字段是否要包含在 _all 字段中,默认值是 true。在一个对象(或根对象)上设置 include_in_all 能够修改这个对象中的全部字段的默认行为。

你可能想要保留 _all 字段做为一个只包含某些特定字段的全文字段,例如只包含 title,overview,summary 和 tags。 相对于彻底禁用 _all 字段,你能够为全部字段默认禁用 include_in_all 选项,仅在你选择的字段上启用:

PUT /my_index/my_type/_mapping
{
    "my_type": {
        "include_in_all": false,
        "properties": {
            "title": {
                "type":           "string",
                "include_in_all": true
            },
            ...
        }
    }
}

记住,_all 字段仅仅是一个 通过分词的 string 字段。它使用默认分词器来分析它的值,无论这个值本来所在字段指定的分词器。就像全部 string 字段,你能够配置 _all 字段使用的分词器:

PUT /my_index/my_type/_mapping
{
    "my_type": {
        "_all": { "analyzer": "whitespace" }
    }
}

元数据:文档标识

文档标识与四个元数据字段 相关:

  • _id 
    文档的 ID 字符串
  • _type 
    文档的类型名
  • _index 
    文档所在的索引
  • _uid 
    _type 和 _id 链接在一块儿构形成 type#id

默认状况下, _uid 字段是被存储(可取回)和索引(可搜索)的。 _type 字段被索引可是没有存储, _id 和 _index 字段则既没有被索引也没有被存储,这意味着它们并非真实存在的。

尽管如此,你仍然能够像真实字段同样查询 _id 字段。Elasticsearch 使用 _uid 字段来派生出 _id 。 虽然你能够修改这些字段的 index 和 store 设置,可是基本上不须要这么作。

  1. 动态映射

当 Elasticsearch 遇到文档中之前 未遇到的字段,它用 dynamic mapping 来肯定字段的数据类型并自动把新的字段添加到类型映射。

有时这是想要的行为有时又不但愿这样。一般没有人知道之后会有什么新字段加到文档,可是又但愿这些字段被自动的索引。也许你只想忽略它们。若是Elasticsearch是做为重要的数据存储,可能就会指望遇到新字段就会抛出异常,这样能及时发现问题。

幸运的是能够用 dynamic 配置来控制这种行为 ,可接受的选项以下:

  • true 
    动态添加新的字段–缺省
  • false 
    忽略新的字段
  • strict 
    若是遇到新字段抛出异常

配置参数 dynamic 能够用在根 object 或任何 object 类型的字段上。你能够将 dynamic 的默认值设置为 strict , 而只在指定的内部对象中开启它, 例如:

PUT /my_index
{
    "mappings": {
        "my_type": {
            "dynamic":      "strict", 
            "properties": {
                "title":  { "type": "string"},
                "stash":  {
                    "type":     "object",
                    "dynamic":  true 
                }
            }
        }
    }
}


    若是遇到新字段,对象 my_type 就会抛出异常。 
    而内部对象 stash 遇到新字段就会动态建立新字段。

使用上述动态映射, 你能够给 stash 对象添加新的可检索的字段:

PUT /my_index/my_type/1
{
    "title":   "This doc adds a new field",
    "stash": { "new_field": "Success!" }
}

可是对根节点对象 my_type 进行一样的操做会失败:

PUT /my_index/my_type/1
{
    "title":     "This throws a StrictDynamicMappingException",
    "new_field": "Fail!"
}

把 dynamic 设置为 false 一点儿也不会改变 _source 的字段内容。 _source 仍然包含被索引的整个JSON文档。只是新的字段不会被加到映射中也不可搜索。

  1. 自定义动态映射

若是你想在运行时增长新的字段,你可能会启用动态映射。 然而,有时候,动态映射 规则 可能不太智能。幸运的是,咱们能够经过设置去自定义这些规则,以便更好的适用于你的数据。

日期检测

当 Elasticsearch 遇到一个新的字符串字段时,它会检测这个字段是否包含一个可识别的日期,好比 2014-01-01 。 若是它像日期,这个字段就会被做为 date 类型添加。不然,它会被做为 string 类型添加。

有些时候这个行为可能致使一些问题。想象下,你有以下这样的一个文档:

{ "note": "2014-01-01" }

假设这是第一次识别 note 字段,它会被添加为 date 字段。可是若是下一个文档像这样:

{ "note": "Logged out" }

这显然不是一个日期,但为时已晚。这个字段已是一个日期类型,这个 不合法的日期 将会形成一个异常。

日期检测能够经过在根对象上设置 date_detection 为 false 来关闭:

PUT /my_index
{
    "mappings": {
        "my_type": {
            "date_detection": false
        }
    }
}

使用这个映射,字符串将始终做为 string 类型。若是你须要一个 date 字段,你必须手动添加。

Elasticsearch 判断字符串为日期的规则能够经过 dynamic_date_formats setting 来设置。

动态模板

使用 dynamic_templates ,你能够彻底控制 新检测生成字段的映射。你甚至能够经过字段名称或数据类型来应用不一样的映射。

每一个模板都有一个名称, 你能够用来描述这个模板的用途, 一个 mapping 来指定映射应该怎样使用,以及至少一个参数 (如 match) 来定义这个模板适用于哪一个字段。

模板按照顺序来检测;第一个匹配的模板会被启用。例如,咱们给 string 类型字段定义两个模板:

  • es :以 _es 结尾的字段名须要使用 spanish 分词器。
  • en :全部其余字段使用 english 分词器。

咱们将 es 模板放在第一位,由于它比匹配全部字符串字段的 en 模板更特殊:

PUT /my_index
{
    "mappings": {
        "my_type": {
            "dynamic_templates": [
                { "es": {
                      "match":              "*_es", 
                      "match_mapping_type": "string",
                      "mapping": {
                          "type":           "string",
                          "analyzer":       "spanish"
                      }
                }},
                { "en": {
                      "match":              "*", 
                      "match_mapping_type": "string",
                      "mapping": {
                          "type":           "string",
                          "analyzer":       "english"
                      }
                }}
            ]
}}}


    匹配字段名以 _es 结尾的字段。
    匹配其余全部字符串类型字段。

match_mapping_type 容许你应用模板到特定类型的字段上,就像有标准动态映射规则检测的同样, (例如 string 或 long)。

match 参数只匹配字段名称, path_match 参数匹配字段在对象上的完整路径,因此 address.*.name 将匹配这样的字段:

{
    "address": {
        "city": {
            "name": "New York"
        }
    }
}

unmatch 和 path_unmatch将被用于未被匹配的字段。

  1. 缺省映射

一般,一个索引中的全部类型共享相同的字段和设置。 default 映射更加方便地指定通用设置,而不是每次建立新类型时都要重复设置。 default 映射是新类型的模板。在设置 default 映射以后建立的全部类型都将应用这些缺省的设置,除非类型在本身的映射中明确覆盖这些设置。

例如,咱们能够使用 default 映射为全部的类型禁用 _all 字段, 而只在 blog 类型启用:

PUT /my_index
{
    "mappings": {
        "_default_": {
            "_all": { "enabled":  false }
        },
        "blog": {
            "_all": { "enabled":  true  }
        }
    }
}

default 映射也是一个指定索引 dynamic templates 的好方法。

  1. 从新索引你的数据

尽管能够增长新的类型到索引中,或者增长新的字段到类型中,可是不能添加新的分析器或者对现有的字段作改动。 若是你那么作的话,结果就是那些已经被索引的数据就不正确, 搜索也不能正常工做。

对现有数据的这类改变最简单的办法就是从新索引:用新的设置建立新的索引并把文档从旧的索引复制到新的索引。

字段 _source 的一个优势是在Elasticsearch中已经有整个文档。你没必要从源数据中重建索引,并且那样一般比较慢。

为了有效的从新索引全部在旧的索引中的文档,用 scroll 从旧的索引检索批量文档 , 而后用 bulk API 把文档推送到新的索引中。

从Elasticsearch v2.3.0开始, Reindex API 被引入。它可以对文档重建索引而不须要任何插件或外部工具。

批量从新索引

同时并行运行多个重建索引任务,可是你显然不但愿结果有重叠。正确的作法是按日期或者时间 这样的字段做为过滤条件把大的重建索引分红小的任务:

GET /old_index/_search?scroll=1m
{
    "query": {
        "range": {
            "date": {
                "gte":  "2014-01-01",
                "lt":   "2014-02-01"
            }
        }
    },
    "sort": ["_doc"],
    "size":  1000
}

若是旧的索引持续会有变化,你但愿新的索引中也包括那些新加的文档。那就能够对新加的文档作从新索引, 但仍是要用日期类字段过滤来匹配那些新加的文档。

  1. 索引别名和零停机

在前面提到的,重建索引的问题是必须更新应用中的索引名称。 索引别名就是用来解决这个问题的!

索引 别名 就像一个快捷方式或软链接,能够指向一个或多个索引,也能够给任何一个须要索引名的API来使用。别名 带给咱们极大的灵活性,容许咱们作下面这些:

  • 在运行的集群中能够无缝的从一个索引切换到另外一个索引
  • 给多个索引分组 (例如, last_three_months)
  • 给索引的一个子集建立 视图

在后面咱们会讨论更多关于别名的使用。如今,咱们将解释怎样使用别名在零停机下从旧索引切换到新索引。

有两种方式管理别名: _alias 用于单个操做, _aliases 用于执行多个原子级操做。

在本章中,咱们假设你的应用有一个叫 my_index 的索引。事实上, my_index 是一个指向当前真实索引的别名。真实索引包含一个版本号: my_index_v1 , my_index_v2 等等。

首先,建立索引 my_index_v1 ,而后将别名 my_index 指向它:

PUT /my_index_v1 
PUT /my_index_v1/_alias/my_index 


    建立索引 my_index_v1 。            
    设置别名 my_index 指向 my_index_v1 。

你能够检测这个别名指向哪个索引:

GET /*/_alias/my_index

或哪些别名指向这个索引:

GET /my_index_v1/_alias/*

二者都会返回下面的结果:

{
    "my_index_v1" : {
        "aliases" : {
            "my_index" : { }
        }
    }
}

而后,咱们决定修改索引中一个字段的映射。固然,咱们不能修改现存的映射,因此咱们必须从新索引数据。 首先, 咱们用新映射建立索引 my_index_v2 :

PUT /my_index_v2
{
    "mappings": {
        "my_type": {
            "properties": {
                "tags": {
                    "type":   "string",
                    "index":  "not_analyzed"
                }
            }
        }
    }
}

而后咱们将数据从 my_index_v1 索引到 my_index_v2 ,下面的过程在 从新索引你的数据 中已经描述过。一旦咱们肯定文档已经被正确地重索引了,咱们就将别名指向新的索引。

一个别名能够指向多个索引,因此咱们在添加别名到新索引的同时必须从旧的索引中删除它。这个操做须要原子化,这意味着咱们须要使用 _aliases 操做:

POST /_aliases
{
    "actions": [
        { "remove": { "index": "my_index_v1", "alias": "my_index" }},
        { "add":    { "index": "my_index_v2", "alias": "my_index" }}
    ]
}

你的应用已经在零停机的状况下从旧索引迁移到新索引了。

即便你认为如今的索引设计已经很完美了,在生产环境中,仍是有可能须要作一些修改的。

作好准备:在你的应用中使用别名而不是索引名。而后你就能够在任什么时候候重建索引。别名的开销很小,应该普遍使用。

11、分片内部原理

在 集群内的原理, 咱们介绍了 分片, 并将它 描述成最小的 工做单元。可是究竟什么 是 一个分片,它是如何工做的? 在这个章节,咱们回答如下问题:

  • 为何搜索是 近 实时的?
  • 为何文档的 CRUD (建立-读取-更新-删除) 操做是 实时 的?
  • Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?
  • 为何删除文档不会马上释放空间?
  • refresh, flush, 和 optimize API 都作了什么, 你什么状况下应该是用他们?

最简单的理解一个分片如何工做的方式是上一堂历史课。 咱们将要审视提供一个带近实时搜索和分析的 分布式持久化数据存储须要解决的问题。

内容警告

本章展现的这些信息仅供您兴趣阅读。为了使用 Elasticsearch 您并不须要理解和记忆全部的细节。 读这个章节是为了了解工做机制,而且为了未来您须要这些信息时,知道这些信息在哪里。可是不要被这些细节所累。

  1. 使文本可被搜索

必须解决的第一个挑战是如何 使文本可被搜索。 传统的数据库每一个字段存储单个值,但这对全文检索并不够。文本字段中的每一个单词须要被搜索,对数据库意味着须要单个字段有索引多值(这里指单词)的能力。

最好的支持 一个字段多个值 需求的数据结构是咱们在 倒排索引 章节中介绍过的 倒排索引 。 倒排索引包含一个有序列表,列表包含全部文档出现过的不重复个体,或称为 词项 ,对于每个词项,包含了它全部曾出现过文档的列表。

Term  | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown |   X   |       |  X    | ...
fox   |   X   |   X   |  X    | ...
quick |   X   |   X   |       | ...
the   |   X   |       |  X    | ...

当讨论倒排索引时,咱们会谈到 文档 标引,由于历史缘由,倒排索引被用来对整个非结构化文本文档进行标引。 Elasticsearch 中的 文档 是有字段和值的结构化 JSON 文档。事实上,在 JSON 文档中, 每一个被索引的字段都有本身的倒排索引。

这个倒排索引相比特定词项出现过的文档列表,会包含更多其它信息。它会保存每个词项出现过的文档总数, 在对应的文档中一个具体词项出现的总次数,词项在文档中的顺序,每一个文档的长度,全部文档的平均长度,等等。这些统计信息容许 Elasticsearch 决定哪些词比其它词更重要,哪些文档比其它文档更重要,这些内容在 什么是相关性? 中有描述。

为了可以实现预期功能,倒排索引须要知道集合中的 全部 文档,这是须要认识到的关键问题。

早期的全文检索会为整个文档集合创建一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化即可以被检索到。

不变性

倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。 不变性有重要的价值:

  • 不须要锁。若是你历来不更新索引,你就不须要担忧多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,因为其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提高。
  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不须要在每次数据改变时被重建,由于数据不会变化。
  • 写入单个大的倒排索引容许数据被压缩,减小磁盘 I/O 和 须要被缓存到内存的索引的使用量。

固然,一个不变的索引也有很差的地方。主要事实是它是不可变的! 你不能修改它。若是你须要让一个新的文档 可被搜索,你须要重建整个索引。这要么对一个索引所能包含的数据量形成了很大的限制,要么对索引可被更新的频率形成了很大的限制。

  1. 动态更新索引

下一个须要被解决的问题是怎样在保留不变性的前提下实现倒排索引的更新? 答案是: 用更多的索引。

经过增长新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每个倒排索引都会被轮流查询到–从最先的开始–查询完后再对结果进行合并。

Elasticsearch 基于 Lucene, 这个 java 库引入了 按段搜索 的概念。 每一 段 自己都是一个倒排索引, 但 索引 在 Lucene 中除表示全部 段 的集合外, 还增长了 提交点 的概念 — 一个列出了全部已知段的文件,就像在 图 16 “一个 Lucene 索引包含一个提交点和三个段” 中描绘的那样。 如 图 17 “一个在内存缓存中包含新文档的 Lucene 索引” 所示,新的文档首先被添加到内存索引缓存中,而后写入到一个基于磁盘的段,如 图 18 “在一次提交后,一个新的段被添加到提交点并且缓存被清空。” 所示。

图 16. 一个 Lucene 索引包含一个提交点和三个段

索引与分片的比较

被混淆的概念是,一个 Lucene 索引 咱们在 Elasticsearch 称做 分片 。 一个 Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每个属于索引的分片(Lucene 索引),而后像 执行分布式检索 提到的那样,合并每一个分片的结果到一个全局的结果集。

逐段搜索会以以下流程进行工做:

  1. 新文档被收集到内存索引缓存, 见 图 17 “一个在内存缓存中包含新文档的 Lucene 索引” 。
  2. 不时地, 缓存被 提交 :

    • 一个新的段–一个追加的倒排索引–被写入磁盘。
    • 一个新的包含新段名字的 提交点 被写入磁盘。
    • 磁盘进行 同步 — 全部在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
  3. 新的段被开启,让它包含的文档可见以被搜索。

  4. 内存缓存被清空,等待接收新的文档。

图 17. 一个在内存缓存中包含新文档的 Lucene 索引

图 18. 在一次提交后,一个新的段被添加到提交点并且缓存被清空。

当一个查询被触发,全部已知的段按顺序被查询。词项统计会对全部段的结果进行聚合,以保证每一个词和每一个文档的关联都被准确计算。 这种方式能够用相对较低的成本将新文档添加到索引。

删除和更新

段是不可改变的,因此既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每一个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然能够被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是相似的操做方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

在 段合并 , 咱们展现了一个被删除的文档是怎样被文件系统移除的。

  1. 近实时搜索

随着按段(per-segment)搜索的发展, 一个新的文档从索引到可被搜索的延迟显著下降了。新文档在几分钟以内便可被检索,但这样仍是不够快。

磁盘在这里成为了瓶颈。 提交(Commiting)一个新的段到磁盘须要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 可是 fsync 操做代价很大; 若是每次索引一个文档都去执行一次的话会形成很大的性能问题。

咱们须要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程当中被移除。

在Elasticsearch和磁盘之间是文件系统缓存。 像以前描述的同样, 在内存索引缓冲区( 图 19 “在内存缓冲区中包含了新文档的 Lucene 索引” )中的文档会被写入到一个新的段中( 图 20 “缓冲区的内容已经被写入一个可被搜索的段中,但尚未进行提交” )。 可是这里新段会被先写入到文件系统缓存–这一步代价会比较低,稍后再被刷新到磁盘–这一步代价比较高。不过只要文件已经在缓存中, 就能够像其它文件同样被打开和读取了。

图 19. 在内存缓冲区中包含了新文档的 Lucene 索引

Lucene 容许新段被写入和打开–使其包含的文档在未进行一次完整提交时便对搜索可见。 这种方式比进行一次提交代价要小得多,而且在不影响性能的前提下能够被频繁地执行。

图 20. 缓冲区的内容已经被写入一个可被搜索的段中,但尚未进行提交

refresh API

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫作 refresh 。 默认状况下每一个分片会每秒自动刷新一次。这就是为何咱们说 Elasticsearch 是 近 实时搜索: 文档的变化并非当即对搜索可见,但会在一秒以内变为可见。

这些行为可能会对新用户形成困惑: 他们索引了一个文档而后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新:

POST /_refresh 
POST /blogs/_refresh 


    刷新(Refresh)全部的索引。     
    只刷新(Refresh) blogs 索引。

尽管刷新是比提交轻量不少的操做,它仍是会有性能开销。 当写测试的时候, 手动刷新颇有用,可是不要在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用须要意识到 Elasticsearch 的近实时的性质,并接受它的不足。

并非全部的状况都须要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 能够经过设置 refresh_interval , 下降每一个索引的刷新频率:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "30s" 
  }
}


    每30秒刷新 my_logs 索引。

refresh_interval 能够在既存索引上进行动态更新。 在生产环境中,当你正在创建一个大的新索引时,能够先关闭自动刷新,待开始使用该索引时,再把它们调回来:

PUT /my_logs/_settings
{ "refresh_interval": -1 } 

PUT /my_logs/_settings
{ "refresh_interval": "1s" } 


    关闭自动刷新。
    每秒自动刷新。

refresh_interval 须要一个 持续时间 值, 例如 1s (1 秒) 或 2m (2 分钟)。 一个绝对值 1 表示的是 1毫秒 –无疑会使你的集群陷入瘫痪。

  1. 持久化变动

若是没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,咱们不能保证数据在断电甚至是程序正常退出以后依然存在。为了保证 Elasticsearch 的可靠性,须要确保数据变化被持久化到磁盘。

在 动态更新索引,咱们说一次完整的提交会将段刷到磁盘,并写入一个包含全部段列表的提交点。Elasticsearch 在启动或从新打开一个索引的过程当中使用这个提交点来判断哪些段隶属于当前分片。

即便经过每秒刷新(refresh)实现了近实时搜索,咱们仍然须要常常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?咱们也不但愿丢失掉这些数据。

Elasticsearch 增长了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操做时均进行了日志记录。经过 translog ,整个流程看起来是下面这样:

  1. 一个文档被索引以后,就会被添加到内存缓冲区,而且 追加到了 translog ,正如 图 21 “新的文档被添加到内存缓冲区而且被追加到了事务日志” 描述的同样。

    图 21. 新的文档被添加到内存缓冲区而且被追加到了事务日志

  2. 刷新(refresh)使分片处于 图 22 “刷新(refresh)完成后, 缓存被清空可是事务日志不会” 描述的状态,分片每秒被刷新(refresh)一次:

    • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操做。
    • 这个段被打开,使其可被搜索。
    • 内存缓冲区被清空。

    图 22. 刷新(refresh)完成后, 缓存被清空可是事务日志不会

  3. 这个进程继续工做,更多的文档被添加到内存缓冲区和追加到事务日志(见 图 23 “事务日志不断积累文档” )。

    图 23. 事务日志不断积累文档

  4. 每隔一段时间–例如 translog 变得愈来愈大–索引被刷新(flush);一个新的 translog 被建立,而且一个全量提交被执行(见 图 24 “在刷新(flush)以后,段被全量提交,而且事务日志被清空” ):

    • 全部在内存缓冲区的文档都被写入一个新的段。
    • 缓冲区被清空。
    • 一个提交点被写入硬盘。
    • 文件系统缓存经过 fsync 被刷新(flush)。
    • 老的 translog 被删除。

translog 提供全部尚未被刷到磁盘的操做的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,而且会重放 translog 中全部在最后一次提交后发生的变动操做。

translog 也被用来提供实时 CRUD 。当你试着经过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索以前, 首先检查 translog 任何最近的变动。这意味着它老是可以实时地获取到文档的最新版本。

图 24. 在刷新(flush)以后,段被全量提交,而且事务日志被清空

flush API

这个执行一个提交而且截断 translog 的行为在 Elasticsearch 被称做一次 flush 。 分片每30分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。请查看 translog 文档 来设置,它能够用来 控制这些阈值:

flush API 能够 被用来执行一个手工的刷新(flush):

POST /blogs/_flush 

POST /_flush?wait_for_ongoing 


    刷新(flush) blogs 索引。            
    刷新(flush)全部的索引而且而且等待全部刷新在返回前完成。

你不多须要本身手动执行一个的 flush 操做;一般状况下,自动刷新就足够了。

这就是说,在重启节点或关闭索引以前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或从新打开一个索引, 它须要重放 translog 中全部的操做,因此若是日志越短,恢复越快。

Translog 有多安全?

translog 的目的是保证操做不会丢失。这引出了这个问题: Translog 有多安全 ?

在文件被 fsync 到磁盘前,被写入的文件在重启以后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成以后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的translog以前,你的客户端不会获得一个 200 OK 响应。

在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践代表这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。

可是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 仍是比较有益的。好比,写入的数据被缓存到内存中,再每5秒执行一次 fsync 。

这个行为能够经过设置 durability 参数为 async 来启用:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

这个选项能够针对索引单独设置,而且能够动态进行修改。若是你决定使用异步 translog 的话,你须要 保证 在发生crash时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。

若是你不肯定这个行为的后果,最好是使用默认的参数( “index.translog.durability”: “request” )来避免数据丢失。

  1. 段合并

因为自动刷新流程每秒会建立一个新的段 ,这样会致使短期内的段数量暴增。而段数目太多会带来较大的麻烦。 每个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每一个搜索请求都必须轮流检查每一个段;因此段越多,搜索也就越慢。

Elasticsearch经过在后台进行段合并来解决这个问题。小的段被合并到大的段,而后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档 从文件系统中清除。 被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

启动段合并不须要你作任何事。进行索引和搜索时会自动进行。这个流程像在 图 25 “两个提交了的段和一个未提交的段正在被合并到一个更大的段” 中提到的同样工做:

一、 当索引的时候,刷新(refresh)操做会建立新的段并将段打开以供搜索使用。

二、 合并进程选择一小部分大小类似的段,而且在后台将它们合并到更大的段中。这并不会中断索引和搜索。

图 25. 两个提交了的段和一个未提交的段正在被合并到一个更大的段

三、 图 26 “一旦合并结束,老的段被删除” 说明合并完成时的活动:

  • 新的段被刷新(flush)到了磁盘。 ** 写入一个包含新段且排除旧的和较小的段的新提交点。
  • 新的段被打开用来搜索。
  • 老的段被删除。

图 26. 一旦合并结束,老的段被删除

合并大的段须要消耗大量的I/O和CPU资源,若是任其发展会影响搜索性能。Elasticsearch在默认状况下会对合并流程进行资源限制,因此搜索仍然 有足够的资源很好地执行。

查看 段和合并 来为你的实例获取关于合并调整的建议。

optimize API

optimize API大可看作是 强制合并 API 。它会将一个分片强制合并到 max_num_segments 参数指定大小的段数目。 这样作的意图是减小段的数量(一般减小到一个),来提高搜索性能。

optimize API 不该该 被用在一个动态索引————一个正在被活跃更新的索引。后台合并流程已经能够很好地完成工做。 optimizing 会阻碍这个进程。不要干扰它!

在特定状况下,使用 optimize API 很有益处。例如在日志这种用例下,天天、每周、每个月的日志被存储在一个索引中。 老的索引实质上是只读的;它们也并不太可能会发生变化。

在这种状况下,使用optimize优化老的索引,将每个分片合并为一个单独的段就颇有用了;这样既能够节省资源,也能够使搜索更加快速:

POST /logstash-2014-10/_optimize?max_num_segments=1 


    合并索引中的每一个分片为一个单独的段

请注意,使用 optimize API 触发段合并的操做一点也不会受到任何资源上的限制。这可能会消耗掉你节点上所有的I/O资源, 使其没有余裕来处理搜索请求,从而有可能使集群失去响应。 若是你想要对索引执行 optimize,你须要先使用分片分配(查看 迁移旧索引)把索引移到一个安全的节点,再执行。

相关文章
相关标签/搜索