Dunwu Blog

大道至简,知易行难

Flink API

Flink 为流式/批式处理应用程序的开发提供了不同级别的抽象。

Programming levels of abstraction

  • Flink API 最底层的抽象为有状态实时流处理。其抽象实现是 Process Function,并且 Process Function 被 Flink 框架集成到了 DataStream API 中来为我们使用。它允许用户在应用程序中自由地处理来自单流或多流的事件(数据),并提供具有全局一致性和容错保障的状态。此外,用户可以在此层抽象中注册事件时间(event time)和处理时间(processing time)回调方法,从而允许程序可以实现复杂计算。

  • Flink API 第二层抽象是 Core APIs。实际上,许多应用程序不需要使用到上述最底层抽象的 API,而是可以使用 Core APIs 进行编程:其中包含 DataStream API(应用于有界/无界数据流场景)和 DataSet API(应用于有界数据集场景)两部分。Core APIs 提供的流式 API(Fluent API)为数据处理提供了通用的模块组件,例如各种形式的用户自定义转换(transformations)、联接(joins)、聚合(aggregations)、窗口(windows)和状态(state)操作等。此层 API 中处理的数据类型在每种编程语言中都有其对应的类。

    Process Function 这类底层抽象和 DataStream API 的相互集成使得用户可以选择使用更底层的抽象 API 来实现自己的需求。DataSet API 还额外提供了一些原语,比如循环/迭代(loop/iteration)操作。

  • Flink API 第三层抽象是 Table APITable API 是以表(Table)为中心的声明式编程(DSL)API,例如在流式数据场景下,它可以表示一张正在动态改变的表。Table API 遵循(扩展)关系模型:即表拥有 schema(类似于关系型数据库中的 schema),并且 Table API 也提供了类似于关系模型中的操作,比如 select、project、join、group-by 和 aggregate 等。Table API 程序是以声明的方式定义应执行的逻辑操作,而不是确切地指定程序应该执行的代码。尽管 Table API 使用起来很简洁并且可以由各种类型的用户自定义函数扩展功能,但还是比 Core API 的表达能力差。此外,Table API 程序在执行之前还会使用优化器中的优化规则对用户编写的表达式进行优化。

    表和 DataStream/DataSet 可以进行无缝切换,Flink 允许用户在编写应用程序时将 Table APIDataStream/DataSet API 混合使用。

  • Flink API 最顶层抽象是 SQL。这层抽象在语义和程序表达式上都类似于 _Table API_,但是其程序实现都是 SQL 查询表达式。SQL 抽象与 Table API 抽象之间的关联是非常紧密的,并且 SQL 查询语句可以在 Table API 中定义的表上执行。

ProcessFunction

ProcessFunction 是 Flink 所提供的最具表达力的接口。ProcessFunction 可以处理一或两条输入数据流中的单个事件或者归入一个特定窗口内的多个事件。它提供了对于时间和状态的细粒度控制。开发者可以在其中任意地修改状态,也能够注册定时器用以在未来的某一时刻触发回调函数。因此,你可以利用 ProcessFunction 实现许多有状态的事件驱动应用所需要的基于单个事件的复杂业务逻辑。

下面的代码示例展示了如何在 KeyedStream 上利用 KeyedProcessFunction 对标记为 STARTEND 的事件进行处理。当收到 START 事件时,处理函数会记录其时间戳,并且注册一个时长 4 小时的计时器。如果在计时器结束之前收到 END 事件,处理函数会计算其与上一个 START 事件的时间间隔,清空状态并将计算结果返回。否则,计时器结束,并清空状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**

* 将相邻的 keyed START 和 END 事件相匹配并计算两者的时间间隔
* 输入数据为 Tuple2<String, String> 类型,第一个字段为 key 值,
* 第二个字段标记 START 和 END 事件。
*/
public static class StartEndDuration
extends KeyedProcessFunction<String, Tuple2<String, String>, Tuple2<String, Long>> {

private ValueState<Long> startTime;

@Override
public void open(Configuration conf) {
// obtain state handle
startTime = getRuntimeContext()
.getState(new ValueStateDescriptor<Long>("startTime", Long.class));
}

/** Called for each processed event. */
@Override
public void processElement(
Tuple2<String, String> in,
Context ctx,
Collector<Tuple2<String, Long>> out) throws Exception {

switch (in.f1) {
case "START":
// set the start time if we receive a start event.
startTime.update(ctx.timestamp());
// register a timer in four hours from the start event.
ctx.timerService()
.registerEventTimeTimer(ctx.timestamp() + 4 * 60 * 60 * 1000);
break;
case "END":
// emit the duration between start and end event
Long sTime = startTime.value();
if (sTime != null) {
out.collect(Tuple2.of(in.f0, ctx.timestamp() - sTime));
// clear the state
startTime.clear();
}
default:
// do nothing
}
}

/** Called when a timer fires. */
@Override
public void onTimer(
long timestamp,
OnTimerContext ctx,
Collector<Tuple2<String, Long>> out) {

// Timeout interval exceeded. Cleaning up the state.
startTime.clear();
}
}

这个例子充分展现了 KeyedProcessFunction 强大的表达力,也因此是一个实现相当复杂的接口。

DataStream API

DataStream API 为许多通用的流处理操作提供了处理原语。这些操作包括窗口、逐条记录的转换操作,在处理事件时进行外部数据库查询等。DataStream API 支持 Java 和 Scala 语言,预先定义了例如map()reduce()aggregate() 等函数。你可以通过扩展实现预定义接口或使用 Java、Scala 的 lambda 表达式实现自定义的函数。

下面的代码示例展示了如何捕获会话时间范围内所有的点击流事件,并对每一次会话的点击量进行计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 网站点击 Click 的数据流
DataStream<Click> clicks = ...

DataStream<Tuple2<String, Long>> result = clicks
// 将网站点击映射为 (userId, 1) 以便计数
.map(
// 实现 MapFunction 接口定义函数
new MapFunction<Click, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(Click click) {
return Tuple2.of(click.userId, 1L);
}
})
// 以 userId (field 0) 作为 key
.keyBy(0)
// 定义 30 分钟超时的会话窗口
.window(EventTimeSessionWindows.withGap(Time.minutes(30L)))
// 对每个会话窗口的点击进行计数,使用 lambda 表达式定义 reduce 函数
.reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1));

SQL & Table API

Flink 支持两种关系型的 API,Table API 和 SQL。这两个 API 都是批处理和流处理统一的 API,这意味着在无边界的实时数据流和有边界的历史记录数据流上,关系型 API 会以相同的语义执行查询,并产生相同的结果。Table API 和 SQL 借助了 Apache Calcite 来进行查询的解析,校验以及优化。它们可以与 DataStream 和 DataSet API 无缝集成,并支持用户自定义的标量函数,聚合函数以及表值函数。

Flink 的关系型 API 旨在简化数据分析数据流水线和 ETL 应用的定义。

下面的代码示例展示了如何使用 SQL 语句查询捕获会话时间范围内所有的点击流事件,并对每一次会话的点击量进行计数。此示例与上述 DataStream API 中的示例有着相同的逻辑。

1
2
3
SELECT userId, COUNT(*)
FROM clicks
GROUP BY SESSION(clicktime, INTERVAL '30' MINUTE), userId

Flink 具有数个适用于常见数据处理应用场景的扩展库。这些库通常嵌入在 API 中,且并不完全独立于其它 API。它们也因此可以受益于 API 的所有特性,并与其他库集成。

  • **复杂事件处理(CEP)**:模式检测是事件流处理中的一个非常常见的用例。Flink 的 CEP 库提供了 API,使用户能够以例如正则表达式或状态机的方式指定事件模式。CEP 库与 Flink 的 DataStream API 集成,以便在 DataStream 上评估模式。CEP 库的应用包括网络入侵检测,业务流程监控和欺诈检测。
  • **DataSet API*:DataSet API 是 Flink 用于批处理应用程序的核心 API。DataSet API 所提供的基础算子包括mapreduce_、(outer) join_、_co-group_、iterate*等。所有算子都有相应的算法和数据结构支持,对内存中的序列化数据进行操作。如果数据大小超过预留内存,则过量数据将存储到磁盘。Flink 的 DataSet API 的数据处理算法借鉴了传统数据库算法的实现,例如混合散列连接(hybrid hash-join)和外部归并排序(external merge-sort)。
  • Gelly: Gelly 是一个可扩展的图形处理和分析库。Gelly 是在 DataSet API 之上实现的,并与 DataSet API 集成。因此,它能够受益于其可扩展且健壮的操作符。Gelly 提供了内置算法,如 label propagation、triangle enumeration 和 page rank 算法,也提供了一个简化自定义图算法实现的 Graph API

参考资料

Flink 架构

Apache Flink 是一个分布式系统,它需要计算资源来执行应用程序。Flink 集成了所有常见的集群资源管理器,例如 Hadoop YARNApache MesosKubernetes,但同时也可以作为独立集群运行。

Flink 被设计为能够很好地工作在上述每个资源管理器中,这是通过资源管理器特定(resource-manager-specific)的部署模式实现的。Flink 可以采用与当前资源管理器相适应的方式进行交互。

部署 Flink 应用程序时,Flink 会根据应用程序配置的并行性自动标识所需的资源,并从资源管理器请求这些资源。在发生故障的情况下,Flink 通过请求新资源来替换发生故障的容器。提交或控制应用程序的所有通信都是通过 REST 调用进行的,这可以简化 Flink 与各种环境中的集成。

运行任意规模应用

Flink 旨在任意规模上运行有状态流式应用。因此,应用程序被并行化为可能数千个任务,这些任务分布在集群中并发执行。所以应用程序能够充分利用无尽的 CPU、内存、磁盘和网络 IO。而且 Flink 很容易维护非常大的应用程序状态。其异步和增量的检查点算法对处理延迟产生最小的影响,同时保证精确一次状态的一致性。

Flink 用户报告了其生产环境中一些令人印象深刻的扩展性数字

  • 处理每天处理数万亿的事件,
  • 应用维护几 TB 大小的状态
  • 应用在数千个内核上运行

利用内存性能

有状态的 Flink 程序针对本地状态访问进行了优化。任务的状态始终保留在内存中,如果状态大小超过可用内存,则会保存在能高效访问的磁盘数据结构中。任务通过访问本地(通常在内存中)状态来进行所有的计算,从而产生非常低的处理延迟。Flink 通过定期和异步地对本地状态进行持久化存储来保证故障场景下精确一次的状态一致性。

img

Flink 运行时由两种类型的进程组成:一个 JobManager 和一个或者多个 **TaskManager**。

The processes involved in executing a Flink dataflow

Client 不是运行时和程序执行的一部分,而是用于准备数据流并将其发送给 JobManager。之后,客户端可以断开连接(_分离模式_),或保持连接来接收进程报告(_附加模式_)。客户端可以作为触发执行 Java/Scala 程序的一部分运行,也可以在命令行进程 ./bin/flink run ... 中运行。

可以通过多种方式启动 JobManagerTaskManager:直接在机器上作为 standalone 集群启动、在容器中启动、或者通过 YARN 等资源框架管理并启动。TaskManager 连接到 JobManager,宣布自己可用,并被分配工作。

JobManager

JobManager 具有许多与协调 Flink 应用程序的分布式执行有关的职责,它决定

  • 何时调度下一个 task(或一组 task)
  • 对完成的 task 或执行失败做出反应
  • 协调 checkpoint
  • 协调从失败中恢复
  • 等等

