什么是 Lucene?
Apache Lucene 是一个开源的、高性能的全文检索库,用 Java 编写,广泛用于实现文本搜索功能。它不是一个完整的搜索引擎,而是一个提供索引和搜索功能的库,开发者可以基于它构建自定义的搜索应用。Lucene 被许多知名项目(如 Elasticsearch、Solr)用作底层核心技术。
索引(Index)
- Lucene 的索引是一个逻辑单元,包含一组文档的倒排索引和其他元数据。
- 索引分为多个不可变的段,支持增量写入和后台合并。每个段独立存储,允许多线程并发查询。
- Lucene 的索引文件是一组二进制文件,用于持久化存储倒排索引、文档元数据、字段数据等信息。
Lucene 的索引文件按功能划分为 词典、倒排列表、存储字段、文档值、段元数据 等模块。
1. 段元数据文件
| 文件名 | 作用 |
|---|---|
segments_N |
记录当前索引的所有段信息(段名、文档数、删除文档数、版本等)。核心元数据文件,每次提交(commit)生成一个新的 segments_N。 |
write.lock |
写入锁文件,防止多个进程同时修改索引。 |
2. 词典与倒排索引文件
| 文件名 | 作用 |
|---|---|
.tim (Terms Dictionary) |
存储词项字典(Term Dictionary),使用 FST(有限状态转换机) 压缩存储所有词项,并指向对应的倒排列表。 |
.tip (Terms Index) |
词项字典的索引文件,加速词项查找(类似 B-Tree 结构)。 |
.doc (Postings List) |
存储倒排列表(Posting List),包含文档 ID、词频(TF)、位置(Positions)等信息。 |
3. 存储字段(Stored Fields)
| 文件名 | 作用 |
|---|---|
.fdt (Stored Fields Data) |
存储原始文档的字段值(按文档顺序存储),例如保留的标题、正文等原始数据。 |
.fdx (Stored Fields Index) |
存储字段的索引,记录每个文档在 .fdt 文件中的偏移量和长度,支持快速定位。 |
4. 文档值(DocValues)
| 文件名 | 作用 |
|---|---|
.dvd (DocValues Data) |
列式存储数据文件,用于排序、聚合、脚本计算等(如数值、日期、字符串类型)。 |
.dvm (DocValues Metadata) |
存储文档值的元数据(数据类型、压缩方式、数据偏移等)。 |
5. 词向量(Term Vectors)
| 文件名 | 作用 |
|---|---|
.tvx (Term Vector Index) |
词向量的索引文件,记录每个文档的词向量在 .tvd 文件中的位置。 |
.tvd (Term Vector Data) |
存储词向量数据(词项、频率、位置等),用于高亮显示或相关性分析。 |
.tvf (Term Vector Fields) |
存储词向量字段的元信息。 |
6. 标准化信息(Norms)
| 文件名 | 作用 |
|---|---|
.nvd (Norms Data) |
存储字段的标准化信息(如长度归一化因子),用于相关性评分(TF-IDF、BM25)。 |
.nvm (Norms Metadata) |
标准化信息的元数据。 |
7. 其他辅助文件
| 文件名 | 作用 |
|---|---|
.si (Segment Info) |
段的元数据文件,记录段的文档数、删除文档、版本等。 |
.del (Deletions) |
标记段中已删除的文档(BitSet 格式)。 |
.liv (Live Documents) |
标记段中存活的文档(BitSet 格式),用于快速过滤已删除文档。 |
8. 复合文件
复合文件格式(Compound File Format) 是一种将多个小型索引文件合并为更少文件的机制,目的是减少文件数量、优化存储和 I/O 性能。
| 扩展名 | 名称 | 作用 |
|---|---|---|
.cfs |
Compound File Data | 存储合并后的所有索引数据(如倒排索引、词条字典、存储字段等)。 |
.cfe |
Compound File Entries | 存储 .cfs 文件内各子文件的元数据(如子文件的起始位置、长度、名称等)。 |
| 关键特性 |
- 合并索引段
- 每个段(Segment)默认生成一个
.cfs和.cfe文件对,替代原本分散的多个小文件(如.fdt,.fdx,.tim,.doc等)。 - 示例:
- 禁用复合格式的段文件:
_0.fdt,_0.fdx,_0.tim,_0.doc - 启用复合格式的段文件:
_0.cfs,_0.cfe
- 禁用复合格式的段文件:
- 每个段(Segment)默认生成一个
- 优化文件管理
- 减少文件句柄:避免操作系统对大量小文件的句柄限制(尤其在 Windows 系统上)。
- 提升顺序读性能:合并后的大文件在机械硬盘(HDD)上顺序读取更高效。
- 简化备份/迁移:只需操作少量文件即可处理整个索引段。
- 灵活配置
- 可通过
IndexWriterConfig.setUseCompoundFile(true/false)启用或禁用复合文件格式。 - 启用(默认):生成
.cfs+.cfe,适合通用场景。 - 禁用:生成多个独立文件,适合需要并发读写或 SSD 优化场景。
- 可通过
索引文件的结构示例
1 | 索引目录/ |
| 文件后缀 | 用途 | 示例内容 |
|---|---|---|
.tim |
词项字典(Term Dictionary) | 存储词项及指向倒排列表的指针。 |
.tip |
词项字典索引(快速定位词项位置) | 类似B树的索引结构,加速词项查找。 |
.doc |
倒排列表(Posting List) | 存储文档ID、词频、位置等数据。 |
.fdt |
存储字段的原始数据(Stored Fields) | 文档1的原始内容:”hello world”。 |
.fdx |
存储字段的索引(指向.fdt中的位置) |
文档1的起始偏移量:0,长度:11字节。 |
.dvd |
文档值数据(DocValues) | 列式存储的数值或排序字段。 |
.dvm |
文档值元数据(如数据类型、压缩方式) | 记录.dvd文件的结构信息。 |
文档(Document)
索引和搜索的基本单元,由多个字段(Field)组成,例如一篇文章的标题、正文、作者等。
- 文档是 Lucene 的最小数据单元,包含一组字段(Fields)。
- 每个字段是一个键值对,键是字段名称,值是字段内容(可以是文本、数字、日期等)。
- 文档通过唯一的文档 ID(Doc ID)标识,Doc ID 是 Lucene 内部的整数,递增分配。
字段(Field)
每个文档都由一个或多个字段组成。字段是键值对,例如 title:"Elasticsearch",body:"这是一个关于 Elasticsearch 的文档"。字段可以有不同的类型(文本、数字、日期等),并且可以配置不同的索引行为(是否被搜索、是否存储原始值、是否计算词频等)。
- 字段是文档的组成部分,可以存储文本、数字、日期等。
- 字段定义了文档的内容和存储方式。Lucene 支持以下字段属性:
- Indexed:是否为字段创建倒排索引(用于搜索)。
- Stored:是否存储字段的原始值(用于返回查询结果)。
- Tokenized:是否对字段进行分词(生成 Term)。
- DocValues:是否为字段生成列式存储(用于排序、聚合)。
- Lucene 的存储是「字段为中心(field-centric)」的,所有的索引/数据组织都围绕“字段”展开。
- Lucene 的倒排索引是 按字段组织的,每个字段有自己独立的一套倒排结构。
分析器(Analyzer)
分析器(Analyzer) 是 Lucene 中处理文本的核心组件,负责将原始文本转换为可索引的 词项(Term)。它的作用类似于“文本加工流水线”,通过 分词、过滤、标准化 等步骤,将复杂文本(如句子、段落)拆解为适合搜索的规范化词项。例如,将句子 "The quick Brown Fox!" 转换为 ["quick", "brown", "fox"]。
分析器的核心组成
- 字符过滤器(CharFilter)
- 分词器(Tokenizer)
- 词元过滤器(TokenFilter)
字符过滤器(CharFilter)
- 作用:预处理原始文本字符(如替换、删除、添加字符)。
- 常见场景:
- 移除 HTML 标签(如
<b>→ 空)。 - 转换字符(如
&→and)。 - 拼音转换(如
中国→zhongguo)。
- 移除 HTML 标签(如
分词器(Tokenizer)
- 作用:将文本拆分为独立的词元(Token)。
- 典型实现:
StandardTokenizer:按空格、标点分割(适用于英文)。IKTokenizer:中文智能分词(如"搜索引擎"→["搜索", "引擎"])。
词元过滤器(TokenFilter)
- 作用:对词元进行进一步处理(过滤、修改、扩展)。
- 常见过滤器:
LowerCaseFilter:转小写("Hello"→"hello")。StopFilter:移除停用词(如"the","is")。SynonymFilter:添加同义词("car"→["car", "automobile"])。StemmingFilter:词干提取("running"→"run")。
常用内置分析器
| 分析器类型 | 行为 | 适用场景 |
|---|---|---|
StandardAnalyzer |
按空格/标点分词,转小写,移除英文停用词(如 the, is)。 |
通用英文文本处理 |
SimpleAnalyzer |
按非字母字符分割,转小写,无停用词过滤。 | 简单分词需求 |
WhitespaceAnalyzer |
仅按空格分割,保留原始大小写。 | 保留大小写的精确匹配 |
KeywordAnalyzer |
不分词,整个文本作为一个词项。 | ID、编码等无需分词的字段 |
StopAnalyzer |
在 SimpleAnalyzer 基础上增加停用词过滤。 |
需要过滤停用词的简单分词 |
CJKAnalyzer |
对中日韩文本按二元语法(Bigram)分词(如 "中国" → "中", "中国", "国")。 |
中日韩混合文本(效果有限) |
自定义分析器
通过组合 CharFilter、Tokenizer 和 TokenFilter,可灵活定制分析逻辑。
1 | // 自定义分析器:中文分词 + 转小写 + 拼音转换 |
效果:
- 原始文本:
"中华人民共和国" - 分词结果:
["中华", "人民", "共和国"] - 拼音扩展:
["zhonghua", "renmin", "gongheguo"] - 索引词项:
中华, 人民, 共和国, zhonghua, renmin, gongheguo
用途:支持中文关键词和拼音混合搜索(如搜索"zhonghua"可匹配到原文)。
倒排索引(Inverted Index)
**倒排索引将文档中的词(Term)映射到包含这些词的文档列表(Posting List)。其核心组成部分包括:
- 词典(Term Dictionary):存储所有唯一的词(Term),通常以B+树或FST(Finite State Transducer)实现,以便高效查找。
- 倒排表(Posting List):记录每个词出现的文档ID、词频(TF)、位置信息(Positions)、偏移量(Offsets)等。
- 文档元数据(Stored Fields):存储原始文档的字段内容(如标题、摘要等),用于搜索结果展示。
- 规范化的因子(Norms):存储用于计算文档相关性得分的归一化因子(如文档长度)。
- DocValues:用于排序、聚合等场景的列式存储结构,优化查询性能。
具体数据例子 假设有以下三个文档:
- Doc1: “hello world”
- Doc2: “hello lucene”
- Doc3: “lucene world”
倒排索引构建结果
| 词项 | 倒排列表(文档ID、词频、位置) |
|---|---|
| hello | Doc1 (TF=1, Positions:[0]), Doc2 (TF=1, Positions:[0]) |
| world | Doc1 (TF=1, Positions:[1]), Doc3 (TF=1, Positions:[1]) |
| lucene | Doc2 (TF=1, Positions:[1]), Doc3 (TF=1, Positions:[0]) |
列式存储结构(DocValues)
DocValues 是 Lucene 在索引阶段将字段的值以「按文档 ID 编号排列」的方式存储下来。这些值是不可变的、持久化到磁盘上的数据结构,主要用于:
- 排序(sort)
- 聚合(faceting / grouping)
- 打分(function query / scoring)
- 快速访问文档字段值(例如高亮)
为何叫“列式存储”?
普通的倒排索引是词 → 文档ID列表(按词组织,像行式),而 DocValues 是文档ID → 字段值(按字段组织,像列式)。
举个例子,假设有个字段叫 price:
| DocID | price |
|---|---|
| 0 | 10 |
| 1 | 15 |
| 2 | 13 |
这个结构就是 DocValues 按列组织的方式,便于系统按 price 排序或做聚合分析。
DocValues 是 Lucene 中为排序和聚合优化的列式存储结构,它以文档为单位存储字段值,适合于高效读取、排序和分析,而不是文本搜索本身。
索引段(Segment)
索引段(Segment) 是 Lucene 索引的基本组成单元,每个段是一个独立的、不可变的子索引,包含完整的倒排索引、存储字段、文档值等数据。Lucene 通过分段机制实现高效的 增量索引写入 和 后台合并优化,是平衡读写性能的核心设计。
索引段的核心特性
- 不可变性(Immutable)
- 段一旦生成,其内容不再修改。新增文档写入新段,删除操作通过标记文档为“已删除”实现。
- 优势:避免锁竞争,提高并发写入效率;简化缓存机制,提升查询性能。
- 分层存储结构
- 每个段包含独立的文件集(如
.tim、.doc、.fdt、.dvd等)。 - 示例:一个索引可能由多个段组成:
1
2
3
4
5segments_1
├── _0.tim # 段0的词项字典
├── _0.doc # 段0的倒排列表
├── _1.tim # 段1的词项字典
└── _1.doc # 段1的倒排列表
- 每个段包含独立的文件集(如
- 段合并(Segment Merging)
- 自动合并:后台线程将小段合并为大段,减少文件数量,提升查询效率。
- 清理机制:合并时物理删除标记为“已删除”的文档,释放磁盘空间。
索引段的生命周期
- 写入阶段
- 新文档首先写入 内存缓冲区(RAM Buffer),达到阈值后刷新为磁盘上的新段。
- 段合并阶段
- 当段数超过
segmentsPerTier,触发合并。 - 根据策略选出多个小段(如段0、段1), 核心词典项、整合倒排索引、重新生成列式存储
- 删除原始段文件,保留新生成的段。
- 更新元数据,更新
segments_N文件,指向新段。
- 当段数超过
- 查询阶段
- 查询需遍历所有段,合并各段的搜索结果(如取并集、排序)。
索引段的文件结构示例
假设一个索引包含两个段(_0 和 _1),其文件如下:
1 | 索引目录/ |
总结:Lucene 的存储哲学
- 一切为了查询:倒排索引加速搜索,Doc Values 加速聚合。
- 不可变性优先:通过段追加和合并实现高吞吐写入。
- 极致压缩:所有数据均经过编码和压缩,减少存储成本。
- 分层缓存:依赖操作系统缓存 + 应用层缓存平衡性能与资源消耗。
从文档索引到搜索的步骤
Lucene的索引和搜索过程可以分为两个主要阶段:索引阶段和搜索阶段。以下是详细步骤及流程图描述。
索引阶段
索引阶段将原始文档转换为倒排索引结构,涉及以下步骤:
- 文档收集:获取一批原始文档(通常是JSON、文本等格式)。
- 分词(Tokenization):
- 使用分析器(Analyzer)对文档字段进行分词,生成Token流。
- 应用分词器(Tokenizer)和过滤器(TokenFilter,如去停用词、词干提取)。
- 构建倒排索引:
- 将分词后的Term映射到文档ID,生成倒排表。
- 记录词频、位置、偏移等信息。
- 存储元数据:
- 将需要显示的字段存储到Stored Fields。
- 计算Norms并存储。
- 写入段(Segment):
- 将倒排索引、元数据等写入磁盘,生成一个不可变的段。
- 段合并(可选):
- 当段数量过多时,合并小段为大段,减少文件句柄和查询开销。
搜索阶段
搜索阶段根据用户查询检索相关文档,涉及以下步骤:
- 查询输入与解析
- 用户输入查询:用户提供查询字符串,如“Java programming”。
- 查询解析:Lucene 的 QueryParser 解析查询字符串,根据语法(如布尔运算符、模糊查询、通配符等)生成 Query 对象。例如,“Java AND programming”会被解析为一个布尔查询。
- 支持多种查询类型:TermQuery(精确匹配)、PhraseQuery(短语匹配)、WildcardQuery(通配符)、FuzzyQuery(模糊匹配)等。
- 倒排索引查找
- 倒排索引结构:Lucene 的核心数据结构是倒排索引,存储了词项(Term)到文档的映射关系。每个词项关联一个倒排列表(Posting List),包含:
- 包含该词项的文档 ID。
- 词频(Term Frequency, TF)。
- 词项在文档中的位置(用于短语查询等)。
- 查询执行:Query 对象根据类型在倒排索引中查找:
- 对于 TermQuery,直接查找对应词项的倒排列表,获取匹配的文档 ID。
- 对于复杂查询(如布尔查询),通过合并多个子查询的结果(交集、并集、差集等)生成最终的文档集。
- 使用 Collector 收集匹配的文档,通常按评分(Relevance Score)排序。
- 倒排索引结构:Lucene 的核心数据结构是倒排索引,存储了词项(Term)到文档的映射关系。每个词项关联一个倒排列表(Posting List),包含:
- 评分与排序
- 评分模型:Lucene 使用基于 TF-IDF(词频-逆文档频率)和其他因素的评分模型(如 BM25)计算文档与查询的相关性:
- TF(词频):词项在文档中出现的频率越高,相关性越高。
- IDF(逆文档频率):词项在整个索引中的稀有程度,稀有词权重更高。
- 字段权重:不同字段(如标题、内容)可设置不同权重。
- 归一化因子:考虑文档长度等因素。
- 排序:默认按评分降序排列,也支持自定义排序(如按时间、字段值)。
- 评分模型:Lucene 使用基于 TF-IDF(词频-逆文档频率)和其他因素的评分模型(如 BM25)计算文档与查询的相关性:
- 结果返回
- 文档获取:根据收集的文档 ID,从索引中读取文档内容或指定字段(如标题、摘要)。
- 高亮处理(可选):通过 Highlighter 对匹配的关键词进行标记(如加粗)。
- 分页:返回指定范围的结果(如前 10 条)。
- 性能优化
- 索引分段:Lucene 将索引分成多个段(Segment),查询时并行处理,提升效率。
- 缓存:使用 FilterCache 或 FieldCache 缓存常用查询结果或字段值。
- 跳跃表(Skip List):在倒排列表中快速定位文档,减少 I/O。
- 布尔查询优化:通过短路逻辑(如优先处理必须匹配的子查询)减少计算量。
常用查询
| 查询类型 | 功能描述 | 典型用法 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| TermQuery | 精确匹配单个词项 | new TermQuery(new Term("title", "Java")) |
ID、标签、单个关键词搜索 | 大小写敏感,分词需与索引一致 |
| BooleanQuery | 组合子查询,支持 AND、OR、NOT 逻辑 | BooleanQuery.Builder 添加 MUST, SHOULD, MUST_NOT 子查询 |
复杂条件组合、排除特定结果 | 子查询过多影响性能,需优化 |
| PhraseQuery | 匹配连续词项序列(短语),可设置 slop | PhraseQuery.Builder 添加词项,设置 slop |
精确或近似短语搜索,如“Java programming” | slop 越大性能开销越高 |
| WildcardQuery | 支持通配符(* 任意字符,? 单字符) |
new WildcardQuery(new Term("content", "prog*")) |
模糊匹配,如“program*” | 性能较低,前导通配符(如“*ing”)慎用 |
| FuzzyQuery | 模糊匹配,基于编辑距离 | new FuzzyQuery(new Term("content", "program"), 2) |
拼写错误匹配,如“programe” | 性能开销大,限制编辑距离(通常 1 或 2) |
| TermRangeQuery | 匹配字段值在指定范围内的文档 | TermRangeQuery.newStringRange("date", "2020-01-01", "2025-12-31", true, true) |
日期、数值范围搜索 | 字段值需可比较,分词可能影响结果 |
| PrefixQuery | 匹配以指定前缀开头的词项 | new PrefixQuery(new Term("content", "prog")) |
自动补全、前缀匹配 | 性能优于 WildcardQuery,但仍需谨慎 |
| MatchAllDocsQuery | 匹配索引中所有文档 | new MatchAllDocsQuery() |
统计文档总数、导出数据 | 通常与 Filter 或 BooleanQuery 结合 |
| DisjunctionMaxQuery | 多字段查询,取评分最高的子查询 | DisjunctionMaxQuery 添加子查询,设置 tie-breaker |
多字段搜索,标题/内容匹配优先 | 调优 tie-breaker,避免评分失衡 |
| BoostQuery | 为子查询设置加权,提升/降低评分 | new BoostQuery(new TermQuery(new Term("title", "Java")), 2.0f) |
调整字段/查询重要性 | boost 值需合理,避免评分失真 |
| QueryParser | 解析用户输入字符串,生成 TermQuery、BooleanQuery 等 | QueryParser parser = new QueryParser("content", analyzer); parser.parse("Java AND programming") |
自由文本搜索 | 分词需一致,防止查询注入 |
Java示例
pom.xml
1 |
|
1 | public class Fields { |
1 | import org.apache.lucene.analysis.Analyzer; |
1 | 找到 2 条结果: |
1 | lucene_index/ |