在上一篇中已经对索引的相关知识做了一定的讲解,本章讲重点讲解分词器与文档操作的API的相关使用。当然了,分享还是基于es 6.2.x版本展开。
目录
1.倒序索引
为了促进这类在全文域中的查询,ES首先分析全文文档,之后根据结果创建倒排索引 (它是一种数据结构,通过拆分成单独的词条或tokens,创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档)。为了解决大小写、复数写法(foxes—>fox)、同义词(jumped==leap)无法匹配问题,对搜索的字符串使用与文本域相同的标准化规则拆分(Quick +fox->quick +fox)。这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。如果对它还不是很了解可以参阅之前Lucene系列中的反向索引。
2.分词器
2.1 分析器的结构组成?
- character filter,字符过滤器,用来去掉HTML或者将&转化成`and`。一个analyzer包含0个或多个字符过滤器,多个按配置顺序依次进行处理
- tokenizer,分词器,字符串被分词器可以按空格和标点分为一个个词条。一个analyzer必需且只可包含一个tokenizer
- token filter,词项过滤器,它可以改变词条,例如:小写化Quick)、删除词条(例如:像 a`,`and`,`the 等无用词)、增加词条(例如:jump和leap)。包含0个或多个字符过滤器,多个按配置顺序依次进行处理
2.2 内置分词器有哪些?
- 标准分析器:根据Unicode联盟定义的单词边界划分文本
- 简单分析器:在任何不是字母的地方分隔文本
- 空格分析器:在空格的地方划分文本
- 语言分析器:根据指定语言特点划分
- 更多
2.3 什么时候使用分析器?
针对文本类型有搜索的需求时,首先需要使用分析器建立建倒序索引,然后你查询一个全文域时(_all字段)或一个精确值域时(指定字段)时,对查询字符串使用相同的分析器来完成检索数据。
2.4 自定义分析器
一个分析器就是在一个包里面组合了三种函数的一个包装器, 三种函数按照顺序被执行(字符过滤器>分词器>词单元过滤器)
PUT /my_index
{ "settings": {
"analysis": {
"char_filter": { //字符过滤器
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "] //把&替换为 " and "
}
},
"tokenizer": { ... custom tokenizers ... },
"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" ]
}
}
}
}
}
2.5 测试分析器
GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze"
}
PUT /my_index/_mapping/my_type //更新映射
{ "properties": {
"title": {
"type": "string",
"analyzer": "my_analyzer"
}
}
}
GET /my_index/_analyze //测试映射
{
"field": "title",
"text": "Black-cats"
}
2.6 控制分词器
分析器可以由每个字段决定。每个字段都可以有不同的分析器,既可以通过配置为字段指定分析器,也可以使用更高层的类型(type)、索引(index)或节点(node)的默认配置。
如果字段层级没有指定分析器,ES会按照以下顺序依次查找,按字段、按索引或全局缺省(global default),直到它找到能够使用的分析器。
索引时的顺序
- 字段映射里定义的 analyzer ,否则
- 索引中default设置的分析器,默认为 standard 标准分析器
搜索时的顺序
- 查询语句上自己定义的 analyzer ,否则
- 字段映射里定义的 analyzer ,否则
- 索引中default设置的分析器,默认为standard 标准分析器
有时候有的字段也可以定义search_analyzer(应用在索引时和搜索时使用不同的分析器),它搜索时流程会不同,
- 查询自己定义的 analyzer ,否则
- 字段映射里定义的search_analyzer ,否则
- 字段映射里定义的analyzer ,否则
- 索引设置中名为 default_search 的分析器,默认为
- 索引设置中名为 default 的分析器,默认为 standard 标准分析器
小结,如果对它还不是很了解可以参阅之前Lucene系列中的分析器
3. 文档存储原理
假如我们有两个主分片,每个主分片有两个副本分片
3.1 文档路由到分片流程
当我们创建文档时,数据如何被存放到主分片Node1或Node3?
这不会是随机的,通过公式shard= hash(routing)%number_of_primary_shards,routing是一个可变值,默认是文档的_id ,也可以设置成一个自定义的值。routing通过hash函数生成一个数字。这样会有什么问题呢?
对,主分片数量是固定的会使索引难以进行扩容,它不会自动进行分片分裂。怎么破?
可以通过事先规划(预分配),如果不行只能通过重新索引你的数据(可以理解为迁移到新索引中,那它肯定比较慢,所以不完全是这样的,为此引入Reindex API)。
3.2 主分片和副本分片交互流程
每个节点都有能力处理任意请求,每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上,不处理实际请求的节点我们称之为协调节点。当发送请求的时候, 为了扩展负载,更好的做法是轮询集群中所有的节点。
3.3 新建、索引和删除文档的存储流程
- 客户端向Node1发送新建、索引或者删除请求
- Node1使用文档的_id 确定文档属于分片P0(主分片),请求会被转发到 Node3
- Node3在主分片上面执行请求。如果成功了,它将请求并行转发到Node1和Node2的副本分片上。一旦所有的副本分片都报告成功, Node3将向协调节点(Node1)报告成功,协调节点向客户端报告成功。
当然这样的强一致性是以牺牲性能为代价的,es提供了配置参数
consistency:
- one,只要主分片状态ok就允许执行_写_操作
- all,必须要主分片和所有副本分片的状态没问题才允许执行_写_操作
- quorum,默认值,大多数的分片副本状态没问题就允许执行写操作,计算公式:int((primary + number_of_replicas) / 2) + 1
3.4 取回一个文档的流程
- 客户端向Node1发送获取请求
- Node1使用文档的_id确定文档属于P0,P0的副本分片存在于2个节点(Node1和Node2)。在这种情况下,它有可能将请求转发到Node2
- Node2将文档返回给Node1 ,然后将文档返回给客户端
在处理读取请求时,协调节点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索过程中,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片?在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。它解释说:一旦索引请求(新增)成功返回给用户,文档在主分片和副本分片都是可用的。
3.5 局部更新文档的流程
- 客户端向Node1发送更新请求
- 它将请求转发到主分片所在的Node3(P0)
- Node3从P0检索文档,修改_source 字段中的 JSON ,并且尝试重新索引当前分片的文档。如果文档已经被另一个进程修改,它会重试步骤3 ,超过retry_on_conflict 次后放弃
- 如果 Node3 成功地更新文档,它将新版本的文档并行转发到Node1和 Node2上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node3向协调节点(Node1)返回成功,协调节点向客户端返回成功。
update API也可以还接受"新建、索引和删除文档的存储流程"介绍的consistency 和timeout参数。
3.6 多文档模式的流程
mget和 bulk API的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成每个分片的多文档请求,并且将这些请求并行转发到每个参与节点。协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端。
4. 文档管理
4.1 新建
create行为,更多,如果文档不存在则创建它,相当于put-if-absent语义
PUT /website/blog/123?op_type=create
{ ... }
PUT /website/blog/123/_create
{ ... }官网上op_type将默认自动设置为create,经验证这说法是错的,应该是index
PUT /{index}/{type}/{id}
{
"field": "value"
}
注意:官方定义docment的op_type行为有create、index(创建新文档或者替换)、update(部分字段更新)、delete(没啥用)
自增ID
索引操作可以在不指定ID的情况下执行。在这种情况下,将自动生成一个ID(20位长的字符串)。
routing
默认情况下,通过使用文档ID值的散列来控制碎片放置(或路由)。为了实现更明确的控制,可以使用路由参数在每次操作的基础上直接指定输入到路由器使用的哈希函数的值。
POST twitter/_doc?routing=kimchy
{
"user" : "kimchy",
"message" : "trying out Elasticsearch"
}
等待活动分区
为了提高系统写入的弹性,可以将索引操作配置为在继续操作之前等待一定数量的活动碎片副本。如果所需数量的活动碎片副本不可用,则写入操作必须等待并重试,直到所需碎片副本已启动或发生超时。默认情况下,写入操作仅在继续之前等待主碎片处于活动状态(wait_for_active_shards=1),在文档存储原理中讲过。
Timeout
执行索引操作时,分配给执行索引操作的主碎片可能不可用。一些原因可能是主碎片当前正在从网关恢复或正在重新定位。默认情况下,索引操作将在主碎片上等待1分钟,然后失败并以错误响应。 如果你需要,你可以使用 timeout 参数 使它更早终止: 100(100毫秒),30s (是30秒)。
PUT twitter/_doc/1?timeout=5m
{
"user" : "kimchy",
"message" : "trying out Elasticsearch"
}
4.2 获取
GET /website/blog/123?pretty //获取文档
HEAD /website/blog/123 //文档是否存在(HTTP,200:存在,404:不存在)
realtime
默认情况下,get API是实时的,不受索引刷新率的影响(数据在搜索时可见)。如果文档已更新但尚未刷新,则get api将在适当位置发出刷新调用,以使文档可见。这还将使上次刷新后更改的其他文档可见。为了禁用realtime GET,可以将realtime参数设置为false。
Source filtering
默认情况下,“获取”操作返回_source的内容,除非您使用了stored_fields参数或禁用了_source。如果您只需要完整源中的一个或两个字段,则可以使用源包含和源排除参数来包含或筛选您需要的部分。这对于大型文档尤其有用,因为部分检索可以节省网络开销。两个参数都采用逗号分隔的字段列表或通配符表达式。例子:
GET twitter/_doc/0?_source=false //可以关闭_source检索
GET twitter/_doc/0?_source_include=*.id&_source_exclude=entities
GET twitter/_doc/0?_source=*.id,retweeted
GET twitter/_doc/2?routing=user1 //控制路由
GET twitter/_doc/1/_source //仅获取文档的_source字段,而不包含任何其他内容
HEAD twitter/_doc/1/_source //_source是否存在
Stored fields
get操作允许指定一组存储字段,这些字段将通过传递stored_fields参数返回。如果未存储请求的字段,则将忽略它们。
GET twitter/_doc/1?stored_fields=tags,counter //counter在mapping时"store": false,不会有返回值
preference
参数偏爱控制要在哪个shard副本上执行get请求的首选项。默认情况下,操作在分片的副本之间随机进行。
- _primary,该操作将只在主分片上执行
- _local,如果可能的话,该操作最好在本地分配的分片上执行
- 自定义值
4.3 删除
DELETE /website/blog/123 //delete行为
DELETE /twitter/_doc/1?routing=kimchy //routing
DELETE /twitter/_doc/1?timeout=5m //timeout
Versioning
索引的每个文档都有版本控制。删除文档时,可以指定版本,以确保我们尝试删除的相关文档实际上已被删除,并且在此期间没有更改。对文档执行的每个写入操作(包括删除)都会导致其版本增加。删除后,已删除文档的版本号在短时间内保持可用,以允许控制并发操作。已删除文档的版本保持可用的时间长度由index.gc_deletes删除索引设置确定,默认为60秒。
4.4 更新
es中有一个观念要牢记:文档具有不可变性,它执行文档更新流程:从旧文档构建JSON 》更改该JSON》删除旧文档》索引一个新文档
PUT test/_doc/1 //index行为,整个覆盖
{
"counter" : 1,
"tags" : ["red"]
}
POST /website/blog/123/_update //部分更新(新增字段)
{
"doc" : {
"title": "title",
"tags" : [ "testing" ],
"views": 0
}
}
POST /website/blog/1/_update //脚本,部分更新(递增效果)
{
"script" : "ctx._source.views+=1"
}
POST test/_doc/1/_update
{
"script" : {
"source": "ctx._source.counter += params.count",
"lang": "painless",
"params" : {
"count" : 4
}
},
"upsert" : { //如果文档尚不存在,则upsert元素的内容将作为新文档插入。如果文档确实存在,则将执行脚本
"counter" : 1
}
}
POST test/_doc/1/_update
{
"doc" : {
"name" : "new_name"
},
"doc_as_upsert" : true //不执行部分更新,整个覆盖
}
注:旧文档它并不会立即消失,ES后台会后续清理
4.5 mget
取回多个文档,多个请求合并成一个,避免单独处理每个请求花费的网络延时和开销。
注意:每个文档都是单独检索和报告的(第二个文档未能找到并不妨碍第一个文档被检索到),检查found标记。
GET /website/blog/_mget //相同的_index 和_type
{
"docs" : [
{ "_id" : 2 },
{ "_type" : "pageviews", "_id" : 1 }
]
}
GET /_mget //不是同一个_type
{
"docs" : [
{
"_index" : "website",
"_type" : "blog",
"_id" : 2
},
{
"_index" : "website",
"_type" : "pageviews",
"_id" : 1,
"_source": "views"
}
]
}
4.6 bulk
代价较小的批量操作,bulk API允许在单个步骤中进行多次create、index、update或delete请求,
语法:
{ action: { metadata }}\n //换行符(\n)连接到一起,包括最后一行,这意味着这个 JSON 不 能使用 pretty 参数打印
{ request body }\n //index、create、update行为操作必须携带
{ action: { metadata }}\n //metadata必须_index _type、_id(非必须)
{ request body }\n
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"} }
响应示例:每个子请求都是独立执行,但某个子请求的失败最顶层的error标志被设置为 true。
{
"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
}}
]
}
不需要重复指定index和type
POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": {}}
{ "event": "db logged in" }
批量多大是太多了?
整个批量请求都需要由接收到请求的节点加载到内存中,因此该请求越大,其他请求所能获得的内存就越少。 批量请求的大小有一个最佳值,大于这个值,性能将不再提升,甚至会下降。最佳点:通过批量索引典型文档,并不断增加批量大小进行尝试。 当性能开始下降,那么你的批量大小就太大了。一般建议是1000-5000个文档,如果你的文档很大,可以适当减少队列,大小建议是5-15MB,默认不能超过100M(es的配置文件elasticsearch.yml中 http.max_content_length: 100mb【不建议修改,太大的话bulk也会慢】)。
为什么bulk API需要有换行符格式,而不是发送包装在JSON数组中的请求,例如mget API?
ES可以直接读取被网络缓冲区接收的原始数据(它使用换行符字符来识别和解析小的 action/metadata行来决定哪个分片应该处理每个请求),避免jvm创建更多的数据结构(也就不会引起jvm频繁的垃圾回收)。
bulk里为什么不支持get呢?
批量操作,里面放get操作,没啥用!所以,官方也不支持
4.7 delete_by_query
按查询删除最简单用法是对匹配查询的每个文档执行删除操作。
POST twitter/_delete_by_query
{
"query": {
"match": {
"message": "some message"
}
}
}
POST twitter,blog/_docs,post/_delete_by_query
{
"query": {
"match_all": {}
}
}
POST twitter/_delete_by_query?routing=1
{
"query": {
"range" : {
"age" : {
"gte" : 10
}
}
}
}
POST twitter/_delete_by_query?scroll_size=5000 //默认情况下,删除查询使用的滚动批次为1000
{
"query": {
"term": {
"user": "kimchy"
}
}
}
4.8 update_by_query
按查询更新最简单用法是在不更改源的情况下对索引中的每个文档执行更新。这对于获取新属性或其他联机映射更改很有用。
POST twitter/_update_by_query?conflicts=proceed
POST twitter/_update_by_query?conflicts=proceed
{
"query": {
"term": {
"user": "kimchy"
}
}
}
POST twitter/_update_by_query?routing=1
POST twitter/_update_by_query?scroll_size=100
注:可以使用conflicts选项防止reindex在版本冲突时中止。
4.7 reindex
reindex不尝试设置目标索引。它不会复制源索引的设置。您应该在运行重新索引操作之前设置目标索引,包括设置映射、碎片计数、副本等。
POST _reindex
{
"source": {
"index": "twitter"
},
"dest": {
"index": "new_twitter"
}
}
POST _reindex
{
"source": {
"remote": {
"host": "http://otherhost:9200",
"username": "user",
"password": "pass"
},
"index": "source",
"query": {
"match": {
"test": "data"
}
}
},
"dest": {
"index": "dest"
}
}
4.8 referesh
The Index, Update, Delete, and Bulk APIs支持将refresh设置为控制此请求所做的更改何时对搜索可见。
写入和打开一个新段的轻量的过程叫做refresh,ES必须通过refresh的过程把内存中的数据转换成Lucene的完整segment后,才可以被搜索。这就是为什么我们说ES是近实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见,每次refresh会产生一个新的segment,这样会导致产生的segment较多,从而segment merge较为频繁,系统开销较大。尽管刷新是比提交轻量很多的操作,不要在生产环境下每次索引一个文档都去手动刷新,它还是会有性能开销。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。
这些是允许的值:
- Empty string or true ,操作发生后立即刷新相关的主碎片和副本碎片(不是整个索引),以便更新的文档立即显示在搜索结果中。
- wait_for,在答复之前,请等待刷新使请求所做的更改可见。这不会强制立即刷新,而是等待刷新发生。ES自动刷新已更改每个index.refresh_interval(默认为一秒钟)的分片。这个设置是动态的。在任何支持刷新API上调用 Refresh API或将刷新设置为true也将导致刷新,从而导致已经在运行的具有refresh=wait_for的请求返回。
- false (the default),不执行与刷新相关的操作。此请求所做的更改将在请求返回后的某个时间点可见。
4.9 乐观锁
_version处理并发修改的问题,使用模式只有2种
PUT /website/blog/1?version=1 //只有_version=1时成功
{ ... }
使用外部系统提供的版本号(检查当前_version 是否小于指定的版本号)
PUT /website/blog/2?version=5&version_type=external
{ ... }
4.10 Term Vectors
返回有关某个特定文档字段中词的信息和统计信息。文档可以存储在索引中,也可以由用户人工提供。词向量在默认情况下是实时的,而不是接近实时的。这可以通过将realtime参数设置为false来更改。term_vector默认值为no(不存储词向量),需要修改mapping的映射参数。
GET /twitter/_doc/1/_termvectors
GET /twitter/_doc/1/_termvectors?fields=message
GET /twitter/_doc/1/_termvectors
{
"fields" : ["text"],
"offsets" : true,
"payloads" : true,
"positions" : true,
"term_statistics" : true,
"field_statistics" : true
}
Return values
可以请求三种类型的值:词信息(term information)、词统计( term statistics )和字段统计(field statistics)。默认情况下,返回所有字段的所有词信息和字段统计信息,但不返回词统计信息。
term information | 字段中的词频率(始终返回)、词位置(positions : true)、开始和结束偏移量(offsets : true)、词报文(payloads : true,以base64编码字节表示) |
term statistics | 将term_statistics设置为true将返回 (默认为 false) 总词频率(词在所有文档中出现的频率)、文档频率(包含当前词的文档数) |
field statistics | 将字段field_statistics设置为false(默认为true)将忽略 文档计数(包含此字段的文档数)、文档频率总和(此字段中所有词的文档频率总和)、总项频率之和(此字段中每个项的总项频率之和) |
4.11 Multi termvectors API
multi-termvectors API允许一次获取多个termvectors。从中检索术语向量的文档由索引、类型和ID指定,但也可以在请求本身中人为地提供这些文档。
{
"docs": [
{
"_index": "twitter",
"_type": "_doc",
"_id": "2",
"term_statistics": true
},
{
"_index": "twitter",
"_type": "_doc",
"_id": "1",
"fields": [
"message"
]
}
]
}
POST /twitter/_mtermvectors
{
"docs": [
{
"_type": "_doc",
"_id": "2",
"fields": [
"message"
],
"term_statistics": true
},
{
"_type": "_doc",
"_id": "1"
}
]
}
POST /twitter/_doc/_mtermvectors
{
"docs": [
{
"_id": "2",
"fields": [
"message"
],
"term_statistics": true
},
{
"_id": "1"
}
]
}
总结,文档API本身还是比较简单的,不过内容还是比较多的,需要花时间了解基本的用法。