JobManager 由三个不同的组件组成:

  • ResourceManager:负责 Flink 集群中的资源提供、回收、分配 - 它管理 task slots,这是 Flink 集群中资源调度的单位(请参考 TaskManager。Flink 为不同的环境和资源提供者(例如 YARN、Kubernetes 和 standalone 部署)实现了对应的 ResourceManager。在 standalone 设置中,ResourceManager 只能分配可用 TaskManager 的 slots,而不能自行启动新的 TaskManager。

  • Dispatcher:提供了一个 REST 接口,用来提交 Flink 应用程序执行,并为每个提交的作业启动一个新的 JobMaster。它还运行 Flink WebUI 用来提供作业执行信息。

  • JobMaster:负责管理单个 JobGraph 的执行。Flink 集群中可以同时运行多个作业,每个作业都有自己的 JobMaster。

始终至少有一个 JobManager。高可用(HA)设置中可能有多个 JobManager,其中一个始终是 _leader_,其他的则是 _standby_(请参考 高可用(HA))。

TaskManager

_TaskManager_(也称为 _worker_)执行作业流的 task,并且缓存和交换数据流。

必须始终至少有一个 TaskManager。在 TaskManager 中资源调度的最小单位是 task _slot_。TaskManager 中 task slot 的数量表示并发处理 task 的数量。请注意一个 task slot 中可以执行多个算子(请参考Tasks 和算子链)。

Tasks 和算子链

对于分布式执行,Flink 将算子的 subtasks 链接成 _tasks_。每个 task 由一个线程执行。将算子链接成 task 是个有用的优化:它减少线程间切换、缓冲的开销,并且减少延迟的同时增加整体吞吐量。链行为是可以配置的;请参考链文档以获取详细信息。

下图中样例数据流用 5 个 subtask 执行,因此有 5 个并行线程。

Operator chaining into Tasks

Task Slots 和资源

每个 worker(TaskManager)都是一个 _JVM 进程_,可以在单独的线程中执行一个或多个 subtask。为了控制一个 TaskManager 中接受多少个 task,就有了所谓的 task slots(至少一个)。

每个 task slot 代表 TaskManager 中资源的固定子集。例如,具有 3 个 slot 的 TaskManager,会将其托管内存 1/3 用于每个 slot。分配资源意味着 subtask 不会与其他作业的 subtask 竞争托管内存,而是具有一定数量的保留托管内存。注意此处没有 CPU 隔离;当前 slot 仅分离 task 的托管内存。

通过调整 task slot 的数量,用户可以定义 subtask 如何互相隔离。每个 TaskManager 有一个 slot,这意味着每个 task 组都在单独的 JVM 中运行(例如,可以在单独的容器中启动)。具有多个 slot 意味着更多 subtask 共享同一 JVM。同一 JVM 中的 task 共享 TCP 连接(通过多路复用)和心跳信息。它们还可以共享数据集和数据结构,从而减少了每个 task 的开销。

A TaskManager with Task Slots and Tasks

默认情况下,Flink 允许 subtask 共享 slot,即便它们是不同的 task 的 subtask,只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 slot 共享有两个主要优点:

  • Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task(具有不同并行度)。
  • 容易获得更好的资源利用。如果没有 slot 共享,非密集 subtask(_source/map()_)将阻塞和密集型 subtask(_window_) 一样多的资源。通过 slot 共享,我们示例中的基本并行度从 2 增加到 6,可以充分利用分配的资源,同时确保繁重的 subtask 在 TaskManager 之间公平分配。

TaskManagers with shared Task Slots

Flink 应用程序 是从其 main() 方法产生的一个或多个 Flink 作业的任何用户程序。这些作业的执行可以在本地 JVM(LocalEnvironment)中进行,或具有多台机器的集群的远程设置(RemoteEnvironment)中进行。对于每个程序,ExecutionEnvironment 提供了一些方法来控制作业执行(例如设置并行度)并与外界交互(请参考 Flink 程序剖析 )。

Flink 应用程序的作业可以被提交到长期运行的 Flink Session 集群、专用的 Flink Job 集群Flink Application 集群。这些选项之间的差异主要与集群的生命周期和资源隔离保证有关。

  • 集群生命周期:在 Flink Session 集群中,客户端连接到一个预先存在的、长期运行的集群,该集群可以接受多个作业提交。即使所有作业完成后,集群(和 JobManager)仍将继续运行直到手动停止 session 为止。因此,Flink Session 集群的寿命不受任何 Flink 作业寿命的约束。
  • 资源隔离:TaskManager slot 由 ResourceManager 在提交作业时分配,并在作业完成时释放。由于所有作业都共享同一集群,因此在集群资源方面存在一些竞争 — 例如提交工作阶段的网络带宽。此共享设置的局限性在于,如果 TaskManager 崩溃,则在此 TaskManager 上运行 task 的所有作业都将失败;类似的,如果 JobManager 上发生一些致命错误,它将影响集群中正在运行的所有作业。
  • 其他注意事项:拥有一个预先存在的集群可以节省大量时间申请资源和启动 TaskManager。有种场景很重要,作业执行时间短并且启动时间长会对端到端的用户体验产生负面的影响 — 就像对简短查询的交互式分析一样,希望作业可以使用现有资源快速执行计算。

以前,Flink Session 集群也被称为 session 模式下的 Flink 集群。

  • 集群生命周期:在 Flink Job 集群中,可用的集群管理器(例如 YARN)用于为每个提交的作业启动一个集群,并且该集群仅可用于该作业。在这里,客户端首先从集群管理器请求资源启动 JobManager,然后将作业提交给在这个进程中运行的 Dispatcher。然后根据作业的资源请求惰性的分配 TaskManager。一旦作业完成,Flink Job 集群将被拆除。
  • 资源隔离:JobManager 中的致命错误仅影响在 Flink Job 集群中运行的一个作业。
  • 其他注意事项:由于 ResourceManager 必须应用并等待外部资源管理组件来启动 TaskManager 进程和分配资源,因此 Flink Job 集群更适合长期运行、具有高稳定性要求且对较长的启动时间不敏感的大型作业。

以前,Flink Job 集群也被称为 job (or per-job) 模式下的 Flink 集群。

Kubernetes 不支持 Flink Job 集群。 请参考 Standalone KubernetesNative Kubernetes

  • 集群生命周期:Flink Application 集群是专用的 Flink 集群,仅从 Flink 应用程序执行作业,并且 main()方法在集群上而不是客户端上运行。提交作业是一个单步骤过程:无需先启动 Flink 集群,然后将作业提交到现有的 session 集群;相反,将应用程序逻辑和依赖打包成一个可执行的作业 JAR 中,并且集群入口(ApplicationClusterEntryPoint)负责调用 main()方法来提取 JobGraph。例如,这允许你像在 Kubernetes 上部署任何其他应用程序一样部署 Flink 应用程序。因此,Flink Application 集群的寿命与 Flink 应用程序的寿命有关。
  • 资源隔离:在 Flink Application 集群中,ResourceManager 和 Dispatcher 作用于单个的 Flink 应用程序,相比于 Flink Session 集群,它提供了更好的隔离。

Flink Job 集群可以看做是 Flink Application 集群”客户端运行“的替代方案。

默认情况下,每个 Flink 集群只有一个 JobManager 实例。这会导致 _单点故障(SPOF)_:如果 JobManager 崩溃,则不能提交任何新程序,运行中的程序也会失败。

使用 JobManager 高可用模式,你可以从 JobManager 失败中恢复,从而消除单点故障。

如何启用集群高可用

JobManager 高可用是指,在任何时候都有一个 JobManager Leader,如果 Leader 出现故障,则有多个备用 JobManager 来接管领导。这解决了单点故障问题,只要有备用 JobManager 担任领导者,程序就可以继续运行。

如下是一个使用三个 JobManager 实例的例子:

img

Flink 的 高可用服务 封装了所需的服务,使一切可以正常工作:

  • 领导者选举:从 n 个候选者中选出一个领导者
  • 服务发现:检索当前领导者的地址
  • 状态持久化:继承程序恢复作业所需的持久化状态(JobGraphs、用户代码 jar、已完成的检查点)

Flink 提供了两种高可用服务实现:

  • ZooKeeper:每个 Flink 集群部署都可以使用 ZooKeeper HA 服务。它们需要一个运行的 ZooKeeper 复制组(quorum)。
  • Kubernetes:Kubernetes HA 服务只能运行在 Kubernetes 上。

高可用数据生命周期

为了恢复提交的作业,Flink 持久化元数据和 job 组件。高可用数据将一直保存,直到相应的作业执行成功、被取消或最终失败。当这些情况发生时,将删除所有高可用数据,包括存储在高可用服务中的元数据。

参考资料

Java 基础语法特性

注释

空白行,或者注释的内容,都会被 Java 编译器忽略掉。

Java 支持多种注释方式,下面的示例展示了各种注释的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloWorld {
/*
* JavaDoc 注释
*/
public static void main(String[] args) {
// 单行注释
/* 多行注释:
1. 注意点a
2. 注意点b
*/
System.out.println("Hello World");
}
}

基本数据类型

img

👉 扩展阅读:深入理解 Java 基本数据类型

变量和常量

Java 支持的变量类型有:

  • 局部变量 - 类方法中的变量。
  • 成员变量(也叫实例变量) - 类方法外的变量,不过没有 static 修饰。
  • 静态变量(也叫类变量) - 类方法外的变量,用 static 修饰。

特性对比:

局部变量 实例变量(也叫成员变量) 类变量(也叫静态变量)
局部变量声明在方法、构造方法或者语句块中。 实例变量声明在方法、构造方法和语句块之外。 类变量声明在方法、构造方法和语句块之外。并且以 static 修饰。
局部变量在方法、构造方法、或者语句块被执行的时候创建,当它们执行完成后,变量将会被销毁。 实例变量在对象创建的时候创建,在对象被销毁的时候销毁。 类变量在第一次被访问时创建,在程序结束时销毁。
局部变量没有默认值,所以必须经过初始化,才可以使用。 实例变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。 类变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定。此外,静态变量还可以在静态语句块中初始化。
对于局部变量,如果是基本类型,会把值直接存储在栈;如果是引用类型,会把其对象存储在堆,而把这个对象的引用(指针)存储在栈。 实例变量存储在堆。 类变量存储在静态存储区。
访问修饰符不能用于局部变量。 访问修饰符可以用于实例变量。 访问修饰符可以用于类变量。
局部变量只在声明它的方法、构造方法或者语句块中可见。 实例变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把实例变量设为私有。通过使用访问修饰符可以使实例变量对子类可见。 与实例变量具有相似的可见性。但为了对类的使用者可见,大多数静态变量声明为 public 类型。
实例变量可以直接通过变量名访问。但在静态方法以及其他类中,就应该使用完全限定名:ObejectReference.VariableName。 静态变量可以通过:ClassName.VariableName 的方式访问。
无论一个类创建了多少个对象,类只拥有类变量的一份拷贝。
类变量除了被声明为常量外很少使用。

变量修饰符

  • 访问级别修饰符
    • 如果变量是实例变量或类变量,可以添加访问级别修饰符(public/protected/private)
  • 静态修饰符
    • 如果变量是类变量,需要添加 static 修饰
  • final
    • 如果变量使用 final 修饰符,就表示这是一个常量,不能被修改。

数组

img

👉 扩展阅读:深入理解 Java 数组

枚举

img

👉 扩展阅读:深入理解 Java 枚举

操作符

Java 中支持的操作符类型如下:

img

👉 扩展阅读:Java 操作符

方法

img

👉 扩展阅读:深入理解 Java 方法

控制语句

img

👉 扩展阅读:Java 控制语句

异常

img

img

👉 扩展阅读:深入理解 Java 异常

泛型

img

👉 扩展阅读:深入理解 Java 泛型

反射

img

img

👉 扩展阅读:深入理解 Java 反射和动态代理

注解

img

img

img

img

👉 扩展阅读:深入理解 Java 注解

序列化

img

👉 扩展阅读:Java 序列化

Elasticsearch 性能优化

Elasticsearch 是当前流行的企业级搜索引擎,设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能确保其的性能和稳定性。如何根据实际情况提高服务的性能,其实有很多技巧。这章我们分享从实战经验中总结出来的 elasticsearch 性能优化,主要从硬件配置优化、索引优化设置、查询方面优化、数据结构优化、集群架构优化等方面讲解。

硬件配置优化

升级硬件设备配置一直都是提高服务能力最快速有效的手段,在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和 IO,可以从这三方面进行 ES 的性能优化工作。

CPU 配置

一般说来,CPU 繁忙的原因有以下几个:

  1. 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算;
  2. 发生了频繁的 GC;
  3. 多线程的上下文切换;

大多数 Elasticsearch 部署往往对 CPU 要求不高。因此,相对其它资源,具体配置多少个(CPU)不是那么关键。你应该选择具有多个内核的现代处理器,常见的集群使用 2 到 8 个核的机器。如果你要在更快的 CPUs 和更多的核数之间选择,选择更多的核数更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。

内存配置

如果有一种资源是最先被耗尽的,它可能是内存。排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的。即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存。因为 Lucene 使用的许多数据结构是基于磁盘的格式,Elasticsearch 利用操作系统缓存能产生很大效果。

64 GB 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的。少于 8 GB 会适得其反(你最终需要很多很多的小机器),大于 64 GB 的机器也会有问题。

由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。

内存分配

当机器内存小于 64G 时,遵循通用的原则,50% 给 ES,50% 留给 lucene。

当机器内存大于 64G 时,遵循以下原则:

  • 如果主要的使用场景是全文检索,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能。
  • 如果主要的使用场景是聚合或排序,并且大多数是 numerics,dates,geo_points 以及 not_analyzed 的字符类型,建议分配给 ES Heap 分配 4~32G 的内存即可,其它内存留给操作系统,供 lucene 使用,提供快速的基于文档的聚类、排序性能。
  • 如果使用场景是聚合或排序,并且都是基于 analyzed 字符数据,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50% 的 ES heap 设置(但不超过 32 G,堆内存设置 32 G 以下时,JVM 使用对象指标压缩技巧节省空间),50% 以上留给 lucene。

禁止 swap

禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。可以通过在 elasticsearch.yml 中 bootstrap.memory_lock: true,以保持 JVM 锁定内存,保证 ES 的性能。

GC 设置

保持 GC 的现有设置,默认设置为:Concurrent-Mark and Sweep(CMS),别换成 G1 GC,因为目前 G1 还有很多 BUG。

保持线程池的现有设置,目前 ES 的线程池较 1.X 有了较多优化设置,保持现状即可;默认线程池大小等于 CPU 核心数。如果一定要改,按公式 ( ( CPU 核心数 * 3 ) / 2 ) + 1 设置;不能超过 CPU 核心数的 2 倍;但是不建议修改默认配置,否则会对 CPU 造成硬伤。

磁盘

硬盘对所有的集群都很重要,对大量写入的集群更是加倍重要(例如那些存储日志数据的)。硬盘是服务器上最慢的子系统,这意味着那些写入量很大的集群很容易让硬盘饱和,使得它成为集群的瓶颈。

在经济压力能承受的范围下,尽量使用固态硬盘(SSD)。固态硬盘相比于任何旋转介质(机械硬盘,磁带等),无论随机写还是顺序写,都会对 IO 有较大的提升。

如果你正在使用 SSDs,确保你的系统 I/O 调度程序是配置正确的。当你向硬盘写数据,I/O 调度程序决定何时把数据实际发送到硬盘。大多数默认 *nix 发行版下的调度程序都叫做 cfq(完全公平队列)。

调度程序分配时间片到每个进程。并且优化这些到硬盘的众多队列的传递。但它是为旋转介质优化的:机械硬盘的固有特性意味着它写入数据到基于物理布局的硬盘会更高效。

这对 SSD 来说是低效的,尽管这里没有涉及到机械硬盘。但是,deadline 或者 noop 应该被使用。deadline 调度程序基于写入等待时间进行优化,noop 只是一个简单的 FIFO 队列。

这个简单的更改可以带来显著的影响。仅仅是使用正确的调度程序,我们看到了 500 倍的写入能力提升。

如果你使用旋转介质(如机械硬盘),尝试获取尽可能快的硬盘(高性能服务器硬盘,15k RPM 驱动器)

使用 RAID0 是提高硬盘速度的有效途径,对机械硬盘和 SSD 来说都是如此。没有必要使用镜像或其它 RAID 变体,因为 Elasticsearch 在自身层面通过副本,已经提供了备份的功能,所以不需要利用磁盘的备份功能,同时如果使用磁盘备份功能的话,对写入速度有较大的影响。

最后,避免使用网络附加存储(NAS)。人们常声称他们的 NAS 解决方案比本地驱动器更快更可靠。除却这些声称,我们从没看到 NAS 能配得上它的大肆宣传。NAS 常常很慢,显露出更大的延时和更宽的平均延时方差,而且它是单点故障的。

索引优化设置

索引优化主要是在 Elasticsearch 的插入层面优化,Elasticsearch 本身索引速度其实还是蛮快的,具体数据,我们可以参考官方的 benchmark 数据。我们可以根据不同的需求,针对索引优化。

批量提交

当有大量数据提交的时候,建议采用批量提交(Bulk 操作);此外使用 bulk 请求时,每个请求不超过几十 M,因为太大会导致内存使用过大。

比如在做 ELK 过程中,Logstash indexer 提交数据到 Elasticsearch 中,batch size 就可以作为一个优化功能点。但是优化 size 大小需要根据文档大小和服务器性能而定。

像 Logstash 中提交文档大小超过 20MB,Logstash 会将一个批量请求切分为多个批量请求。

如果在提交过程中,遇到 EsRejectedExecutionException 异常的话,则说明集群的索引性能已经达到极限了。这种情况,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集 Warn、Error 级别以上的日志。

增加 Refresh 时间间隔

为了提高索引性能,Elasticsearch 在写入数据的时候,采用延迟写入的策略,即数据先写到内存中,当超过默认 1 秒(index.refresh_interval)会进行一次写入操作,就是将内存中 segment 数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么 Elasticsearch 提供的是近实时搜索功能,而不是实时搜索功能。

如果我们的系统对数据延迟要求不高的话,我们可以通过延长 refresh 时间间隔,可以有效地减少 segment 合并压力,提高索引速度。比如在做全链路跟踪的过程中,我们就将 index.refresh_interval 设置为 30s,减少 refresh 次数。再如,在进行全量索引时,可以将 refresh 次数临时关闭,即 index.refresh_interval 设置为-1,数据导入成功后再打开到正常模式,比如 30s。

在加载大量数据时候可以暂时不用 refresh 和 repliccas,index.refresh_interval 设置为-1,index.number_of_replicas 设置为 0。

修改 index_buffer_size 的设置

索引缓冲的设置可以控制多少内存分配给索引进程。这是一个全局配置,会应用于一个节点上所有不同的分片上。

1
2
indices.memory.index_buffer_size: 10%
indices.memory.min_index_buffer_size: 48mb

indices.memory.index_buffer_size 接受一个百分比或者一个表示字节大小的值。默认是 10%,意味着分配给节点的总内存的 10%用来做索引缓冲的大小。这个数值被分到不同的分片(shards)上。如果设置的是百分比,还可以设置 min_index_buffer_size (默认 48mb)和 max_index_buffer_size(默认没有上限)。

修改 translog 相关的设置

一是控制数据从内存到硬盘的操作频率,以减少硬盘 IO。可将 sync_interval 的时间设置大一些。默认为 5s。

1
index.translog.sync_interval: 5s

也可以控制 tranlog 数据块的大小,达到 threshold 大小时,才会 flush 到 lucene 索引文件。默认为 512m。

1
index.translog.flush_threshold_size: 512mb

注意 _id 字段的使用

_id 字段的使用,应尽可能避免自定义 _id,以避免针对 ID 的版本管理;建议使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键。

注意 _all 字段及 _source 字段的使用

**_**all 字段及 _source 字段的使用,应该注意场景和需要,_all 字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用;_source 存储了原始的 document 内容,如果没有获取原始文档数据的需求,可通过设置 includes、excludes 属性来定义放入 _source 的字段。

合理的配置使用 index 属性

合理的配置使用 index 属性,analyzed 和 not_analyzed,根据业务需求来控制字段是否分词或不分词。只有 groupby 需求的字段,配置时就设置成 not_analyzed,以提高查询或聚类的效率。

减少副本数量

Elasticsearch 默认副本数量为 3 个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率。

在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束。使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为 3 个,但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为 1 个。

查询方面优化

Elasticsearch 作为业务搜索的近实时查询时,查询效率的优化显得尤为重要。

路由优化

当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来的。

1
shard = hash(routing) % number_of_primary_shards

routing 默认值是文档的 id,也可以采用自定义值,比如用户 ID。

不带 routing 查询

在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤:

  1. 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
  2. 聚合:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。

带 routing 查询

查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。

向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。

Filter VS Query

尽可能使用过滤器上下文(Filter)替代查询上下文(Query)

  • Query:此文档与此查询子句的匹配程度如何?
  • Filter:此文档和查询子句匹配吗?

Elasticsearch 针对 Filter 查询只需要回答“是”或者“否”,不需要像 Query 查询一样计算相关性分数,同时 Filter 结果可以缓存。

深度翻页

在使用 Elasticsearch 过程中,应尽量避免大翻页的出现。

正常翻页查询都是从 from 开始 size 条数据,这样就需要在每个分片中查询打分排名在前面的 from+size 条数据。协同节点收集每个分配的前 from+size 条数据。协同节点一共会受到 N*(from+size) 条数据,然后进行排序,再将其中 from 到 from+size 条数据返回出去。如果 from 或者 size 很大的话,导致参加排序的数量会同步扩大很多,最终会导致 CPU 资源消耗增大。

可以通过使用 Elasticsearch scroll 和 scroll-scan 高效滚动的方式来解决这样的问题。

也可以结合实际业务特点,文档 id 大小如果和文档创建时间是一致有序的,可以以文档 id 作为分页的偏移量,并将其作为分页查询的一个条件。

脚本(script)合理使用

我们知道脚本使用主要有 3 种形式,内联动态编译方式、_script 索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 _script 索引库中,起到提前编译,然后通过引用脚本 id,并结合 params 参数使用,即可以达到模型(逻辑)和数据进行了分离,同时又便于脚本模块的扩展与维护。具体 ES 脚本的深入内容请参考 Elasticsearch 脚本模块的详解

数据结构优化

基于 Elasticsearch 的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据。

尽量减少不需要的字段

如果 Elasticsearch 用于业务搜索服务,一些不需要用于搜索的字段最好不存到 ES 中,这样即节省空间,同时在相同的数据量下,也能提高搜索性能。

避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重。

以下是 ES 关于字段数、mapping 深度的一些默认设置:

1
2
3
index.mapping.nested_objects.limit: 10000
index.mapping.total_fields.limit: 1000
index.mapping.depth.limit: 20

Nested Object vs Parent/Child

尽量避免使用 nested 或 parent/child 的字段,能不用就不用;nested query 慢,parent/child query 更慢,比 nested query 慢上百倍;因此能在 mapping 设计阶段搞定的(大宽表设计或采用比较 smart 的数据结构),就不要用父子关系的 mapping。

如果一定要使用 nested fields,保证 nested fields 字段不能过多,目前 ES 默认限制是 50。因为针对 1 个 document,每一个 nested field,都会生成一个独立的 document,这将使 doc 数量剧增,影响查询效率,尤其是 JOIN 的效率。

1
index.mapping.nested_fields.limit: 50
对比 Nested Object Parent/Child
优点 文档存储在一起,因此读取性高 父子文档可以独立更新,互不影响
缺点 更新父文档或子文档时需要更新整个文档 为了维护 join 关系,需要占用部分内存,读取性能较差
场景 子文档偶尔更新,查询频繁 子文档更新频繁

选择静态映射,非必需时,禁止动态映射

尽量避免使用动态映射,这样有可能会导致集群崩溃,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务。

此外,Elasticsearch 作为搜索引擎时,主要承载 query 的匹配和排序的功能,那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对 query 匹配用,另一类字段是用做粗排用到的特征字段,如 ctr、点击数、评论数等等。

集群架构设计

合理的部署 Elasticsearch 有助于提高服务的整体可用性。

主节点、数据节点和协调节点分离

Elasticsearch 集群在架构拓朴时,采用主节点、数据节点和负载均衡节点分离的架构,在 5.x 版本以后,又可将数据节点再细分为“Hot-Warm”的架构模式。

Elasticsearch 的配置文件中有 2 个参数,node.master 和 node.data。这两个参数搭配使用时,能够帮助提供服务器性能。

主(master)节点

配置 node.master:truenode.data:false,该 node 服务器只作为一个主节点,但不存储任何索引数据。我们推荐每个集群运行 3 个专用的 master 节点来提供最好的弹性。使用时,你还需要将 discovery.zen.minimum_master_nodes setting 参数设置为 2,以免出现脑裂(split-brain)的情况。用 3 个专用的 master 节点,专门负责处理集群的管理以及加强状态的整体稳定性。因为这 3 个 master 节点不包含数据也不会实际参与搜索以及索引操作,在 JVM 上它们不用做相同的事,例如繁重的索引或者耗时,资源耗费很大的搜索。因此不太可能会因为垃圾回收而导致停顿。因此,master 节点的 CPU,内存以及磁盘配置可以比 data 节点少很多的。

数据(data)节点

配置 node.master:falsenode.data:true,该 node 服务器只作为一个数据节点,只用于存储索引数据,使该 node 服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。

在 Elasticsearch 5.x 版本之后,data 节点又可再细分为“Hot-Warm”架构,即分为热节点(hot node)和暖节点(warm node)。

hot 节点:

hot 节点主要是索引节点(写节点),同时会保存近期的一些频繁被查询的索引。由于进行索引非常耗费 CPU 和 IO,即属于 IO 和 CPU 密集型操作,建议使用 SSD 的磁盘类型,保持良好的写性能;我们推荐部署最小化的 3 个 hot 节点来保证高可用性。根据近期需要收集以及查询的数据量,可以增加服务器数量来获得想要的性能。

将节点设置为 hot 类型需要 elasticsearch.yml 如下配置:

1
node.attr.box_type: hot

如果是针对指定的 index 操作,可以通过 settings 设置 index.routing.allocation.require.box_type: hot 将索引写入 hot 节点。

warm 节点:

这种类型的节点是为了处理大量的,而且不经常访问的只读索引而设计的。由于这些索引是只读的,warm 节点倾向于挂载大量磁盘(普通磁盘)来替代 SSD。内存、CPU 的配置跟 hot 节点保持一致即可;节点数量一般也是大于等于 3 个。

将节点设置为 warm 类型需要 elasticsearch.yml 如下配置:

1
node.attr.box_type: warm

同时,也可以在 elasticsearch.yml 中设置 index.codec:best_compression 保证 warm 节点的压缩配置。

当索引不再被频繁查询时,可通过 index.routing.allocation.require.box_type:warm,将索引标记为 warm,从而保证索引不写入 hot 节点,以便将 SSD 磁盘资源用在刀刃上。一旦设置这个属性,ES 会自动将索引合并到 warm 节点。

协调(coordinating)节点

协调节点用于做分布式里的协调,将各分片或节点返回的数据整合后返回。该节点不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个 node 服务器上查询数据,并将请求分发到多个指定的 node 服务器,并对各个 node 服务器返回的结果进行一个汇总处理,最终返回给客户端。在 ES 集群中,所有的节点都有可能是协调节点,但是,可以通过设置 node.masternode.datanode.ingest 都为 false 来设置专门的协调节点。需要较好的 CPU 和较高的内存。

  • node.master:false 和 node.data:true,该 node 服务器只作为一个数据节点,只用于存储索引数据,使该 node 服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。
  • node.master:true 和 node.data:false,该 node 服务器只作为一个主节点,但不存储任何索引数据,该 node 服务器将使用自身空闲的资源,来协调各种创建索引请求或者查询请求,并将这些请求合理分发到相关的 node 服务器上。
  • node.master:false 和 node.data:false,该 node 服务器即不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个 node 服务器上查询数据,并将请求分发到多个指定的 node 服务器,并对各个 node 服务器返回的结果进行一个汇总处理,最终返回给客户端。

关闭 data 节点服务器中的 http 功能

针对 Elasticsearch 集群中的所有数据节点,不用开启 http 服务。将其中的配置参数这样设置,http.enabled:false,同时也不要安装 head, bigdesk, marvel 等监控插件,这样保证 data 节点服务器只需处理创建/更新/删除/查询索引数据等操作。

http 功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控 Elasticsearch 集群状态等数据信息。这样做一来出于数据安全考虑,二来出于服务性能考虑。

一台服务器上最好只部署一个 node

一台物理服务器上可以启动多个 node 服务器节点(通过设置不同的启动 port),但一台服务器上的 CPU、内存、硬盘等资源毕竟有限,从服务器性能考虑,不建议一台服务器上启动多个 node 节点。

集群分片设置

ES 一旦创建好索引后,就无法调整分片的设置,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:

  1. 控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。
  2. 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍

参考资料

Elasticsearch 排序

在 Elasticsearch 中,默认排序是按照相关性的评分(_score)进行降序排序,也可以按照字段的值排序多级排序多值字段排序、基于 geo(地理位置)排序以及自定义脚本排序,除此之外,对于相关性的评分也可以用 rescore 二次、三次打分,它可以限定重新打分的窗口大小(window size),并针对作用范围内的文档修改其得分,从而达到精细化控制结果相关性的目的。

默认相关性排序

在 Elasticsearch 中,默认情况下,文档是按照相关性得分倒序排列的,其对应的相关性得分字段用 _score 来表示,它是浮点数类型,_score 评分越高,相关性越高。评分模型的选择可以通过 similarity 参数在映射中指定。

相似度算法可以按字段指定,只需在映射中为不同字段选定即可,如果要修改已有字段的相似度算法,只能通过为数据重新建立索引来达到目的。关于更多 es 相似度算法可以参考 深入理解 es 相似度算法(相关性得分计算)

TF-IDF 模型

Elasticsearch 在 5.4 版本以前,text 类型的字段,默认采用基于 tf-idf 的向量空间模型。

在开始计算得分之时,Elasticsearch 使用了被搜索词条的频率以及它有多常见来影响得分。一个简短的解释是,一个词条出现在某个文档中的次数越多,它就越相关;但是,如果该词条出现在不同的文档的次数越多,它就越不相关。这一点被称为 TF-IDF,TF 是词频(term frequency),IDF 是逆文档频率(inverse document frequency)。

考虑给一篇文档打分的首要方式,是统计一个词条在文本中出现的次数。举个例子,如果在用户的区域搜索关于 Elasticsearch 的 get-together,用户希望频繁提及 Elasticsearch 的分组被优先展示出来。

1
2
"We will discuss Elasticsearch at the next Big Data group."
"Tuesday the Elasticsearch team will gather to answer questions about Elasticsearch."

第一个句子提到 Elasticsearch 一次,而第二个句子提到 Elasticsearch 两次,所以包含第二句话的文档应该比包含第一句话的文档拥有更高的得分。如果我们要按照数量来讨论,第一句话的词频(TF)是 1,而第二句话的词频将是 2。

逆文档频率比文档词频稍微复杂一点。这个听上去很酷炫的描述意味着,如果一个分词(通常是单词,但不一定是)在索引的不同文档中出现越多的次数,那么它就越不重要。使用如下例子更容易解释这一点。

1
2
3
"We use Elasticsearch to power the search for our website."
"The developers like Elasticsearch so far."
"The scoring of documents is calculated by the scoring formula."

如上述例子,需要理解以下几点:

  • 词条 “Elasticsearch” 的文档频率是 2(因为它出现在两篇文档中)。文档频率的逆源自得分乘以 1/DF,这里 DF 是该词条的文档频率。这就意味着,由于词条拥有更高的文档频率,它的权重就会降低。
  • 词条 “the” 的文档频率是 3,因为它出现在所有的三篇文档中。请注意,尽管 “the” 在最后一篇文档中出现了两次,它的文档频率还是 3。这是因为,逆文档频率只检查一个词条是否出现在某文档中,而不检查它出现多少次。那个应该是词频所关心的事情。

逆文档频率是一个重要的因素,用于平衡词条的词频。举个例子,考虑有一个用户搜索词条 “the score”,单词 the 几乎出现在每个普通的英语文本中,如果它不被均衡一下,单词 the 的频率要完全淹没单词 score 的频率。逆文档频率 IDF 均衡了 the 这种常见词的相关性影响,所以实际的相关性得分将会对查询的词条有一个更准确的描述。

一旦词频 TF 和逆文档频率 IDF 计算完成,就可以使用 TF-IDF 公式来计算文档的得分。

BM25 模型

Elasticsearch 在 5.4 版本之后,针对 text 类型的字段,默认采用的是 BM25 评分模型,而不是基于 tf-idf 的向量空间模型,评分模型的选择可以通过 similarity 参数在映射中指定。

字段的值排序

在 Elasticsearch 中按照字段的值排序,可以利用 sort 参数实现。

1
2
3
4
5
6
7
8
GET books/_search
{
"sort": {
"price": {
"order": "desc"
}
}
}

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"took": 132,
"timed_out": false,
"_shards": {
"total": 10,
"successful": 10,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 749244,
"max_score": null,
"hits": [
{
"_index": "books",
"_type": "book",
"_id": "8456479",
"_score": null,
"_source": {
"id": 8456479,
"price": 1580.00,
...
},
"sort": [
1580.00
]
},
...
]
}
}

