☰
Lucene总体架构详解:从倒排索引到核心实现
2026/10/6 4:15:57 网站建设 项目流程

做后端这些年,凡是涉及搜索的项目,最后几乎都会绕到 Lucene 头上。Elasticsearch 的底层是它,Solr 的底层是它,很多公司自研的站内搜索同样是直接调 Lucene 或者在此基础上做一层业务封装。所以搞懂 Lucene 的总体架构,比单纯会调某个搜索框架的 API 要值钱得多。这篇是 Lucene 学习总结的第二篇,重点从整体视角梳理它的架构组成,把核心类、数据结构、数据流串成一张清晰的图,最后落到最小可运行的代码上。适合刚接触搜索引擎、想弄明白索引和查询背后原理的 Java 开发,也适合已经在用 ES 但想补底层知识的同学。

很多人学 Lucene 容易一头扎进某个 API 的用法里,结果越看越乱。我的建议是先跳出代码,把它的总体架构在脑子里立起来:数据进来之后走什么流程,用户查询进来之后走什么流程,哪些组件是写索引用的,哪些组件是读索引用的,它们之间靠什么衔接。把这条主线理清楚,后面学细节才不会迷路。

1. 从搜索需求到 Lucene 架构:先看整体画面

1.1 Lucene 到底解决了什么问题

在没有搜索引擎之前,数据库里的文本匹配是一个很难受的问题。用 MySQL 的 LIKE 做模糊查询,本质是全表扫描,数据量一上来,性能直接不可用;而且它只能告诉你“有没有匹配”,给不出“哪条结果跟你的需求最相关”这种排序能力。

搜索场景真正的需求有三个维度:第一是快,千万级文档不能每条都扫一遍;第二是相关度,搜索结果要按跟用户意图的接近程度排序;第三是表达能力,用户能输入关键词、短语、范围条件和组合条件,系统得能解析并高效处理。

Lucene 解决这三件事的底牌就是倒排索引加打分机制。所谓倒排索引,就是维护一个“词项到文档”的映射关系,搜索时直接按词项查找对应文档集合,完全避开全表扫描;打完候选集之后再用相似度算法给每个文档打分排序。这套设计思想从上世纪末定下来之后,到现在最新的 Lucene 版本也没有变过。

1.2 整体架构的分层逻辑

如果让我给 Lucene 画一张架构图,我不会按包结构画,而是按职责和依赖关系画成三层。

最上层是 API 层,也是你写业务代码时直接接触的部分。写索引的核心入口是 IndexWriter,读索引的核心入口是 IndexReader 和 IndexSearcher,文本加工的工具是 Analyzer,查询语句解析靠 QueryParser,业务数据要装进 Document 和 Field 里才能交给 Lucene。

中间层是索引逻辑层,负责描述数据是怎么组织的。一个索引包含很多个段(Segment),每个段包含若干篇文档(Document),每篇文档又包含若干个字段(Field),字段经过分词后变成一条条词项(Term),词项通过词典和倒排表挂在段上。这一层决定了检索能做到多快、查询能表达多细。

最底层是存储层,用 Directory 抽象来屏蔽底层介质。你可以把索引存在本地文件系统上,用 FSDirectory;也可以完全放内存里,用 ByteBuffersDirectory;还可以接上自己的存储实现。Lucene 的索引文件后缀名非常多,比如存字段值的 .fdt、存文档编号映射的 .fdx、存词典的 .tim 和 .tip、存倒排表的 .doc 和 .pos,看到这些文件你就知道索引落盘之后是什么样子了。

有了这三层的概念,再去看 IndexWriter、IndexSearcher 这些类的源码和配置项,脑子里就会自动归类:哪一个动作在影响存储布局,哪一个动作在影响查询计划,不会再看一会儿就乱。

2. 核心抽象:Document-Field-Term 三级模型

2.1 为什么是三级模型

Lucene 对一条业务记录的抽象是 Document。你可以把 Document 理解为数据库里的一行,或者一篇博客文章,它代表一个完整的可检索单元。Document 本身没有太多逻辑,它就相当于一个容器,里面装若干个 Field。

