大数据之ES:原理详解、技能大赏与API操作示例

来吧,架构深入和技能大赏以及HTTP操作示例
说明:部分图片和概念叙述来自于atguigu公开资料和ES官网

文章目录

  • ES 技能大赏
  • ES原理深入
    • 核心概念
    • 故障转移
    • 路由计算
    • 分片控制
      • 写操作
      • 读操作
      • 更新流程
      • 多文档操作流程
    • 分片原理
      • 动态更新索引
      • 近实时搜索
      • 持久化变更
      • 段合并
    • 文档分析
      • 分析器使用场景
      • 指定分析器
      • 自定义分词器
    • 文档处理
      • 文档冲突
      • 外部系统版本控制
  • HTTP 操作
    • 索引操作
    • 文档操作
    • 映射操作
      • 创建映射
      • 查看映射
      • 索引映射关联
    • 高级查询
      • 查询所有文档
      • 匹配查询
      • 字段匹配查询
      • 关键字精确查询
      • 多关键字精确查询
      • 指定查询字段
      • 过滤字段
      • 组合查询
      • 范围查询
      • 模糊查询
      • 按字段排序
      • 高亮查询
      • 分页查询
      • 聚合查询
      • 桶聚合查询
  • API 查询

ES 技能大赏

大数据之ES:原理详解、技能大赏与API操作示例_第1张图片

ES原理深入

核心概念

  • 索引

    一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。

  • 类型 type

    在一个索引中,你可以定义一种或多种类型。
    一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化

版本 Type
5.x 支持多种 type
6.x 只能有一种 type
7.x 默认不再支持自定义索引类型(默认类型为: _doc)
  • 文档

    一个文档是一个可被索引的基础信息单元,也就是一条数据
    比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。
    在一个 index/type 里面,你可以存储任意多的文档

  • 字段:相当于数据表的字段

  • 映射(mapping):对数据处理做一些规则限制:按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好

  • 分片(shards):数据分几片存储,每一片可以存在集群任意一个节点。分片可以方便水平分割、扩展容量;可以提高并行度;但分片过度也会给ES额外管理压力

被混淆的概念是,一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。 一个Elasticsearch 索引 是分片的集合。 当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集。

  • 副本:高可用、扩展吞吐量、搜索量

  • 分配(Allocation):将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分
    片复制数据的过程。这个过程是由 master 节点完成的。

    {
        "settings" : {
            "number_of_shards" : 3,
            "number_of_replicas" : 1
        }
    }
    
  • 架构

大数据之ES:原理详解、技能大赏与API操作示例_第2张图片

故障转移

  • green:主分片和副本分片都正常运行

  • yellow:副本存在部分不能正常工作

  • red:不是所有主分片都在正常工作

(Master)主节点丢失,变red,副节点升级为主节点,变yellow

路由计算

当索引一个文档的时候,文档会被存储到一个主分片中。 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 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。

分片控制

  • 相同分片的副本不会在同一个节点
  • 所有请求可以请求到任意节点,因为任意节点都知道集群中任何一个文档的位置

写操作

新建、索引和删除 请求都是 写 操作, 必须在主分片上面完成之后才能被复制到相关的副本分片

大数据之ES:原理详解、技能大赏与API操作示例_第3张图片

新建,索引和删除文档所需要的步骤顺序:

  1. 客户端向 Node 1 发送新建、索引或者删除请求。
  2. 节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
  3. Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。这些选项很少使用,因为 Elasticsearch 已经很快,但是为了完整起见, 请参考下面表格:

