ElasticSearch:权威指南读书笔记

607 阅读17分钟
  • 这是我读es权威指南的笔记
  • es权威指南基于es2.x因此和现在版本可能有不同
  • 在里面有些写法我会使用6.x的语法。比如_doc
  • 如有问题,欢迎在评论中指正,谢谢 各个es版本的区别

基础入门

更新文档

es更新文档是

  1. 从旧文档构建 JSON
  2. 更改该 JSON
  3. 删除旧文档
  4. 索引一个新文档

es不会立刻删除文档,会等更新一定的版本数后再在后台清理这些文档 ​

创建文档

使用create创建文档时带上id如果es中有该id数据,创建会失败 ​

index和create index操作没有该文档会创建文档,有该文档会覆盖操作。 create操作没有该文档会创建文档,有该文档会报错 ​

更新冲突

es采用了乐观锁的方式做并发控制,通过_version来进行是否更新的判断。只要_vision大于当前系统数据中的_version就可以更新 ​

更新脚本

POST /website/blog/1/_update
{
   "script" : "ctx._source.views+=new_tag",
   "params": {
   		"new_tag": "awesome"
   }
}

//使用op来决定本次操作方式
POST /website/blog/1/_update
{
   "script" : "ctx.op = ctx._source.views == count ? 'delete' : 'none'",
    "params" : {
        "count": 1
    }
}


POST /website/pageviews/1/_update
{
   "script" : "ctx._source.views+=1",
   "upsert": { //使用upsert来创建不存在文档
       "views": 1
   }
}

//使用retry_on_conflict来做冲突的重试
POST /website/pageviews/1/_update?retry_on_conflict=5 
{
   "script" : "ctx._source.views+=1",
   "upsert": {
       "views": 0
   }
}

脚本语言是Groovy,Groovy有漏洞。可以在elasticsearch.yml配置文件中禁用 ​

批量操作

POST /website/log/_bulk
{ "index": {}}
{ "event": "User logged in" }
{ "index": { "_type": "blog" }}//会覆盖默认的type
{ "title": "Overriding the default type" }

密切关注你的批量请求的物理大小往往非常有用,一千个 1KB 的文档是完全不同于一千个 1MB 文档所占的物理大小。 一个好的批量大小在开始处理后所占用的物理大小约为 5-15 MB。 ​

检索方式

es会将所有字段信息在底层拼到一起,形成一个_all字段。检索时会用_all字段来进行检索。 ​

分析器

不同的分析器会把内容解析为不同的内容。甚至会对其进行同义解析。也会删除一些无用词。英语分词器就会删除to ,the,and这类无用词 ​

排序

一般我们通过简单搜索比如match,term查询时会返回一个_score属性。他用来表示查询到的数据和查询语句之间的相关度。 但是我们使用filter查询时,因为filter会过滤到数据,因此他的score没有意义(无效的数据被过滤,无需考虑_score代表的相关度)。因此他的score得分为0。当然如果嫌弃0不好看,可以使用constant_score,这个查询和filter类似,不过返回的score得分恒定为1。 我们可以通过sort自定义排序方式

GET /_search
{
    "query" : {
        "bool" : {
            "must":   { "match": { "tweet": "manage text search" }},
            "filter" : { "term" : { "user_id" : 2 }}
        }
    },
    "sort": [
        { "date":   { "order": "desc" }},
        { "_score": { "order": "desc" }} //只有data完全相同时才会使用第二个字段进行排序
    ]
}


深分页

es默认的分页查询的最大值是1w。 由于分页查询需要排序,在大量数据查询中,排序的过程可能会消耗大量资源(cpu,内存,带宽)。 因此es不建议使用深分页。在设计者的思路中,人们查询时很少会使用到深分页,会使用的大部分是爬虫。 ​

scroll查询

scroll查询区别与分页查询是他不会进行排序,因此查询的成本就很低。 scroll查询在启动后会生成类似于数据快照的东西,之后的改变不会在scroll的考虑范围中。 我们可以设置一个超时时间,在超时后结束这个任务(超时时间是单次超时)。 scroll中设置的size不是确定的。比如设置100,实际获取的可能最大数量是100×主分片数。 ​

索引

索引可以手动生成,也可以在数据进来之后自动生成。建议手动生成,避免类型错误造成的问题

