文章

ES存储检索与预览PDF等文本文档方案

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
Pipelineattachment 提取文本 → 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 -1 for 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_binary to true

③ 社区反馈:请求体超限

用户索引含大量图片的 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-attachmentPython 抽取 + 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:与方案一「创建索引与写入示例」一致即可(contenttext,可配 ik_max_word);不要content 再走 attachment pipeline
  • 写入字段:至少包含 filenamefile_urlobject_keycontentupload_timeuser_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_urlcontent
  • 检索:调用 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,以支持前端跨域预览
本文由作者按照 CC BY 4.0 进行授权