从如上返回结果,可以看出,max_score_score 字段都返回 null,返回字段多出 sort 字段,包含排序字段的分值。计算 _score 的花销巨大,如果不根据相关性排序,记录 _score 是没有意义的。如果无论如何都要计算 _score,可以将 track_scores 参数设置为 true

多字段排序

如果我们想要结合使用 price、date 和 _score 进行查询,并且匹配的结果首先按照价格排序,然后按照日期排序,最后按照相关性排序,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
GET books/_search
{
"query": {
"bool": {
"must": {
"match": { "content": "java" }
},
"filter": {
"term": { "user_id": 4868438 }
}
}
},
"sort": [{
"price": {
"order": "desc"
}
}, {
"date": {
"order": "desc"
}
}, {
"_score": {
"order": "desc"
}
}
]
}

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

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

多值字段的排序

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

对于数字或日期,你可以将多值字段减为单值,这可以通过使用 minmaxavg 或是 sum 排序模式。例如你可以按照每个 date 字段中的最早日期进行排序,通过以下方法:

1
2
3
4
5
6
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}

地理位置上的距离排序

es 的地理位置排序使用 _geo_distance 来进行距离排序,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"sort" : [
{
"_geo_distance" : {
"es_location_field" : [116.407526, 39.904030],
"order" : "asc",
"unit" : "km",
"mode" : "min",
"distance_type" : "plane"
}
}
],
"query" : {
......
}
}