Field 表示 Document 的一个属性或一个内容区域。比如一篇文章的标题、正文、发布时间、作者、URL,在 Lucene 里都各自是独立的 Field。为什么要拆这么细?因为不同字段的处理方式完全不一样:标题和正文需要分词,这样用户搜“架构”才能用“架构”这个词去匹配;发布时间需要按范围筛选,不能做分词;作者可能只需要精确匹配;URL 只需要存下来展示,甚至不参与搜索。

Term 是 Lucene 索引和查询的最小原子单位。一个 Term 由“字段名 + 词项文本”组成,比如“content:lucene”和“title:lucene”是两个完全不同的 Term。换句话说,Lucene 在做关键词匹配时不是只比“是不是同一个词”,还要看在哪个字段里出现的。

我经常用一本书来打比方:Document 是整本书,Field 是书里的章节,Term 是章节里被提炼出来的关键词。你在图书馆里查“架构设计”的时候,不会把整本书从头翻到尾,而是先看关键词索引表,定位到相关章节,再翻到具体页码去读内容。Lucene 的检索逻辑本质就是这样。

2.2 倒排索引的本质

正向索引很容易理解,就是“文档 ID 到词项列表”的映射,存的是每篇文档包含哪些词。问题在于用户搜索时输入的是词,而正向索引没办法直接从词跳到文档,只能一篇一篇扫描,这叫顺序遍历。

倒排索引反过来了,它建立“词项到文档列表”的映射。每个 Term 在词典里查一次,就能拿到包含这个词的全部文档 ID 列表,这个列表叫 Posting List。Posting List 里除了文档 ID,往往还会记录词频和位置信息,词频用来计算相关度,位置信息用来支持短语匹配。

多关键词查询时,Lucene 会先把每个 Term 各自的 Posting List 找出来,再做集合运算。比如用户输入“架构 设计”,如果按 AND 语义处理,就取两个列表的交集;如果按 OR 语义,就取并集。由于这些文档 ID 列表在底层是有序的,取交集可以利用跳表加速,成本远低于你想象的全量比对。

倒排索引里最关键的两个部分是词典和倒排表。词典负责快速定位 Term,结构上用了类似 FST 的压缩前缀树,既省空间又支持前缀查找;倒排表负责高效拿出文档 ID 和词频,配合跳表做范围跳跃和集合合并。这是 Lucene 查询性能的根基,也是面试里最常被追问的点。

3. 写入链路:从文本到可搜索的索引

3.1 IndexWriter 是如何工作的

IndexWriter 是 Lucene 写索引的唯一入口,它的地位有点像数据库里的主写入连接。Lucene 设计上不允许同一个索引目录同时被两个 IndexWriter 打开,这是为了维护数据一致性。单实例内部,多线程共享同一个 IndexWriter 是线程安全的,你不需要自己加锁去保护 addDocument 和 deleteDocuments,只管并发往里提交就行。

一条文档写进去后,会走这样一条流水线。第一步,业务方构造好 Document,塞进一个个 Field。第二步,Analyzer 登场,把需要分词的字段文本拆成一个个词项,同时做归一化处理,比如转小写、去停用词。第三步,分词后的词项进入内存中的写入缓冲,在 Lucene 内部有多个 DocumentsWriterPerThread 并行处理不同的文档,尽可能利用多核。第四步,当内存缓冲达到阈值或者文档数达到条件时,内存中的这批数据会被刷成磁盘上的一个 Segment。第五步,后台线程按照合并策略把小 Segment 合并成大 Segment。

这里有两个概念很多人会混淆:flush 和 commit。flush 是把内存中的文档固化成一个新的 Segment,但此时这个 Segment 还处于“未提交”状态,对新的 IndexReader 不可见;commit 才是真正提交一个提交点,把当前索引状态标记为完成,并保证崩溃恢复时能回到这个状态。生产中不建议频繁 commit,因为 commit 要落盘提交点、做一致性保护,开销不小,实时性需求一般靠 refresh 完成,而不是靠 commit。

