深入 Elasticsearch(07):BM25 与打分机制
上一篇解决了搜索的执行流程——Query-Then-Fetch 两阶段在集群中怎样运转。这一篇进入每条结果的分数是怎么算出来的。 ES 的搜索结果按 _score 降序排列。_score 是 BM25 算法算出的相关性分数。理解 BM25,才能理解为什么有些文档排在前面、有些排在后面,也才能有针对性地调整搜索相关性。 本文只抓一个问题:BM25 的三个因子分别衡量什么,以及它们怎样组合成最终分数。 BM25 的直觉 BM25 的名字来自"Best Matching 25"(第 25 号最佳匹配公式)。它取代了 ES 5.x 之前使用的 TF-IDF 模型,成为 Lucene 和 ES 的默认相似度算法。 BM25 的核心思路可以用一句话概括:一个词项越稀有(IDF 高)、在文档中出现越多次(TF 高)、文档越短(字段长度归一化),包含这个词项的文档就越相关。 分数由三个因子相乘构成: 1score(q, d) = IDF(q) × TF_saturation(q, d) × length_norm(d) flowchart TB IDF["...
深入 Elasticsearch(06):Query-Then-Fetch 的两阶段流程
上一篇解决了文档怎样变成可搜索的——写入路径经过 buffer、translog、refresh 三步后文档才对搜索可见。这一篇进入一次搜索请求在集群内部怎样执行。 ES 是分布式搜索引擎,一个 index 的数据分布在多个 shard 上。一次搜索需要在所有相关 shard 上执行,然后合并结果。这个过程分成两个阶段:Query 阶段和 Fetch 阶段。 本文只抓一个问题:Query-Then-Fetch 的两阶段搜索是怎么工作的,以及这个设计为什么是必要的。 为什么需要两个阶段 如果 ES 只有一个 shard,搜索很简单:在 shard 上执行查询,取 top-N 结果,返回。但数据分布在多个 shard 上时,直接从每个 shard 取 top-N 的完整文档(包含 _source),再在 coordinating node 合并,会传输大量不需要的数据——大部分文档在全局排序后会被丢弃。 两阶段设计的核心思路:先用轻量数据(docId + score)做全局排序,再只取需要的文档内容。 sequenceDiagram participant C as Cli...
深入 Elasticsearch(05):近实时、Translog 与 Refresh/Flush
上一篇解决了文本怎样变成词项——analysis pipeline 把原始文本标准化为可索引的 term。这一篇进入文档写入后怎样变成可搜索的。 一条文档经过 analysis 生成词项后,并不会立即对搜索可见。ES 的写入路径涉及内存缓冲、translog 持久化、refresh 生成新 segment、flush 提交到磁盘四个步骤。这套机制让 ES 在写入吞吐和搜索延迟之间取得平衡,也是"近实时搜索"(near-real-time, NRT)这个名字的由来。 本文只抓一个问题:ES 的近实时搜索是怎么实现的,refresh 和 flush 的区别在哪里。 写入路径的完整流程 一条文档从客户端发出到最终持久化到磁盘,经过以下步骤: 123456789101112131415Document → Coordinating Node(路由到目标 shard 的 primary) → Primary Shard: 1. 写入 in-memory buffer(indexing buffer) 2. 追加到 translog(顺序写磁盘,...
深入 Elasticsearch(04):从原始文本到可搜索词项
上一篇解决了文档的结构定义——mapping 把 JSON 字段翻译成 Lucene 数据结构。这一篇进入 text 类型字段怎样从原始字符串变成可搜索的词项。 mapping 决定一个字段"要不要做全文索引",analysis 决定"怎样做"。一个 text 字段在写入时经过 analysis pipeline,产生一组标准化的词项(terms),这些词项被写入倒排索引。搜索时,查询文本也经过 analysis,产生同样标准化的词项,然后在倒排索引中匹配。 本文只抓一个问题:analysis pipeline 的三个阶段怎样工作,以及索引时分析和搜索时分析为什么可能用不同的 analyzer。 Analysis 三阶段 一个 analyzer 由三个组件按顺序组成: 12345原始文本 → Character Filter(s) 字符级预处理 → Tokenizer 切分为 token → Token Filter(s) token 级后处理 → [term1, term2, ...] 写入倒...
深入 Elasticsearch(03):Mapping 与字段类型
上一篇解决了 Lucene 内部的段和索引结构。这一篇进入 ES 如何把 JSON 文档映射到 Lucene 字段。 一个 JSON 文档写入 ES 后,每个字段会变成 Lucene 内部的某种数据结构——倒排索引、BKD-tree、Doc Values 或 Stored Fields。这个从"JSON 字段"到"Lucene 数据结构"的翻译规则,就是 mapping。 本文只抓一个问题:mapping 在 ES 中扮演什么角色,以及 dynamic mapping 和 explicit mapping 的差异会怎样影响搜索行为。 Mapping 是什么 Mapping 定义了一个 index 中文档的字段名、字段类型和索引方式。可以类比为关系型数据库的 DDL(CREATE TABLE),但有几个关键区别: Mapping 允许动态推断。写入一个未知字段时,ES 可以自动猜测类型并添加到 mapping 中。 Mapping 一旦创建,字段类型不可修改。只能添加新字段,不能把一个 text 字段改成 keyword。要改变已有字段的...
深入 Elasticsearch(02):Segment、倒排索引与 Doc Values
上一篇解决了 ES 集群级架构——节点角色、集群状态和数据路径。这一篇进入单个 shard 内部的 Lucene 存储结构。 ES 的每个 shard 就是一个 Lucene index。Lucene index 不是一整块文件,而是由多个不可变的段(segment)组成。理解 segment 的内部结构,才能理解后续文章中 refresh、flush、merge、搜索延迟等机制为什么是那样设计的。 本文只抓一个问题:一个 Lucene segment 内部有哪些数据结构,它们各自承担什么角色。 Segment:不可变的存储单元 一个 Lucene index 由零个或多个 segment 组成。每个 segment 是一批文档的完整索引——包含这批文档的倒排索引、正排数据、字段信息等所有内容。 12345Lucene Index (= ES Shard)├── Segment 0 (committed, immutable)├── Segment 1 (committed, immutable)├── Segment 2 (committed, immutable)└─...
深入 Elasticsearch(01):Node、Cluster 与集群状态
上一篇解决了 ES 的核心数据结构是什么——倒排索引。这一篇进入 ES 的运行时架构。 一个 Elasticsearch 集群不是若干台机器简单地连在一起。集群内部有明确的角色分工、中心化的状态管理和分布式的数据路径。理解这些,才能在后续文章中准确地定位每个机制发生在哪一层。 本文只抓一个问题:一个 ES 集群由哪些角色的节点组成,它们之间通过什么机制协调。 节点角色 一个 Elasticsearch 进程启动后就是一个节点(node)。多个节点通过相同的 cluster.name 组成一个集群(cluster)。每个节点可以承担一个或多个角色,角色通过配置文件或启动参数指定。 ES 8.x 中的主要节点角色: 角色 配置值 职责 Master-eligible master 参与 master 选举,管理集群状态 Data data 存储数据分片,执行搜索和聚合 Data Content data_content 存储非时序数据 Data Hot/Warm/Cold/Frozen data_hot 等 分层存储(ILM 相关) Ingest in...
深入 Elasticsearch(00):为什么 Elasticsearch 的核心是一张倒排索引
Elasticsearch 容易被归类成"分布式搜索引擎"或"日志分析平台"。这些标签描述了它的用途,但没有说清楚它的内部结构。把 Elasticsearch 拆到最底层,剩下的核心数据结构只有一个:倒排索引(inverted index)。mapping、analysis、shard、replica、aggregation 五个机制围绕这个数据结构逐层展开。 Elasticsearch 是一个以倒排索引为核心数据结构的分布式搜索与分析引擎。 本篇是系列导读。核心任务只有一个:说清楚倒排索引是什么,以及它和关系型数据库里的 B+Tree 索引有什么本质区别。 倒排索引的基本结构 关系型数据库的 B+Tree 索引从行出发。给定一个索引键值,B+Tree 能定位到存储这行数据的磁盘页。查找路径是: 1索引键值 → B+Tree 内部节点 → 叶子节点 → 行指针 → 数据页 倒排索引从词项(term)出发。给定一个词项,倒排索引能定位到包含这个词项的所有文档。查找路径是: 1词项 → Term Dictionary → Posting ...
上下文管理全景:Agentic Coding 工具操纵 Messages 数组的六种策略
一次工具调用返回了几万 token 的日志。十轮之后,这段日志是否还在上下文里?如果还在,是否每轮都按普通输入价格重新结算?如果触发 compact,工具定义、Skill 和缓存又会发生什么? 这三个问题经常被混成一个问题。实际上,它们分别属于逻辑上下文、请求结构和计费统计。旧日志仍在有效上下文里,不代表它每轮都按未缓存输入计费;一次请求命中了 prompt cache,也不代表这些 token 不占 context window。 本文保留原来的六种 Messages 操纵策略,但把分析边界收紧到可验证的结构:稳定前缀、可变尾部、渐进式能力加载,以及 API usage 中可以实际测量的缓存结果。未经公开文档或请求轨迹验证的产品排名,不再作为结论。 先分清三本账 讨论上下文成本前,需要同时记三本账。 账本 记录什么 常见观测方式 上下文占用 当前请求中模型可见的 token 工具的 /context、token 估算或请求追踪 请求变动 本轮新增、删除或重写了哪些 segment 对 tools、system、messages 分段哈希 计费输入 未缓存...
NRW 仲裁参数——分布式副本读写的数值问题
问题 分布式系统把数据复制到 N 个节点上。写入时让 W 个节点确认,读取时查询 R 个节点。W 和 R 取多少,才能保证读到最新写入的数据? "取多数派"是工程师最常见的直觉回答,但"多数"到底是多少——N 的一半多一个?还是 N 的三分之二?这两个数字分别在什么条件下出现,背后的推导和权衡,远比"取多数派"三个字复杂。 起源:Gifford 加权投票(1979) NRW 模型的源头是 David K. Gifford 在 1979 年 SOSP 会议上发表的论文 Weighted Voting for Replicated Data。Gifford 的模型比今天常见的等权版本更一般化:每个副本节点被分配一个投票权重 wᵢ,所有节点的总票数 V = Σwᵢ。读仲裁所需票数 Vr 和写仲裁所需票数 Vw 必须满足两个约束: 12约束 1:Vr + Vw > V约束 2:Vw > V / 2 约束 1 保证任何一次读操作至少触碰到一个持有最新版本的节点——读集合和写集合必然有交集。约束 2 保证任何两次写操作至...