参数 含义
consistency consistency,即一致性。在默认设置下,即使仅仅是在试图执行一个_写_操作之 前,主分片都会要求 必须要有 规定数量(quorum)(或者换种说法,也即必须要 有大多数)的分片副本处于活跃可用状态,才会去执行_写_操作(其中分片副本 可以是主分片或者副本分片)。这是为了避免在发生网络分区故障( network partition)的时候进行_写_操作,进而导致数据不一致。 规定数量_即: int( (primary + number_of_replicas) / 2 ) + 1 consistency 参数的值可以设为 one (只要主分片状态 ok 就允许执行_写_操 作) ,all(必须要主分片和所有副本分片的状态没问题才允许执行_写_操作) , 或 quorum 。默认值为 quorum , 即大多数的分片副本状态没问题就允许执行_写 操作。 注意,规定数量 的计算公式中 number_of_replicas 指的是在索引设置中的设定 副本分片数,而不是指当前处理活动状态的副本分片数。如果你的索引设置中指定了当前索引拥有三个副本分片,那规定数量的计算结果即: int( (primary + 3 replicas) / 2 ) + 1 = 3 如果此时你只启动两个节点,那么处于活跃状态的分片副本数量就达不到规定数 量,也因此您将无法索引和删除任何文档。
timeout 如果没有足够的副本分片会发生什么? Elasticsearch 会等待,希望更多的分片出 现。默认情况下,它最多等待 1 分钟。 如果你需要,你可以使用 timeout 参数 使它更早终止: 100 100 毫秒, 30s 是 30 秒。

新索引默认有 1 个副本分片,这意味着为满足规定数量应该需要两个活动的分片副本。 但是,这些默认的设置会阻止我们在单一节点上做任何事情。为了避免这个问题,要求只有当 number_of_replicas 大于 1 的时候,规定数量才会执行。

读操作

我们可以从主分片或者从其它任意副本分片检索文档

大数据之ES:原理详解、技能大赏与API操作示例_第4张图片

从主分片或者副本分片检索文档的步骤顺序:

  1. 客户端向 Node 1 发送获取请求。
  2. 节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个
    节点上。 在这种情况下,它将请求转发到 Node 2 。
  3. Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

更新流程

大数据之ES:原理详解、技能大赏与API操作示例_第5张图片

部分更新一个文档的步骤如下:

  1. 客户端向 Node 1 发送更新请求。
  2. 它将请求转发到主分片所在的 Node 3 。
  3. Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。
  4. 如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

当主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

多文档操作流程

mget 和 bulk API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端

大数据之ES:原理详解、技能大赏与API操作示例_第6张图片

用单个 mget 请求取回多个文档所需的步骤顺序:

  1. 客户端向 Node 1 发送 mget 请求。
  2. Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的
    主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客
    户端。

可以对 docs 数组中每个文档设置 routing 参数。

bulk API, 允许在单个批量请求中执行多个创建、索引、删除和更新请求。

大数据之ES:原理详解、技能大赏与API操作示例_第7张图片

bulk API 按如下步骤顺序执行:

  1. 客户端向 Node 1 发送 bulk 请求。
  2. Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
  3. 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。 一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。

分片原理

文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。

你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。 这样两个文档都会匹配!分词和标准化的过程称为分析

动态更新索引

如何在保留不变性的前提下实现倒排索引的更新?

答案是: 用更多的索引 。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到, 从最早的开始查询完后再对结果进行合并。

Elasticsearch 基于 Lucene, 这个 java 库引入了按段搜索的概念。 每一 段 本身都是一个倒排索引, 但索引在 Lucene 中除表示所有段的集合外, 还增加了提交点的概念 — 一个列出了所有已知段的文件

大数据之ES:原理详解、技能大赏与API操作示例_第8张图片

按段搜索会以如下流程执行:

  1. 新文档被收集到内存索引缓存

大数据之ES:原理详解、技能大赏与API操作示例_第9张图片

​ 2.不时地, 缓存被 提交

​ (1) 一个新的段—一个追加的倒排索引—被写入磁盘。
​ (2) 一个新的包含新段名字的 提交点 被写入磁盘
​ (3) 磁盘进行 同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们
被写入物理文件

  1. 新的段被开启,让它包含的文档可见以被搜索
  2. 内存缓存被清空,等待接收新的文档

大数据之ES:原理详解、技能大赏与API操作示例_第10张图片

  • 当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
  • 段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。
  • 当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的 文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
  • 文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

近实时搜索

