探索 Tallate
一个从后端系统走向 AI Agent 的工程师,正在思考、构建和记录什么?
Neo4j学习
建模
图数据结构
属性图结构
- 包含节点和边,同时节点和边上又有属性property和标签label
- 边有名字和方向
图数据库查询语言
- Cypher
应用于Neo4j、GDB等 - Gremlin
应用于GDB
跨域模型
在节点和关联关系不是很复杂的情况下,我们建的图通常只有一个领域的内容,比如人与人之间的关联图谱。但是随着业务的发展,我们会觉得单一的人物关系不能满足我们的使用需求,我还希望加入企业注册数据,来进行企业与人之间的管理分析。随后,我们陆续加入了法律诉讼信息、新闻资讯信息等,这样我们的图从一张小图变成了大图,也从只有一个领域(人)的模型变成了多个领域的跨域模型。
跨域模型有助于我们理解复杂的价值链背后的关联,不仅可以联合多个领域,而且每个领域的内容又能单独区分开来。这里主要借助了图数据里的2个概念:属性图和标签。属性图模型让不同的领域很容易联系起来,这样每个域都是可达的;标签既能表示不同节点在域中扮演的角色,又可以让我们将它归属的节点和元数据结合起来。
面向查询设计
建模其实就是利用图结构来描述问题的过程,为了使我们的模型更接近业务需求,有一个方法,叫做面向查询设计:
- 将需求转化为领域问题
- 明确领域内出现的节点和关系
- 把这些节点和联系翻译成查询语言
- 使用路径表达式,描述需要解决的问题
使用图查询语言构建数据模型
1 | # 创建带有属性name和age的People节点 |
参考
RocketMQStream学习
架构

- 采用 shared-nothing 的分布式架构设计,依赖消息队列做负载均衡和容错机制,单实例可启动,增加实例实现能力扩展,并发能力取决于分片数;
其实就是stateless - 利用消息队列的分片做 shuffle,利用消息队列负载均衡实现容错;
- 利用存储实现状态备份,实现 Exactly-ONCE 的语义。用结构化远程存储实现快速启动,不等本地存储恢复;
- 重力打造过滤优化器,通过前置指纹过滤,同源规则自动归并,hyperscan 加速,表达式指纹提高过滤性能
RocketMQ Streams消费一条消息的过程
ISource消费数据源
触发条件:
分类:
RocketMQSource读取来自RocketMQ的消息
流程(RocketMQSource):
1.
IStreamOperator算子处理来自Source的数据
触发条件:
AbstractSource处理消息AbstractSource#executeMessage
ISink存储数据
触发条件:
SinkAction算子触发
流程(SinkAction):
- 默认写到cache
IMessageCache#addCache
触发时机:接收消息时SinkAction#doMessage - 启动自动flush
ISink#openAutoFlush
触发时机:刷新配置后的处理SinkAction#doProcessAfterRefreshConfigurable
触发逻辑:把队列排空,并写入到存储中MessageCache#flush()
触发间隔:100msScheduleManager#start
shared-nothing
shared-nothing体现在RocketMQ的以下属性:
- 消息的存储采用Topic+Queue的模式;
- Consumer端消费采取pull模式而不是push;
- Consumer端负载均衡Rebalance,同时Rebalance也可以实现容错;
RocketMQ 消息的存储和查询原理
RocketMQ 如何发送一条消息
负载均衡
RocketMQ Streams在启动时会对上游的ISource拆分分片,AbstractPullSource#startSource
- 获取所有分片
IPullSource#fetchAllSplits - 拆分分片
ISourceBalance#doBalance - 定时监听分片的变更
AbstractPullSource#doSplitChanged
Exactly-ONCE 语义
shuffle
Kubernetes原理
容器的原理:Namespace做隔离,Cgroups做限制,rootfs做文件系统。
k8s的主要作用是调度容器,管理容器的生命周期,在k8s中调度的最小单位是pod,除此之外还有service、cluster这些概念。
性能分析-CPU

