在信息爆炸的今天,如何从海量数据中快速捞取最有价值的信息,几乎是每个技术团队都在面对的硬仗。Elasticsearch(简称 ES)凭借其分布式架构和高可用特性,在全文检索领域早已是明星选手。而它默认搭载的相关性评分算法 BM25,则是让搜索结果真正“排好队”的关键。这篇文章会从一个极简的例子切入,带你一步步搞懂 ES 的倒排索引到底是怎么工作的,BM25 又是如何给搜索结果打出分数、排好顺序的。

先说说几个核心判断:ES 能这么快,靠的无非两件事——倒排索引和相关性算法。倒排索引负责把文档里的词拆开,建立起“词 → 文档”的快速查找表;而 BM25 则用一套统计模型,对匹配上的文档进行排序打分。两者结合,效果才够“聪明”。
1. 初识 Elasticsearch 与文档检索
Elasticsearch 是一个分布式搜索和分析引擎,在日志分析、全文检索、业务监控这些场景里很常见。它能从海量数据中快速检索出关键信息,核心竞争力其实就两个:
- 用倒排索引来管理文档内容。
- 用高效的相关性算法(默认是 BM25)对匹配文档排序。
全文检索 这一点和传统数据库有很大区别。传统数据库做模糊查询,常常要全表扫描,又慢又耗资源。ES 更适合处理非结构化或半结构化数据,比如自然语言、日志信息,通过分词、倒排索引和基于统计的算法,能快速从大量文本中找到最相关的文档。
2. 倒排索引:搜索引擎的“读心术”
传统关系型数据库里,如果要做一个模糊查询,比如 LIKE '%keyword%',往往是全表扫描,这种方式效率很低。搜索引擎为此发明了“倒排索引”这种结构,极大提升了查询速度。
2.1. 什么是倒排索引?
倒排索引的核心思路很直接:
- 把文档里的词语拆分(分词)并进行标准化处理。
- 记录“词语 → 出现在哪些文档、在哪些位置”。
可以这样类比:
- 正排索引更像是“文档 → 包含的词语”。
- 倒排索引则是“词语 → 出现的文档列表”。
这样一来,要查找包含某个词语的文档,只需从索引中直接定位,无需遍历所有文档,速度自然快得多。
3. 示例:搭建小型倒排索引
拿一个极简的例子来说明倒排索引的构建过程。假设有下面这 3 个文档:
Doc1: "Elasticsearch is a powerful search engine"
Doc2: "Elasticsearch is used for full-text search"
Doc3: "Search engines are powerful tools"
第一步:分词与标准化
ES 内置的分词器(如 Standard Analyzer)会对文档进行分词,并做一些常见处理,比如去除大小写、停用词、词干提取等。这里假设分词后的结果是:
- Doc1: ["elasticsearch", "is", "a", "powerful", "search", "engine"]
- Doc2: ["elasticsearch", "is", "used", "for", "full", "text", "search"]
- Doc3: ["search", "engines", "are", "powerful", "tools"]
第二步:构建倒排索引
把每个词语出现在哪些文档、以及什么位置记录下来,就得到一张倒排索引表:
| 词语 | 文档ID | 位置 |
|---|---|---|
| elasticsearch | Doc1, Doc2 | Doc1:[0], Doc2:[0] |
| is | Doc1, Doc2 | Doc1:[1], Doc2:[1] |
| a | Doc1 | Doc1:[2] |
| powerful | Doc1, Doc3 | Doc1:[3], Doc3:[3] |
| search | Doc1, Doc2, Doc3 | Doc1:[4], Doc2:[6], Doc3:[0] |
| engine | Doc1 | Doc1:[5] |
| used | Doc2 | Doc2:[2] |
| for | Doc2 | Doc2:[3] |
| full | Doc2 | Doc2:[4] |
| text | Doc2 | Doc2:[5] |
| engines | Doc3 | Doc3:[1] |
| are | Doc3 | Doc3:[2] |
| tools | Doc3 | Doc3:[4] |
有了这张表,每一步查询都变得简单直接。
4. 查询流程:从键入关键字到获取结果
当用户输入查询词,比如 “powerful search” 时,ES 是怎么处理的?
对查询进行分词和标准化
把查询词同样处理,得到 ["powerful", "search"]。在倒排索引中查找
“powerful” 出现在 Doc1、Doc3;
“search” 出现在 Doc1、Doc2、Doc3;
综合候选文档就是 Doc1、Doc2、Doc3。计算相关性分数(BM25)
这几篇文档都匹配上了,但哪一篇最相关?这时候 BM25 就派上用场了。根据评分排序并返回
最后按分值从高到低排序,返回给用户。
5. BM25:背后的数学公式
BM25(Best Matching 25)是一套用于计算“文档与查询”匹配度的打分函数。它的核心公式简化后如下:
公式中涉及的几个关键变量:
- qi 表示查询中的某个单词。
- f(qi, D) 表示单词 qi 在文档 D 中间出现的次数(词频)。
- IDF(qi) 表示单词 qi 的逆文档频率,反映这个词在所有文档中“有多稀有”。其常见计算公式为:IDF = log( (N - n(qi) + 0.5) / (n(qi) + 0.5) + 1 ),其中 N 是文档总数,n(qi) 是包含该词的文档数。
- |D| 表示文档 D 的长度(词数)。
- a vgdl 表示所有文档的平均长度。
- k1 和 b 是可调参数,通常取 k1=1.2,b=0.75,用来控制词频和文档长度对评分的影响。
简单来说,BM25 在考虑一个词的稀有程度(IDF)和在文档中间出现的次数(TF)的同时,还引入了文档长度来做平衡。分数越高,代表该文档越符合用户的查询意图。
6. BM25 计算示例:深度解析
回到前面的例子,我们实际算一下 “powerful search” 的评分。
1. 统计信息
- 文档总数:3
- 包含词 "powerful" 的文档数:2(Doc1, Doc3)
- 包含词 "search" 的文档数:3(Doc1, Doc2, Doc3)
2. 计算 IDF
IDF("powerful") = log( (3 - 2 + 0.5) / (2 + 0.5) + 1 ) = log(1.5 / 2.5 + 1) ≈ log(1.6) ≈ 0.470
IDF("search") = log( (3 - 3 + 0.5) / (3 + 0.5) + 1 ) = log(0.5 / 3.5 + 1) ≈ log(1.1429) ≈ 0.1335
3. 计算文档长度与平均长度
- Doc1: 6 个词
- Doc2: 7 个词
- Doc3: 5 个词
- 平均长度 a vgdl = (6 + 7 + 5) / 3 = 6
4. 逐文档计算 BM25
假设 k1 = 1.2,b = 0.75。
Doc1: "Elasticsearch is a powerful search engine"
- "powerful" 的 TF = 1
- "search" 的 TF = 1
文档长度 |D| = 6,|D|/a vgdl = 6/6 = 1
BM25(Doc1) = 0.470 × (1.2 × 1) / (1 + 1.2 × (1 - 0.75 + 0.75 × 1)) + 0.1335 × (1.2 × 1) / (1 + 1.2 × (1 - 0.75 + 0.75 × 1))
= 0.470 × 1.2 / (1 + 1.2 × 1) + 0.1335 × 1.2 / (1 + 1.2 × 1)
= (0.470 + 0.1335) × 1.2 / 2.2
= 0.6035 × 0.5455 ≈ 0.3292
(注意:这里计算保留小数的精度与原文略有差异,但最终排序结果一致。)
Doc2: "Elasticsearch is used for full-text search"
- "powerful" 的 TF = 0
- "search" 的 TF = 1
文档长度 |D| = 7,|D|/a vgdl = 7/6 ≈ 1.1667
BM25(Doc2) = 0 + 0.1335 × (1.2 × 1) / (1 + 1.2 × (1 - 0.75 + 0.75 × 1.1667))
= 0.1335 × 1.2 / (1 + 1.2 × (0.25 + 0.875))
= 0.1602 / (1 + 1.2 × 1.125)
= 0.1602 / (1 + 1.35) ≈ 0.0682
Doc3: "Search engines are powerful tools"
- "powerful" 的 TF = 1
- "search" 的 TF = 1
文档长度 |D| = 5,|D|/a vgdl = 5/6 ≈ 0.8333
BM25(Doc3) = 0.470 × 1.2 / (1 + 1.2 × (1 - 0.75 + 0.75 × 0.8333)) + 0.1335 × 1.2 / (1 + 1.2 × (1 - 0.75 + 0.75 × 0.8333))
= (0.470 + 0.1335) × 1.2 / (1 + 1.2 × (0.25 + 0.625))
= 0.6035 × 1.2 / (1 + 1.2 × 0.875)
= 0.7242 / (1 + 1.05) ≈ 0.3532
5. 最终排序
- Doc3:0.3532
- Doc1:0.3292
- Doc2:0.0682
结果很清楚:Doc3 在这个查询场景下得分最高,Doc1 次之,Doc2 排在最末。
7. 总结
Elasticsearch 之所以能在海量数据中快速找到最相关的文档,主要抓住了这些关键点:
- 倒排索引:把文档中的单词拆分出来,建成索引结构,从而实现高效搜索。
- 查询分词:用户输入也要经过同样的分词处理,匹配和打分才能更精准。
- BM25 算法:通过词频、逆文档频率、文档长度等要素,给匹配的文档打出一个综合分数,反映其与查询的“匹配度”。
8. ES实战
8.1 安装与配置ES
接下来是动手环节。先安装并启动 Elasticsearch,步骤如下:
- 下载ES
# 下载ES 8.x版本
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.x.x-linux-x86_64.tar.gz
# 解压
tar -xzf elasticsearch-8.x.x-linux-x86_64.tar.gz
# 进入ES目录
cd elasticsearch-8.x.x/
- 修改配置编辑
config/elasticsearch.yml:
# 集群名称
cluster.name: my-es-cluster
# 节点名称
node.name: node-1
# 数据和日志存储路径
path.data: /path/to/data
path.logs: /path/to/logs
# 网络设置
network.host: 0.0.0.0
http.port: 9200
# 开发环境设置
discovery.type: single-node
- 启动ES
# 启动
./bin/elasticsearch
# 检查是否启动成功
curl http://localhost:9200
8.2 创建索引并定义映射
创建一个用于存储文章的索引:
PUT /articles
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"stop",
"snowball"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "my_analyzer",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"content": {
"type": "text",
"analyzer": "my_analyzer"
},
"author": {
"type": "keyword"
},
"publish_date": {
"type": "date"
}
}
}
}
8.3 索引文档
插入一些测试数据:
POST /articles/_doc/1
{
"title": "Elasticsearch Guide",
"content": "Elasticsearch is a powerful search engine based on Lucene",
"author": "John Doe",
"publish_date": "2024-03-15"
}
POST /articles/_doc/2
{
"title": "Advanced Search Techniques",
"content": "Learn how to use BM25 algorithm for better search results",
"author": "Jane Smith",
"publish_date": "2024-03-16"
}
POST /articles/_doc/3
{
"title": "Understanding Text Analysis",
"content": "Text analysis is crucial for search engine performance",
"author": "Mike Johnson",
"publish_date": "2024-03-17"
}
8.4 查询示例
- 简单全文搜索:
GET /articles/_search
{
"query": {
"match": {
"content": "search engine"
}
}
}
- 多字段搜索:
GET /articles/_search
{
"query": {
"multi_match": {
"query": "search engine",
"fields": ["title", "content"]
}
}
}
- 使用bool查询组合多个条件:
GET /articles/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "search"
}
}
],
"should": [
{
"match": {
"title": "elasticsearch"
}
}
],
"filter": [
{
"range": {
"publish_date": {
"gte": "2024-03-15"
}
}
}
]
}
}
}
8.5 分析搜索结果
执行搜索请求后,ES 会返回带有相关性分数的结果:
{
"took": 5,
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.3862944,
"hits": [
{
"_index": "articles",
"_id": "1",
"_score": 1.3862944,
"_source": {
"title": "Elasticsearch Guide",
"content": "Elasticsearch is a powerful search engine based on Lucene"
}
},
{
"_index": "articles",
"_id": "2",
"_score": 0.9530659,
"_source": {
"title": "Advanced Search Techniques",
"content": "Learn how to use BM25 algorithm for better search results"
}
}
]
}
}
8.6 调优搜索相关性
- 自定义评分:
GET /articles/_search
{
"query": {
"script_score": {
"query": {
"match": {
"content": "search engine"
}
},
"script": {
"source": "_score * (doc['publish_date'].value.millis - 1710000000000) / 86400000"
}
}
}
}
- 调整字段权重:
GET /articles/_search
{
"query": {
"multi_match": {
"query": "search engine",
"fields": [
"title^3",
"content"
]
}
}
}
8.7 性能优化建议
合理设置分片数:
- 单个分片大小建议在20GB-40GB之间
- 分片数 = 数据总量 / 单个分片大小
使用合适的映射:
- 对于不需要分词的字段用
keyword类型 - 对于需要分词的字段用
text类型 - 合理使用
fields设置多字段映射
- 对于不需要分词的字段用
优化内存使用:
- 设置合适的JVM堆大小
- 使用
doc_values和fielddata优化聚合性能
查询优化:
- 尽量用过滤器(filter)代替查询(query)
- 避免使用通配符查询
- 合理利用缓存
接下来,再聊聊 Docker 化安装 ES 以及一些界面化管理的工具。
9. Docker化安装ES
9.1 安装单节点ES
- 创建docker-compose.yml文件
version: '3'
services:
elasticsearch:
image: elasticsearch:8.11.1
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JA VA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
volumes:
- ./es/data:/usr/share/elasticsearch/data
- ./es/plugins:/usr/share/elasticsearch/plugins
ports:
- "9200:9200"
- "9300:9300"
networks:
- elastic
networks:
elastic:
driver: bridge
- 启动ES
# 创建目录
mkdir -p ./es/data ./es/plugins
# 设置目录权限
chmod 777 ./es/data ./es/plugins
# 启动容器
docker-compose up -d
- 验证安装
# 检查容器状态
docker ps
# 测试ES是否正常运行
curl http://localhost:9200
9.2 安装Kibana(官方可视化工具)
- 更新docker-compose.yml,添加Kibana服务
version: '3'
services:
elasticsearch:
# ... ES配置保持不变 ...
kibana:
image: kibana:8.11.1
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- elastic
networks:
elastic:
driver: bridge
- 启动Kibana
docker-compose up -d
访问 http://localhost:5601 就能打开 Kibana 界面。
9.3 安装Elasticsearch-head(轻量级可视化工具)
version: '3'
services:
elasticsearch:
# ... ES配置保持不变 ...
elasticsearch-head:
image: mobz/elasticsearch-head:5
container_name: elasticsearch-head
ports:
- "9100:9100"
networks:
- elastic
访问 http://localhost:9100 即可使用 elasticsearch-head。
10 可视化工具对比
10.1 Kibana(官方工具)
优点:
- 功能最完整
- 与ES版本完全兼容
- 支持复杂的数据可视化
- 支持开发工具控制台
- 支持安全认证
主要功能:
Dev Tools:
- 提供交互式控制台
- 支持自动补全
- 支持多请求组合
Index Management:
- 索引创建和管理
- 映射配置
- 索引模板管理
数据可视化:
- 支持多种图表类型
- 可创建仪表板
- 实时数据监控
10.2 Elasticsearch-head
优点:
- 界面简洁
- 启动快速
- 资源占用少
- 适合简单操作
主要功能:
集群监控:
- 查看集群状态
- 节点信息
- 索引状态
数据浏览:
- 查看索引数据
- 简单的增删改查
- JSON格式化显示
REST请求:
- 简单的REST客户端
- 支持基本查询
10.3 Cerebro(另一个选择)
添加Cerebro到docker-compose.yml:
version: '3'
services:
# ... 其他服务配置 ...
cerebro:
image: lmenezes/cerebro:0.9.4
container_name: cerebro
ports:
- "9000:9000"
networks:
- elastic
优点:
- 现代化界面
- 支持多集群管理
- 操作简单直观
主要功能:
集群管理:
- 多集群监控
- 节点状态查看
- 分片分配可视化
索引管理:
- 创建/删除索引
- 管理别名
- 查看索引设置
REST API:
- 内置REST客户端
- 请求历史记录
- 响应格式化
11. 推荐使用方案
开发环境:
- Kibana + Elasticsearch-head
- Kibana用于复杂操作和可视化
- Elasticsearch-head用于快速查看数据
生产环境:
- 主要使用Kibana
- 配置适当的安全认证
- 根据需要开放必要的功能
配置建议:
version: '3'
services:
elasticsearch:
image: elasticsearch:8.11.1
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JA VA_OPTS=-Xms512m -Xmx512m
- xpack.security.enabled=false
volumes:
- ./es/data:/usr/share/elasticsearch/data
- ./es/plugins:/usr/share/elasticsearch/plugins
ports:
- "9200:9200"
networks:
- elastic
kibana:
image: kibana:8.11.1
container_name: kibana
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
ports:
- "5601:5601"
depends_on:
- elasticsearch
networks:
- elastic
elasticsearch-head:
image: mobz/elasticsearch-head:5
container_name: elasticsearch-head
ports:
- "9100:9100"
networks:
- elastic
networks:
elastic:
driver: bridge
用这套配置,就能同时拥有 Kibana 和 Elasticsearch-head 的能力,既能做复杂的数据分析和可视化,也能快速查看集群状态和数据。