_geo_distance 的选项具体如下:

  • 如上的 es_location_field 指的是 es 存储经纬度数据的字段名。
  • **_order**:指定按距离升序或降序,分别对应 **asc** 和 **desc_**。
  • **_unit**:计算距离值的单位,默认是 **m**,表示米(meters),其它可选项有 **micmmmNMkmftyd** 和 **in_**。
  • **_mode**:针对数组数据(多个值)时,指定的取值模式,可选值有 **minmaxsumavg** 和 **median_**,当排序采用升序时,默认为 _min_;排序采用降序时,默认为 _max_。
  • **_distance_type**:用来设置如何计算距离,它的可选项有 **sloppy_arcarc** 和 **plane**,默认为 sloppy_arc_,_arc 它相对更精确些,但速度会明显下降,_plane 则是计算快,但是长距离计算相对不准确。
  • **_ignore_unmapped**:未映射字段时,是否忽略处理,可选项有 **true** 和 **false_**;默认为 _false_,表示如果未映射字段,查询将引发异常;若设置 _true_,将忽略未映射的字段,并且不匹配此查询的任何文档。
  • **_validation_method**:指定检验经纬度数据的方式,可选项有 **IGNORE_MALFORMEDCOERCE** 和 **STRICT**;_IGNORE_MALFORMED 表示可接受纬度或经度无效的地理点,即忽略数据;COERCE 表示另外尝试并推断正确的地理坐标;STRICT 为默认值,表示遇到不正确的地理坐标直接抛出异常。

参考资料

Elasticsearch 聚合

Elasticsearch 是一个分布式的全文搜索引擎,索引和搜索是 Elasticsearch 的基本功能。事实上,Elasticsearch 的聚合(Aggregations)功能也十分强大,允许在数据上做复杂的分析统计。Elasticsearch 提供的聚合分析功能主要有指标聚合(metrics aggregations)桶聚合(bucket aggregations)管道聚合(pipeline aggregations)矩阵聚合(matrix aggregations) 四大类,管道聚合和矩阵聚合官方说明是在试验阶段,后期会完全更改或者移除,这里不再对管道聚合和矩阵聚合进行讲解。

聚合的具体结构

所有的聚合,无论它们是什么类型,都遵从以下的规则。

  • 使用查询中同样的 JSON 请求来定义它们,而且你是使用键 aggregations 或者是 aggs 来进行标记。需要给每个聚合起一个名字,指定它的类型以及和该类型相关的选项。
  • 它们运行在查询的结果之上。和查询不匹配的文档不会计算在内,除非你使用 global 聚集将不匹配的文档囊括其中。
  • 可以进一步过滤查询的结果,而不影响聚集。

以下是聚合的基本结构:

1
2
3
4
5
6
7
8
9
10
"aggregations" : { <!-- 最外层的聚合键,也可以缩写为 aggs -->
"<aggregation_name>" : { <!-- 聚合的自定义名字 -->
"<aggregation_type>" : { <!-- 聚合的类型,指标相关的,如 max、min、avg、sum,桶相关的 terms、filter 等 -->
<aggregation_body> <!-- 聚合体:对哪些字段进行聚合,可以取字段的值,也可以是脚本计算的结果 -->
}
[,"meta" : { [<meta_data_body>] } ]? <!-- 元 -->
[,"aggregations" : { [<sub_aggregation>]+ } ]? <!-- 在聚合里面在定义子聚合 -->
}
[,"<aggregation_name_2>" : { ... } ]* <!-- 聚合的自定义名字 2 -->
}
  • 在最上层有一个 aggregations 的键,可以缩写为 aggs
  • 在下面一层,需要为聚合指定一个名字。可以在请求的返回中看到这个名字。在同一个请求中使用多个聚合时,这一点非常有用,它让你可以很容易地理解每组结果的含义。
  • 最后,必须要指定聚合的类型。

关于聚合分析的值来源,可以取字段的值,也可以是脚本计算的结果

但是用脚本计算的结果时,需要注意脚本的性能和安全性;尽管多数聚集类型允许使用脚本,但是脚本使得聚集变得缓慢,因为脚本必须在每篇文档上运行。为了避免脚本的运行,可以在索引阶段进行计算。

此外,脚本也可以被人可能利用进行恶意代码攻击,尽量使用沙盒(sandbox)内的脚本语言。

示例:查询所有球员的平均年龄是多少,并对球员的平均薪水加 188(也可以理解为每名球员加 188 后的平均薪水)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /player/_search?size=0
{
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
},
"avg_salary_188": {
"avg": {
"script": {
"source": "doc.salary.value + 188"
}
}
}
}
}

指标聚合

指标聚合(又称度量聚合)主要从不同文档的分组中提取统计数据,或者,从来自其他聚合的文档桶来提取统计数据。

这些统计数据通常来自数值型字段,如最小或者平均价格。用户可以单独获取每项统计数据,或者也可以使用 stats 聚合来同时获取它们。更高级的统计数据,如平方和或者是标准差,可以通过 extended stats 聚合来获取。

Max Aggregation

Max Aggregation 用于最大值统计。例如,统计 sales 索引中价格最高的是哪本书,并且计算出对应的价格的 2 倍值,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /sales/_search?size=0
{
"aggs" : {
"max_price" : {
"max" : {
"field" : "price"
}
},
"max_price_2" : {
"max" : {
"field" : "price",
"script": {
"source": "_value * 2.0"
}
}
}
}
}

指定的 field,在脚本中可以用 _value 取字段的值

聚合结果如下:

1
2
3
4
5
6
7
8
9
10
11
{
...
"aggregations": {
"max_price": {
"value": 188.0
},
"max_price_2": {
"value": 376.0
}
}
}

Min Aggregation

Min Aggregation 用于最小值统计。例如,统计 sales 索引中价格最低的是哪本书,查询语句如下:

1
2
3
4
5
6
7
8
9
10
GET /sales/_search?size=0
{
"aggs" : {
"min_price" : {
"min" : {
"field" : "price"
}
}
}
}

聚合结果如下:

1
2
3
4
5
6
7
8
{
...
"aggregations": {
"min_price": {
"value": 18.0
}
}
}

Avg Aggregation

Avg Aggregation 用于计算平均值。例如,统计 exams 索引中考试的平均分数,如未存在分数,默认为 60 分,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
GET /exams/_search?size=0
{
"aggs" : {
"avg_grade" : {
"avg" : {
"field" : "grade",
"missing": 60
}
}
}
}

如果指定字段没有值,可以通过 missing 指定默认值;若未指定默认值,缺失该字段值的文档将被忽略(计算)

聚合结果如下:

1
2
3
4
5
6
7
8
{
...
"aggregations": {
"avg_grade": {
"value": 78.0
}
}
}

除了常规的平均值聚合计算外,elasticsearch 还提供了加权平均值的聚合计算,详情参见 Elasticsearch 指标聚合之 Weighted Avg Aggregation

Sum Aggregation

Sum Aggregation 用于计算总和。例如,统计 sales 索引中 type 字段中匹配 hat 的价格总和,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /exams/_search?size=0
{
"query" : {
"constant_score" : {
"filter" : {
"match" : { "type" : "hat" }
}
}
},
"aggs" : {
"hat_prices" : {
"sum" : { "field" : "price" }
}
}
}

聚合结果如下:

1
2
3
4
5
6
7
8
{
...
"aggregations": {
"hat_prices": {
"value": 567.0
}
}
}

Value Count Aggregation

Value Count Aggregation 可按字段统计文档数量。例如,统计 books 索引中包含 author 字段的文档数量,查询语句如下:

1
2
3
4
5
6
7
8
GET /books/_search?size=0
{
"aggs" : {
"doc_count" : {
"value_count" : { "field" : "author" }
}
}
}

聚合结果如下:

1
2
3
4
5
6
7
8
{
...
"aggregations": {
"doc_count": {
"value": 5
}
}
}

Cardinality Aggregation

Cardinality Aggregation 用于基数统计,其作用是先执行类似 SQL 中的 distinct 操作,去掉集合中的重复项,然后统计去重后的集合长度。例如,在 books 索引中对 language 字段进行 cardinality 操作可以统计出编程语言的种类数,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
GET /books/_search?size=0
{
"aggs" : {
"all_lan" : {
"cardinality" : { "field" : "language" }
},
"title_cnt" : {
"cardinality" : { "field" : "title.keyword" }
}
}
}

假设 title 字段为文本类型(text),去重时需要指定 keyword,表示把 title 作为整体去重,即不分词统计

聚合结果如下:

1
2
3
4
5
6
7
8
9
10
11
{
...
"aggregations": {
"all_lan": {
"value": 8
},
"title_cnt": {
"value": 18
}
}
}

Stats Aggregation

Stats Aggregation 用于基本统计,会一次返回 count、max、min、avg 和 sum 这 5 个指标。例如,在 exams 索引中对 grade 字段进行分数相关的基本统计,查询语句如下:

1
2
3
4
5
6
7
8
GET /exams/_search?size=0
{
"aggs" : {
"grades_stats" : {
"stats" : { "field" : "grade" }
}
}
}

聚合结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
...
"aggregations": {
"grades_stats": {
"count": 2,
"min": 50.0,
"max": 100.0,
"avg": 75.0,
"sum": 150.0
}
}
}

Extended Stats Aggregation

Extended Stats Aggregation 用于高级统计,和基本统计功能类似,但是会比基本统计多出以下几个统计结果,sum_of_squares(平方和)、variance(方差)、std_deviation(标准差)、std_deviation_bounds(平均值加/减两个标准差的区间)。在 exams 索引中对 grade 字段进行分数相关的高级统计,查询语句如下:

1
2
3
4
5
6
7
8
GET /exams/_search?size=0
{
"aggs" : {
"grades_stats" : {
"extended_stats" : { "field" : "grade" }
}
}
}

聚合结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
...
"aggregations": {
"grades_stats": {
"count": 2,
"min": 50.0,
"max": 100.0,
"avg": 75.0,
"sum": 150.0,
"sum_of_squares": 12500.0,
"variance": 625.0,
"std_deviation": 25.0,
"std_deviation_bounds": {
"upper": 125.0,
"lower": 25.0
}
}
}
}

Percentiles Aggregation

Percentiles Aggregation 用于百分位统计。百分位数是一个统计学术语,如果将一组数据从大到小排序,并计算相应的累计百分位,某一百分位所对应数据的值就称为这一百分位的百分位数。默认情况下,累计百分位为 [ 1, 5, 25, 50, 75, 95, 99 ]。以下例子给出了在 latency 索引中对 load_time 字段进行加载时间的百分位统计,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_outlier" : {
"percentiles" : {
"field" : "load_time"
}
}
}
}

需要注意的是,如上的 load_time 字段必须是数字类型

聚合结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
...
"aggregations": {
"load_time_outlier": {
"values" : {
"1.0": 5.0,
"5.0": 25.0,
"25.0": 165.0,
"50.0": 445.0,
"75.0": 725.0,
"95.0": 945.0,
"99.0": 985.0
}
}
}
}

百分位的统计也可以指定 percents 参数指定百分位,如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_outlier" : {
"percentiles" : {
"field" : "load_time",
"percents": [60, 80, 95]
}
}
}
}

Percentiles Ranks Aggregation

Percentiles Ranks Aggregation 与 Percentiles Aggregation 统计恰恰相反,就是想看当前数值处在什么范围内(百分位), 假如你查一下当前值 500 和 600 所处的百分位,发现是 90.01 和 100,那么说明有 90.01 % 的数值都在 500 以内,100 % 的数值在 600 以内。

1
2
3
4
5
6
7
8
9
10
11
12
GET latency/_search
{
"size": 0,
"aggs" : {
"load_time_ranks" : {
"percentile_ranks" : {
"field" : "load_time",
"values" : [500, 600]
}
}
}
}

同样 load_time 字段必须是数字类型

返回结果大概类似如下:

1
2
3
4
5
6
7
8
9
10
11
{
...
"aggregations": {
"load_time_ranks": {
"values" : {
"500.0": 90.01,
"600.0": 100.0
}
}
}
}

可以设置 keyed 参数为 true,将对应的 values 作为桶 key 一起返回,默认是 false

1
2
3
4
5
6
7
8
9
10
11
12
13
GET latency/_search
{
"size": 0,
"aggs": {
"load_time_ranks": {
"percentile_ranks": {
"field": "load_time",
"values": [500, 600],
"keyed": true
}
}
}
}

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
...
"aggregations": {
"load_time_ranks": {
"values": [
{
"key": 500.0,
"value": 90.01
},
{
"key": 600.0,
"value": 100.0
}
]
}
}
}

桶聚合

bucket 可以理解为一个桶,它会遍历文档中的内容,凡是符合某一要求的就放入一个桶中,分桶相当于 SQL 中的 group by。从另外一个角度,可以将指标聚合看成单桶聚合,即把所有文档放到一个桶中,而桶聚合是多桶型聚合,它根据相应的条件进行分组。

种类 描述/场景
词项聚合(Terms Aggregation) 用于分组聚合,让用户得知文档中每个词项的频率,它返回每个词项出现的次数。
差异词项聚合(Significant Terms Aggregation) 它会返回某个词项在整个索引中和在查询结果中的词频差异,这有助于我们发现搜索场景中有意义的词。
过滤器聚合(Filter Aggregation) 指定过滤器匹配的所有文档到单个桶(bucket),通常这将用于将当前聚合上下文缩小到一组特定的文档。
多过滤器聚合(Filters Aggregation) 指定多个过滤器匹配所有文档到多个桶(bucket)。
范围聚合(Range Aggregation) 范围聚合,用于反映数据的分布情况。
日期范围聚合(Date Range Aggregation) 专门用于日期类型的范围聚合。
IP 范围聚合(IP Range Aggregation) 用于对 IP 类型数据范围聚合。
直方图聚合(Histogram Aggregation) 可能是数值,或者日期型,和范围聚集类似。
时间直方图聚合(Date Histogram Aggregation) 时间直方图聚合,常用于按照日期对文档进行统计并绘制条形图。
空值聚合(Missing Aggregation) 空值聚合,可以把文档集中所有缺失字段的文档分到一个桶中。
地理点范围聚合(Geo Distance Aggregation) 用于对地理点(geo point)做范围统计。