//elasticsearch.yml禁止自动创建索引
action.auto_create_index: false

删除索引可以单独删除,也可以用通配符匹配删除,也能全量删除。可以在配置文件中设置只能单独删除

action.destructive_requires_name: true

生成索引默认5个主分片,每个主片一个副分片。主分片数量生产之后无法修改,可以修改副分片数量

PUT /my_temp_index/_settings
{
    "number_of_replicas": 1
}

搜索更新原理

倒排索引

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

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

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。 image.png

es会将每个content拆成单独的单词,然后我们查询quick brown的时候,会将quick brown也拆开,去匹配content拆开的单词中出现了几个。1中出现了两个单词,2中只出现了一个,因此1的相关性更高。 因为有排序,因此是用的二分查找 倒排索引 es对最开始的所有文档会建立一个很大的倒排索引写入磁盘,并且不会进行改动。 倒排索引不变的优点是:

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

缺点就是因为不可修改,因此对es更新的代价很大。 ​

更新方式

  1. 新文档被收集到内存索引缓存。一个 Lucene 索引包含一个提交点和三个段
  2. 不时地, 缓存被 提交
    • 一个新的段—一个追加的倒排索引—被写入磁盘。
    • 一个新的包含新段名字的 提交点 被写入磁盘。
    • 磁盘进行 同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
  3. 新的段被开启,让它包含的文档可见以被搜索。
  4. 内存缓存被清空,等待接收新的文档。

我的理解是,在一个时间段内,新文档是存储在内存中,是一个新的倒排索引。如果在这个时间段中,有新的文档写入,追加到这个倒排索引中 。因为倒排索引的不可更改,所以每个新文档进来都是要将之前的倒排索引新建,删除旧索引。在这个时间段结束之后,这个新的倒排索引会写入磁盘中。 ​

因为倒排索引不可更改,所以对文档的删除操作不是对倒排索引进行操作。是将文档的编号写入一个.del文件中,文件会记录被删除的文档信息。这时文档依旧可以被检索到,但是在返回结果的时候,将该文档从结果集中移除。 更新也是同样,在返回结果时将旧文档从结果中删除,保留新文档。这个新文档会在一个新的倒排索引中。

近实时搜索

在Elasticsearch和磁盘之间是文件系统缓存。之外还有个内存索引缓冲区。 在写入一个文档时,我们是将其先写入内存索引缓冲区。然后这个内存索引缓冲区会生成一个新的段。这个段是写在文件系统缓存中的。当缓冲区到达了一定的规模,我们才会进行一个提交,写入硬盘,重建倒排索引。 这个缓冲区的内容是可搜索的。但是写入和打开这个缓冲区的段过程不是实时的,这个过程叫refresh,es refresh的时间是一秒。 因为我们在更新数据之后马上查看可能就无法看到。好的解决方案是在更新之后,手动刷新一次。使用_refresh api

POST /_refresh 
POST /blogs/_refresh

每个索引的刷新频率可以通过设置修改

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

甚至可以关闭刷新

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

事务日志

Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。 这个事务日志用来确保在程序正常或意外退出之后能恢复数据。 在新的段被刷到磁盘以前,translog都会一直保留。在一个提交点生成,数据被刷入磁盘之后,文件系统缓存刷新,老的translog删除。 并且在使用CRUD时会优先查询translog中的数据,这样获得的数据就是最近更新的数据。 安全性: translog是每5秒就被保存到硬盘,或者在有更新请求之后刷新到硬盘。这个过程在主分片和副分片中都会发生。因此,理论是translog的安全性是有保证的.

合并段

在写入多个文档之后,会生成多个段。由于我们在搜索时是先搜索这些新生成的段。由于段的数量可能很多,因此效率就很低。 es对此采取了在后台合并的方式。es会将大小相近的段合并成为一个大的段,然后再将大的段在进行合并。 es对合并采取的策略是不影响检索的正常运行。 合并是一个很消耗I/O的过程。一般情况下对我们检索是没有影响的,但是在大段进行合并时,有可能会影响到索引速率。 我们可以对合并的限制流量进行设置。默认情况下是20M/s,但是如果你是用的ssd,那可以考虑提高这个速率限制。如果你要进行大批量的导入,暂时不进行检索的情况下,你可以关闭这个限流,让合并使用所有的磁盘功能。

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}
PUT /_cluster/settings
{
    "transient" : {
        "indices.store.throttle.type" : "none" 
    }
}