性能指标
平均负载
平均负载是指单位时间内,处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数。所以,它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程,因此和CPU使用率也并没有直接关系。
- 所谓可运行状态的进程,是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(Running 或 Runnable)的进程。
- 不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。
当平均负载为2时,意味着:
- 在只有2个CPU的系统上,意味着所有CPU都刚好被完全占用;
- 在4个CPU的系统上,意味着CPU有50%的空闲;
- 在只有一个CPU的系统中,意味着有一半的进程竞争不到CPU。
平均负载为多少比较合适?
- 先看系统有几个CPU,可以通过top命令或文件/proc/cpuinfo读取
- 经验讲当平均负载高于CPU数量70%时,需要分析排查负载过高的问题
- uptime查看负载变化趋势,把系统的平均负载监控起来,根据更多的历史数据来判断负载变化趋势,如果有明显升高趋势再做分析。
CPU使用率
CPU使用率的定义和计算
CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。
比如:
CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。
Linux 通过 /proc 虚拟文件系统,向用户空间提供了系统内部状态的信息,而 /proc/stat 提供的就是系统的 CPU 和任务统计信息:
1 | $ cat /proc/stat | grep ^cpu |
- 第一行cpu指所有cpu的累加,后面的cpu0, cpu1表示各个cpu的统计
- user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
- nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越
- 大,优先级反而越低。
- system(通常缩写为sys),代表内核态 CPU 时间。
- idle(通常缩写为id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。
- iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。
- irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。
- softirq(通常缩写为 si),代表处理软中断的 CPU 时间。
- steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
- guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
- guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。
而CPU使用率,就是除了空闲时间外的其他时间占总CPU时间的百分比,用公式来表示就是:
1 | CPU使用率 = 1 - 空闲时间 / 总CPU时间 |
但是这个值是开机以来的CPU使用率,没什么参考价值,重要的是计算单位时间内的CPU使用率或简称平均CPU使用率。
1 | 平均CPU使用率 = 1 - (空闲时间new - 空闲时间old) / (总CPU时间new - 总CPU时间old) |
平均CPU使用率可以通过top、ps命令来查看。
造成CPU使用率过高的原因可能是:
- 用户CPU过高
- 系统CPU过高(如上下文切换)
- 等待IO的CPU(如等待磁盘的响应)
- 中断CPU(包括软中断和硬中断)
CPU上下文切换
CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
根据任务的不同,CPU 的上下文切换可以分为几个不同的场景,也就是进程上下文切换、线程上下文切换以及中断上下文切换。
进程上下文切换
根据Linux的特权等级分级:
- 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
- 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过 系统调用 陷入到内核中,才能访问这些特权资源。
需要注意的是,系统调用陷入内核态执行完毕后,还需要切换回用户态,此时其实又发生了一次上下文切换,所以一次系统调用伴随了2次的CPU上下文切换。
系统调用过程的CPU上下文切换:
- CPU 寄存器里原来用户态的指令位置,需要先保存起来。
- 接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
- 最后才是跳转到内核态运行内核任务。
- 系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。
进程上下文切换:
进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
- 进程上下文切换,是指从一个进程切换到另一个进程运行。而系统调用过程中一直是同一个进程在运行。
- 进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
进程上下文潜在的性能问题:
- 频繁的上下文切换容易导致平均负载升高:每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是上一节中我们所讲的,导致平均负载升高的一个重要因素。
- Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
切换进程上下文的时机:
只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。
- 进程执行完终止了,它之前使用的 CPU 会释放出来,这个时候再从就绪队列里,拿一个新的进程过来运行。
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
当进程只有一个线程时,可以认为进程就等于线程。
当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
因此,根据切换的多个线程所属的进程不同,有2种情况:
- 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
- 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
同进程内的线程切换消耗的资源更少。
中断上下文切换
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
CPU瓶颈定位
1、平均负载
系统平均活跃进程数,反映了系统的整体负载情况
2、CPU使用率
根据CPU上运行任务的不同,CPU使用率可以分为:
- 用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。
- 系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。
- 等待 I/O 的CPU使用率,通常也称为iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
- 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
- 除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。
3、进程上下文切换
过多的上下文切换会将原本运行进程的CPU时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和回复上,缩短进程真正运行的时间,成为性能瓶颈。包括:
- 无法获取资源而导致的自愿上下文切换;
- 被系统强制调度导致的非自愿上下文切换。
4、CPU缓存的命中率
包括L1、L2、L3等三级缓存。
性能剖析
uptime - 查看平均负载
平均负载最理想情况下等于CPU个数,超过时表示发生了过载,达到70%时就应该分析排查负载高的问题,。
watch - 可用于观察负载变化情况
top 或 读取/proc/cpuinfo - 查看系统有几个CPU
top - 查看CPU使用率
mpstat - 处理器统计
pidstat - 进程统计
-w 查看进程上下文切换情况
- cswch 每秒自愿上下文切换(voluntary context switches)的次数
是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。 - nvcswch 每秒非自愿上下文切换(non voluntary context switches)的次数。
是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
stress - 系统压测
sysstat - Linux常用性能工具
sysstat包含mpstat、pidstat
mpstat 是一个常用的多核 CPU 性能分析工具,用来实时查看每个 CPU 的性能指标,以及所有 CPU 的平均指标。
pidstat 是一个常用的进程性能分析工具,用来实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。
vmstat - 查看系统上下文切换情况
主要用来分析系统的内存使用情况,也常用来分析CPU上下文切换和中断的次数
- cs(context switch)是每秒上下文切换的次数。
- in(interrupt)则是每秒中断的次数。
- r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待CPU的进程数。
- b(Blocked)则是处于不可中断睡眠状态的进程数。
vmstat主要用于看系统整体的上下文切换情况,如果想看每个进程的详细情况,可以用pidstat
top - 默认以3秒时间间隔统计CPU使用率
ps - 统计进程整个生命周期的CPU使用率
pidstat - 分析每个进程CPU使用情况
perf 分析性能问题 比如CPU使用率过高
perf top 实时显示占用CPU时钟最多的函数或指令,因此可以用来查找热点函数
perf record、perf report 保存性能分析结果及展示结果
并发和线程任务编排
Future - 好莱坞原则的应用
Future
FutureTask
并行化
刚开始接触并发编程,容易写出下面的代码:
1 | long startTime = System.currentTimeMillis(); |
此时开发很有可能会误以为结果是 1s 左右,毕竟 3 个FutureTask同时执行、每个只等待 1s,结果确实应该是 1s,但是这里并没有实现并行化,因为future.get是轮询调用的,第一个执行完毕后,第二个仍然需要等待 1s,因此结果是 3s。
虽然每个 FutureTask 是同时开始执行的,但是future.get并不是同时开始等待的,如果想要达到并行执行的效果,一定是在上一个执行完毕的时候,下一个就已经执行完毕了,此时就可以直接获取结果了。
1 | long startTime = System.currentTimeMillis(); |
阻塞还是轮询
阻塞虽然看起来很廉价,似乎线程阻塞后就不占用资源了,实际上很多阻塞都是通过轮询标志位实现的,比如 FutureTask 内部实现了这样的一种状态机:
每次状态的变更都是通过CAS(UNSAFE)实现线程安全的,比如set方法:
1 | protected void set(V v) { |
Guava - ListenableFuture
ListenableFuture 是在 JDK1.8 之前出现的,现在 CompletableFuture 一般是更好的选择。
CompletionService
1 | long start = System.currentTimeMillis(); |
- 将线程任务提交到线程池执行,并将 Future 提交到阻塞队列供客户端获取,阻塞队列默认是
LinkedBlockingQueue; - CompletionService 内部只有在任务完成的时候才会把 Future 丢到队列中,因此如果任务特别耗时,会导致
take调用一直不返回,注意下面源码中的done方法:1
2
3
4
5
6
7
8private class QueueingFuture extends FutureTask<Void> {
QueueingFuture(RunnableFuture<V> task) {
super(task, null);
this.task = task;
}
protected void done() { completionQueue.add(task); }
private final Future<V> task;
}
CompletableFuture
CompletableFuture是对Future的加强,CompletableFuture实现了 Future 接口,这意味着它本身也提供了通过阻塞或轮询获取结果的方式,相对 Future 来说,它的相对优势在于任务的编排,相对 CompletionService 来说,CompletableFuture又有接口更灵活的优势。
1 | long start = System.currentTimeMillis(); |
注意上面的CompletableFuture.supplyAsync和CompletableFuture.join
- 提交异步执行任务
CompletableFuture.supplyAsync,有2个方法,其中有个需要用户指定Executor,如果没有指定则使用JDK内置的线程池ForkJoinPool.commonPool()- 提交任务到CompletableFuture和直接提交到ExecutorService的区别是线程任务提交前会用
AsyncSupply封装
- 提交任务到CompletableFuture和直接提交到ExecutorService的区别是线程任务提交前会用
- 线程池执行任务
- 任务经过AsyncSupply封装,会调用重写的
AsyncSupply.exec() - 执行完毕后,回调
CompletableFuture.internalComplete
- 任务经过AsyncSupply封装,会调用重写的
- 任务结果聚合
CompletableFuture::join- 如果结果已经计算出来,则直接取result
- 否则
CompletableFuture.waitingGet
创建等待队列q = new WaitNode(interruptible, 0L, 0L);
排队queued = UNSAFE.compareAndSwapObject(this, WAITERS, q.next = waiters, q);
调ForkJoinPool.managedBlock(q);阻塞,会调WaitNode.block()来判断是否还阻塞着 - 回头看
CompletableFuture.internalComplete里的代码,会调LockSupport.unpark(t);唤醒等待的线程
参考
长长的回廊
桐生枝梨子(高显秘书、相貌平平、认真、火灾毁容)
里中二郎(死者、高显亲生骨肉)
克子
一原高显(社长、癌症)
一原苍介(高显弟弟、大学教授)
一原直之(苍介弟弟、精明)
一原纪代美(苍介另一哥哥的太太)
由香(纪代美女儿)
一原曜子(苍介妹妹)
加奈江(曜子女儿)
本间重太郎(高显朋友)
本间菊代(重太郎太太、现假扮)
一原健彦(苍介儿子、喜欢由香)
小林真穗(店长、高显情人)
矢崎警部
古木律师
鲹泽弘美(古木助理)
尹之壹
火灾
车祸逃逸
高显遗嘱
七七法事
八泽温泉(墓地)
枝梨子遗书
由香的死:拿走信封、之后死了、倒着的N(俄语字母)、刀伤、勒脖
红酒
安眠药
审讯
直之的珍珠领带夹
私生子
脚印
纪代美的一对珍珠
头发
茶道
碎冰锥
ES3_6算分和排序
Doc Values / fielddata - 正排索引
在搜索的时候,我们能通过搜索关键词快速得到结果集。当排序的时候,我们需要倒排索引里面某个字段值的集合,此时倒排索引无法发挥作用。换句话说,我们需要 转置 倒排索引。转置 结构在其他系统中经常被称作 列存储 。实质上,它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序。
ES有2种方法实现:
- Fielddata(可以存储Text类型)
- Doc Values(列式存储,对Text类型无效)
| Doc Values | Field data | |
|---|---|---|
| 何时创建 | 索引时,和倒排索引一起创建 | 搜索时动态创建 |
| 创建位置 | 磁盘文件 | JVM Heap |
| 优点 | 避免大量内存占用 | 索引速度快,不占用额外的磁盘空间 |
| 缺点 | 降低索引速度,占用额外磁盘空间 | 文档过多时,动态创建开销大,占用过多JVM Heap |
| 缺省值 | ES 2.x 之后 | ES 1.x 及之前 |
当 working set 远小于节点的可用内存,系统会自动将所有的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。
关闭 Doc Values
Doc Value默认是启用的,可以通过Mapping设置关闭
1 | PUT test_keyword/_mapping |
- 关闭有什么好处?增加索引速度、减少磁盘占用空间
- 关闭会有什么问题?如果后续需要重新打开,则需要重建索引
- 什么时候需要关闭?明确不需要做排序及聚合分析
排序与相关性
- sort将目标字段转换为排序所需的格式,date 字段的值表示为自 epoch (January 1, 1970 00:00:00 UTC)以来的毫秒数,通过 sort 字段的值进行返回。
- 如果字段是一个数组(多值),可以使用 mode 指定其中 min 、 max 、 avg 或是 sum 进行排序;
- 评分的计算方式取决于 查询类型 不同的查询语句用于不同的目的: fuzzy 查询会计算与关键词的拼写相似程度,terms 查询会计算 找到的内容与关键词组成部分匹配的百分比,但是通常我们说的 relevance 是我们用来计算全文本字段的值相对于全文本检索词相似程度的算法,Elasticsearch 的相似度算法 被定义为检索词频率/反向文档频率(TF/IDF),包括以下内容:
- 检索词频率
检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。 - 反向文档频率
每个检索词在索引中出现的频率?频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低。 - 字段长度准则
字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。
- 检索词频率
- 单个查询可以联合使用 TF/IDF 和其他方式,比如短语查询中检索词的距离或模糊查询里的检索词相似度。如果多条查询子句被合并为一条复合查询语句 ,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
- 字符串索引后(analusis)会有变化,排序时希望使用原字段(not_analyzed)进行排序,我们想要对同一个字段索引两次,而不是在_source 中保存两份字符串字段,这可以通过为字段添加一个 not_analyzed 子字段来实现:主字段用于搜索、子字段用于排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16"tweet": {
"type": "string",
"analyzer": "english",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed"
}
}
}
"sort" : {
"date": {
"order": "desc",
"mode": "min"
}
} - 默认情况下,返回结果是按相关性倒序排列的
- 可以在查询参数中加入 explain 参数解释排序结果
1
2
3
4GET /_search?explain
{
"query" : { "match" : { "tweet" : "honeymoon" }}
}
分页
几种分页方式及应用场景
- Regular
平时查询ES只会返回头部的10条数据,一般用于实时获取顶部的部分文档,例如查询最新的订单。 - Scroll
需要全部文档时,例如导出全部数据 - Pagination
from + size的方式
如果需要深度分页,则选用Search After
from 和 size(分页)、Search After
ES中的分页是从每个分片上获取from + size条数据,然后协调节点聚合所有结果,再选取前from + size条数据。
因为是from + size,所以from特别大时会有深分页问题。
解决办法是Search After:
1 | POST users/_search |
缺点是:
- 不支持指定页数(From)
- 只能往下翻
需要指定搜索sort:
- 需要保证值是唯一的,可以加入_id保证唯一性
- 每次查询使用上一次查询得到的最后一个文档的sort值进行查询(即上边的13和”idididid”)
Search After会通过唯一排序值定位,将每次要处理的文档数都控制在size个。
游标查询 Scroll
scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。
启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。
1 | GET /old_index/_search?scroll=1m // 保持游标查询窗口一分钟。 |
这个查询的返回结果包括一个字段 _scroll_id, 它是一个 base64 编码的长字符串。现在我们能传递字段 _scroll_id 到 _search/scroll 查询接口获取下一批结果:
1 | GET /_search/scroll |
这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为 1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards。
当没有更多结果返回的时候,我们就处理完所有匹配的文档了。
缺点:
- Scroll会创建一个快照,如果查询期间有新的数据写入以后,无法被查到
算分优化 - Function Score Query
算分函数
ES提供了几种默认的计算分值的函数:
weight
设置权重
field_value_factor
使用某个数值修改_score的值,比如乘以某个系数
原算分乘以某个字段得到最终结果,比如下面就是乘以原文档中的count字段
1 | GET doc/_search |
还可以根据某个函数来计算评分,比如如下命令新算分 = 老算分 * log(1 + factor * count):
1 | GET doc/_search |
random_score
为每一个用户使用一个不同的,随机算分结果
使用场景:让每个用户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序保持一致
1 | GET doc/_search |
衰减函数
以某个字段的值为标准,距离某个值越近,得分越高
script_score
自定义脚本完全控制所需逻辑
elasticsearch painless脚本评分
Elasticsearch中使用painless实现评分
boost
- boost mode
multiply:默认方式,算分与函数值的乘积
sum:算分与函数的和
min / max:算分与函数取最小/最大值
replace:使用函数值取代算分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16GET doc/_search
{
"query": {
"function_score": {
"query": {
"match": {
"title": "a"
}
},
"field_value_factor": {
"field": "count"
},
"boost_mode": "sum"
}
}
} - max boost
将算分控制在一个最大值
算分原理
计算目标文档和origin之间的距离
- NumericFieldDataScoreFunction#distance
数值距离=max(0, |doubleValue - origin| - offset) - GeoFieldDataScoreFunction#distance:
地域距离
衰减函数
- GaussDecayFunction
高斯衰减=e^(distance^2 / 2scale)