IndexWriterConfig 里有几个参数直接决定写入行为。RAMBufferSizeMB 控制内存缓冲区占用多少 MB 后触发 flush,默认大概在 16MB 左右,具体值以你所用版本为准;MaxBufferedDocs 控制缓冲多少篇文档后触发 flush,默认是 -1,表示只用内存阈值。大批量建索引时,调大缓冲区可以减少 Segment 数量,但同时也会占用更多堆内存,需要平衡。

3.2 段合并与索引生命周期

Segment 是 Lucene 索引里不可变的存储单元。为什么设计成不可变?因为不可变意味着不需要加锁就能安全地并发读,还能利用操作系统的页缓存,所以查询性能会非常稳定。但不可变也带来了一个问题:索引会不断产生新 Segment,时间一长,Segment 数量膨胀,查询时要同时打开很多文件,性能会下降。

Lucene 解决这个问题的办法是后台合并。默认的合并策略是 TieredMergePolicy,它按照“层级”来组织 Segment。小 Segment 慢慢被合并成中等 Segment,中等 Segment 再往上合并成大 Segment,目标是控制索引里的 Segment 数量保持在合理范围。索引大小不同,策略算出来的理想段数也不同,但总体上会尽量让大段少而小段适当存在,既保证查询效率,又不让合并操作本身太频繁。

合并是个重 IO 操作,尤其是在大索引上。我见过有同学在做全量导入的时候,没关合并线程,结果后台一直在合并,前台写入速度被拖慢,CPU 和磁盘都在抗议。其实 Lucene 的 ConcurrentMergeScheduler 支持设置合并线程数,导入大索引时可以临时调低,或者先索引完再统一做一次 forceMerge 把段压到合理数量,这样可以明显缩短全量导入的耗时。

删除文档也是理解生命周期的一个关键点。调用 deleteDocuments 并不会把文档从 Segment 里物理删掉,而是打一个"墓碑标记",告诉查询端这篇文档已经删除了。真正把数据从磁盘上清掉,要等合并操作选中包含这些文档的 Segment,并在合并生成新 Segment 时把它们排除在外。理解了这一点,就能解释很多现象:为什么删了大量数据后磁盘空间没立刻下降,为什么刚删完文档的索引变大了,都跟 Segment 的不可变和延迟清理有关。

4. 查询链路:从用户输入到 TopDocs 结果

4.1 一条查询语句的完整旅程

查询的起点是用户的输入字符串,比如“content:倒排索引 AND title:搜索”。第一步先交给 QueryParser 做语法解析,它会按照查询语法把这段文本转成 Lucene 的 Query 对象。对于普通的关键词,会生成 TermQuery;多个条件用 AND、OR、NOT 连接,会生成 BooleanQuery;带引号的短语会产生 PhraseQuery;范围条件会产生 RangeQuery。QueryParser 是使用层的东西,它和底层执行是解耦的,如果你自己构造 Query 对象,完全可以跳过这一步。

拿到 Query 对象之后,IndexSearcher 会调用 search 方法执行真正的检索。这个过程在底层会很复杂,会为每个词项创建 Weight 和 Scorer,遍历倒排表拿到候选文档,再合并不同词项的结果,最后通过评分公式计算每个文档的相关度分数。但有一点值得注意:Lucene 并不会把全部命中文档都算完分数再排序,它借助一个自定义的收集器 Collector,内部维护了一个 TopN 大小的最小堆,只保留分数最高的前 N 篇文档。所以无论你的索引里有多少篇命中文档,返回的 TopDocs 里始终只有你需要的那么多,这正是搜索能在大数据量下保持响应速度的重要原因。

TopDocs 包含了两个核心信息:命中的总数量 totalHits,以及一个由 ScoreDoc 组成的数组,每个 ScoreDoc 里有文档 ID 和对应的分数。拿到文档 ID 之后,通过 IndexSearcher.doc() 就能读取这个 ID 对应的原始 Document 内容,前提是写入时设置了 Field.Store.YES。如果你写入时没有存字段,搜索时只能拿到文档 ID 和分数,拿不回原文,这是初学者特别容易踩的坑。

4.2 相关性评分:从 TF-IDF 到 BM25