Terms Aggregation

Terms Aggregation 用于词项的分组聚合。最为经典的用例是获取 X 中最频繁(top frequent)的项目,其中 X 是文档中的某个字段,如用户的名称、标签或分类。由于 terms 聚集统计的是每个词条,而不是整个字段值,因此通常需要在一个非分析型的字段上运行这种聚集。原因是, 你期望“big data”作为词组统计,而不是“big”单独统计一次,“data”再单独统计一次。

用户可以使用 terms 聚集,从分析型字段(如内容)中抽取最为频繁的词条。还可以使用这种信息来生成一个单词云。

1
2
3
4
5
6
7
8
9
10
{
"aggs": {
"profit_terms": {
"terms": { // terms 聚合 关键字
"field": "profit",
......
}
}
}
}

在 terms 分桶的基础上,还可以对每个桶进行指标统计,也可以基于一些指标或字段值进行排序。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"aggs": {
"item_terms": {
"terms": {
"field": "item_id",
"size": 1000,
"order":[{
"gmv_stat": "desc"
},{
"gmv_180d": "desc"
}]
},
"aggs": {
"gmv_stat": {
"sum": {
"field": "gmv"
}
},
"gmv_180d": {
"sum": {
"script": "doc['gmv_90d'].value*2"
}
}
}
}
}
}

返回的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
...
"aggregations": {
"hospital_id_agg": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 260,
"buckets": [
{
"key": 23388,
"doc_count": 18,
"gmv_stat": {
"value": 176220
},
"gmv_180d": {
"value": 89732
}
},
{
"key": 96117,
"doc_count": 16,
"gmv_stat": {
"value": 129306
},
"gmv_180d": {
"value": 56988
}
},
...
]
}
}
}

默认情况下返回按文档计数从高到低的前 10 个分组,可以通过 size 参数指定返回的分组数。

Filter Aggregation

Filter Aggregation 是过滤器聚合,可以把符合过滤器中的条件的文档分到一个桶中,即是单分组聚合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"aggs": {
"age_terms": {
"filter": {"match":{"gender":"F"}},
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
}
}

Filters Aggregation

Filters Aggregation 是多过滤器聚合,可以把符合多个过滤条件的文档分到不同的桶中,即每个分组关联一个过滤条件,并收集所有满足自身过滤条件的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"size": 0,
"aggs": {
"messages": {
"filters": {
"filters": {
"errors": { "match": { "body": "error" } },
"warnings": { "match": { "body": "warning" } }
}
}
}
}
}

在这个例子里,我们分析日志信息。聚合会创建两个关于日志数据的分组,一个收集包含错误信息的文档,另一个收集包含告警信息的文档。而且每个分组会按月份划分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
...
"aggregations": {
"messages": {
"buckets": {
"errors": {
"doc_count": 1
},
"warnings": {
"doc_count": 2
}
}
}
}
}

Range Aggregation

Range Aggregation 范围聚合是一个基于多组值来源的聚合,可以让用户定义一系列范围,每个范围代表一个分组。在聚合执行的过程中,从每个文档提取出来的值都会检查每个分组的范围,并且使相关的文档落入分组中。注意,范围聚合的每个范围内包含 from 值但是排除 to 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"aggs": {
"age_range": {
"range": {
"field": "age",
"ranges": [{
"to": 25
},
{
"from": 25,
"to": 35
},
{
"from": 35
}]
},
"aggs": {
"bmax": {
"max": {
"field": "balance"
}
}
}
}
}
}
}

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
...
"aggregations": {
"age_range": {
"buckets": [{
"key": "*-25.0",
"to": 25,
"doc_count": 225,
"bmax": {
"value": 49587
}
},
{
"key": "25.0-35.0",
"from": 25,
"to": 35,
"doc_count": 485,
"bmax": {
"value": 49795
}
},
{
"key": "35.0-*",
"from": 35,
"doc_count": 290,
"bmax": {
"value": 49989
}
}]
}
}
}

参考资料

Elasticsearch 查询

Elasticsearch 查询语句采用基于 RESTful 风格的接口封装成 JSON 格式的对象,称之为 Query DSL。Elasticsearch 查询分类大致分为全文查询词项查询复合查询嵌套查询位置查询特殊查询。Elasticsearch 查询从机制分为两种,一种是根据用户输入的查询词,通过排序模型计算文档与查询词之间的相关度,并根据评分高低排序返回;另一种是过滤机制,只根据过滤条件对文档进行过滤,不计算评分,速度相对较快。

全文查询

ES 全文查询主要用于在全文字段上,主要考虑查询词与文档的相关性(Relevance)。

intervals query

intervals query 根据匹配词的顺序和近似度返回文档。

intervals query 使用匹配规则,这些规则应用于指定字段中的 term。

示例:下面示例搜索 query 字段,搜索值是 my favorite food,没有任何间隙;然后是 my_text 字段搜索匹配 hot watercold porridge 的 term。

当 my_text 中的值为 my favorite food is cold porridge 时,会匹配成功,但是 when it's cold my favorite food is porridge 则匹配失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
POST _search
{
"query": {
"intervals" : {
"my_text" : {
"all_of" : {
"ordered" : true,
"intervals" : [
{
"match" : {
"query" : "my favorite food",
"max_gaps" : 0,
"ordered" : true
}
},
{
"any_of" : {
"intervals" : [
{ "match" : { "query" : "hot water" } },
{ "match" : { "query" : "cold porridge" } }
]
}
}
]
}
}
}
}
}

match query

match query 用于搜索单个字段,首先会针对查询语句进行解析(经过 analyzer),主要是对查询语句进行分词,分词后查询语句的任何一个词项被匹配,文档就会被搜到,默认情况下相当于对分词后词项进行 or 匹配操作。

match query 是执行全文搜索的标准查询,包括模糊匹配选项。

1
2
3
4
5
6
7
8
9
10
GET kibana_sample_data_ecommerce/_search
{
"query": {
"match": {
"customer_full_name": {
"query": "George Hubbard"
}
}
}
}

等同于 or 匹配操作,如下:

1
2
3
4
5
6
7
8
9
10
11
GET kibana_sample_data_ecommerce/_search
{
"query": {
"match": {
"customer_full_name": {
"query": "George Hubbard",
"operator": "or"
}
}
}
}

match query 简写

可以通过组合 <field>query 参数来简化匹配查询语法。

示例:

1
2
3
4
5
6
7
8
GET /_search
{
"query": {
"match": {
"message": "this is a test"
}
}
}

match query 如何工作

匹配查询是布尔类型。这意味着会对提供的文本进行分析,分析过程从提供的文本构造一个布尔查询。 operator 参数可以设置为 orand 来控制布尔子句(默认为 or)。可以使用 minimum_should_match 参数设置要匹配的可选 should 子句的最小数量。

1
2
3
4
5
6
7
8
9
10
11
GET kibana_sample_data_ecommerce/_search
{
"query": {
"match": {
"customer_full_name": {
"query": "George Hubbard",
"operator": "and"
}
}
}
}

可以设置 analyzer 来控制哪个分析器将对文本执行分析过程。它默认为字段显式映射定义或默认搜索分析器。

lenient 参数可以设置为 true 以忽略由数据类型不匹配导致的异常,例如尝试使用文本查询字符串查询数字字段。默认为 false

match query 的模糊查询

fuzziness 允许基于被查询字段的类型进行模糊匹配。请参阅 Fuzziness 的配置。

在这种情况下可以设置 prefix_lengthmax_expansions 来控制模糊匹配。如果设置了模糊选项,查询将使用 top_terms_blended_freqs_${max_expansions} 作为其重写方法,fuzzy_rewrite 参数允许控制查询将如何被重写。

默认情况下允许模糊倒转 (abba),但可以通过将 fuzzy_transpositions 设置为 false 来禁用。

1
2
3
4
5
6
7
8
9
10
11
GET /_search
{
"query": {
"match": {
"message": {
"query": "this is a testt",
"fuzziness": "AUTO"
}
}
}
}

zero terms 查询

如果使用的分析器像 stop 过滤器一样删除查询中的所有标记,则默认行为是不匹配任何文档。可以使用 zero_terms_query 选项来改变默认行为,它接受 none(默认)和 all (相当于 match_all 查询)。

1
2
3
4
5
6
7
8
9
10
11
12
GET /_search
{
"query": {
"match": {
"message": {
"query": "to be or not to be",
"operator": "and",
"zero_terms_query": "all"
}
}
}
}

match_bool_prefix query

match_bool_prefix query 分析其输入并根据这些词构造一个布尔查询。除了最后一个术语之外的每个术语都用于术语查询。最后一个词用于 prefix query

示例:

1
2
3
4
5
6
7
8
GET /_search
{
"query": {
"match_bool_prefix" : {
"message" : "quick brown f"
}
}
}

等价于

1
2
3
4
5
6
7
8
9
10
11
12
GET /_search
{
"query": {
"bool" : {
"should": [
{ "term": { "message": "quick" }},
{ "term": { "message": "brown" }},
{ "prefix": { "message": "f"}}
]
}
}
}

match_bool_prefix querymatch_phrase_prefix query 之间的一个重要区别是:match_phrase_prefix query 将其 term 匹配为短语,但 match_bool_prefix query 可以在任何位置匹配其 term。

上面的示例 match_bool_prefix query 查询可以匹配包含 quick brown fox 的字段,但它也可以快速匹配 brown fox。它还可以匹配包含 quickbrown 和以 f 开头的字段,出现在任何位置。

match_phrase query

match_phrase query 即短语匹配,首先会把 query 内容分词,分词器可以自定义,同时文档还要满足以下两个条件才会被搜索到:

  1. 分词后所有词项都要出现在该字段中(相当于 and 操作)
  2. 字段中的词项顺序要一致

例如,有以下 3 个文档,使用 match_phrase 查询 “How are you”,只有前两个文档会被匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT demo/_create/1
{ "desc": "How are you" }

PUT demo/_create/2
{ "desc": "How are you, Jack?"}

PUT demo/_create/3
{ "desc": "are you"}

GET demo/_search
{
"query": {
"match_phrase": {
"desc": "How are you"
}
}
}

说明:

一个被认定为和短语 How are you 匹配的文档,必须满足以下这些要求:

  • How、 are 和 you 需要全部出现在域中。
  • are 的位置应该比 How 的位置大 1 。
  • you 的位置应该比 How 的位置大 2 。

match_phrase_prefix query

match_phrase_prefix querymatch_phrase query 类似,只不过 match_phrase_prefix query 最后一个 term 会被作为前缀匹配。

1
2
3
4
5
6
7
8
GET demo/_search
{
"query": {
"match_phrase_prefix": {
"desc": "are yo"
}
}
}

multi_match query

multi_match querymatch query 的升级,用于搜索多个字段

示例:

1
2
3
4
5
6
7
8
9
10
11
12
GET kibana_sample_data_ecommerce/_search
{
"query": {
"multi_match": {
"query": 34.98,
"fields": [
"taxful_total_price",
"taxless_total_price"
]
}
}
}

multi_match query 的搜索字段可以使用通配符指定,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET kibana_sample_data_ecommerce/_search
{
"query": {
"multi_match": {
"query": 34.98,
"fields": [
"taxful_*",
"taxless_total_price"
]
}
}
}

同时,也可以用指数符指定搜索字段的权重

示例:指定 taxful_total_price 字段的权重是 taxless_total_price 字段的 3 倍,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET kibana_sample_data_ecommerce/_search
{
"query": {
"multi_match": {
"query": 34.98,
"fields": [
"taxful_total_price^3",
"taxless_total_price"
]
}
}
}

combined_fields query

combined_fields query 支持搜索多个文本字段,就好像它们的内容已被索引到一个组合字段中一样。该查询会生成以 term 为中心的输入字符串视图:首先它将查询字符串解析为独立的 term,然后在所有字段中查找每个 term。当匹配结果可能跨越多个文本字段时,此查询特别有用,例如文章的标题、摘要和正文:

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"combined_fields" : {
"query": "database systems",
"fields": [ "title", "abstract", "body"],
"operator": "and"
}
}
}

字段前缀权重

字段前缀权重根据组合字段模型进行计算。例如,如果 title 字段的权重为 2,则匹配度打分时会将 title 中的每个 term 形成的组合字段,按出现两次进行打分。

common_terms query

7.3.0 废弃

common_terms query 是一种在不牺牲性能的情况下替代停用词提高搜索准确率和召回率的方案。

查询中的每个词项都有一定的代价,以搜索“The brown fox”为例,query 会被解析成三个词项“the”“brown”和“fox”,每个词项都会到索引中执行一次查询。很显然包含“the”的文档非常多,相比其他词项,“the”的重要性会低很多。传统的解决方案是把“the”当作停用词处理,去除停用词之后可以减少索引大小,同时在搜索时减少对停用词的收缩。

虽然停用词对文档评分影响不大,但是当停用词仍然有重要意义的时候,去除停用词就不是完美的解决方案了。如果去除停用词,就无法区分“happy”和“not happy”, “The”“To be or not to be”就不会在索引中存在,搜索的准确率和召回率就会降低。

common_terms query 提供了一种解决方案,它把 query 分词后的词项分成重要词项(低频词项)和不重要的词项(高频词,也就是之前的停用词)。在搜索的时候,首先搜索和重要词项匹配的文档,这些文档是词项出现较少并且词项对其评分影响较大的文档。然后执行第二次查询,搜索对评分影响较小的高频词项,但是不计算所有文档的评分,而是只计算第一次查询已经匹配的文档得分。如果一个查询中只包含高频词,那么会通过 and 连接符执行一个单独的查询,换言之,会搜索所有的词项。

词项是高频词还是低频词是通过 cutoff frequency 来设置阀值的,取值可以是绝对频率(频率大于 1)或者相对频率(0 ~ 1)。common_terms query 最有趣之处在于它能自适应特定领域的停用词,例如,在视频托管网站上,诸如“clip”或“video”之类的高频词项将自动表现为停用词,无须保留手动列表。

例如,文档频率高于 0.1% 的词项将会被当作高频词项,词频之间可以用 low_freq_operator、high_freq_operator 参数连接。设置低频词操作符为“and”使所有的低频词都是必须搜索的,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET books/_search
{
"query": {
"common": {
"body": {
"query": "nelly the elephant as a cartoon",
"cutoff_frequency": 0.001,
"low_freq_operator": "and"
}
}
}
}

上述操作等价于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET books/_search
{
"query": {
"bool": {
"must": [
{ "term": { "body": "nelly" } },
{ "term": { "body": "elephant" } },
{ "term": { "body": "cartoon" } }
],
"should": [
{ "term": { "body": "the" } },
{ "term": { "body": "as" } },
{ "term": { "body": "a" } }
]
}
}
}

query_string query

query_string query 是与 Lucene 查询语句的语法结合非常紧密的一种查询,允许在一个查询语句中使用多个特殊条件关键字(如:AND | OR | NOT)对多个字段进行查询,建议熟悉 Lucene 查询语法的用户去使用。

