ES存储检索与预览PDF等文本文档方案
ES存储检索与预览PDF等文本文档方案
一、方案概述
面向 Word、PDF 等文档的上传、全文检索和预览需求,采用 Elasticsearch + 对象存储(MinIO) 文件双写的架构:
- Elasticsearch:索引提取后的文本,做全文检索;文档中存储
file_url,与源文件关联 - MinIO(或 S3/OSS):存储原始文件,提供下载和预览 URL
文本进入 ES 有两种实现路径,二选一或按场景分流:
| 方案 | 说明 |
|---|---|
| ingest-attachment 插件 | 在 ES 内完成内容提取,无需在应用层集成 Tika(方案一) |
| 应用层抽取(如 Python) | 在业务进程内用库抽取 PDF/Word 文本,经 elasticsearch 官方客户端写入索引,无需 ingest-attachment(方案二) |
二、架构与公共设计
1. 职责划分
| 组件 | 职责 |
|---|---|
| MinIO | 存储原始文件(PDF、Word 等),提供 object_key/URL,用于下载、预览 |
| ES | 存储元数据 + 提取文本,做全文检索;存储 file_url 与源文件关联 |
| ingest-attachment | 从 base64 中提取文本,输出到 content 等字段(仅插件方案) |
| 应用层抽取(Python 等) | 在应用内解析文件得到纯文本,直接写入 content;ES 侧仅需普通 text 索引,无 pipeline(仅应用层方案) |
双写:上传时同时写入 MinIO(文件)和 ES(元数据 + 文本 + file_url),二者互补,非冗余。
2. 索引文档结构(两种方案共用)
1
2
3
4
5
6
7
8
{
"filename": "示例文档.pdf",
"file_url": "https://minio.example.com/bucket/2025/03/doc001.pdf",
"object_key": "bucket/2025/03/doc001.pdf",
"content": "提取的文档正文内容...",
"upload_time": "2025-03-09T10:00:00",
"user_id": "user_id"
}
content:全文检索字段file_url:前端预览/下载用(或存 object_key,查询时生成预签名 URL)
3. 检索时返回链接(两种方案共用)
搜索时指定 _source,命中文档会带上 file_url:
1
2
3
4
5
GET /documents/_search
{
"query": { "match": { "content": "关键词" } },
"_source": ["filename", "file_url", "content", "upload_time"]
}
4. 预览与下载(两种方案共用)
前端用 file_url 做 iframe 或跳转;若用预签名 URL,后端用 object_key 生成临时链接。
三、方案一:ingest-attachment 插件
1. 流程
1
2
3
4
5
6
7
8
9
10
11
12
13
用户上传 Word/PDF
↓
① 存入 MinIO,得到 object_key / file_url
↓
② 读取文件 → base64 编码
↓
③ 通过 ingest pipeline(attachment 处理器)提取文本
↓
④ 索引到 ES:metadata + content + file_url
↓
用户检索 → ES 返回命中文档(含 file_url)
↓
前端预览/下载 → 使用 file_url 请求 MinIO
2. 实现要点
| 步骤 | 实现方式 |
|---|---|
| 写入时 | 先存 MinIO → 得到 URL → 和 base64 一起送入 ingest pipeline,index 请求中显式传入 file_url |
| Pipeline | attachment 提取文本 → content;remove 掉 base64;file_url 等字段不经过 processor,原样进入索引 |
| 查询时 | search 返回 _source,包含 file_url |
3. Docker 部署 ES 与安装插件
1
2
3
4
5
6
7
8
9
10
11
12
13
# 创建网络
docker network create elastic
# 启动 ES(单节点,开发/测试)
docker run -d \
--name elasticsearch \
--network elastic \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "xpack.security.enabled=false" \
docker.elastic.co/elasticsearch/elasticsearch:8.11.0
生产环境建议调整内存、挂载数据卷、开启安全配置等。
安装 ingest-attachment 插件:
1
2
docker exec -it elasticsearch elasticsearch-plugin install ingest-attachment
docker restart elasticsearch
验证插件:
1
2
curl -s http://localhost:9200/_cat/plugins
# 应包含 ingest-attachment
4. 创建 ingest pipeline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PUT _ingest/pipeline/attachment
{
"description": "从 base64 提取文本",
"processors": [
{
"attachment": {
"field": "data",
"indexed_chars": -1,
"ignore_missing": true
}
},
{
"remove": {
"field": "data"
}
}
]
}
data:传入的 base64 字段名,attachment 从此字段读取indexed_chars:-1 表示不限制提取长度remove:删除 base64,避免存储原始文件内容
5. 创建索引与写入示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建索引
PUT /documents
{
"mappings": {
"properties": {
"filename": { "type": "keyword" },
"file_url": { "type": "keyword" },
"object_key": { "type": "keyword" },
"content": { "type": "text", "analyzer": "ik_max_word" },
"upload_time": { "type": "date" },
"user_id": { "type": "keyword" }
}
}
}
写入文档(使用 pipeline):
1
2
3
4
5
6
7
8
9
10
# 将 PDF 转为 base64 后,data 字段传入(示例为占位)
POST /documents/_doc?pipeline=attachment
{
"filename": "示例文档.pdf",
"file_url": "https://minio.example.com/bucket/2025/03/doc001.pdf",
"object_key": "bucket/2025/03/doc001.pdf",
"data": "JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCg==",
"upload_time": "2025-03-09T10:00:00",
"user_id": "user_id"
}
attachment 会将提取的文本写入 attachment.content,如需映射到 content 可在 pipeline 中加 rename 或应用层写入时做字段映射。
6. 将 attachment.content 映射到 content
若希望检索字段名为 content,可调整 pipeline:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PUT _ingest/pipeline/attachment
{
"description": "从 base64 提取文本并映射到 content",
"processors": [
{
"attachment": {
"field": "data",
"indexed_chars": -1,
"ignore_missing": true,
"properties": ["content"]
}
},
{
"rename": {
"field": "attachment.content",
"target_field": "content"
}
},
{
"remove": {
"field": ["data", "attachment"]
}
}
]
}
7. 检索示例
1
2
3
4
5
6
7
8
9
10
GET /documents/_search
{
"query": {
"match": { "content": "关键词" }
},
"_source": ["filename", "file_url", "content", "upload_time"],
"highlight": {
"fields": { "content": {} }
}
}
8. base64 过大带来的影响
采用 ingest-attachment 时,请求内需携带 base64 字段,需注意以下限制与官方依据。
8.1 会有什么影响
| 影响类型 | 说明 |
|---|---|
| 堆内存压力 | 提取大文档时,attachment 处理器需在内存中解码 base64 并解析,占用大量 JVM heap,可能导致节点 OOM |
| HTTP 请求体限制 | 单次 index 请求的 body 受 http.max_content_length 限制,超出会被拒绝(413);部分环境或代理可能另有更小限制(如 10MB) |
| 存储与处理开销 | 若未通过 pipeline 的 remove 删除 base64,原始字段会进入 _source,增加索引体积和 IO 开销 |
8.2 官方文档依据
① indexed_chars 与内存
To prevent extracting too many chars and overload the node memory, the number of chars being used for extraction is limited by default to
100000. Use-1for no limit but ensure when setting this that your node will have enough HEAP to extract the content of very big documents.
② 保留 binary 字段的资源消耗
Keeping the binary as a field within the document might consume a lot of resources. It is highly recommended to remove that field from the document, by setting
remove_binarytotrue…
③ 社区反馈:请求体超限
用户索引含大量图片的 PDF 时出现 413 - Request size exceeded 10485760 bytes,官方回复指向 HTTP 模块配置:
8.3 实践建议
大文件(如 >10MB)建议在应用层先用 Tika 等工具提取文本,再将纯文本写入 ES,不传 base64。这样可:
- 避免请求体过大触发 HTTP 限制
- 减轻 attachment 处理器的 heap 压力
- 保持与官方文档一致的资源使用策略
(亦可改用 方案二:应用层抽取,从源头避免大 base64 请求。)
四、方案二:应用层抽取(如 Python)
1. 流程
1
2
3
4
5
6
7
8
9
上传文件 → 存 MinIO,得到 file_url / object_key
↓
应用读取文件(本地路径或下载流)
↓
pypdf 等库抽取纯文本 → content
↓
Elasticsearch().index() / bulk() 写入 documents 索引(不带 pipeline)
↓
检索、预览与上方「架构与公共设计」相同(file_url 指向 MinIO)
在应用进程内完成文本抽取,向 ES 提交纯 JSON(含 content 字符串),不经过 ingest-attachment pipeline,也无需在请求中携带 base64。
2. 适用场景
- 希望控制大文件处理逻辑(分块、限流、失败重试),减轻 ES 节点 heap 与 HTTP 请求体压力
- 需在入库前做清洗(去页眉页脚、正则替换、分段落存储等)
- 团队以 Python 为主,便于与现有服务集成;ES 集群不安装 ingest-attachment 亦可工作
3. 与 ingest-attachment 的对比
| 维度 | ingest-attachment | Python 抽取 + elasticsearch 客户端 |
|---|---|---|
| 文本提取位置 | ES ingest 节点(Tika) | 应用进程 |
| 写入 ES 的内容 | base64 → pipeline 提取后多为纯文本字段 | 直接写入已提取的 content |
| ES 依赖 | 需安装插件、配置 pipeline | 仅需索引 mapping,无 attachment pipeline |
| 大文件 | 易受请求体大小与节点内存限制 | 可在应用侧流式/分片处理后再写 ES |
| Word 等格式 | Tika 统一支持 | 需选用 python-docx 等额外库或统一走 Tika 子进程 |
4. 依赖与环境
1
2
pip install elasticsearch pypdf python-docx
# 可选:pdfplumber(表格/版面)、PyMuPDF(fitz,速度较快)
- elasticsearch:官方 Python 客户端,ES 8.x 对应
elasticsearch>=8.0 - PDF 库:简单文本型 PDF 可用
pypdf;扫描件/复杂版式需 OCR 等能力时要在应用层另选方案
5. 索引与写入要点
- 索引 mapping:与方案一「创建索引与写入示例」一致即可(
content为text,可配ik_max_word);不要对content再走 attachment pipeline - 写入字段:至少包含
filename、file_url、object_key、content、upload_time、user_id;无需data(base64)字段 - 批量:多文档可用
helpers.bulk批量索引,注意refresh与批次大小调优
6. 代码示例(PDF 抽取 + 写入 ES)
以下仅演示「读 PDF → 抽文本 → 写入 ES」,生产环境需补全异常处理、日志与配置外置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from elasticsearch import Elasticsearch
from pypdf import PdfReader
from datetime import datetime, timezone
def extract_pdf_text(path: str) -> str:
reader = PdfReader(path)
parts = []
for page in reader.pages:
parts.append(page.extract_text() or "")
return "\n".join(parts)
def main():
es = Elasticsearch("http://localhost:9200") # 按实际地址与安全配置修改
file_path = "/path/to/示例文档.pdf"
text = extract_pdf_text(file_path)
doc = {
"filename": "示例文档.pdf",
"file_url": "https://minio.example.com/bucket/2025/03/doc001.pdf",
"object_key": "bucket/2025/03/doc001.pdf",
"content": text,
"upload_time": datetime.now(timezone.utc).isoformat(),
"user_id": "user_id",
}
es.index(index="documents", document=doc)
if __name__ == "__main__":
main()
7. Word(.docx)抽取示例
1
2
3
4
5
from docx import Document
def extract_docx_text(path: str) -> str:
doc = Document(path)
return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
8. 批量写入示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
def gen_docs(file_list, extract_fn, base_url_fn):
for f in file_list:
yield {
"_index": "documents",
"_source": {
"filename": f["filename"],
"file_url": base_url_fn(f),
"object_key": f["object_key"],
"content": extract_fn(f["path"]),
"upload_time": f["upload_time"],
"user_id": f["user_id"],
},
}
bulk(es, gen_docs(...), chunk_size=100)
9. 其他说明
- 异步:可选用
AsyncElasticsearch与异步 Web 框架配合 - 安全:生产环境启用 ES 安全认证时,在客户端配置 API Key 或用户名密码
- 与方案一并存:同一索引可统一由 Python 写入;若历史数据来自 attachment pipeline,需保证
content字段含义一致以便检索
五、通用注意事项与集成要点
1. 应用层集成(两种方案共通)
- 上传(ingest-attachment 路径):接收文件 → 存 MinIO → 读文件做 base64 → 调用 ES index(带 pipeline、
file_url) - 上传(应用层抽取路径):接收文件 → 存 MinIO → 应用内抽文本 → 调用 ES index(无 pipeline,带
file_url与content) - 检索:调用 ES search → 返回命中文档(含
file_url)→ 前端用file_url预览/下载 - 预览:PDF 可用 iframe 或 pdf.js 加载
file_url;Word 需转 PDF 或使用在线预览服务
2. 注意事项
- ingest-attachment 基于 Apache Tika,支持 PDF、Word、Excel、HTML 等常见格式
- 大文件(如 >10MB)若用插件方案,建议在应用层先做文本提取再写入 ES,降低 heap 压力与请求体大小(见方案一「base64 过大带来的影响」)
- MinIO 需配置 CORS 或预签名 URL,以支持前端跨域预览
