ES - 基本概念
* ES集群会在生产环境被长期实践, 一些重要概念, 包括应用和优化调试方法值得记录分享
* 所以, 会有关于ES的一系列分享, 先从基础开始, 成体系了后再加目录
ES:
Written in Java, based on Lucene
Realtime analytics & Full Text Search Engine
Distributed, Easy to scale, High availability
Multi tenant architecture (多租户)
Document oriented(Json)
Schema Free
Restful API, Json over HTTP
Open Source
Easy to Configure
倒排:
Apache Lucene将所有信息写到一个称为倒排索引(inverted index)的结构中。不同于关系型数据库中表的处理方式,倒排索引建立索引中词和文档之间的映射。你可以把倒排索引看成这样一种数据结构,其中的数据是面向词而不是面向文档的。来看一个简单的例子。我们有一些文档,只有它们的标题字段需要被索引,它们看起来如下所示:
Elasticsearch Server 1.0 (document 1);
Mastering Elasticsearch (document 2);
Apache Solr 4 Cookbook (document 3)。
那么,简化版的索引可以看成是这样的:
每一个词指向包含它的文档编号。这样就可以执行一种非常高效且快速的搜索,比如基于词的查询。此外,每个词有一个计数,告诉Lucene该词出现的频率。
分析:
分析的工作, 由分词器完成, [字符映射器] --> 分词器(tokenizer) -----(标记流, token stream)----> 标记过滤器(token filter)
token filter通常做的事情, lowercase filter, synonyms filter(同义), multiole language stemming filter(词干)
过滤器是一个接一个处理的。所以我们通过使用多个过滤器,几乎可以达到无限的分析可能性. 这里应该记住的是, 索引应该和查询词匹配. 如果它们不匹配,Lucene 不会返回所需文档. 比如, 你在建立索引时使用了词干提取和小写, 那你应该保证查询中的词也必须是词干和小写, 否则你的查询不会返回任何结果.
评分:
文档和查询的匹配程度用公式计算的结果. Lucene默认使用TF/IDF(词频/逆向文档频率)评分, 这是一种计算文档在上下文中相关度的算法.
_id & _type & _all & _source & _index :
每个文档存储在一个索引中并有一个Elasticsearch自动生成的唯一标识符和文档类型. 文档需要有对应文档类型的唯一标识符, 这意味着在一个索引中, 两个不同类型的文档可以有相同的唯一标识符.
_id可以直接在mapping里指定用哪个字段值填充, 这个很有帮助, 有利于数据重写和更新. _type字段也是必须的, 默认情况下编入索引, 但不会被分析和存储
_source可以通过includes或者excludes来指定存储哪些字段.
_index, 用来确定文档源自那个索引, 在使用别名时很重要. 默认未启用.
Elasticsearch使用文档的唯一标识符来计算文档应该被放到哪个分片中.
从这部分的原理可以看出路由的重要性.
分片和副本:
副本的数量可以线上在集群中实时调整, 不过分片的数量一旦创建好, 更改分片的数量就只能另外创建一个新的索引重新索引数据.
索引的创建, 在复杂系统往往需要控制索引的创建规则, 不可随意创建, 通过auto_create_index的true, false和模式控制
action.auto_create_index: -ainemo*, +ai*, -* (ainemo不可以, ai开头的可以, 不可以随意创建)
模式的顺序很重要, ES按顺序检查, 一旦条件成立, 则后面的无效, 所以是程序语言中顺序"或" 的关系
几个比较重要的属性(field properties):
index: analyzed, no, not_analyzed. analyzed表示字段分析后编入索引, no表示无法搜索改字段. 字符串的话还有一个not_analyzed, 表示字符串不做上面提到的分析进入索引 (这个很重要, 在使用聚合分析的时候很有效果)
omit_norms: true, false。对于经过分析的字符串字段,默认值为false,而对于未经分析但已编入索引的字符串字段,默认值设置为true。当属性为true时,它会禁用Lucene对该字段的加权基准计算(norms calculation),这样就无法使用索引期间的加权,从而可以为只用于过滤器中的字段节省内存(在计算所述文件的得分时不会被考虑在内)
boost: 文档的加权值, 表示这个文档中的字段的重要性, 默认为1, 分值越高越重要. 匹配计算结果的时候很重要.
include_in_all: 是否包含在_all中, 不包含的话可以减少索引, 但不被全文检索
通常通过字段冗余实现不同的业务, 比如一个字段用于搜索,一个字段用于排序或一个经语言分析器分析,一个只基于空白字符来分析.
"name": {
"type": "string",
"fields": {
"raw": { "type" : "string", "index": "not_analyzed" }
}
}
上述定义将创建两个字段:我们将第一个字段称为name,第二个称为name.raw. 当然, 你不必在索引的过程中指定两个独立字段, 指定一个name字段就足够了. Elasticsearch会处理余下的工作,将该字段的数值复制到多字段定义的所有字段。
相似度模型(TODO):
BM25, 基于概率模型, 简短文本文档表现较好;
随机性偏差模型, 基于具有相同名称的概率模型, 处理自然语言文本时表现好;
信息基础模型, 类似与随机性偏差.
段合并 (Segment Merging): [TODO: update it according to mastering elasticsearch]
Segment mergingis the process during which the underlying Lucene library takes several segments and creates a new segment based on the information found in them. The resulting segment has all the documents stored in the original segments except the ones that were marked for deletion. After the merge operation, the source segments are deleted from the disk. Because segment merging is rather costly in terms of CPU and I/O usage, it is crucial to appropriately control when and how often this process is invoked.
路由 (route):
默认情况下, Elasticsearch会在所有索引的分片中均匀地分配文档. 为了获得文档, Elasticsearch必须查询所有分片并合并结果. 然而, 如果你可以把数据按照一定的依据来划分, 就可以使用一个强大的文档和查询分布控制机制: 路由. 简而言之, 它允许选择用于索引和搜索数据的分片.
先看两个图, 索引和搜索
左边是索引, 右边是查询. 通常来说, ES会查询所有的节点来得到标识符和匹配文档的得分. 紧接着通过一个内部请求, 发送到相关分片, 最后获取所需文档. 显然效率很低, 所以一个重要的优化就是路由, 路由可以控制文档和查询转发的目的分片。现在,你可能已经猜到了,可以在索引和查询时都指定路由值。实际上, 如果使用路由, 那么检索和查询阶段都必须使用路由. 使用路由值之后, 查询的请求就只会发送到单个分片上.
实现来说, 可以使用相同的路由参数 (路由值)来实现:
curl -XPUT "/_id?routing=12" [索引]; curl -XGET "/_search?routing=12" [查询]
通常的实现不会通过每个请求添加路由值的, 一般性做法是在类型定义时添加路由字段 (会比使用路由参数的方法慢, 因为需要额外的解析) [post是type]:
搜索:
查询过程, 默认分成两个阶段, Scatter (发散) 阶段 和 Gather (收集) 阶段
发散阶段会查询所有的shards, 得到document identifier 和文档得分. 会等待所有查询结束, 汇总数据, 排序. 然后再通过一个内部请求, 到相应的shard获得最终数据, 称为收集阶段. 这个是默认的流程, 如果我们不做任何的优化和配置. 如何改变:
1 通过搜索类型search_type: query_then_fetch, query_and_fetch, dfs_query_and_fetch, count, scan
2 通过搜索执行偏好preference: _primary, _primary_first, _local, _only_node:id, _shards
通过_search_shards可以看查询如何执行
基本查询:
这块的记录其实主要是让大家了解下ES的几种常用查询方式和搜索的基本原理, 简单看看就好, 需要详细了解的还是看官方文档: ES DOC
- Term query:
"term": {"title": "crime"}, term查询是未经分析的, 所以是完全匹配.
"terms" : {"tags": ["novel", "books"], "minimun_match": 1}, 匹配多个词条, 可控制匹配度
- Match query:
和term的区别是, match查询是需要分析器介入的, 比如默认的
"match": {"title": "nemo and office"}, 会匹配所有titile含有nemo, and或office词条的文档,
operator: or, and. 控制匹配词条的关系, 默认是or
fuzziness: 模糊查询的相似度
"multi_match": 多字段查询, 结果默认通过布尔查询(还有最大分查询), 结合评分高低间的平衡来计算结果
"multi_match": {"query": "nemo office", "fields": ["title", "otitle"]}
"match_phrase": 类似于布尔查询, 不过是使用分析后的文本构建的查询
slop: 1, 词条间的未知词条数量, 所以 "nemo office" 和 "nemo and office"是匹配的
- query_string: 这个很好, 支持全部的Lucene查询语法, 会使用一个查询解析器构建成实际的查询
"query_string" : { "query" : "title:nemo^10 +title:office -otitle:xylink +author:(+junjian +dory)",
"default_field" : "title"}
// title中包含crime并且重要程度为10, 并且希望包含office, otitle不包含xylink, 而且需要author字段包含junjian和dory词条, ps: 还有个simple_query_string, 解析错误不会抛异常, 丢弃无效部分.
- 模糊查询:
"fuzzy": {"title": "offi"}, 基于编辑距离(ED)来匹配文档
- 通配符查询:
"wildcard": {"title": "off?ce"},
查询还有很多, 参看文档就好.
过滤器:
post_filter, filtered, filter
过滤器类型: range, exists, missing, script
当然, 之前讲的查询都可以封装到过滤器中, 区别是通过过滤器返回的文档得分都是1.0
应该尽可能使用过滤器。过滤器不影响评分,而得分计算让搜索变得复杂,而且需要CPU资源。另一方面,过滤是一种相对简单的操作。由于过滤应用在整个索引的内容上,过滤的结果独立于找到的文档,也独立于文档之间的关系。过滤器很容易被缓存,从而进一步提高过滤查询的整体性能
关于搜索最后, 补充一个api, 就是验证_validate, 因为复杂的查询经历了什么我们不是特别的好控制, 而且部分查询出错, _search依旧可以返回貌似正确的结果. 使用方法是把_search 换成 _validate, 返回结果类似于:
{
"valid": false,
"_shards": { "total": 1, "successful": 1, "failed": 0 }
}
想要了解出错的具体问题, 加一个explain参数 (通常还需要加一个--data-binary参数, 以保留换行符, 方便定位问题), 就会知道哪些地方写错了.
另外, 关于查询重写, 数据排序以及高亮的原理, 可以看看官方文档.
查询性能分析:
假设在elasticsearch.yml文件中设置了以下的日志配置:
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.query.trace: 1s
并且在logging.yml配置文件中设置了以下日志级别:logger:
index.search.slowlog: TRACE, index_search_slow_log_file
注意, index.search.slowlog.threshold.query.trace属性设置成1s, 而日志级别index.search.slowlog属性设置成TRACE. 这意味着每当一个查询执行时间查过1秒(在分片上,不是总时间)时, 它将被记录到慢查询日志文件中(日志文件由logging.yml配置文件中的index_search_slow_log_file节点指定). 例如, 慢查询日志文件中可能找到下面的条目:
[2013-01-24 13:33:05,518][TRACE][index.search.slowlog.query] [Local test] [library][1] took[1400.7ms], took_millis[1400], search_type[QUERY_THEN_FETCH], total_shards[32], source[{"query":{"match_all":{}}}], extra_source[]
可以看到, 前面的日志行包含查询时间, 搜索类型和搜索源本身,它显示了执行的查询. 当然,你的配置文件中可能有不同的值,但慢查询日志可以是一个很有价值的源,从中可以找到执行时间过长的、可能需要定义预热的查询,它们可能是父子查询,需要获取一些标识符来提高性能,或者你第一次使用了一些费时的过滤器.
到这里, 我觉得已经把ES关键的一些实用信息都分享到了, 如有遗漏欢迎讨论.