对于一些我们不再需要修改的数据,我们可以将其强行合并为一个段。例如对于定期的日志,之前的日志是不会进行修改的。因此我们可以将之前的日志索引合并成一个段。这可以节约空间,也可以提升检索速度。

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

结构化搜索

精确值查找

term查询是精准匹配。在倒排索引 中我们知道,es存储不会存储整个语句,是分词存储的。因此term查询可能会遇到查询结果失败的情况。特别是针对text类型的数据。 因此我建议是term查询只针对数字,时间,布尔和keyword类型的数据。ip类型待考证??? term查询和terms查询是包含查询而不是精准匹配查询。因为我们的倒排索引方式,因此term匹配的是分词之后的数据,检索到这个数据之后是将文档返回。 比如文档1 weather: Today is sunny day 和文档2 weather: Today is rain day | 1 is | 1,2 rain | 2 sunny | 1 today | 1,2 倒排索引之后如上,我们使用

{
    "term" : {
        "weather" : "day"
    }
}

这就是查询包含day关键字的文档,只返回文档1. ​

过滤器

我们可以使用filter等过滤器来实现非评分查询。获取到的数据_score评分都是1,因为没有计算_score,因为查询更快。并且,es会缓存非评分查询的数据,虽然有些使用少的数据也被缓存了。es对这些缓存也进行了优化。 ​

1.查找匹配文档. term 查询在倒排索引中查找 XHDK-A-1293-#fJ3 然后获取包含该 term 的所有文档。本例中,只有文档 1 满足我们要求。 2.创建 bitset. 过滤器会创建一个 bitset (一个包含 0 和 1 的数组),它描述了哪个文档会包含该 term 。匹配文档的标志位是 1 。本例中,bitset 的值为 [1,0,0,0] 。在内部,它表示成一个 "roaring bitmap",可以同时对稀疏或密集的集合进行高效编码。 3.迭代 bitset(s) 一旦为每个查询生成了 bitsets ,Elasticsearch 就会循环迭代 bitsets 从而找到满足所有过滤条件的匹配文档的集合。执行顺序是启发式的,但一般来说先迭代稀疏的 bitset (因为它可以排除掉大量的文档)。 4.增量使用计数. Elasticsearch 能够缓存非评分查询从而获取更快的访问,但是它也会不太聪明地缓存一些使用极少的东西。非评分计算因为倒排索引已经足够快了,所以我们只想缓存那些我们 知道 在将来会被再次使用的查询,以避免资源的浪费。

为了实现以上设想,Elasticsearch 会为每个索引跟踪保留查询使用的历史状态。如果查询在最近的 256 次查询中会被用到,那么它就会被缓存到内存中。当 bitset 被缓存后,缓存会在那些低于 10,000 个文档(或少于 3% 的总索引数)的段(segment)中被忽略。这些小的段即将会消失,所以为它们分配缓存是一种浪费。 ​

理论上非评分查询 先于 评分查询执行。非评分查询任务旨在降低那些将对评分查询计算带来更高成本的文档数量,从而达到快速搜索的目的。 ​

过滤器的缓存

如果一个非评分查询在最近的 256 次查询中被使用过(次数取决于查询类型),那么这个查询就会作为缓存的候选。但是,并不是所有的片段都能保证缓存 bitset 。只有那些文档数量超过 10,000 (或超过总文档数量的 3% )才会缓存 bitset 。因为小的片段可以很快的进行搜索和合并,这里缓存的意义不大。 一旦缓存了,非评分计算的 bitset 会一直驻留在缓存中直到它被剔除。剔除规则是基于 LRU 的:一旦缓存满了,最近最少使用的过滤器会被剔除。 ​

范围查询

range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:

  • gt: > 大于(greater than)
  • lt: < 小于(less than)
  • gte: >= 大于或等于(greater than or equal to)
  • lte: <= 小于或等于(less than or equal to)
"range" : {
    "price" : {
        "gte" : 20,
        "lte" : 40
    }
}

日期查询 es查询日期除了可以直接给定字符串查询之外,还可以对给定的字符串进行操作。

"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-07 00:00:00"
    }
}

"range" : {
    "timestamp" : {
        "gt" : "now-1h" //这个now是可以使用来表示现在的时间
    }
}