用户可以使用 query_string query 来创建包含通配符、跨多个字段的搜索等复杂搜索。虽然通用,但查询是严格的,如果查询字符串包含任何无效语法,则会返回错误。

示例:

1
2
3
4
5
6
7
8
9
GET /_search
{
"query": {
"query_string": {
"query": "(new york city) OR (big apple)",
"default_field": "content"
}
}
}

simple_query_string query

simple_query_string query 是一种适合直接暴露给用户,并且具有非常完善的查询语法的查询语句,接受 Lucene 查询语法,解析过程中发生错误不会抛出异常。

虽然语法比 query_string query 更严格,但 simple_query_string query 不会返回无效语法的错误。相反,它会忽略查询字符串的任何无效部分。

示例:

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"simple_query_string" : {
"query": "\"fried eggs\" +(eggplant | potato) -frittata",
"fields": ["title^5", "body"],
"default_operator": "and"
}
}
}

simple_query_string 语义

  • +:等价于 AND 操作
  • |:等价于 OR 操作
  • -:相当于 NOT 操作
  • ":包装一些标记以表示用于搜索的短语
  • *:词尾表示前缀查询
  • ( and ):表示优先级
  • ~N:词尾表示表示编辑距离(模糊性)
  • ~N:在一个短语之后表示溢出量

注意:要使用上面的字符,请使用反斜杠 / 对其进行转义。

全文查询完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#设置 position_increment_gap
DELETE groups
PUT groups
{
"mappings": {
"properties": {
"names":{
"type": "text",
"position_increment_gap": 0
}
}
}
}

GET groups/_mapping

POST groups/_doc
{
"names": [ "John Water", "Water Smith"]
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": {
"query": "Water Water",
"slop": 100
}
}
}
}

POST groups/_search
{
"query": {
"match_phrase": {
"names": "Water Smith"
}
}
}

DELETE groups

词项查询

Term(词项)是表达语意的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理 Term。

全文查询在执行查询之前会分析查询字符串。

与全文查询不同,词项查询不会分词,而是将输入作为一个整体,在倒排索引中查找准确的词项。并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。一言以概之:词项查询是对词项进行精确匹配。词项查询通常用于结构化数据,如数字、日期和枚举类型。

词项查询有以下类型:

exists query

exists query 会返回字段中至少有一个非空值的文档。

由于多种原因,文档字段可能不存在索引值:

  • JSON 中的字段为 null[]
  • 该字段在 mapping 中配置了 "index" : false
  • 字段值的长度超过了 mapping 中的 ignore_above 设置
  • 字段值格式错误,并且在 mapping 中定义了 ignore_malformed

示例:

1
2
3
4
5
6
7
8
GET kibana_sample_data_ecommerce/_search
{
"query": {
"exists": {
"field": "email"
}
}
}

以下文档会匹配上面的查询:

  • { "user" : "jane" } 有 user 字段,且不为空。
  • { "user" : "" } 有 user 字段,值为空字符串。
  • { "user" : "-" } 有 user 字段,值不为空。
  • { "user" : [ "jane" ] } 有 user 字段,值不为空。
  • { "user" : [ "jane", null ] } 有 user 字段,至少一个值不为空即可。

下面的文档都不会被匹配:

  • { "user" : null } 虽然有 user 字段,但是值为空。
  • { "user" : [] } 虽然有 user 字段,但是值为空。
  • { "user" : [null] } 虽然有 user 字段,但是值为空。
  • { "foo" : "bar" } 没有 user 字段。

fuzzy query

**fuzzy query**(模糊查询)返回包含与搜索词相似的词的文档。ES 使用 Levenshtein edit distance(Levenshtein 编辑距离)测量相似度或模糊度。

编辑距离是将一个术语转换为另一个术语所需的单个字符更改的数量。这些变化可能包括:

  • 改变一个字符:(box -> fox)
  • 删除一个字符:(black -> lack)
  • 插入一个字符:(sic -> sick
  • 反转两个相邻字符:(act → cat)

为了找到相似的词条,fuzzy query 会在指定的编辑距离内创建搜索词条的所有可能变体或扩展集。然后返回完全匹配任意扩展的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET books/_search
{
"query": {
"fuzzy": {
"user.id": {
"value": "ki",
"fuzziness": "AUTO",
"max_expansions": 50,
"prefix_length": 0,
"transpositions": true,
"rewrite": "constant_score"
}
}
}
}

注意:如果配置了 search.allow_expensive_queries ,则 fuzzy query 不能执行。

ids query

ids query 根据 ID 返回文档。 此查询使用存储在 _id 字段中的文档 ID。

1
2
3
4
5
6
7
8
GET /_search
{
"query": {
"ids" : {
"values" : ["1", "4", "100"]
}
}
}

prefix query

prefix query 用于查询某个字段中包含指定前缀的文档。

比如查询 user.id 中含有以 ki 为前缀的关键词的文档,那么含有 kindkid 等所有以 ki 开头关键词的文档都会被匹配。

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"prefix": {
"user.id": {
"value": "ki"
}
}
}
}

range query

range query 即范围查询,用于匹配在某一范围内的数值型、日期类型或者字符串型字段的文档。比如搜索哪些书籍的价格在 50 到 100 之间、哪些书籍的出版时间在 2015 年到 2019 年之间。使用 range 查询只能查询一个字段,不能作用在多个字段上

range 查询支持的参数有以下几种:

  • **gt**:大于

  • **gte**:大于等于

  • **lt**:小于

  • **lte**:小于等于

  • **format**:如果字段是 Date 类型,可以设置日期格式化

  • **time_zone**:时区

  • **relation**:指示范围查询如何匹配范围字段的值。

    • **INTERSECTS (Default)**:匹配与查询字段值范围相交的文档。
    • **CONTAINS**:匹配完全包含查询字段值的文档。
    • **WITHIN**:匹配具有完全在查询范围内的范围字段值的文档。

示例:数值范围查询

1
2
3
4
5
6
7
8
9
10
11
GET kibana_sample_data_ecommerce/_search
{
"query": {
"range": {
"taxful_total_price": {
"gt": 10,
"lte": 50
}
}
}
}

示例:日期范围查询

1
2
3
4
5
6
7
8
9
10
11
12
GET kibana_sample_data_ecommerce/_search
{
"query": {
"range": {
"order_date": {
"time_zone": "+00:00",
"gte": "2018-01-01T00:00:00",
"lte": "now"
}
}
}
}

regexp query

regexp query 返回与正则表达式相匹配的 term 所属的文档。

正则表达式是一种使用占位符字符匹配数据模式的方法,称为运算符。

示例:以下搜索返回 user.id 字段包含任何以 k 开头并以 y 结尾的文档。 .* 运算符匹配任何长度的任何字符,包括无字符。匹配项可以包括 kykaykimchy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /_search
{
"query": {
"regexp": {
"user.id": {
"value": "k.*y",
"flags": "ALL",
"case_insensitive": true,
"max_determinized_states": 10000,
"rewrite": "constant_score"
}
}
}
}

注意:如果配置了search.allow_expensive_queries ,则 regexp query 会被禁用。

term query

term query 用来查找指定字段中包含给定单词的文档,term 查询不被解析,只有查询词和文档中的词精确匹配才会被搜索到,应用场景为查询人名、地名等需要精准匹配的需求。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 1. 创建一个索引
DELETE my-index-000001
PUT my-index-000001
{
"mappings": {
"properties": {
"full_text": { "type": "text" }
}
}
}

# 2. 使用 "Quick Brown Foxes!" 关键字查 "full_text" 字段
PUT my-index-000001/_doc/1
{
"full_text": "Quick Brown Foxes!"
}

# 3. 使用 term 查询
GET my-index-000001/_search?pretty
{
"query": {
"term": {
"full_text": "Quick Brown Foxes!"
}
}
}
# 因为 full_text 字段不再包含确切的 Term —— "Quick Brown Foxes!",所以 term query 搜索不到任何结果

# 4. 使用 match 查询
GET my-index-000001/_search?pretty
{
"query": {
"match": {
"full_text": "Quick Brown Foxes!"
}
}
}

DELETE my-index-000001

:warning: 注意:应避免 term 查询对 text 字段使用查询。

默认情况下,Elasticsearch 针对 text 字段的值进行解析分词,这会使查找 text 字段值的精确匹配变得困难。

要搜索 text 字段值,需改用 match 查询。

terms query

terms queryterm query 相同,但可以搜索多个值。

terms query 查询参数:

  • **index**:索引名
  • **id**:文档 ID
  • **path**:要从中获取字段值的字段的名称,即搜索关键字
  • **routing**(选填):要从中获取 term 值的文档的自定义路由值。如果在索引文档时提供了自定义路由值,则此参数是必需的。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 1. 创建一个索引
DELETE my-index-000001
PUT my-index-000001
{
"mappings": {
"properties": {
"color": { "type": "keyword" }
}
}
}

# 2. 写入一个文档
PUT my-index-000001/_doc/1
{
"color": [
"blue",
"green"
]
}

# 3. 写入另一个文档
PUT my-index-000001/_doc/2
{
"color": "blue"
}

# 3. 使用 terms query
GET my-index-000001/_search?pretty
{
"query": {
"terms": {
"color": {
"index": "my-index-000001",
"id": "2",
"path": "color"
}
}
}
}

DELETE my-index-000001

type query

7.0.0 后废弃

type query 用于查询具有指定类型的文档。

示例:

1
2
3
4
5
6
7
8
GET /_search
{
"query": {
"type": {
"value": "_doc"
}
}
}

wildcard query

wildcard query 即通配符查询,返回与通配符模式匹配的文档。

? 用来匹配一个任意字符,* 用来匹配零个或者多个字符。

示例:以下搜索返回 user.id 字段包含以 ki 开头并以 y 结尾的术语的文档。这些匹配项可以包括 kiykitykimchy

1
2
3
4
5
6
7
8
9
10
11
12
GET /_search
{
"query": {
"wildcard": {
"user.id": {
"value": "ki*y",
"boost": 1.0,
"rewrite": "constant_score"
}
}
}
}

注意:如果配置了search.allow_expensive_queries ,则wildcard query 会被禁用。

词项查询完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
DELETE products
PUT products
{
"settings": {
"number_of_shards": 1
}
}

POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }

GET /products

POST /products/_search
{
"query": {
"term": {
"desc": {
//"value": "iPhone"
"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
//"value": "iPhone"
//"value":"iphone"
}
}
}
}

POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
//"explain": true,
"query": {
"term": {
"productID.keyword": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}

POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}

}
}
}

复合查询

复合查询就是把一些简单查询组合在一起实现更复杂的查询需求,除此之外,复合查询还可以控制另外一个查询的行为。

bool query

bool 查询可以把任意多个简单查询组合在一起,使用 must、should、must_not、filter 选项来表示简单查询之间的逻辑,每个选项都可以出现 0 次到多次,它们的含义如下:

  • must 文档必须匹配 must 选项下的查询条件,相当于逻辑运算的 AND,且参与文档相关度的评分。
  • should 文档可以匹配 should 选项下的查询条件也可以不匹配,相当于逻辑运算的 OR,且参与文档相关度的评分。
  • must_not 与 must 相反,匹配该选项下的查询条件的文档不会被返回;需要注意的是,must_not 语句不会影响评分,它的作用只是将不相关的文档排除
  • filter 和 must 一样,匹配 filter 选项下的查询条件的文档才会被返回,但是 filter 不评分,只起到过滤功能,与 must_not 相反

假设要查询 title 中包含关键词 java,并且 price 不能高于 70,description 可以包含也可以不包含虚拟机的书籍,构造 bool 查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
GET books/_search
{
"query": {
"bool": {
"filter": {
"term": {
"status": 1
}
},
"must_not": {
"range": {
"price": {
"gte": 70
}
}
},
"must": {
"match": {
"title": "java"
}
},
"should": [
{
"match": {
"description": "虚拟机"
}
}
],
"minimum_should_match": 1
}
}
}

有关布尔查询更详细的信息参考 bool query(组合查询)详解

boosting query

boosting 查询用于需要对两个查询的评分进行调整的场景,boosting 查询会把两个查询封装在一起并降低其中一个查询的评分。

boosting 查询包括 positive、negative 和 negative_boost 三个部分,positive 中的查询评分保持不变,negative 中的查询会降低文档评分,negative_boost 指明 negative 中降低的权值。如果我们想对 2015 年之前出版的书降低评分,可以构造一个 boosting 查询,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET books/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"title": "python"
}
},
"negative": {
"range": {
"publish_time": {
"lte": "2015-01-01"
}
}
},
"negative_boost": 0.2
}
}
}

boosting 查询中指定了抑制因子为 0.2,publish_time 的值在 2015-01-01 之后的文档得分不变,publish_time 的值在 2015-01-01 之前的文档得分为原得分的 0.2 倍。

constant_score query

constantscore query 包装一个 filter query,并返回匹配过滤器查询条件的文档,且它们的相关性评分都等于 _boost 参数值(可以理解为原有的基于 tf-idf 或 bm25 的相关分固定为 1.0,所以最终评分为 _1.0 * boost_,即等于 boost 参数值)。下面的查询语句会返回 title 字段中含有关键词 elasticsearch 的文档,所有文档的评分都是 1.8:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET books/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"title": "elasticsearch"
}
},
"boost": 1.8
}
}
}

dis_max query

dis_max query 与 bool query 有一定联系也有一定区别,dis_max query 支持多并发查询,可返回与任意查询条件子句匹配的任何文档类型。与 bool 查询可以将所有匹配查询的分数相结合使用的方式不同,dis_max 查询只使用最佳匹配查询条件的分数。请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET books/_search
{
"query": {
"dis_max": {
"tie_breaker": 0.7,
"boost": 1.2,
"queries": [{
"term": {
"age": 34
}
},
{
"term": {
"age": 35
}
}
]
}
}
}

function_score query

function_score query 可以修改查询的文档得分,这个查询在有些情况下非常有用,比如通过评分函数计算文档得分代价较高,可以改用过滤器加自定义评分函数的方式来取代传统的评分方式。

使用 function_score query,用户需要定义一个查询和一至多个评分函数,评分函数会对查询到的每个文档分别计算得分。

下面这条查询语句会返回 books 索引中的所有文档,文档的最大得分为 5,每个文档的得分随机生成,权重的计算模式为相乘模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
GET books/_search
{
"query": {
"function_score": {
"query": {
"match all": {}
},
"boost": "5",
"random_score": {},
"boost_mode": "multiply"
}
}
}

使用脚本自定义评分公式,这里把 price 值的十分之一开方作为每个文档的得分,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET books/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "java"
}
},
"script_score": {
"inline": "Math.sqrt(doc['price'].value/10)"
}
}
}
}

关于 function_score 的更多详细内容请查看 Elasticsearch function_score 查询最强详解

indices query