随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting)一个新的段到磁盘需要一个 fsync 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。
我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync 要从整个过程中被移除。在Elasticsearch 和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到文件系统缓存—这一步代价会比较低,稍后再被刷新到磁盘—这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。

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

大数据之ES:原理详解、技能大赏与API操作示例_第11张图片

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh 。 默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是 近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

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

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

并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件,你可能想优化索引速度而不是近实时搜索, 可以通过设置 refresh_interval , 降低每个索引的刷新频率

{
    "settings": {
    	"refresh_interval": "30s"
    }
}

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

# 关闭自动刷新
PUT /users/_settings
{ "refresh_interval": -1 }
# 每一秒刷新
PUT /users/_settings
{ "refresh_interval": "1s" }

持久化变更

​ 如果没有用 fsync 把数据从文件系统缓存刷( flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。在 动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一 个包含所有段列表的提交点。 Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

​ 即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。 Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录

整个流程如下:

  1. 一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了 translog

大数据之ES:原理详解、技能大赏与API操作示例_第12张图片

  1. 刷新(refresh)使分片每秒被刷新(refresh)一次:

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

大数据之ES:原理详解、技能大赏与API操作示例_第13张图片

  1. 这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志

大数据之ES:原理详解、技能大赏与API操作示例_第14张图片

  1. 每隔一段时间—例如 translog 变得越来越大—索引被刷新(flush);一个新的 translog被创建,并且一个全量提交被执行

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

    translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。

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

大数据之ES:原理详解、技能大赏与API操作示例_第15张图片

  • 执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush分片每 30 分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新 你很少需要自己手动执行 flush 操作;通常情况下,自动刷新就足够了。 这就是说,在重启节点或关闭索引之前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢复越快。translog 的目的是保证操作不会丢失,在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的 translog 之前,你的客户端不会得到一个 200 OK 响应。
  • 在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是 bulk 导入,它在一次请求中平摊了大量文档的开销)。
  • 但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync还是比较有益的。比如,写入的数据被缓存到内存中,再每 5 秒执行一次 fsync 。如果你决定使用异步 translog 的话,你需要 保证 在发生 crash 时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。如果你不确定这个行为的后果,最好是使用默认的参数( “index.translog.durability”: “request” )来避免数据丢失。

段合并

  • 由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和 cpu 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
  • Elasticsearch 通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
  • 段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
  • 启动段合并不需要你做任何事。进行索引和搜索时会自动进行。
    • 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
    • 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会
      中断索引和搜索。

大数据之ES:原理详解、技能大赏与API操作示例_第16张图片

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

大数据之ES:原理详解、技能大赏与API操作示例_第17张图片

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

文档分析

就是将文档进行词条和标准化分析

  • 字符过滤器:特殊字符处理
  • 分词器:分词
  • Token过滤器:词条有效性过滤处理

分析器使用场景

全文检索:全文查询、精确词条查询

指定分析器

ES有自己标准的分析器,若希望对某些字符串指定自定义的分析器,就需要手动指定相关字符串映射

这里来自定义使用IK中分分词器

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.8.0

将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用。

我们这次加入新的查询参数"analyzer":“ik_max_word”

# GET http://localhost:9200/_analyze
{
    "text":"测试单词",
    "analyzer":"ik_max_word"
}
  • ik_max_word:会将文本做最细粒度的拆分
  • ik_smart:会将文本做最粗粒度的拆分
{
    "tokens": [
        {
            "token": "测试",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "单词",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 1
        }
    ]
}

扩展词汇:就是把我们认为是词语的而IK分词器认为不是词语的进行扩展

首先进入 ES 根目录中的 plugins 文件夹下的 ik 文件夹,进入 config 目录,创建 custom.dic文件,写入弗雷尔卓德词汇。同时打开 IKAnalyzer.cfg.xml 文件,将新建的 custom.dic 配置其中,重启 ES 服务器。

大数据之ES:原理详解、技能大赏与API操作示例_第18张图片

自定义分词器