"range" : {
    "timestamp" : {
        "gt" : "2014-01-01 00:00:00",
        "lt" : "2014-01-01 00:00:00||+1M" //在一个日期之后加上一个双管符号||后面加一个日期数学表达式就能做到
    }
}

字符串查询 range同样也可用于字符串查询,字符串范围按照字典中顺序来。 ​

null值

es是使用倒排索引,因此理论上来说,null值并不会被存储。但是es内部对null值做了一个占位的处理。我们put一个null值的数据

PUT /test_search/_doc/4
{
  "weather": null
}

然后查询 image.png 我们能看到null值的数据是写进去了的。仅限于null值,js中的undefined是不合法的。 ​

对于null值的数据,我们可以使用exist和missing查询来进行查询 ​

exist和missing查询对象时需要对象里面的值都为空才能查到 ​

全文搜索

match查询

match查询是我们会经常用到的一个查询方式。他的底层逻辑是基于term查询,但是match查询可以对多个词进行查询。换句话说,match查询就是将查询语句分词之后的term查询。

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": "BROWN"
        }
    }
}

这个查询就是将BROWN进行小写变成brown之后再使用term查询。 ​

多个词的查询

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": "BROWN DOG!"
        }
    }
}

这是将BROWN DOG进行小写之后进行两个term查询,然后在进行结果合并。在这个地方是使用bool查询来包裹了两个term查询。需要注意的是!这类的非有效字符会忽略 ​

match其实相当于是一种OR查询,OR查询并不能覆盖我们所有的场景,因此我们可以接受两个参数 operator设置为and可以将match变为AND查询。 minimun_should_match这个更人性化的可以输入一个百分数,作为一个最小匹配参数。这个参数设置了之后会根据我们输入的查询语句来进行。 例如我们设置的是75%,但是输入只有3个单词。这个75%会被截断为66.6%。 ​

Bool查询

bool 查询会为每个文档计算相关度评分 _score ,再将所有匹配的 must 和 should 语句的分数 _score 求和,最后除以 must 和 should 语句的总数。 must_not 语句不会影响评分;它的作用只是将不相关的文档排除。 所有 must 语句必须匹配,所有 must_not 语句都必须不匹配,但有多少 should 语句应该匹配呢?默认情况下,没有 should 语句是必须匹配的,只有一个例外:那就是当没有 must 语句的时候,至少有一个 should 语句必须匹配。 就像我们能控制 match 查询的精度 一样,我们可以通过 minimum_should_match 参数控制需要匹配的 should 语句的数量,它既可以是一个绝对的数字,又可以是个百分比: ​

bool查询的评分 因为bool查询对must的要求是要必须包含,因此should的查询就成了提升_score的关键。 在用should进行匹配时,我们可以使用boost来改变该条查询的权重

GET /_search
{
    "query": {
        "bool": {
            "must": {
                "match": {  
                    "content": {
                        "query":    "full text search",
                        "operator": "and"
                    }
                }
            },
            "should": [
                { "match": {
                    "content": {
                        "query": "Elasticsearch",
                        "boost": 3 
                    }
                }},
                { "match": {
                    "content": {
                        "query": "Lucene",
                        "boost": 2 
                    }
                }}
            ]
        }
    }
}

在搜索请求后添加 ?search_type=dfs_query_then_fetch , dfs 是指 分布式频率搜索(Distributed Frequency Search) , 它告诉 Elasticsearch ,先分别获得每个分片本地的 IDF(文档频率) ,然后根据结果再计算整个索引的全局 IDF 。

分析器

如果考虑到这些额外参数,一个搜索时的 完整 顺序会是下面这样:

  • 查询自己定义的 analyzer ,否则
  • 字段映射里定义的 search_analyzer ,否则
  • 字段映射里定义的 analyzer ,否则
  • 索引设置中名为 default_search 的分析器,默认为
  • 索引设置中名为 default 的分析器,默认为
  • standard 标准分析器

多字段搜索

最佳字段

现在有两个文档

PUT /my_index/my_type/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /my_index/my_type/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

我们使用如下查询语句

{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

查出来的结果文档1的评分会更高。因为虽然在文档2中body查询到了两个词,但在计算得分的时候,因为文档1title,body中都有一个词,因此计算下来分数高于文档2 ​

不使用 bool 查询,可以使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回