indices query 适用于需要在多个索引之间进行查询的场景,它允许指定一个索引名字列表和内部查询。indices query 中有 query 和 no_match_query 两部分,query 中用于搜索指定索引列表中的文档,no_match_query 中的查询条件用于搜索指定索引列表之外的文档。下面的查询语句实现了搜索索引 books、books2 中 title 字段包含关键字 javascript,其他索引中 title 字段包含 basketball 的文档,查询语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET books/_search
{
"query": {
"indices": {
"indices": ["books", "books2"],
"query": {
"match": {
"title": "javascript"
}
},
"no_match_query": {
"term": {
"title": "basketball"
}
}
}
}
}

嵌套查询

在 Elasticsearch 这样的分布式系统中执行全 SQL 风格的连接查询代价昂贵,是不可行的。相应地,为了实现水平规模地扩展,Elasticsearch 提供了以下两种形式的 join:

  • nested query(嵌套查询)

    文档中可能包含嵌套类型的字段,这些字段用来索引一些数组对象,每个对象都可以作为一条独立的文档被查询出来。

  • has_child query(有子查询)和 has_parent query(有父查询)

    父子关系可以存在单个的索引的两个类型的文档之间。has_child 查询将返回其子文档能满足特定查询的父文档,而 has_parent 则返回其父文档能满足特定查询的子文档。

nested query

文档中可能包含嵌套类型的字段,这些字段用来索引一些数组对象,每个对象都可以作为一条独立的文档被查询出来(用嵌套查询)。

1
2
3
4
5
6
7
8
9
10
11
12
PUT /my_index
{
"mappings": {
"type1": {
"properties": {
"obj1": {
"type": "nested"
}
}
}
}
}

has_child query

文档的父子关系创建索引时在映射中声明,这里以员工(employee)和工作城市(branch)为例,它们属于不同的类型,相当于数据库中的两张表,如果想把员工和他们工作的城市关联起来,需要告诉 Elasticsearch 文档之间的父子关系,这里 employee 是 child type,branch 是 parent type,在映射中声明,执行命令:

1
2
3
4
5
6
7
8
9
PUT /company
{
"mappings": {
"branch": {},
"employee": {
"parent": { "type": "branch" }
}
}
}

使用 bulk api 索引 branch 类型下的文档,命令如下:

1
2
3
4
5
6
7
POST company/branch/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster","city": "London","country": "UK" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central","city": "Liverpool","country": "UK" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Elysees","city": "Paris","country": "France" }

添加员工数据:

1
2
3
4
5
6
7
8
9
POST company/employee/_bulk
{ "index": { "_id": 1,"parent":"london" }}
{ "name": "Alice Smith","dob": "1970-10-24","hobby": "hiking" }
{ "index": { "_id": 2,"parent":"london" }}
{ "name": "Mark Tomas","dob": "1982-05-16","hobby": "diving" }
{ "index": { "_id": 3,"parent":"liverpool" }}
{ "name": "Barry Smith","dob": "1979-04-01","hobby": "hiking" }
{ "index": { "_id": 4,"parent":"paris" }}
{ "name": "Adrien Grand","dob": "1987-05-11","hobby": "horses" }

通过子文档查询父文档要使用 has_child 查询。例如,搜索 1980 年以后出生的员工所在的分支机构,employee 中 1980 年以后出生的有 Mark Thomas 和 Adrien Grand,他们分别在 london 和 paris,执行以下查询命令进行验证:

1
2
3
4
5
6
7
8
9
10
11
GET company/branch/_search
{
"query": {
"has_child": {
"type": "employee",
"query": {
"range": { "dob": { "gte": "1980-01-01" } }
}
}
}
}

搜索哪些机构中有名为 “Alice Smith” 的员工,因为使用 match 查询,会解析为 “Alice” 和 “Smith”,所以 Alice Smith 和 Barry Smith 所在的机构会被匹配,执行以下查询命令进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
GET company/branch/_search
{
"query": {
"has_child": {
"type": "employee",
"score_mode": "max",
"query": {
"match": { "name": "Alice Smith" }
}
}
}
}

可以使用 min_children 指定子文档的最小个数。例如,搜索最少含有两个 employee 的机构,查询命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
GET company/branch/_search?pretty
{
"query": {
"has_child": {
"type": "employee",
"min_children": 2,
"query": {
"match_all": {}
}
}
}
}

has_parent query

通过父文档查询子文档使用 has_parent 查询。比如,搜索哪些 employee 工作在 UK,查询命令如下:

1
2
3
4
5
6
7
8
9
10
11
GET company/employee/_search
{
"query": {
"has_parent": {
"parent_type": "branch",
"query": {
"match": { "country": "UK }
}
}
}
}

位置查询

Elasticsearch 可以对地理位置点 geo_point 类型和地理位置形状 geo_shape 类型的数据进行搜索。为了学习方便,这里准备一些城市的地理坐标作为测试数据,每一条文档都包含城市名称和地理坐标这两个字段,这里的坐标点取的是各个城市中心的一个位置。首先把下面的内容保存到 geo.json 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
{"index":{ "_index":"geo","_type":"city","_id":"1" }}
{"name":"北京","location":"39.9088145109,116.3973999023"}
{"index":{ "_index":"geo","_type":"city","_id": "2" }}
{"name":"乌鲁木齐","location":"43.8266300000,87.6168800000"}
{"index":{ "_index":"geo","_type":"city","_id":"3" }}
{"name":"西安","location":"34.3412700000,108.9398400000"}
{"index":{ "_index":"geo","_type":"city","_id":"4" }}
{"name":"郑州","location":"34.7447157466,113.6587142944"}
{"index":{ "_index":"geo","_type":"city","_id":"5" }}
{"name":"杭州","location":"30.2294080260,120.1492309570"}
{"index":{ "_index":"geo","_type":"city","_id":"6" }}
{"name":"济南","location":"36.6518400000,117.1200900000"}

创建一个索引并设置映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT geo
{
"mappings": {
"city": {
"properties": {
"name": {
"type": "keyword"
},
"location": {
"type": "geo_point"
}
}
}
}
}

然后执行批量导入命令:

1
curl -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @geo.json

geo_distance query

geo_distance query 可以查找在一个中心点指定范围内的地理点文档。例如,查找距离天津 200km 以内的城市,搜索结果中会返回北京,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET geo/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_distance": {
"distance": "200km",
"location": {
"lat": 39.0851000000,
"lon": 117.1993700000
}
}
}
}
}
}

按各城市离北京的距离排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET geo/_search
{
"query": {
"match_all": {}
},
"sort": [{
"_geo_distance": {
"location": "39.9088145109,116.3973999023",
"unit": "km",
"order": "asc",
"distance_type": "plane"
}
}]
}

其中 location 对应的经纬度字段;unit 为 km 表示将距离以 km 为单位写入到每个返回结果的 sort 键中;distance_type 为 plane 表示使用快速但精度略差的 plane 计算方式。

geo_bounding_box query

geo_bounding_box query 用于查找落入指定的矩形内的地理坐标。查询中由两个点确定一个矩形,然后在矩形区域内查询匹配的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET geo/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 38.4864400000,
"lon": 106.2324800000
},
"bottom_right": {
"lat": 28.6820200000,
"lon": 115.8579400000
}
}
}
}
}
}
}

geo_polygon query

geo_polygon query 用于查找在指定多边形内的地理点。例如,呼和浩特、重庆、上海三地组成一个三角形,查询位置在该三角形区域内的城市,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GET geo/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
}
},
"filter": {
"geo_polygon": {
"location": {
"points": [{
"lat": 40.8414900000,
"lon": 111.7519900000
}, {
"lat": 29.5647100000,
"lon": 106.5507300000
}, {
"lat": 31.2303700000,
"lon": 121.4737000000
}]
}
}
}
}
}

geo_shape query

geo_shape query 用于查询 geo_shape 类型的地理数据,地理形状之间的关系有相交、包含、不相交三种。创建一个新的索引用于测试,其中 location 字段的类型设为 geo_shape 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT geoshape
{
"mappings": {
"city": {
"properties": {
"name": {
"type": "keyword"
},
"location": {
"type": "geo_shape"
}
}
}
}
}

关于经纬度的顺序这里做一个说明,geo_point 类型的字段纬度在前经度在后,但是对于 geo_shape 类型中的点,是经度在前纬度在后,这一点需要特别注意。

把西安和郑州连成的线写入索引:

1
2
3
4
5
6
7
8
9
10
11
POST geoshape/city/1
{
"name": "西安-郑州",
"location": {
"type": "linestring",
"coordinates": [
[108.9398400000, 34.3412700000],
[113.6587142944, 34.7447157466]
]
}
}

查询包含在由银川和南昌作为对角线上的点组成的矩形的地理形状,由于西安和郑州组成的直线落在该矩形区域内,因此可以被查询到。命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
GET geoshape/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_shape": {
"location": {
"shape": {
"type": "envelope",
"coordinates": [
[106.23248, 38.48644],
[115.85794, 28.68202]
]
},
"relation": "within"
}
}
}
}
}
}

特殊查询

more_like_this query

more_like_this query 可以查询和提供文本类似的文档,通常用于近似文本的推荐等场景。查询命令如下:

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"more_like_ this": {
"fields": ["title", "description"],
"like": "java virtual machine",
"min_term_freq": 1,
"max_query_terms": 12
}
}
}

可选的参数及取值说明如下:

  • fields 要匹配的字段,默认是 _all 字段。
  • like 要匹配的文本。
  • min_term_freq 文档中词项的最低频率,默认是 2,低于此频率的文档会被忽略。
  • max_query_terms query 中能包含的最大词项数目,默认为 25。
  • min_doc_freq 最小的文档频率,默认为 5。
  • max_doc_freq 最大文档频率。
  • min_word length 单词的最小长度。
  • max_word length 单词的最大长度。
  • stop_words 停用词列表。
  • analyzer 分词器。
  • minimum_should_match 文档应匹配的最小词项数,默认为 query 分词后词项数的 30%。
  • boost terms 词项的权重。
  • include 是否把输入文档作为结果返回。
  • boost 整个 query 的权重,默认为 1.0。

script query

Elasticsearch 支持使用脚本进行查询。例如,查询价格大于 180 的文档,命令如下:

1
2
3
4
5
6
7
8
9
10
11
GET books/_search
{
"query": {
"script": {
"script": {
"inline": "doc['price'].value > 180",
"lang": "painless"
}
}
}
}

percolate query

一般情况下,我们是先把文档写入到 Elasticsearch 中,通过查询语句对文档进行搜索。percolate query 则是反其道而行之的做法,它会先注册查询条件,根据文档来查询 query。例如,在 my-index 索引中有一个 laptop 类型,文档有 price 和 name 两个字段,在映射中声明一个 percolator 类型的 query,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PUT my-index
{
"mappings": {
"laptop": {
"properties": {
"price": { "type": "long" },
"name": { "type": "text" }
},
"queries": {
"properties": {
"query": { "type": "percolator" }
}
}
}
}
}

注册一个 bool query,bool query 中包含一个 range query,要求 price 字段的取值小于等于 10000,并且 name 字段中含有关键词 macbook:

1
2
3
4
5
6
7
8
9
10
11
12
PUT /my-index/queries/1?refresh
{
"query": {
"bool": {
"must": [{
"range": { "price": { "lte": 10000 } }
}, {
"match": { "name": "macbook" }
}]
}
}
}

通过文档查询 query:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /my-index/_search
{
"query": {
"percolate": {
"field": "query",
"document_type": "laptop",
"document": {
"price": 9999,
"name": "macbook pro on sale"
}
}
}
}

文档符合 query 中的条件,返回结果中可以查到上文中注册的 bool query。percolate query 的这种特性适用于数据分类、数据路由、事件监控和预警等场景。

Spring Bean

在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。Bean 是由 Spring IoC 容器实例化、装配和管理的对象。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。

Spring Bean 定义

BeanDefinition

Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——BeanDefinition 对象。

BeanDefinition 是 Spring 中定义 Bean 的配置元信息接口,它包含:

  • Bean 类名
  • Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等
  • 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies)
  • 配置设置,如 Bean 属性(Properties)

BeanDefinition 元信息

BeanDefinition 元信息如下:

属性(Property) 说明
Class 全类名,必须是具体类,不能用抽象类或接口
Name Bean 的名称或者 ID
Scope Bean 的作用域(如:singletonprototype 等)
Constructor arguments Bean 构造器参数(用于依赖注入)
Properties Bean 属性设置(用于依赖注入)
Autowiring mode Bean 自动绑定模式(如:通过名称 byName)
Lazy initialization mode Bean 延迟初始化模式(延迟和非延迟)
Initialization method Bean 初始化回调方法名称
Destruction method Bean 销毁回调方法名称

BeanDefinition 构建

BeanDefinition 构建方式:

  • 通过 BeanDefinitionBuilder

  • 通过 AbstractBeanDefinition 以及派生类

💻 Spring Bean 定义示例源码:BeanDefinitionTests

Spring Bean 命名

Spring Bean 命名规则

每个 Bean 拥有一个或多个标识符(identifiers),这些标识符在 Bean 所在的容器必须是唯一的。通常,一个 Bean 仅有一个标识符,如果需要额外的,可考虑使用别名(Alias)来扩充。

在基于 XML 的配置元信息中,开发人员可以使用 id 属性、name 属性或来指定 Bean 标识符。通常,Bean 的标识符由字母组成,允许出现特殊字符。如果要想引入 Bean 的别名的话,可在 name 属性使用半角逗号(“,”)或分号(“;”) 来间隔。

Spring 中,为 Bean 指定 idname 属性不是必须的。如果不指定,Spring 会自动为 Bean 分配一个唯一的名称。尽管 Bean 的命名没有限制,不过官方建议采用驼峰命名法来命名 Bean

Spring Bean 命名生成器

Spring 提供了两种 Spring Bean 命名生成器:

  • DefaultBeanNameGenerator:默认通用 BeanNameGenerator 实现。
  • AnnotationBeanNameGenerator:基于注解扫描的 BeanNameGenerator 实现。
1
2
3
public interface BeanNameGenerator {
String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}

Spring Bean 别名

Spring 支持通过 <alias> 属性为 Bean 设置别名。

Bean 别名(Alias)的作用:

  • 复用现有的 BeanDefinition
  • 更具有场景化的命名方法,比如:
    • <alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
    • <alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
1
2
3
4
<bean id="user" class="io.github.dunwu.spring.core.bean.entity.person.User">
<!-- 属性略 -->
</bean>
<alias name="user" alias="aliasUser" />