就是把三种函数给实现:字符过滤器、分词器、词单元过滤器

PUT http://localhost:9200/my_index

# PUT http://localhost:9200/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 http://localhost:9200/my_index/_analyze

# GET http://127.0.0.1:9200/my_index/_analyze
{
    "text":"The quick & brown fox",
    "analyzer": "my_analyzer"
}
{
    "tokens": [
        {
            "token": "quick",
            "start_offset": 4,
            "end_offset": 9,
            "type": "",
            "position": 1
        },
        {
            "token": "and",
            "start_offset": 10,
            "end_offset": 11,
            "type": "",
            "position": 2
        },
        {
            "token": "brown",
            "start_offset": 12,
            "end_offset": 17,
            "type": "",
            "position": 3
        },
        {
            "token": "fox",
            "start_offset": 18,
            "end_offset": 21,
            "type": "",
            "position": 4
        }
    ]
}

文档处理

文档冲突

多人更改时,只会保存一个,其他人的更改将会丢失

  • ES使用乐观并发控制来确保并发更新时变更不会丢失:Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。

外部系统版本控制

一个常见的设置是使用其它数据库作为主要的数据存储,使用 Elasticsearch 做数据检索, 这意味着主数据库的所有更改发生时都需要被复制到 Elasticsearch ,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。

如果你的主数据库已经有了版本号 — 或一个能作为版本号的字段值比如 timestamp — 那么你就可以在 Elasticsearch 中通过增加 version_type=external 到查询字符串的方式重用这些相同的版本号, 版本号必须是大于零的整数, 且小于 9.2E+18 — 一个 Java 中 long类型的正值。

外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同,Elasticsearch 不是检查当前 _version 和请求中指定的版本号是否相同, 而是检查当前_version 是否 小于 指定的版本号。 如果请求成功,外部的版本号作为文档的新 _version进行存储。

外部版本号不仅在索引和删除请求是可以指定,而且在 创建 新文档时也可以指定。

HTTP 操作

样例使用

索引操作

  • 创建索引

    PUT http://localhost:9200/shopping
    
    {
        "acknowledged": true,【响应结果】
        "shards_acknowledged": true,【分片结果】
        "index": "shopping"【索引名称】
    }
    
  • 查看索引

    查看所有 GET http://localhost:9200/_cat/indices?v
    
    health status index    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
    yellow open   shopping nlNiu1DvQeOXiOk4IrbEDg   1   1          0            0       208b           208b
    
    

    这里请求路径中的_cat 表示查看的意思, indices 表示索引,所以整体含义就是查看当前 ES
    服务器中的所有索引

表头 含义
health 当前服务器健康状态: green(集群完整) yellow(单点正常、集群不完整) red(单点不正常)
status 索引打开、关闭状态
index 索引名
uuid 索引统一编号
pri 主分片数量
rep 副本数量
docs.count 可用文档数量
docs.deleted 文档删除状态(逻辑删除)
store.size 主分片和副分片整体占空间大小
pri.store.size 主分片占空间大小
查看单个索引 GET http://localhost:9200/shopping
{
    "shopping"【索引名】 : {
    "aliases"【别名】 : {},
    "mappings"【映射】 : {},
    "settings"【设置】 : {
        "index"【设置 - 索引】 : {
                "creation_date"【设置 - 索引 - 创建时间】 : "1614265373911",
                "number_of_shards"【设置 - 索引 - 主分片数量】 : "1",
                "number_of_replicas"【设置 - 索引 - 副分片数量】 : "1",
                "uuid"【设置 - 索引 - 唯一标识】 : "eI5wemRERTumxGCc1bAk2A",
                "version"【设置 - 索引 - 版本】 : {
                	"created": "7080099"
                },
                "provided_name"【设置 - 索引 - 名称】 : "shopping"
            }
        }
    }
}
  • 删除索引

    DELETE http://localhost:9200/shopping
    
    {
        "acknowledged": true
    }
    

文档操作