搜索引擎能“按相关度排序”,靠的是评分模型。老版本 Lucene 默认使用 TF-IDF 的变体,核心思想很直观:一个词项在一篇文档里出现的次数越多,文档得分越高,这叫词频因子;但这个词项在整个索引里越常见,它对区分文档的贡献反而越小,这叫逆文档频率因子。打个比方,你搜“搜索”,几乎每篇文档里都有这个词,它对排序的区分度就很弱;你搜“倒排索引”,出现在少数几篇文档里,那这几篇的权重就天然更高。

从 Lucene 6 开始,默认评分模型换成了 BM25。BM25 在 TF-IDF 的基础上做了两个重要改进:一是词频不是无限线性增长,而是存在饱和效应,同一个词出现 20 次和出现 100 次,对分数的贡献差距不会像 TF-IDF 那样拉得巨大;二是引入了文档长度归一化,字段越短,命中词的权重越高。这个直觉也很好理解:同样出现“Lucene”这个词,一篇 200 字的笔记比一篇 2 万字的论文更能说明它讲的就是 Lucene。

评分模型可以通过 Similarity 接口替换,不是写死的东西。有些场景下其实不需要相关性打分,比如做标签过滤、权限过滤,只要判断“包含不包含”,完全不关心排序,这时可以用 ConstantScoreQuery 固定所有命中文档的分数,能省下大量打分计算。我在做后台管理系统里的简单筛选功能时,就经常这么处理,执行速度快不少,而且逻辑更清晰。

打分计算在整个查询链路里占的比重不小,尤其是命中的候选文档数很多时。所以如果你的业务就是不需要分数,不要犹豫,直接绕过默认评分逻辑,收益在压测时非常明显。

5. 容易被忽略的两个架构细节

5.1 NRT 近实时搜索到底实时到什么程度

Lucene 经常被叫做近实时搜索引擎,这个“近”字很关键。当你调用 IndexWriter.addDocument 写入一篇文档后,如果你立刻用一个新打开的 IndexReader 去搜,可能查不到这篇文档。原因很简单:数据还在内存缓冲区里,还没有被刷新成 Segment,或者已经刷新了但还没有被当前这个 IndexReader 看到。

Lucene 的可见性机制是这样的:IndexWriter 把文档刷成 Segment 之后,默认情况下并不会马上让所有旧的 IndexReader 重新感知。你需要调用 DirectoryReader.openIfChanged(reader) 检查索引是否有变化并打开新的 Reader。另外,控制刷新频率的参数是 IndexWriterConfig 里的 setMaxRefreshDuration,或者通过 IndexWriter 的 maybeRefresh 方法主动触发,多数上层框架比如 Elasticsearch 就是靠固定间隔刷新来实现近实时的,默认大概一秒。

我在第一次接业务的时候就在这里踩过坑,写完文档刷新页面查不到数据,查了半天还以为是索引坏了。后来反应过来是 NRT 语义搞错了。如果你的业务需求是“写入即查”,比如用户发完文章马上要能看到搜索结果,那就要么把刷新间隔调短,要么在写完后主动触发一次 refresh。但也要注意,刷新频繁意味着磁盘上小 Segment 会变多,合并压力会变大,所以这个间隔不能拍脑袋调到极限,要根据业务容忍度去折中。

5.2 锁、线程安全与多 Writer 问题

同一个索引目录同一时间只能有一个 IndexWriter 打开,这是 Lucene 的一条硬约束。为了避免多进程或者同一进程里误开多个 Writer 把索引写坏,Lucene 在打开 IndexWriter 时会创建 write.lock 文件来占位。另一个进程或者另一个 Writer 实例尝试打开同一个目录时会发现锁已经被占用,然后抛出 LockObtainFailedException。

日常开发里最常见的锁异常来自两种操作。第一种是程序没有正常调用 writer.close(),进程退出后残留的锁没有释放,下一次启动就直接报锁冲突;第二种是在测试代码里反复 new IndexWriter 操作同一个临时目录,上一个还没关,下一个就来了。前者要养成在 finally 中关闭 Writer 的习惯,后者要检查是不是有旧的锁文件残留在目录里。

