Lucene
Lucene Core 是一个 Java 库,提供索引和搜索功能,以及拼写检查、命中高亮和高级分析/分词能力。Solr 和 Elasticsearch 以 Lucene 为搜索引擎库开发而来,Lucene 充当两者的存储引擎。
https://lucene.apache.org/core/downloads.html
https://archive.apache.org/dist/lucene/java/8.11.4/
https://www.bilibili.com/video/BV17S4y1U7xb
Lucene Index 内存结构
- 索引,Lucene Index:存储一类数据用于检索。
- 段,segment:段用于组织索引数据,一个索引中包含多个段,段与段之间相互独立。
- 文档,document:用户写入一条数据,将会以一个文档形式存储,检索时会返回该文档信息。
- 域,field,一个文档中会包含若干字段,每个字段是一个域,field 是 Lucene 中索引的最小单位,Lucene提供多种不同类型的Field,例如StringField、TextField、LongFiled或NumericDocValuesField等,Lucene根据Field的类型(FieldType)来判断该数据要采用哪种类型的索引方式(Invert Index、Store Field、DocValues或N-dimensional等)。
- 词,word:每个域由若干词组成,Lucene中索引和搜索的最小单位,一个Field会由一个或多个Term组成,Term是由Field经过Analyzer(分词)产生。Term Dictionary即Term词典,是根据条件查找Term的基本索引。
Lucene Index 硬盘结构
Lucene 磁盘
Lucene Index 在文件系统中为一个目录。
- segments_2 :Lucene每次Flush(ES的Refresh)或者Merge时,生成一个新的segments文件,替换原有的,切末尾的序号自增1,里面存储了当前索引含有几个有效的 segment,以及每个segment的序号。
- _0.* :以 _ + 序号 开头的文件,表示的是 segment文件的一部分,一个segment由多个文件构成,不同的文件存储不同的信息,.si存储segmentInfo信息,比如创建时间,第一个document的全局id序号,doc数量等等
- .liv:当有对Document做了修改或者删除时,会生成这个文件,里面记录了document的修改状况,当检索时最后需要判断得到的数据是否是失效的或者是删除的,在Merge时会应用此文件的数据,最后删除文件。
- .cfs:初始segment的数据存储文件,当Merge时文件组织结构会变化,不再有这个文件
Lucene Index 声明周期操作
查询
query -> weight -> collector -> scorer -> reduce
sequenceDiagram
participant 客户端
participant IndexSearcher
participant Weight
participant BulkScorer
participant Collector
participant TopDocs
客户端->>IndexSearcher: search(query, 10)
IndexSearcher->>IndexSearcher: rewrite & createWeight
IndexSearcher->>Weight: 全局通行证
loop 每个段
IndexSearcher->>Weight: bulkScorer(ctx)
Weight-->>IndexSearcher: 段内扫描器
IndexSearcher->>BulkScorer: score(collector, liveDocs)
loop 存活doc
BulkScorer->>Collector: collect(doc)
end
end
Collector->>TopDocs: 合并各段堆
IndexSearcher-->>客户端: TopDocs
sequenceDiagram
participant 用户
participant IndexSearcher
participant Query
participant Weight
participant BulkScorer
participant Collector
participant CollectorManager
participant TopDocs
用户->>IndexSearcher: search(query,10)
activate IndexSearcher
IndexSearcher->>Query: rewrite(reader)
activate Query
Query-->>IndexSearcher: 新Query树(原子节点)
deactivate Query
IndexSearcher->>Query: createWeight(searcher,TOP_SCORES,1.0f)
activate Query
Query->>Weight: 构造Weight(全局统计+boost)
Weight-->>IndexSearcher: Weight通行证
deactivate Query
IndexSearcher->>CollectorManager: new TopScoreDocCollectorManager(10)
activate CollectorManager
CollectorManager-->>IndexSearcher: manager(篮子管理员)
deactivate CollectorManager
IndexSearcher->>IndexSearcher: search(leaves,weight,manager)
loop 对每段LeafReaderContext
IndexSearcher->>CollectorManager: newCollector()
activate CollectorManager
CollectorManager-->>IndexSearcher: 小篮子LeafCollector
deactivate CollectorManager
IndexSearcher->>Weight: bulkScorer(ctx)
activate Weight
Weight->>BulkScorer: 构造段内扫描器
BulkScorer-->>IndexSearcher: BulkScorer
deactivate Weight
alt scorer!=null
IndexSearcher->>BulkScorer: score(collector,getLiveDocs())
activate BulkScorer
loop 段内nextDoc
BulkScorer->>BulkScorer: 检查liveDocs位图
alt 存活
BulkScorer->>Collector: collect(doc)
activate Collector
Collector->>Collector: 小顶堆插入
deactivate Collector
end
end
deactivate BulkScorer
end
end
IndexSearcher->>CollectorManager: reduce(collectors)
activate CollectorManager
loop 遍历各小篮子
CollectorManager->>Collector: topDocs()
activate Collector
Collector-->>CollectorManager: ScoreDoc[]
deactivate Collector
CollectorManager->>CollectorManager: 全局大顶堆合并
end
CollectorManager->>TopDocs: new TopDocs(totalHits,results)
TopDocs-->>IndexSearcher: final TopDocs
deactivate CollectorManager
IndexSearcher-->>用户: TopDocs
deactivate IndexSearcher
// ========== 1. 用户入口 ==========
TopDocs hits = indexSearcher.search(query, 10);
// ========== 2. 入口方法内部 ==========
public TopDocs search(Query query, int numHits) {
// 2.1 重写
Query rewritten = query.rewrite(reader); // ① rewrite
// 2.2 创建 Weight(通行证)
Weight weight = rewritten.createWeight(
this, // IndexSearcher
ScoreMode.TOP_SCORES, // 需要打分
1.0f); // 全局 boost
// 2.3 提前构造最后要用的 CollectorManager
CollectorManager<TopScoreDocCollector, TopDocs> manager =
new TopScoreDocCollectorManager(numHits);
// 2.4 真正搜索
TopDocs result = search(leaves, weight, manager);
return result;
}
// ========== 3. 多段搜索核心 ==========
TopDocs search(List<LeafReaderContext> leaves,
Weight weight,
CollectorManager<C, T> collectorManager) {
List<C> collectors = new ArrayList<>(leaves.size());
// 3.1 给每段预先 new 一个 LeafCollector(小篮子)
for (LeafReaderContext ctx : leaves) {
collectors.add(collectorManager.newCollector());
}
// 3.2 顺序或并发扫描各段
for (int i = 0; i < leaves.size(); i++) {
LeafReaderContext ctx = leaves.get(i);
C collector = collectors.get(i);
// 创建段内扫描器
BulkScorer scorer = weight.bulkScorer(ctx); // ③ scorer
if (scorer != null) { // 该段可能无命中
scorer.score(
collector, // ④ collect
ctx.reader().getLiveDocs()); // 过滤已删除
}
}
// 3.3 合并所有小篮子 → 全局大篮子
return collectorManager.reduce(collectors);
}
// ========== 4. CollectorManager 合并 ==========
public TopDocs reduce(List<TopScoreDocCollector> collectors) {
// 把所有小顶堆里的候选再丢进一个大顶堆
PriorityQueue<ScoreDoc> globalPQ = ...;
for (TopScoreDocCollector c : collectors) {
ScoreDoc[] segmentTop = c.topDocs().scoreDocs;
for (ScoreDoc sd : segmentTop) {
globalPQ.insertWithOverflow(sd);
}
}
// 转数组并返回
TopDocs finalDocs = new TopDocs(totalHits, globalPQ.getResults());
return finalDocs;
}
客户端
│
│ TopDocs hits = indexSearcher.search(query, 10)
▼
IndexSearcher::search
├─① rewrite(Query) ──→ 新 Query 树(只含原子节点)
├─② createWeight(ScoreMode.TOP_SCORES) ──→ Weight(含全局统计)
├─③ new TopScoreDocCollectorManager(10) ──→ 提前造好“篮子管理员”
▼
IndexSearcher::search(leaves, weight, manager)
├─3.1 for 段 in leaves
│ collectors += manager.newCollector() // 每段一个小篮子
│
├─3.2 for 段 in leaves // 可并发 ForkJoin
│ BulkScorer s = weight.bulkScorer(ctx) // ③ 段内扫描器
│ s.score(collector, ctx.reader().getLiveDocs())
│ │ │
│ │ └─> Bits(跳过已删文档)
│ └─> collector.collect(doc) // ④ 存活 doc 进小顶堆
▼
manager.reduce(collectors) // 4. 合并小顶堆
├─ PriorityQueue<ScoreDoc> globalPQ // 大顶堆
└─ TopDocs(finalHits, globalPQ.results)
▲
返回给用户
增加、更新、删除
Lucene 中写操作(增加、更新、删除)由 IndexWriter 完成。
flowchart TD
IW[IndexWriter<br/>全局唯一] --> DW[DocumentsWriter<br/>单例]
DW -->|Thread-1| DWPT1[DWPT<br/>DocumentsWriterPerThread]
DW -->|Thread-2| DWPT2[DWPT<br/>DocumentsWriterPerThread]
DW -->|Thread-n| DWPTn[DWPT<br/>DocumentsWriterPerThread]
DWPT1 --> Buf1[五大管道<br/>内存 buffer]
DWPT2 --> Buf2[五大管道<br/>内存 buffer]
DWPTn --> Bufn[五大管道<br/>内存 buffer]
一个索引目录下仅允许一个 IndexWriter,其由 write.lock 保证。
一个 IndexWriter 有且仅有一个 DocumentsWriter。
一个 DocumentsWriter 基于 CAS 和 ThreadLocal 为每个线程分配 DocumentsWriterPerThread(DWPT)。
每个 DWPT 均有五大管道及其各自对应的内存 Buffer,循环依次写每个管道:
管道 负责结构 内存组件 最终文件 ① 倒排管道 词项 → 文档列表 FreqProxTermsWriter.tim.tip.doc.pos.pay② 行存管道 原始字段值 StoredFieldsWriter.fdt.fdx③ 列存管道 排序/聚合字段 DocValuesWriter.dvd.dvm④ 向量管道 词向量(可选) TermVectorsWriter.tvx.tvd.tvf⑤ 长度管道 字段长度/加权 NormsWriter.nvd.nvm
增加和更新流程
客户端
│
│ addDocument(doc) / updateDocument(term,doc)
▼
IndexWriter.updateDocument(...)
├─进入 synchronized 块
│ ├─ 更新:BufferedUpdates.addTerm(term) // 画删除位图
│ └─ 新增:DWPT 五大管道写内存 buffer
│
├─检测 flush 条件
│ ├─ buffer 满 │ 文档数超限 │ 显式 flush()
│ └─ 条件到?→ 同线程立即 flush
│ ├─ 锁定 DWPT
│ ├─ 内存 buffer → 新段(磁盘 I/O)
│ └─ 注册 SegmentInfos(段列表变)
│
├─退出 synchronized,返回客户端
│
└─通知 MergeScheduler
├─MergePolicy.findMerges() // 必调用,返回建议列表
├─若列表非空→ 提交 OneMerge 任务 → 后台线程异步执行
└─若列表空→ 无任务,直接结束
删除流程
客户端线程
│
│ deleteDocuments(term/query)
▼
IndexWriter.deleteDocuments(...)
├─进入 synchronized 块
│ └─ 仅 FrozenBufferedUpdates.addTerm/Query // 写删除位图
│
├─检测 flush 条件(同新增)
│ └─ 条件到?→ 同线程立即 flush
│ ├─ 无文档 buffer,仅落 .del 文件
│ └─ 注册 SegmentInfos(段列表变)
│
├─退出 synchronized,返回客户端
│
└─通知 MergeScheduler
├─MergePolicy.findMerges() // 必调用,返回建议列表
├─若列表非空→ 提交 OneMerge 任务 → 后台线程异步执行
└─若列表空→ 无任务,直接结束
删除只是内存位图/队列,segment 文件里追加 .del 文件;真正的 doc 移除在 merge 阶段
flush、refresh、merge、commit
flowchart LR
A[内存] -->|flush| B[段文件<br/>PageCache]
B -->|refresh| C[Reader可见]
B -->|merge| D[大段+空间释放]
D -->|commit| E[磁盘持久化]
| 维度 | flush | refresh | merge | commit |
|---|---|---|---|---|
| 目的 | 内存→段文件 | 段→Reader 视图 | 多段→少段 | 事务点落盘 |
| 产出 | 新段全套文件 | 新 DirectoryReader | 合并后大段 | 新 segments_N + fsync |
| 可见性 | ❌ 不可搜 | ✅ 立即可搜 | 不影响 | ✅ 立即可搜 |
| fsync | ❌ 不写盘 | ❌ 不写盘 | ❌ 不写盘 | ✅ 强制落盘 |
| 触发 | RAM 满 / 手动 | 默认 1 s / 手动 | 段数/大小阈值 | 手动 / 定时 |
| 代价 | 中等(压缩+写缓存) | 极低(CAS 换指针) | 高(I/O+CPU) | 高(刷盘) |
| 参数 | ramBufferSizeMB |
indexWriter.getReader() |
TieredMergePolicy |
commit() |
flush 生段,refresh 可见,merge 瘦身,commit 落盘,前三写缓存,后一写硬盘。
flush(内存 → 文件系统缓存)
触发时机
- RAM 缓冲满(默认 16 MB 或 1000 条)
- 显式
IndexWriter.flush()/refresh() - 删除队列超过
maxBufferedDeleteTerms
| 阶段 | 源码 | 生成文件 | 说明 |
|---|---|---|---|
| 1. 冻结 DWPT | DWPT.abort() 不再接受新 doc |
— | 保证并发安全 |
| 2. 刷倒排 | FreqProxTerms.flush() → .tim/.tip/.doc |
倒排表 + 字典 | FOR+VarInt 压缩 |
| 3. 刷行存 | StoredFieldsWriter.finish() → .fdt/.fdx |
原始文档 | 行存压缩 |
| 4. 刷列存 | DocValuesConsumer.flush() → .dvd/.dvm |
排序/聚合 | 按数据类型选编码 |
| 5. 写删除 | BufferedUpdatesStream.applyDeletes() → .del |
删除位图 | 只写逻辑删 |
| 6. 生成 SegmentInfo | new SegmentInfo() → .si |
段元数据 | 包含版本、docCount、文件列表 |
| 7. 写 CFE 清单 | CompoundFileWriter → .cfs/.cfe |
单文件 | 默认合并写减少 fd |
| 8. 更新 SegmentInfos | SegmentInfos.update() → segments_N |
全局指针 | CAS 换指针,无锁 |
数据产出
一次 flush 会将 DWPT 中的五大管道缓冲区数据写入 OS 页缓冲,生成一套新文件,等待内核刷盘
_5.fdt _5.fdx 行存
_5.tim _5.tip 倒排字典
_5.doc _5.pos 倒排列表
_5.dvd _5.dvm 列存
_5.nvd _5.nvm 长度
_5.tvx _5.tvd 词向量(可选)
_5.del 删除位图(如果有)
_5.si 段元数据
_5.cfe _5.cfs 复合文件(默认开启)
注意:flush 不调用 fsync,数据仍在 OS 文件系统页缓存;只有 commit 才做 Directory.sync()
luke
Luke 是一款专为 Apache Lucene 设计的图形化索引诊断工具,作用类似于“Lucene 的 DBA 客户端”。它把索引文件里那些不可读的二进制数据变成可视化界面,让你不用写代码就能浏览、查询、调试甚至轻度维护索引。
refresh
- 自动:
ControlledRealTimeReopenThread默认 1 秒 - 手动:
DirectoryReader.openIfChanged(oldReader) - ES 映射:
refresh_interval - 代价:毫秒级,可忽略
merge(段合并 → 真正物理删除)
索引Flush时每个dwpt会单独生成一个segment,当segment过多时进行全文检索可能会跨多个segment,产生多次加载的情况,因此需要对过多的segment进行合并。
触发时机
- 每次 flush 后
maybeMerge() - 显式
IndexWriter.forceMerge() - 后台
ConcurrentMergeScheduler线程
| 步骤 | 源码 | 输入 | 输出 | 关键点 |
|---|---|---|---|---|
| 1. 选段 | TieredMergePolicy.findMerges() |
所有 SegmentInfo |
OneMerge 对象列表 |
按分层打分(5% 阶梯) |
| 2. 加锁 | mergingSegments Set |
— | — | 防止重复被选 |
| 3. 应用删除 | mergeReader.applyDeletes() |
.del 位图 |
新段无被删 doc | 真正丢弃已删文档 |
| 4. 合并字典 | SegmentMerger.mergeTerms() |
多段 .tim |
新 .tim/.tip |
多路归并(优先队列) |
| 5. 合并倒排 | mergeTerms() 里 appendPostings() |
.doc/.pos/.pay |
新倒排 | docID 重新连续编号 |
| 6. 合并行存 | StoredFieldsMerger |
.fdt |
新 .fdt |
顺序拷贝,去删行 |
| 7. 合并列存 | DocValuesMerger |
.dvd |
新 .dvd |
按类型选合并策略 |
| 8. 生成新段 | SegmentInfo newSegmentName() |
— | .si + .cfs |
新段大小 ≈ 旧段和 |
| 9. 提交清单 | IndexWriter.mergeFinish() |
— | segments_N 原子切换 |
旧段文件异步删除 |
合并后才物理抹去已删文档,磁盘空间此时真正释放
commit
唯一会进行 fsync 的操作
Elasticsearch
Elasticsearch 底层使用 Lucene 作为搜索引擎库,Lucene 中没有分布式和并发等功能,仅适用于单机数据处理,若想要处理海量数据,则需要基于 Lucene 进行相关设计,Elasticsearch 就是在 Lucene 基础之上,进行功能增强而实现的分布式搜索引擎。
存储
Elasticsearch 集群由若干 Node 构成,ELasticsearch 中存储 Elasticsearch Index,Elasticsearch Index 由若干 Shard 构成,若干 Shard 按照一定策略分布在整个集群的若干 Node 上,由此实现 Elasticsearch Index 的分布式存储,Shard 分为主分片和副本分片,主分片负责处理写入请求和存储数据,副本分片只负责存储数据,是主分片的拷贝,文档会存储在具体的某个主分片和副本分片上,每一个 Shard 对应一个 Lucene Index。
- ES 客户端将写索引操作包装为 HTTP 请求发送至 ES 服务器
- ES 服务器解析写索引操作并执行,数据被写入 index buffer,同时写Translog,执行成功向客户端响应结果
当达成下述条件时触发 refresh index 操作,index buffer 数据被写入 index segment,此时 segment 存储在内存中还未持久化到磁盘
- 按照时间频率触发,默认情况是每 1 秒触发 1 次 Refresh,可通过
index.refresh_interval设置; - 当Index Buffer 被占满的时候,会触发 Refresh,Index Buffer 的大小默认值是 JVM 所占内存容量的 10%;
- 手动调用调用Refresh API。
POST /my_index/_refresh,refresh 索引my_indexPOST /*/_refresh,refresh 所有索引
由于Refresh操作默认间隔为1s,因此会产生大量的小Segment,ES查询时会同时查询所有的Segment,并对结果进行汇总,大量小Segment会使性能变差。因此ES会对小Segment进行段合并(Merge),合并操作会丢弃掉重复的键,并只保留每个键最近的更新。段合并之后搜索请求可以直接访问合并之后的Segment,从而提高搜索性能。
当达成下述条件时触发 merge 操作
- ES自动启动后台任务进行Merge;
- 手动调用_forcemerge API主动触发。
在段合并完成之后,ES会将Segment文件Flush到磁盘中,并创建一个Commit Point文件,用来标识被Flush到磁盘的Segment。Commit Point其实是记录所有的Segment信息,关于移除的Segment的信息会记录在“.del”文件中,查询结果后会从该文件中进行过滤。
Flush操作是将Segment从文件系统缓存写入到磁盘进行持久化,在执行 Flush 的时候会依次执行下面操作:
- 清空Index Buffer
- 记录 Commit Point
- 刷新Segment到磁盘
- 删除translog
Elasticsearch 写数据流程
Elasticsearch 持久化模型
客户端向 Elasticsearch 集群某节点发起请求,该节点成为协调节点
协调节点基于hash取模计算文档要写入的主分片,路由请求到该主分片所在的数据节点
数据节点上处理该请求,写入数据到对应主分片,并将数据同步到对应的副分片
写 Lucene Index 内存缓存,同时写 Translog,此时文档在缓冲区还无法被搜索到,仅可以通过 id 访问。
如果希望该文档能立刻被搜索,需要手动调用refresh 操作。在 Elasticsearch 中,默认情况下 _refresh 操作设置为每秒执行一次。 在此操作期间,内存中缓冲区的内容将复制到内存中新创建的 Segment 中,如下图所示。 结果,新数据可用于搜索。
这个refresh的时间间隔可以由 index 设置中 index.refresh_interval 来定义。只有在 buffer 的内容写入到 Segement 后,这个被写入的文档才变为可以搜索的文档。通常 buffer 里的内容被写入到 Segment 里去,有三个条件:
- 由索引中的设置所指定的 refresh_interval 启动的周期性的 refresh。在默认的情况下为1s
- 在导入文档时强制 refresh:PUT twitter/_doc/1?refresh=true
- 当 In Memory Buffer 满了,在默认的情况下为 node Heap 的 10%
fdfresh 会触发 Lucene Flush,实质上意味着将内存缓冲区中的所有文档都写入新的 Lucene Segment,如下面的图所示。 这些连同所有现有的内存段一起被提交到磁盘,该磁盘清除事务日志(参见图4)。 此提交本质上是 Lucene 提交(commit)。
协调节点等待主分片和副分片均写成功后返回客户端结果
Lucene 中的 Reopen
当调用 Lucene Reopen 时,将使累积的数据可用于搜索。 尽管可以搜索最新数据,但这不能保证数据的持久性或未将其写入磁盘。 我们可以调用 n 次重新打开功能,并使最新数据可搜索,但不能确定磁盘上是否存在数据。
Lucene 中的 Commits
Lucene 提交使数据安全。 对于每次提交,来自不同段的数据将合并并推送到磁盘,从而使数据持久化。 尽管提交是持久保存数据的理想方法,但问题是每个提交操作都占用大量资源。 每个提交操作都有其自己的内部 I/O 操作以及与其相关的读/写周期。 这就是为什么我们希望在基于 Lucene 的系统中一次又一次地重新使用重新打开功能以使新数据可搜索的确切原因。
Elasticsearch 读数据流程
Query-then-Fetch运行机制
Elasticsearch的分布式搜索的运行机制称为Query-then-Fetch。具体分为Query和Fetch两个阶段:
Query阶段
用户发出搜索请求到达ES节点。节点收到请求后,会以协调节点(Coordinating Node)的身份,在6个主副分片中随机选择3个分片,发送查询请求。
被选中的节点,进行排序(根据score值进行排序)。然后每个分片都返回 From+size 个排序后的文档id和排序值给协调节点。 注意这里返回的是文档id。
Fetch阶段
Coordinating节点将Query阶段从每个分片获取的排序的文档id列表重新进行排序,选取 From 到 From+size 个文档的id。
以multi get请求的方式,到相应的分片获取详细的文档数据。
4.2.2 为什么需要两阶段才能完成搜索
因为Elasticsearch在查询的时候不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。例如现在有5个分片,需要查询匹配度Top10的数据,那么每个分片都要查询出当前分片的Top10的数据,协调节点将5×10个结果再次进行排序,返回Top10的结果给客户端。
4.2.3 Query-then-Fetch存在问题和解决方案
Query-then-Fetch存在问题分为两方面,一个是性能问题,一个是相关性算分问题。
- 性能问题
性能问题主要表现为深度分页的问题。Elasticsearch数据是分片存储的,数据分布在多台机器上。有这样一个场景,如何获取前1000个文档?当获取从990-1000的文档时候,会在每个分片上面都先获取1000个文档,然后再由协调节点聚合所有分片的结果在排序选取前1000个文档。
这个过程有什么问题吗?当然是有的,页数越深,处理文档越多,占用内存越多,耗时越长。所以要尽量避免深度分页。当然,ES官方也注意了这个问题,所以通过index.max_result_window限定最多到10000条数据。当然我们也可以根据业务需要修改这个参数,这也解释了:为什么Google搜索结果只有相关度最高的17页结果,百度只有76页的结果,原因之一是受限于Elasticsearch深度分页的性能问题。
- 相关性算分问题
另外一个问题是相关性算分不准确问题。每个分片都基于自己分片上面的数据进行相关度计算。这会导致打分偏离的情况,特别是数据量很少的时候。相关性算分在分片之间是相互独立。当文档总数很少的情况下,如果主分片大于1,如果主分片数越多,相关性算分会越不准。 - 如何解决算分不准的问题?
- 当数据量不大的时候,将主分片数设置为1;当数据量足够大的时候,只要保证文档均匀分布在各个分片上面,结果一般不会出现偏差
- 使用
DFS Query Then Fetch
在搜索的URL中指定参数_search?search_type=dfs_query_then_fetch;这样就可以保证每个分片把各个分片的词频和文档频率进行搜集,然后进行一次相关性算分。但是这样会耗费更多的CPU和内存资源,执行性能较低。
- 如何避免深度分页的问题?
使用Search_After:
ES提供实时的下一页文档获取功能,这个功能只能下一页,不能上一页;
不能指定页数,不能使用from参数;
- 三种分页方式对比:
| 类型 | 场景 |
|---|---|
| From/Size | 需要实时获取顶部的部分文档,且需要自由翻页 |
| Scroll | 需要全部文档,如导出所有数据的功能 |
| Search_After | 需要全部文档,不需要自由翻页 |
ES的分片存储决定了搜索机制。其实存储和搜索不能分割开来看,只存储不可搜索,这个存储是没有意义的;只搜索没有存储(数据源)是空中楼阁。
节点有不同角色
https://www.elastic.co/guide/en/elasticsearch/reference/7.10/modules-node.html
许多编程语言内置的简单哈希函数可能并不适合分片,例如,Java的Object.hashCode和Ruby的Object#hash,同一键在不同的进程中可能返回不同的哈希值。许多数据系统使用MurmurHash算法进行哈希计算,例如Redis、Memcached等
并发控制
物理文件分析
https://blog.csdn.net/qq_27529917/article/details/108590073
https://www.zhangxt.top/2022/02/06/elasticsearch-storage-model/
https://zhuanlan.zhihu.com/p/443722667
ES为什么搜索这么快
index 中的每一个字段都会建立索引,其中最为著名的是text使用倒排索引进行全文检索
关联表数据存储es方案
ES中通过join类型字段构建父子关联
https://blog.csdn.net/w1014074794/article/details/119355162
es重建索引标准步骤,???
聚合查询结果不精确
https://blog.csdn.net/laoyang360/article/details/107133008
cardinality聚合不精确问题
https://www.cnblogs.com/jingdongkeji/p/17378245.html
模糊查询
支持错字
动态类型
局部性原理告诉我们要尽可能减少IO损耗,尽可能IO的数据都是需要的数据,所以才会有专门的索引结构
索引别名
ik分词器词库热更新
https://blog.csdn.net/qq_40592041/article/details/107856588
分片策略
持久化机制
00