什么是倒排索引?
倒排索引(Inverted Index)是搜索引擎实现毫秒级全文检索的基石。与传统关系型数据库通过ID查找内容的正排索引方式相反,倒排索引是通过内容中的关键词来反向查找包含它的文档ID列表。
我们可以用一个简单的类比来理解两者的区别:
- 正排索引:文档ID → 文档内容
- 类似于一本书的目录,通过章节号(文档ID)找到具体内容。
- 倒排索引:关键词 → 文档ID列表
- 类似于一本书末尾的索引,通过关键词(如“集合”)找到所有出现该词的页码(文档ID)。
倒排索引的核心组成结构
一个倒排索引系统通常包含以下几个核心部分,我们可以通过一个简化的类结构来理解:
// 简化表示倒排索引的数据结构
public class InvertedIndex {
// 词项字典 (Term Dictionary)
Map<String, TermInfo> termDictionary;
// 倒排列表 (Posting List)
class TermInfo {
String term; // 词项,例如 “java”
int docFreq; // 文档频率(包含该词的文档数)
List<Posting> postings; // 倒排列表
}
class Posting {
int docId; // 文档ID
int termFreq; // 词项频率(在该文档中出现的次数)
List<Integer> positions; // 位置信息(用于短语查询)
List<Integer> offsets; // 偏移量
}
}
倒排索引是如何构建的?
假设我们有以下三篇文档:
文档1:Java is a programming language
文档2:Java is widely used
文档3:Elasticsearch is a search engine
其构建过程主要分为两步:
1. 文档分词(Analysis)
对每篇文档进行分词处理,得到词元(Token)列表:
- 文档1:
["java", "is", "a", "programming", "language"]
- 文档2:
["java", "is", "widely", "used"]
- 文档3:
["elasticsearch", "is", "a", "search", "engine"]
2. 构建倒排索引表
统计每个词项出现在哪些文档中,并记录相关信息,形成下表:
| 词项 (Term) |
文档频率 (DF) |
倒排列表 (Posting List) |
| java |
2 |
[(文档1, 词频1), (文档2, 词频1)] |
| is |
3 |
[(文档1,1), (文档2,1), (文档3,1)] |
| a |
2 |
[(文档1,1), (文档3,1)] |
| programming |
1 |
[(文档1,1)] |
| language |
1 |
[(文档1,1)] |
| widely |
1 |
[(文档2,1)] |
| used |
1 |
[(文档2,1)] |
| elasticsearch |
1 |
[(文档3,1)] |
| search |
1 |
[(文档3,1)] |
| engine |
1 |
[(文档3,1)] |
当用户搜索“java”时,系统能立刻从倒排索引中找到它对应的倒排列表 [文档1, 文档2],从而快速定位到相关文档,这正是其高效的核心。
Elasticsearch 中的倒排索引实现
Elasticsearch 对上述基础模型进行了高度优化,其内部实现包含多个精密的组件:
// Elasticsearch倒排索引的核心组件示意
public class ElasticsearchInvertedIndex {
// 1. 词项索引(Term Index)- 使用FST(有限状态转换器)
// 常驻内存的轻量级结构,用于快速定位词项在词项字典中的位置
FST termIndex;
// 2. 词项字典(Term Dictionary)
// 存储所有词项并按字典序排序,通常存储在磁盘
BlockTreeTermsReader termDictionary;
// 3. 倒排列表(Posting List)
// 使用多种压缩技术存储,以减少磁盘占用和内存开销
PostingsReader postingsReader;
// 4. 文档值(Doc Values)- 列式存储的正排索引
// 用于聚合、排序、脚本计算等操作,与倒排索引相辅相成
DocValues docValues;
}
关键优化:高效的压缩技术
为了应对海量数据,Elasticsearch 采用了先进的压缩算法来存储倒排列表。
1. FOR (Frame Of Reference) 压缩
主要用于对有序的文档ID列表进行高效压缩。
原始文档ID列表:[73, 300, 302, 332, 343, 372]
1. 差分编码(存储差值):[73, 227, 2, 30, 11, 29]
2. 分块处理(例如每128个为一组),取块内最大差值(如227)。
3. 根据最大值选择比特位存储每个差值(227需要8位),压缩率通常超过50%。
2. RBM (Roaring Bitmaps) 压缩
专门用于加速倒排列表之间的布尔运算(AND/OR/NOT)。
- 将文档ID空间划分为高16位和低16位。
- 根据每个桶(高16位值相同)内文档ID的密度,动态选择三种容器存储:
- 数组容器:存储稀疏数据。
- 位图容器:存储密集数据。
- 运行长度编码容器:存储连续数据。
- 这种混合策略在速度和内存消耗之间取得了极佳的平衡。
一次完整的查询流程
以查询 “Java programming” 为例:
- 分词:查询语句被分析为
["java", "programming"] 两个词项。
- 查找词项:通过内存中的 FST词项索引 快速定位到这两个词在 词项字典 中的位置。
- 获取倒排列表:从磁盘读取对应的倒排列表。
java → [文档1, 文档2]
programming → [文档1]
- 合并结果:对两个列表进行交集(AND) 运算,得到最终结果
[文档1]。
- 相关性评分:根据 BM25 等算法,计算文档1与查询的相关性得分并排序返回。
Java面试高频考点解析
Q1:为什么Elasticsearch的全文检索比MySQL的LIKE ‘%keyword%’快得多?
A1:根本原因在于数据结构。MySQL的LIKE操作通常需要全表扫描,时间复杂度为O(n)。而Elasticsearch利用倒排索引,查询时先通过字典O(1)或O(log n)定位词项,再对压缩过的文档ID列表进行高效的位图运算。倒排索引本质上是预先计算了所有词项到文档的映射关系,用空间换取了时间。如果你需要优化传统数据库的查询模式,可以了解我们整理的数据库与中间件优化方案。
Q2:倒排索引有哪些缺点或局限性?
A2:
- 存储开销大:需要额外存储索引数据,通常比原始数据大。
- 更新成本高:文档增删改可能导致倒排列表的重建或大量更新,影响写入性能。
- 实时性有延迟:虽然Elasticsearch是近实时(NRT)系统,但数据从写入到可搜仍有短暂延迟(通常1秒)。
Q3:Elasticsearch如何进行精确的短语查询(Phrase Query)?
A3:这依赖于倒排索引中存储的位置信息(position)。在索引时,系统不仅记录词项出现在哪些文档,还记录了它在文档中的顺序位置。进行短语查询时(如“quick brown”),系统会先分别找到“quick”和“brown”的倒排列表,然后合并那些文档ID相同且位置相邻(如brown的位置紧挨着quick)的文档作为结果。
性能优化实战技巧
合理的配置是发挥Elasticsearch性能的关键,以下是一些核心的Mapping和Setting设置示例:
// 1. 根据场景设计字段类型(Mapping优化)
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text", // 用于全文搜索,会建立倒排索引
"analyzer": "ik_max_word", // 使用中文分词器
"fields": {
"keyword": {
"type": "keyword" // 用于精确匹配、聚合,不分词
}
}
},
"category": {
"type": "keyword", // 精确值过滤,高效
"doc_values": true // 默认开启,用于聚合排序
},
"view_count": {
"type": "integer",
"doc_values": true // 数值范围查询和聚合
}
}
}
}
// 2. 索引级别参数调优(Setting优化)
PUT /my_index/_settings
{
"index": {
"number_of_shards": 3, // 根据数据量和集群节点数合理设置分片
"number_of_replicas": 1,
"refresh_interval": "30s" // 适当调大刷新间隔,降低I/O压力,提升写入吞吐
}
}
这些索引和分片的调整策略,是运维与DevOps工作中保证Elasticsearch集群稳定高效运行的基础。
实际应用场景示例:电商商品搜索
倒排索引在复杂查询场景下威力巨大,以下是一个电商商品搜索的简化示例:
// 商品搜索场景中的组合查询
public class ProductSearch {
public SearchResponse searchProducts() {
// 构建布尔查询
BoolQueryBuilder query = QueryBuilders.boolQuery()
// MUST:标题中包含“智能手机”,使用倒排索引进行相关性搜索
.must(QueryBuilders.matchQuery("title", "智能手机").boost(2.0f))
// FILTER:品类为“电子产品”,使用倒排索引进行精确过滤,结果不参与评分,可缓存
.filter(QueryBuilders.termQuery("category", "electronics"))
// FILTER:价格在1000到5000之间,使用BKD树(Lucene用于数值范围查询的结构)
.filter(QueryBuilders.rangeQuery("price").gte(1000).lte(5000))
// SHOULD:标签中包含“旗舰”或“新品”,用于提升相关文档的评分
.should(QueryBuilders.termQuery("tags", "旗舰"))
.should(QueryBuilders.termQuery("tags", "新品"));
// 执行查询...
return client.prepareSearch("products").setQuery(query).get();
}
}
在这个例子中,title字段的全文匹配、category和tags的精确过滤都重度依赖于倒排索引的高效查找能力。
核心要点总结
- 核心价值反转:倒排索引颠覆了“通过文档找词”的传统思维,实现了“通过词项找文档”,这是全文检索高效的根本。
- 三层检索结构:Elasticsearch通过内存FST词项索引、磁盘词项字典和压缩倒排列表的三级结构,在速度和资源消耗上取得平衡。
- 压缩技术是关键:FOR压缩和RBM位图等技术大幅降低了存储开销并提升了集合运算性能。
- 近实时与分布式:Elasticsearch在倒排索引基础上,实现了近实时搜索和分布式存储,使其能处理PB级数据。
- 适用场景广泛:从网站全文搜索、日志分析(如ELK栈)、到商业智能和推荐系统,倒排索引都是其底层的核心支撑技术。对于Java开发者而言,深入理解其原理是应对高性能服务开发与面试挑战的关键,更多Java高级应用与架构知识可以帮助你构建更坚实的后端技术体系。