Spring Bean 生命周期

  1. Spring 对 Bean 进行实例化(相当于 new XXX())

  2. Spring 将值和引用注入到 Bean 对应的属性中

  3. 如果 Bean 实现了 BeanNameAware 接口,Spring 将 Bean 的 ID 传递给 setBeanName 方法

    • 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的
  4. 如果 Bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanDactory 方法,并把 BeanFactory 容器实例作为参数传入。

    • 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等
  5. 如果 Bean 实现了 ApplicationContextAware 接口,Spring 容器将调用 setApplicationContext 方法,把应用上下文作为参数传入

    • 作用与 BeanFactory 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 setApplicationContext 方法时会把它自己作为 setApplicationContext 的参数传入,而 Spring 容器在调用 setBeanFactory 前需要使用者自己指定(注入)setBeanFactory 里的参数 BeanFactory
  6. 如果 Bean 实现了 BeanPostProcess 接口,Spring 将调用 postProcessBeforeInitialization 方法

    • 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能
  7. 如果 Bean 实现了 InitializingBean 接口,Spring 将调用 afterPropertiesSet 方法,作用与在配置文件中对 Bean 使用 init-method 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。

  8. 如果 Bean 实现了 BeanPostProcess 接口,Spring 将调用 postProcessAfterInitialization 方法

    • postProcessBeforeInitialization 是在 Bean 初始化前执行的,而 postProcessAfterInitialization 是在 Bean 初始化后执行的
  9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁

  10. 如果 Bean 实现了 DispostbleBean 接口,Spring 将调用它的 destory 方法,作用与在配置文件中对 Bean 使用 destory-method 属性的作用一样,都是在 Bean 实例销毁前执行的方法。

Spring Bean 注册

注册 Spring Bean 实际上是将 BeanDefinition 注册到 IoC 容器中。

XML 配置元信息

Spring 的传统配置方式。在 <bean> 标签中配置元数据内容。

缺点是当 JavaBean 过多时,产生的配置文件足以让你眼花缭乱。

注解配置元信息

使用 @Bean@Component@Import 注解注册 Spring Bean。

Java API 配置元信息

  • 命名方式:BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)
  • 非命名方式:BeanDefinitionReaderUtils#registerWithGeneratedName(AbstractBeanDefinition,BeanDefinitionRegistry)
  • 配置类方式:AnnotatedBeanDefinitionReader#register(Class...)

💻 Spring Bean 注册示例源码:BeanRegistryTests

Spring Bean 实例化

Spring Bean 实例化方式:

  • 常规方式
    • 通过构造器(配置元信息:XML、Java 注解和 Java API)
    • 通过静态方法(配置元信息:XML、Java 注解和 Java API)
    • 通过 Bean 工厂方法(配置元信息:XML、Java 注解和 Java API)
    • 通过 FactoryBean(配置元信息:XML、Java 注解和 Java API)
  • 特殊方式
    • 通过 ServiceLoaderFactoryBean(配置元信息:XML、Java 注解和 Java API )
    • 通过 AutowireCapableBeanFactory#createBean(java.lang.Class, int, boolean)
    • 通过 BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)

💻 Spring Bean 实例化示例源码:BeanInstantiationTestsBeanInstantiationSpecialTests

Spring Bean 初始化和销毁

Spring Bean 初始化和销毁的方式有以下几种:

  1. 使用 @PostConstruct@PreDestroy 注解分别指定相应的初始化方法和销毁方法。

  2. 实现 InitializingBean 接口的 afterPropertiesSet() 方法来编写初始化方法;实现 DisposableBean 接口的 destroy() 方法来编写销毁方法。

    • InitializingBean 接口包含一个 afterPropertiesSet 方法,可以通过实现该接口,然后在这个方法中编写初始化逻辑。
    • DisposableBean接口包含一个 destory 方法,可以通过实现该接口,然后在这个方法中编写销毁逻辑。
  3. 自定义初始化方法

    • XML 配置:<bean init-method="init" destroy="destroy" ... />
    • Java 注解:@Bean(initMethod = "init", destroyMethod = "destroy")
    • Java API:AbstractBeanDefinition#setInitMethodName(String)AbstractBeanDefinition#setDestroyMethodName(String) 分别定义初始化和销毁方法

注意:如果同时存在,执行顺序会按照序列执行。

Bean 的延迟初始化

  • xml 方式:<bean lazy-init="true" ... />
  • 注解方式:@Lazy

Spring 提供了一个 BeanPostProcessor 接口,提供了两个方法 postProcessBeforeInitializationpostProcessAfterInitialization。其中postProcessBeforeInitialization 在组件的初始化方法调用之前执行,postProcessAfterInitialization 在组件的初始化方法调用之后执行。它们都包含两个入参:

  • bean:当前组件对象;
  • beanName:当前组件在容器中的名称。

💻 Spring Bean 初始化和销毁示例源码:BeanInitDestroyTests

Spring Bean 垃圾回收

Spring Bean 垃圾回收步骤:

  1. 关闭 Spring 容器(应用上下文)
  2. 执行 GC
  3. Spring Bean 覆盖的 finalize() 方法被回调

Spring Bean 作用范围

Scope Description
singleton (Default) Scopes a single bean definition to a single object instance for each Spring IoC container.
prototype Scopes a single bean definition to any number of object instances.
request Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext.
session Scopes a single bean definition to the lifecycle of an HTTP Session. Only valid in the context of a web-aware Spring ApplicationContext.
application Scopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext.
websocket Scopes a single bean definition to the lifecycle of a WebSocket. Only valid in the context of a web-aware Spring ApplicationContext.

参考资料

SpringBoot 之快速入门

Spring Boot 简介

Spring Boot 可以让使用者非常方便的创建 Spring 应用。

Spring Boot 的目标是:

  • 为所有 Spring 开发者提供更快且可广泛访问的入门体验。
  • 开箱即用
  • 提供一系列通用的非功能特性(例如嵌入式服务、安全、指标、健康检查和外部化配置)
  • 完全不需要代码生成,也不需要 XML 配置。

Spring Boot 系统要求

Spring Boot 的构建工具要求:

Build Tool Version
Maven 3.5+
Gradle 6.8.x, 6.9.x, and 7.x

Spring Boot 支持的 Servlet 容器:

Name Servlet Version
Tomcat 9.0 4.0
Jetty 9.4 3.1
Jetty 10.0 4.0
Undertow 2.0 4.0

部署第一个 Spring Boot 项目

本节介绍如何开发一个小的“Hello World!” web 应用示例,来展示 Spring Boot 的一些关键功能。我们使用 Maven 来构建这个项目,因为大多数 IDE 都支持它。

环境检查

Spring Boot 项目依赖于 Java 环境和 Mave,开始项目之前需要先检查一下环境。

本地是否已安装 Java:

1
2
3
4
$ java -version
java version "1.8.0_102"
Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)

本地是否已安装 Maven:

1
2
3
4
$ mvn -v
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00)
Maven home: /usr/local/Cellar/maven/3.3.9/libexec
Java version: 1.8.0_102, vendor: Oracle Corporation

创建 pom

我们需要从创建 Maven pom.xml 文件开始。 pom.xml 是 Maven 用于构建项目的配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.1</version>
</parent>

<!-- Additional lines to be added here... -->

</project>

使用者可以通过运行 mvn package 来测试它

添加依赖

Spring Boot 提供了许多启动器(Starters)以应对不同的使用场景。使用者可将 jars 添加到类路径中。我们的示例程序在 POM 的 parent 使用 spring-boot-starter-parent。 spring-boot-starter-parent 是一个特殊的启动器,提供有用的 Maven 默认值。它还提供了一个依赖项的版本管理,可以让使用者使用时不必显示指定版本。

其他启动器(Starters)提供了各种针对不同使用场景的功能。比如,我们需要开发一个 Web 应用程序,就可以添加了一个 spring-boot-starter-web 依赖项。在此之前,我们可以通过运行以下命令来查看我们当前拥有的 maven 依赖:

1
2
3
$ mvn dependency:tree

[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT

mvn dependency:tree 命令打印项目依赖项的层级结构。可以看到 spring-boot-starter-parent 本身没有提供任何依赖。要添加必要的依赖,需要编辑 pom.xml 并在 <dependencies> 部分添加 spring-boot-starter-web 依赖项:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

编写代码

要运行应用程序,我们需要创建一个启动类。默认情况下,Maven 从 src/main/java 编译源代码,因此您需要创建该目录结构,然后添加一个名为 src/main/java/MyApplication.java 的文件以包含以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@EnableAutoConfiguration
public class MyApplication {

@RequestMapping("/")
String home() {
return "Hello World!";
}

public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}

}

说明:

@RestController 注解告诉 Spring,这个类是用来处理 Rest 请求的。

@RequestMapping 注解提供了“路由”信息。它告诉 Spring 任何带有 / 路径的 HTTP 请求都应该映射到 home 方法。 @RestController 注解告诉 Spring 将结果字符串直接呈现给调用者。

@EnableAutoConfiguration 注解告诉 Spring Boot 根据你添加的 jar 依赖去自动装配 Spring。

自动配置旨在与“Starters”配合使用,但这两个概念并没有直接联系。您可以自由选择 starters 之外的 jar 依赖项。 Spring Boot 仍然尽力自动配置您的应用程序。

Spring Boot 的 main 方法通过调用 run 委托给 Spring Boot 的 SpringApplication 类。 SpringApplication 引导我们的应用程序,启动 Spring,进而启动自动配置的 Tomcat Web 服务器。我们需要将 MyApplication.class 作为参数传递给 run 方法,以告诉 SpringApplication 哪个是入口类。还传递 args 数组以公开任何命令行参数。

运行示例

此时,您的应用程序应该可以工作了。由于您使用了 spring-boot-starter-parent POM,因此您有一个有用的运行目标,可用于启动应用程序。从项目根目录键入 mvn spring-boot:run 以启动应用程序。您应该会看到类似于以下内容的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mvn spring-boot:run

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.1)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 2.222 seconds (JVM running for 6.514)

如果您打开 Web 浏览器访问 localhost:8080,您应该会看到以下输出:

1
Hello World!

要正常退出应用程序,请按 ctrl-c

创建可执行 jar

要创建一个可执行的 jar,我们需要将 spring-boot-maven-plugin 添加到我们的 pom.xml 中。为此,请在依赖项部分下方插入以下行:

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

保存 pom.xml 并从命令行运行 mvn package,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ mvn package

[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building myproject 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] .... ..
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject ---
[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (default) @ myproject ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

如果您查看 target 目录,应该会看到 myproject-0.0.1-SNAPSHOT.jar。该文件的大小应约为 10 MB。如果想看里面,可以使用 jar tvf,如下:

1
$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar

您还应该在目标目录中看到一个更小的名为 myproject-0.0.1-SNAPSHOT.jar.original 的文件。这是 Maven 在 Spring Boot 重新打包之前创建的原始 jar 文件。

要运行该应用程序,请使用 java -jar 命令,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ java -jar target/myproject-0.0.1-SNAPSHOT.jar

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.1)
....... . . .
....... . . . (log output here)
....... . . .
........ Started MyApplication in 2.536 seconds (JVM running for 2.864)

和以前一样,要退出应用程序,请按 ctrl-c

通过 SPRING INITIALIZR 创建 Spring Boot 项目

创建项目

通过 SPRING INITIALIZR 工具产生基础项目

  1. 访问:http://start.spring.io/
  2. 选择构建工具Maven Project、Spring Boot 版本 1.5.10 以及一些工程基本信息,可参考下图所示:

img

  1. 点击Generate Project下载项目压缩包
  2. 解压压缩包,包中已是一个完整的项目。

如果你使用 Intellij 作为 IDE,那么你可以直接使用 SPRING INITIALIZR,参考下图操作:

img

项目说明

重要文件

  • src/main/java 路径下的 Chapter1Application 类 :程序入口
  • src/main/resources 路径下的 application.properties :项目配置文件
  • src/test/java 路径下的 Chapter01ApplicationTests :程序测试入口

pom.xml

pom 中指定 parent 为以下内容,表示此项目继承了 spring-boot-starter-parent 的 maven 配置(主要是指定了常用依赖、插件的版本)。

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

此外,pom 中默认引入两个依赖包,和一个插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
  • spring-boot-starter-web:核心模块,包括自动配置支持、日志和 YAML。
  • spring-boot-starter-test:测试模块,包括 JUnit、Hamcrest、Mockito。
  • spring-boot-maven-plugin:spring boot 插件, 提供了一系列 spring boot 相关的 maven 操作。
    • spring-boot:build-info,生成 Actuator 使用的构建信息文件 build-info.properties
    • spring-boot:repackage,默认 goal。在 mvn package 之后,再次打包可执行的 jar/war,同时保留 mvn package 生成的 jar/war 为.origin
    • spring-boot:run,运行 Spring Boot 应用
    • spring-boot:start,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理
    • spring-boot:stop,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理

编写 REST 服务

  • 创建 package ,名为 io.github.zp.springboot.chapter1.web(根据项目情况修改)
  • 创建 HelloController 类,内容如下:
1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@RequestMapping("/hello")
public String index() {
return "Hello World";
}

}
  • 启动主程序 XXXApplication,打开浏览器访问http://localhost:8080/hello ,可以看到页面输出Hello World

编写单元测试用例

XXXApplicationTests 类中编写一个简单的单元测试来模拟 HTTP 请求,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MockServletContext.class)
@WebAppConfiguration
public class SpringBootHelloWorldApplicationTest {

private MockMvc mvc;

@Before
public void setUp() {
mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
}

@Test
public void getHello() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World")));
}

}

使用MockServletContext来构建一个空的WebApplicationContext,这样我们创建的HelloController就可以在@Before函数中创建并传递到MockMvcBuilders.standaloneSetup()函数中。

  • 注意引入下面内容,让statuscontentequalTo函数可用
1
2
3
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

至此已完成目标,通过 Maven 构建了一个空白 Spring Boot 项目,再通过引入 web 模块实现了一个简单的请求处理。

示例源码

示例源码:spring-boot-web-helloworld

参考资料

分布式基础理论

分布式特性和分类

分布式特性

  • 性能:用于衡量一个系统处理各种任务的能力。
    • 吞吐量:系统在一定时间内可以处理的任务数。
      • QPS,即每秒查询数
      • TPS,即每秒事务数
    • 响应时间:系统响应一个请求或输入需要花费的时间。
  • 可用性:指的是系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停止服务的时间与总的时间之比衡量。
  • 可扩展性:指的是分布式系统通过扩展集群机器规模提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性,是分布式系统的特有性质。

分布式分类

  • 分布式计算:解决应用的分布式计算问题。基于分布式计算模式,包括批处理计算、离线计算、在线计算、融合计算等,根据应用类型构建高效智能的分布式计算框架。
  • 分布式存储:解决数据的分布式和多元化问题。包括分布式数据库、分布式文件系统、分布式缓存等,支持不同类型的数据的存储和管理。
  • 分布式通信:解决进程间的分布式通信问题。通过消息队列、远程调用等方式,实现简单高效的通信。
  • 分布式资源管理:解决资源的分布式和异构性问题。将 CPU、内存、IO 等物理资源虚拟化,新城逻辑资源池,以便统一管理。

错误的分布式假设

内容摘自 The Eight Fallacies of Distributed Computing - Tech Talk

随着时间的推移,每一条都会被证明是错误的,也都会导致严重的问题,以及痛苦的学习体验:

  • 网络是稳定的
  • 网络传输的延迟是零
  • 网络的带宽是无穷大
  • 网络是安全的
  • 网络的拓扑不会改变
  • 只有一个系统管理员
  • 传输数据的成本为零
  • 整个网络是同构的

为什么我们要深刻地认识这 8 个错误?

是因为,这要我们清楚地认识到——分布式系统中,错误是不可能避免的。既然错误无可避免,那么,我们应该做的是,将容错也作为功能去实现。

参考资料