多线程并发写同一份索引的正确姿势是共享同一个 IndexWriter 实例,由 Lucene 内部负责线程安全调度,而不是每个线程各自创建一个 Writer。如果换成了多进程或者多台机器向同一个索引目录写数据,Lucene 本身是不支持的,需要在上层自己协调,比如做分片设计或者采用分布式方案。遇到这种需求,老老实实升级到 Elasticsearch 这类分布式搜索引擎,强行用单机 Lucene 去硬扛多写者场景,后面运维会非常痛苦。

6. 实操:最小可运行的索引与查询

6.1 最小建索引代码

为了把上面这些架构概念落到实处,这里给一个最小可运行的示例。我用的是 Lucene 8.11.2 版本,Maven 依赖只需要三块:lucene-core 是核心库,lucene-analyzers-common 提供 StandardAnalyzer 标准分词器,lucene-queryparser 提供查询语法解析。

<dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>8.11.2</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>8.11.2</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>8.11.2</version> </dependency>

建索引代码如下,逻辑非常简单:打开一个文件系统目录,创建 IndexWriter,构造几篇 Document 写进去,最后 commit 并关闭。

import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import java.nio.file.Paths; public class IndexDemo { public static void main(String[] args) throws Exception { Directory directory = FSDirectory.open(Paths.get("data/index")); Analyzer analyzer = new StandardAnalyzer(); IndexWriterConfig config = new IndexWriterConfig(analyzer); config.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); IndexWriter writer = new IndexWriter(directory, config); addDoc(writer, "Lucene架构笔记", "Lucene的总体架构由索引写入、存储和检索三部分组成"); addDoc(writer, "倒排索引", "倒排索引是Lucene最核心的数据结构,通过词项查找文档"); addDoc(writer, "搜索入门", "全文检索入门通常从索引库维护和查询开始"); writer.commit(); writer.close(); } private static void addDoc(IndexWriter writer, String title, String content) throws Exception { Document doc = new Document(); doc.add(new StringField("title", title, Field.Store.YES)); doc.add(new TextField("content", content, Field.Store.YES)); writer.addDocument(doc); } }

注意 title 字段用的是 StringField,它不分词,适合精确匹配和排序;content 字段用的是 TextField,它会被分词器处理,适合全文检索。两个字段都设置了 Field.Store.YES,意思是把原始值也存进索引里,这样搜索阶段能把原文取回来。如果你只需要用某个字段做过滤而不需要展示,就可以设成 Field.Store.NO,能省点磁盘。

跑完代码之后,打开 data/index 目录,会看到 segments_* 文件、write.lock 文件,以及若干 .cfs、.si、.fdt 之类的索引文件。这些文件就是索引在磁盘上的真实形态,对应前面架构图里的存储层。

6.2 最小查询代码与结果解读

查询端的代码同样很精简。打开同一份索引目录,创建 IndexReader 和 IndexSearcher,用 QueryParser 把用户输入的字符串解析成 Query,然后调用 search 方法拿 TopN 结果。

import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import java.nio.file.Paths; public class SearchDemo { public static void main(String[] args) throws Exception { Directory directory = FSDirectory.open(Paths.get("data/index")); IndexReader reader = DirectoryReader.open(directory); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new StandardAnalyzer(); QueryParser parser = new QueryParser("content", analyzer); Query query = parser.parse("倒排索引"); TopDocs topDocs = searcher.search(query, 10); System.out.println("totalHits=" + topDocs.totalHits); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document doc = searcher.doc(scoreDoc.doc); System.out.println("title=" + doc.get("title") + ", score=" + scoreDoc.score + ", content=" + doc.get("content")); } reader.close(); } }

运行后会输出命中的文档数量和每篇文档的标题、分数、正文内容。由于 StandardAnalyzer 会按英文空格和标点分词,中文文本在 StandardAnalyzer 下会被当成整句或按 Unicode 特性处理,所以中文搜索效果并不会太好,这也是为什么国内生产环境通常要搭配 IK Analyzer 这类中文分词器。示例代码主要是为了演示整体链路,分词这块在后面的学习总结里再展开。

