全文检索深入
全文检索是搜索引擎的核心技术,相比传统的 SQL LIKE 查询,它支持分词、相关性评分、高亮显示、模糊匹配。Elasticsearch 是最流行的全文检索引擎,本节深入讨论其高级特性和调优方法。
中文分词
分词原理
中文分词是将连续的汉字序列切分成有意义的词语。与英文不同,中文没有天然的词边界,需要通过算法识别词语。分词算法包括基于词典的算法、基于统计的算法、基于深度学习的算法。
基于词典的算法通过匹配词典中的词语进行切分,包括正向最大匹配、逆向最大匹配、双向最大匹配。正向最大匹配从左到右扫描,每次匹配最长的词语。这种算法简单高效,但无法处理未登录词。
基于统计的算法通过统计词语共现频率进行切分,包括 HMM、CRF、深度学习序列标注。jieba 分词结合了词典和统计算法,对于词典中的词语直接使用词典匹配,对于未登录词使用 HMM 分词。
IK 分词器
IK 分词器是 Elasticsearch 最常用的中文分词插件,支持智能分词和细粒度分词两种模式。智能分词模式进行最粗粒度的切分,细粒度分词模式进行最细粒度的切分。
{
"analyzer": "ik_smart",
"text": "中华人民共和国国歌"
}
// 输出:中华人民共和国、国歌
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}
// 输出:中华人民共和国、中华、人民、共和、国、国歌IK 分词器支持自定义词典,可以添加行业术语、专有名词、新词。自定义词典文件放在 config 目录,每行一个词,支持词频设置。修改词典后需要重启 Elasticsearch。
自定义分词器
Elasticsearch 支持自定义分词器,由字符过滤器、分词器、词项过滤器组成。字符过滤器处理原始文本,例如 HTML 清理、字符替换。分词器将文本切分为词项。词项过滤器处理分词结果,例如小写化、同义词、停用词。
{
"settings": {
"analysis": {
"char_filter": {
"html_strip": {
"type": "html_strip"
}
},
"filter": {
"my_synonym": {
"type": "synonym",
"synonyms": ["电脑,计算机,PC"]
}
},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": ["html_strip"],
"tokenizer": "ik_max_word",
"filter": ["lowercase", "my_synonym"]
}
}
}
}
}相关性评分
BM25 算法
BM25 是 Elasticsearch 默认的相关性评分算法,它基于词频和逆文档频率计算文档相关性。BM25 公式考虑词频、文档长度、平均文档长度、词频饱和度。
BM25 的特点是词频有饱和效应,高频词的贡献不会无限增长。BM25 有两个可调参数:k1 控制词频饱和度,b 控制文档长度归一化。默认值 k1 = 1.2,b = 0.75。对于长文档,可以增加 b 值;对于短文档,可以减少 b 值。
{
"query": {
"match": {
"content": {
"query": "Elasticsearch 教程",
"boost": 2.0
}
}
}
}评分调优
评分调优通过查询上下文和过滤上下文实现。查询上下文计算相关性评分,过滤上下文不计算评分但缓存结果。对于精确匹配条件,使用过滤上下文可以提高性能。
{
"query": {
"bool": {
"must": {
"match": { "content": "搜索" }
},
"filter": {
"term": { "status": "published" }
}
}
}
}function_score 允许自定义评分函数,支持字段值、衰减函数、脚本评分。例如根据文档时间、点赞数、评论数调整评分。
{
"query": {
"function_score": {
"query": { "match": { "content": "搜索" } },
"field_value_factor": {
"field": "popularity",
"factor": 1.2,
"modifier": "sqrt",
"missing": 1
}
}
}
}ES 集群规划
节点规划
Elasticsearch 集群包含多种角色节点:master-eligible 节点负责集群状态管理、data 节点存储数据、coordinating 节点协调查询、ingest 节点预处理数据。生产环境建议角色分离,避免单一节点过载。
master-eligible 节点建议至少 3 个,避免脑裂。这些节点应该只做 master,不存储数据,不处理查询,保持轻量。data 节点根据数据量规划,每个节点存储 500GB-2TB 数据,过多会导致恢复慢。coordinating 节点部署在应用层或独立节点,分担查询压力。
分片规划
分片是索引的水平切分,每个分片是一个独立的 Lucene 索引。分片数量创建后不能修改,主分片数量需要提前规划。分片大小建议 10GB-50GB,过小导致分片数量多,过大会导致恢复慢。
分片数量 = 数据总量 / 分片大小。例如 1TB 数据,每个分片 20GB,需要 50 个主分片。副本分片数量可以动态调整,建议至少 1 个副本,保证高可用。时间序列索引可以按日期创建,每天一个索引,定期删除旧索引。
硬件规划
内存是 Elasticsearch 最重要的资源。JVM 堆内存建议不超过 31GB,因为超过 31GB 后指针压缩失效,内存效率下降。堆内存设置为系统内存的 50%,留出 50% 给文件系统缓存。例如 64GB 内存的服务器,堆内存设置为 31GB,文件系统缓存自动使用剩余 33GB。
磁盘选择 SSD 而非 HDD,SSD 的随机 I/O 性能是 HDD 的百倍。对于日志场景,可以考虑使用 HDD 降低成本,但查询性能会受影响。磁盘容量规划需要考虑数据增长、保留周期、副本占用,建议预留 50% 空间用于索引合并和 segment 操作。
CPU 对于写入和聚合查询重要,对于简单查询影响较小。建议至少 8 核,对于高并发或复杂聚合场景,建议 16 核或更多。
性能优化
写入优化
批量写入比单条写入快得多,Elasticsearch 支持批量 API。每批建议 1000-5000 条文档,总大小 5MB-15MB。过大的批次会消耗内存,过小的批次增加网络开销。
POST _bulk
{ "index": { "_index": "logs", "_id": "1" } }
{ "timestamp": "2023-01-01", "message": "..." }
{ "index": { "_index": "logs", "_id": "2" } }
{ "timestamp": "2023-01-01", "message": "..." }refresh 间隔控制数据可搜索的延迟,默认 1 秒。对于日志场景,可以增加到 30 秒或更长,减少 segment 数量和合并压力。translog 间隔控制数据持久化的延迟,默认每次请求都刷盘。对于可以容忍数据丢失的场景,可以设置为异步,每 5 秒刷盘。
查询优化
查询优化首先使用过滤上下文而非查询上下文,避免不必要的评分计算。其次避免深度分页,使用 scroll 或 search_after 进行大量数据遍历。再次避免 wildcard 前缀通配符,这种查询会扫描所有文档。
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "published" } },
{ "range": { "created_at": { "gte": "2023-01-01" } } }
]
}
}
}聚合查询可以使用 doc_values 而非 fielddata,避免堆内存溢出。对于高基数聚合,可以使用 cardinality 聚合的 HyperLogLog 算法,减少内存占用。
索引设计
索引设计是性能优化的基础。字段类型选择很重要:text 类型适合全文检索,keyword 类型适合精确匹配和聚合。避免过多字段,可以使用 nested 或 object 类型减少字段数量。动态映射可能推断错误,建议显式定义 mapping。
索引模板是管理索引的利器,可以预先定义 settings 和 mappings,新创建的索引自动应用模板。索引生命周期管理(ILM)可以自动管理索引的滚动、删除、收缩,适合时间序列索引。
全文检索是搜索和日志分析的基础技术,Elasticsearch 提供了完整的全文检索解决方案。通过合理的分词、评分、集群规划、性能优化,可以构建高效、可靠的搜索引擎。