文档相当于数据库一条记录

  • 创建文档

    POST http://localhost:9200/shopping/_doc
    如果增加数据时明确数据主键,那么请求方式也可以为 PUT
    BODY为json如下
    {
        "title":"小米手机",
        "category":"小米",
        "images":"http://www.gulixueyuan.com/xm.jpg",
        "price":3999.00
    }
    
    响应
    {
        "_index"【索引】 : "shopping",
        "_type"【类型-文档】 : "_doc",
        "_id"【唯一标识】 : "Xhsa2ncBlvF_7lxyCE9G", #可以类比为 MySQL 中的主键,随机生成
        "_version"【版本】 : 1,
        "result"【结果】 : "created", #这里的 create 表示创建成功
        "_shards"【分片】 : {
            "total"【分片 - 总数】 : 2,
            "successful"【分片 - 成功】 : 1,
            "failed"【分片 - 失败】 : 0
        },
        "_seq_no": 0,
        "_primary_term": 1
    }
    

    上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下, ES 服务器会随机生成一个。如果想要自定义唯一性标识,需要在创建时指定: http://127.0.0.1:9200/shopping/_doc/1

    此处需要注意:如果增加数据时明确数据主键,那么请求方式也可以为 PUT

  • 查看文档

    根据唯一标识查看文档

    GET http://localhost:9200/shopping/_doc/1
    
    {
        "_index": "shopping",
        "_type": "_doc",
        "_id": "1",
        "_version": 3,
        "_seq_no": 5,
        "_primary_term": 1,
        "found": true,
        "_source": {
            "title": "小米手机1",
            "category": "小米",
            "images": "http://www.gulixueyuan.com/xm.jpg",
            "price": 3000.0
        }
    }
    
  • 修改文档

    和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖。响应结果version会递增,result是updated

    POST http://localhost:9200/shopping/_doc/1
    
  • 修改字段

    修改数据时,也可以只修改某一给条数据的局部信息

    POST http://localhost:9200/shopping/_update/1
    
    {
        "doc": {
        	"price":3000.00
        }
    }
    
    响应
    {
        "_index": "shopping",
        "_type": "_doc",
        "_id": "1",
        "_version": 4,
        "result": "updated",
        "_shards": {
            "total": 2,
            "successful": 1,
            "failed": 0
        },
        "_seq_no": 6,
        "_primary_term": 1
    }
    
  • 删除文档

    删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)

    DELETE http://localhost:9200/shopping/_doc/1
    
    {
        "_index": "shopping",
        "_type": "_doc",
        "_id": "1",
        "_version": 5,
        "result": "deleted",
        "_shards": {
            "total": 2,
            "successful": 1,
            "failed": 0
        },
        "_seq_no": 7,
        "_primary_term": 1
    }
    
  • 条件删除文档

    一般删除数据都是根据文档的唯一性标识进行删除,实际操作时,也可以根据条件对多条数
    据进行删除

    先创建多个文档然后测试删除

    POST http://localhost:9200/shopping/_delete_by_query
    {
        "query": {
            "match": {
                "price": 4001.00
            }
        }
    }
    
    响应
    {
        "took": 933,【耗时】
        "timed_out": false,【是否超时】
        "total": 2,【总量】
        "deleted": 2,【删除总量】
        "batches": 1,
        "version_conflicts": 0,
        "noops": 0,
        "retries": {
            "bulk": 0,
            "search": 0
        },
        "throttled_millis": 0,
        "requests_per_second": -1.0,
        "throttled_until_millis": 0,
        "failures": []
    }
    

映射操作

索引库(index)中的映射,类似于数据库(database)中的表结构(table)。创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。

创建映射

PUT http://localhost:9200/student/_mapping

{
    "properties": {
        "title": {
            "type": "text",
            "index": true
        },
        "category": {
            "type": "text",
            "index": true
        },
        "images": {
            "type": "text",
            "index": false
        }
    }
}

响应