这里还有个小细节:searcher 对象不负责持有索引文件描述符,真正持有资源的是 IndexReader。用完后应该关闭 reader,而不是去 close searcher。很多从数据库转过来的同学习惯性想关掉最外层对象,结果没关 reader,索引目录一直被占着,文件句柄悄悄泄露。养成对称释放的习惯很重要:谁打开谁关闭,先关 IndexSearcher 没有意义,直接关 IndexReader 就对了。

7. 架构视角下的调优与常见排障

7.1 关键参数与我的实际调优体验

写索引侧的调优,核心思路是控制 Segment 的产生速度和数量。RAMBufferSizeMB 调高以后,每次刷出去的 Segment 会更大、更少,索引整体会更整洁。但代价是堆内存占用上升,GC 压力变大。我做过一次千万级文档的批量导入测试,把 RAMBufferSizeMB 从 16 调到 64,Segment 数量少了接近一半,导入时间明显下降,但中间 JVM 老年代增长也确实变快了,所以最终值要结合机器内存来定,不是越大越好。

查询侧的调优,最重要的是复用 IndexReader。DirectoryReader.open 每次都会重新打开索引、加载词典和倒排表,代价极高。如果一个长服务里每次查询都重新 open,再厉害的机器也撑不住。正确做法是常驻一个 IndexReader,定时通过 DirectoryReader.openIfChanged 增量更新。我自己维护索引服务时,就是用一个后台线程每分钟检查一次 openIfChanged,这样既保证数据延迟可控,又避免了反复加载。

还有一个容易被忽视的点是查询返回条数。很多同学 search 时直接写 10000,甚至更大,觉得这样用户想看多少都有。实际上 Collector 的堆维护成本、文档读取成本都和返回数量线性相关,这个值超过业务真实需求太多,就是白烧 CPU。从架构角度来看,搜索交互更适合分页或者游标,一次性拉几万条结果往往是后端性能瓶颈的一个隐藏元凶。

7.2 高频异常与排查思路速查

接触 Lucene 多了之后,遇到的无非就是下面这几类问题,这里整理成一张速查表,方便对照排查。

现象常见原因排查方向
LockObtainFailedException目录被其他 Writer 占用,或者残留 write.lock看是否多次打开 IndexWriter,检查进程是否存活,清理残留锁文件
IndexNotFoundException索引目录里没有完整索引确认索引路径是否正确,确认是否先执行过建索引流程
刚写入数据搜不到没有触发 refresh,NRT 延迟理解近实时语义,主动调用 maybeRefresh 或等待刷新周期
CorruptIndexException索引文件损坏检查磁盘和 IO,使用 CheckIndex 工具检测索引一致性
Too many open filesSegment 数量过多或 Reader 未关闭检查是否频繁 open IndexReader,检查段数量,考虑合并策略
搜索变慢但数据量没变返回条数太大、分词词项过多、评分计算量大减小 topN,审查查询语句复杂度,考虑恒分数字段

索引文件损坏这个异常虽然不常遇到,一旦遇到就非常棘手。排查时要先判断是不是磁盘坏道或者掉电导致的损坏,然后试着找到最后一个稳定的 commit 点做回滚。平时给索引做定期备份和演练恢复流程,比等出了问题再研究 CheckIndex 怎么用要靠谱得多。

关于索引目录残留锁的问题,我再多说一句。在 Linux 环境下,write.lock 文件往往是程序非正常退出留下的。有些同学图省事会直接删除 write.lock 再重启,这个操作在单机上通常有效,但如果索引目录是共享存储,删锁文件前一定要确认没有其他进程还在写这个目录,否则会造成两个 Writer 同时操作同一份索引,后果远比锁异常严重。

最后再分享一个我从架构角度得到的体会:Lucene 的代码庞大,但核心模型非常稳定。你花时间吃透 Document、Field、Term、Segment、IndexWriter、IndexReader 这些概念之后,无论以后切换到新版本还是阅读上层框架源码,都会觉得很多东西是相通的。学习时不要贪多,先把总体架构刻在脑子里,再逐个击破细节,这条路是最省力的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询