{
    "acknowledged": true
}
  • 字段名:任意填写,下面指定许多属性,例如: title、 subtitle、 images、 price

    • type:类型, Elasticsearch 中支持的数据类型非常丰富,说几个关键的:
      String 类型,又分两种:text:可分词 keyword:不可分词,数据会作为完整字段进行匹

    • Numerical:数值类型,分两类

      • 基本数据类型: long、 integer、 short、 byte、 double、 float、 half_float
      • 浮点数的高精度类型: scaled_float
    • Date:日期类型

    • Array:数组类型

    • Object:对象

  • index:是否索引,默认为 true,也就是说你不进行任何配置,所有字段都会被索引。

    • true:字段会被索引,则可以用来进行搜索
    • false:字段不会被索引,不能用来搜索
  • store:是否将数据进行独立存储,默认为 false
    原始的文本会存储_source 里面,默认情况下其他提取出来的字段都不是独立存储的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置"store": true 即可,获取独立存储的字段要比从_source 中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置。

  • analyzer:分词器,这里的 ik_max_word 即使用 ik 分词器,后面会有专门的章节学习

查看映射

GET http://localhost:9200/shopping/_mapping

索引映射关联

即直接在索引下创建映射

PUT http://localhost:9200/shopping1/

{
    "settings": {},
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "index": true
            },
            "sex": {
                "type": "text",
                "index": false
            },
            "age": {
                "type": "long",
                "index": false
            }
        }
    }
}

响应

{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "shopping1"
}

高级查询

测试数据如下

并且建立映射:name、sex、age。参考索引映射关联

# POST /student/_doc/1001
{
    "name":"zhangsan",
    "nickname":"zhangsan",
    "sex":"男",
    "age":30
}
# POST /student/_doc/1002
{
    "name":"lisi",
    "nickname":"lisi",
    "sex":"男",
    "age":20
}
# POST /student/_doc/1003
{
    "name":"wangwu",
    "nickname":"wangwu",
    "sex":"女",
    "age":40
}
# POST /student/_doc/1004
{
    "name":"zhangsan1",
    "nickname":"zhangsan1",
    "sex":"女",
    "age":50
}
# POST /student/_doc/1005
{
    "name":"zhangsan2",
    "nickname":"zhangsan2",
    "sex":"女",
    "age":30
}

查询所有文档

GET http://localhost:9200/student/_search

{
    "query": {
        "match_all": {}
    }
}

响应

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1001",
                "_score": 1.0,
                "_source": {
                    "name": "zhangsan",
                    "nickname": "zhangsan",
                    "sex": "男",
                    "age": 30
                }
            },
            ....
        ]
    }
}
{
    "took【查询花费时间,单位毫秒】 ": 1116,
    "timed_out【是否超时】 ": false,
    "_shards【分片信息】 ": {
        "total【总数】 ": 1,
        "successful【成功】 ": 1,
        "skipped【忽略】 ": 0,
        "failed【失败】 ": 0
    },
    "hits【搜索命中结果】 ": {
        "total"【搜索条件匹配的文档总数】 : {
            "value"【总命中计数的值】 : 3,
            "relation"【计数规则】 : "eq" # eq 表示计数准确, gte 表示计数不准确
        },
        "max_score【匹配度分值】 ": 1.0,
        "hits【命中结果集合】 ": [
        
    	]
	}
}

匹配查询

match 匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系

GET http://localhost:9200/student/_search

{
    "query": {
        "match": {
            "name": "zhangsan"
        }
    }
}

响应

{
    "took": 34,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.3862942,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1001",
                "_score": 1.3862942,
                "_source": {
                    "name": "zhangsan",
                    "nickname": "zhangsan",
                    "sex": "男",
                    "age": 30
                }
            }
        ]
    }
}

字段匹配查询

multi_match 与 match 类似,不同的是它可以在多个字段中查询。

GET http://localhost:9200/student/_search

{
    "query": {
        "multi_match": {
            "query": "zhangsan",
            "fields": [
                "name",
                "nickname"
            ]
        }
    }
}

response

{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.3862942,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1001",
                "_score": 1.3862942,
                "_source": {
                    "name": "zhangsan",
                    "nickname": "zhangsan",
                    "sex": "男",
                    "age": 30
                }
            }
        ]
    }
}

关键字精确查询

term 查询,精确的关键词匹配查询,不对查询条件进行分词。

GET http://localhost:9200/student/_search

{
    "query": {
        "term": {
            "name": {
                "value": "zhangsan"
            }
        }
    }
}

结果类似 【字段匹配查询】 的结果

多关键字精确查询

terms 查询和 term 查询一样,但它允许你指定多值进行匹配。
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in

GET http://localhost:9200/student/_search

{
    "query": {
        "terms": {
            "name": [
                "zhangsan",
                "lisi"
            ]
        }
    }
}

指定查询字段

默认情况下, Elasticsearch 在搜索的结果中,会把文档中保存在_source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加_source 的过滤

GET http://localhost:9200/student/_search

{
    "_source": [
        "name",
        "nickname"
    ],
    "query": {
        "terms": {
            "nickname": [
                "zhangsan"
            ]
        }
    }
}

response

{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.0,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1001",
                "_score": 1.0,
                "_source": {
                    "name": "zhangsan",
                    "nickname": "zhangsan"
                }
            }
        ]
    }
}

过滤字段

​ 我们也可以通过:

includes:来指定想要显示的字段
excludes:来指定不想要显示的字段

GET http://localhost:9200/student/_search

{
    "_source": {
        "includes": [
            "name",
            "nickname"
        ]
    },
    "query": {
        "terms": {
            "nickname": [
                "zhangsan"
            ]
        }
    }
}

组合查询

bool把各种其它查询通过must(必须 )、 must_not(必须不)、 should(应该)的方
式进行组合

GET http://localhost:9200/student/_search

{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "name": "zhangsan"
                    }
                }
            ],
            "must_not": [
                {
                    "match": {
                        "age": 40
                    }
                }
            ],
            "should": [
                {
                    "match": {
                        "sex": "男"
                    }
                }
            ]
        }
    }
}

范围查询

range 查询找出那些落在指定区间内的数字或者时间。 range 查询允许以下字符

操作符 说明
gt 大于>
gte 大于等于>=
lt 小于<
lte 小于等于<=

GET http://localhost:9200/student/_search

{
    "query": {
        "range": {
            "age": {
                "gte": 30,
                "lte": 35
            }
        }
    }
}

模糊查询

返回包含与搜索字词相似的字词的文档。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:

  • 更改字符(box → fox)
  • 删除字符(black → lack)
  • 插入字符(sic → sick)
  • 转置两个相邻字符(act → cat)

为了找到相似的术语, fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体
或扩展。然后查询返回每个扩展的完全匹配。

通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。

GET http://localhost:9200/student/_search

{
    "query": {
        "fuzzy": {
            "title": {
                "value": "zhangsan"
            }
        }
    }
}

按字段排序

sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式。 desc 降序, asc
升序。

GET http://localhost:9200/student/_search

{
    "query": {
        "match": {
            "name": "zhangsan"
        }
    },
    "sort": [
        {
            "age": {
                "order": "desc"
            }
        },
        {
            "其他字段": {
                "order": "desc"
            }
        }
    ]
}

高亮查询

在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。

Elasticsearch 可以对查询内容中的关键字部分,进行标签和样式(高亮)的设置。
在使用 match 查询的同时,加上一个 highlight 属性:

  • pre_tags:前置标签
  • post_tags:后置标签
  • fields:需要高亮的字段
  • title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置, 也可以空

GET http://localhost:9200/student/_search

{
    "query": {
        "match": {
            "name": "zhangsan"
        }
    },
    "highlight": {
        "pre_tags": "",
        "post_tags": "",
        "fields": {
            "name": {}
        }
    }
}

response

{
    "took": 154,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 1.3862942,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1001",
                "_score": 1.3862942,
                "_source": {
                    "name": "zhangsan",
                    "nickname": "zhangsan",
                    "sex": "男",
                    "age": 30
                },
                "highlight": {
                    "name": [
                        "zhangsan"
                    ]
                }
            }
        ]
    }
}

分页查询

from:当前页的起始索引,默认从 0 开始。 from = (pageNum - 1) * size
size:每页显示多少条

GET http://localhost:9200/student/_search

{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "age": {
                "order": "desc"
            }
        }
    ],
    "from": 0,
    "size": 2
}

response

{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": null,
        "hits": [
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1004",
                "_score": null,
                "_source": {
                    "name": "zhangsan1",
                    "nickname": "zhangsan1",
                    "sex": "女",
                    "age": 50
                },
                "sort": [
                    50
                ]
            },
            {
                "_index": "student",
                "_type": "_doc",
                "_id": "1003",
                "_score": null,
                "_source": {
                    "name": "wangwu",
                    "nickname": "wangwu",
                    "sex": "女",
                    "age": 40
                },
                "sort": [
                    40
                ]
            }
        ]
    }
}

聚合查询

聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值、平均值等等。

GET http://localhost:9200/student/_search

{
    "aggs": {
        "max_age": {
            "max": {
                "field": "age"
            }
        },
        "min_age": {
            "min": {
                "field": "age"
            }
        },
        "sumAge": {
            "sum": {
                "field": "age"
            }
        },
        "avgAge": {
            "avg": {
                "field": "age"
            }
        },
        "distinct_age": {
            "cardinality": {
                "field": "age"
            }
            
        }
    },
    "size": 0
}

response

{
    "took": 2,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "max_age": {
            "value": 50.0
        },
        "sumAge": {
            "value": 170.0
        },
        "distinct_age": {
            "value": 4
        },
        "avgAge": {
            "value": 34.0
        },
        "min_age": {
            "value": 20.0
        }
    }
}

一次性聚合计算:会一次性把max、min、avg等输出

{
    "aggs": {
        "stats_age": {
            "stats": {
                "field": "age"
            }
        }
    },
    "size": 0
}

response

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "stats_age": {
            "count": 5,
            "min": 20.0,
            "max": 50.0,
            "avg": 34.0,
            "sum": 170.0
        }
    }
}

桶聚合查询

桶聚和相当于 sql 中的 group by 语句

GET http://localhost:9200/student/_search

  • terms 聚合,分组统计

    {
        "aggs": {
            "age_groupby": {
                "terms": {
                    "field": "age"
                }
            }
        },
        "size": 0
    }
    

response

{
    "took": 5,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "age_groupby": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
                {
                    "key": 30,
                    "doc_count": 2
                },
                {
                    "key": 20,
                    "doc_count": 1
                },
                {
                    "key": 40,
                    "doc_count": 1
                },
                {
                    "key": 50,
                    "doc_count": 1
                }
            ]
        }
    }
}
  • 在 terms 分组下再进行聚合
{
    "aggs": {
        "age_groupby": {
            "terms": {
                "field": "age"
            },
            "aggs": {
                "sumAge": {
                    "sum": {
                        "field": "age"
                    }
                }
            }
        }
    },
    "size": 0
}

response

{
    "took": 3,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 5,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "aggregations": {
        "age_groupby": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
                {
                    "key": 30,
                    "doc_count": 2,
                    "sumAge": {
                        "value": 60.0
                    }
                },
                {
                    "key": 20,
                    "doc_count": 1,
                    "sumAge": {
                        "value": 20.0
                    }
                },
                {
                    "key": 40,
                    "doc_count": 1,
                    "sumAge": {
                        "value": 40.0
                    }
                },
                {
                    "key": 50,
                    "doc_count": 1,
                    "sumAge": {
                        "value": 50.0
                    }
                }
            ]
        }
    }
}

API 查询

API查询分各种语言,这里贴上官网文档地址,上面还是比较详细

https://www.elastic.co/guide/en/elasticsearch/client/java-api/7.x/index.html

https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html

你可能感兴趣的:(ES,大数据,elasticsearch,ES原理,全文检索)