用 FSCrawler 和 Elasticsearch 构建知识库搜索引擎

使用 FSCrawler 将 PDF、Word 文档和扫描件索引到 Elasticsearch。涵盖 OCR、自定义映射和生产环境部署。

zhuermu··16 分钟
FSCrawlerElasticsearchKnowledge BaseOCRDocument SearchFull-Text Search

每个组织都会积累大量文档——来自供应商的 PDF、各团队的 Word 报告、扫描的合同、会议上的幻灯片。这些内容承载着组织的知识资产,却被锁在任何搜索引擎都触及不到的文件里。Google 无法索引你的内部文件服务器,你的 wiki 搜索也读不懂一张扫描的发票。

FSCrawler 正是为解决这一问题而生。它监控一个目录(本地目录、远程目录,或通过 REST API 投递的文件),使用 Apache Tika 从任意文档格式中提取文本,可选地用 Tesseract 对扫描页面执行 OCR,并把所有内容索引到 Elasticsearch 以供全文检索。基础管线无需编写任何自定义代码——只需配置即可。

本文将从零开始搭建 FSCrawler,配置面向多语言文档的 OCR,构建自定义索引映射,用 Python 集成 REST API,并让整个系统在生产环境中运行。我们还会探讨 FSCrawler 在更宏观的知识库架构中的定位,以及它与 Apache Tika Server、Ingest Attachment 插件等替代方案的对比。


架构概览

在动手安装之前,我们先来理解 FSCrawler 在知识库管线中的位置。

知识库架构

整个架构分为四层:

  1. 文件来源 —— 本地文件系统、挂载的网络驱动器、S3 存储桶、SSH/FTP 服务器,或通过 REST API 上传的文件。
  2. FSCrawler —— 摄取引擎。它检测文件格式,用 Apache Tika 提取文本,对扫描文档执行 Tesseract OCR,并将所有内容批量索引到 Elasticsearch。
  3. Elasticsearch —— 存储全文内容和元数据。通过 BM25 评分、过滤、高亮和聚合来处理搜索查询。
  4. 搜索层 —— Kibana 的 Search Application 功能、自定义 REST API、Web 前端,或将结果喂给 LLM 的 RAG 管线。

这种关注点分离很重要。FSCrawler 不是搜索 UI,而是一条索引管线。你可以在不触碰摄取端的情况下更换搜索层,也可以在不改动搜索应用的情况下用其他索引器替换 FSCrawler。


文档处理管线

下面是 FSCrawler 处理每个文件时发生的过程:

文档处理管线

  1. 文件发现 —— FSCrawler 按固定间隔(可配置,默认 15 分钟)扫描配置的目录,检测新增文件、修改过的文件和已删除的文件。
  2. 格式检测 —— Apache Tika 识别每个文件的 MIME 类型。
  3. 文本提取 —— Tika 针对各种格式的解析器提取文本内容。它支持 PDF、DOC、DOCX、XLS、XLSX、PPT、PPTX、TXT、HTML、RTF 以及数十种其他格式。
  4. OCR(按需) —— 如果文档是扫描版 PDF 或图片,且启用了 OCR,Tesseract 会从图像像素中提取文本。
  5. 索引 —— 提取出的文本和元数据(文件名、路径、大小、内容类型、作者、创建日期、自定义标签)通过 Bulk API 发送到 Elasticsearch。

支持的文件格式包括:PDF(文本版和扫描版)、DOC/DOCXXLS/XLSXPPT/PPTXTXTHTMLRTFODTODSODPEPUB 以及图片文件(通过 OCR)。


使用 Docker 安装

FSCrawler 2.10 是当前的稳定版本。Docker 镜像是运行它最简单的方式——镜像中已经打包了 Java、Apache Tika 和 Tesseract OCR。

3.1 拉取镜像

docker pull dadoonet/fscrawler:2.10

3.2 创建工作目录

mkdir -p /data/fscrawler/config/job_name
mkdir -p /data/fscrawler/documents

目录结构如下:

/data/fscrawler/
├── config/
│   └── job_name/            # Job configuration directory
│       └── _settings.yaml   # Job settings (you create this)
└── documents/               # Files to be indexed
    ├── report.pdf
    ├── contract.docx
    └── presentation.pptx

3.3 运行 FSCrawler

docker run -it --rm \
  --name fscrawler \
  -v /data/fscrawler/config:/root/.fscrawler \
  -v /data/fscrawler/documents:/tmp/es:ro \
  dadoonet/fscrawler:2.10 fscrawler job_name
  • /root/.fscrawler 是配置目录。FSCrawler 会从 job 子目录中读取 _settings.yaml
  • /tmp/es:ro 是文档目录(以只读方式挂载)。这里的所有文件都会被爬取并索引。
  • job_name 是 job 标识符,同时也会成为默认的 Elasticsearch 索引名(文档用 job_name,文件夹条目用 job_name_folder)。

如果 _settings.yaml 不存在,FSCrawler 会在首次运行时创建一个默认配置。但只要不是玩具级 demo,你就应该自己编写配置文件。


配置:_settings.yaml

所有重要的决策都体现在这里。下面是一份启用了 OCR、可直接用于生产环境的配置:

---
name: "job_name"
fs:
  url: "/tmp/es"
  update_rate: "5m"
  excludes:
    - "*/~*"
    - "*/.DS_Store"
    - "*/Thumbs.db"
  json_support: false
  filename_as_id: false
  add_filesize: true
  remove_deleted: true
  store_source: false
  index_content: true
  index_folders: true
  lang_detect: false
  continue_on_error: true
  follow_symlinks: false
  ocr:
    language: "chi_sim+eng"
    enabled: true
    pdf_strategy: "ocr_and_text"
elasticsearch:
  nodes:
    - url: "https://your-elasticsearch-host:9200"
  api_key: "your-base64-encoded-api-key"
  bulk_size: 100
  flush_interval: "5s"
  byte_size: "10mb"
  ssl_verification: true
  push_templates: true

关键配置项说明

fs.update_rate —— FSCrawler 检查文件变化的频率。开发时设为 1m,生产环境设为 5m15m。数值越低,I/O 负载越高。

fs.continue_on_error —— 生产环境请设为 true。单个损坏的文件不应中断整个爬取过程。

fs.ocr.language —— Tesseract 语言包。仅英文用 eng,简体中文加英文用 chi_sim+eng,或任意组合的 Tesseract 语言代码

fs.ocr.pdf_strategy —— 控制 PDF 的处理方式:

  • "ocr_and_text" —— 提取内嵌文本,并对图像页面执行 OCR。最适合混合型 PDF。
  • "ocr_only" —— 只执行 OCR,忽略内嵌文本。适用于纯扫描文档。
  • "no_ocr" —— 完全跳过 OCR。如果所有 PDF 都有内嵌文本,这是最快的选项。

认证 —— FSCrawler 2.10 弃用了 username/password,改用 api_key。可以在 Kibana 的 Stack Management > API Keys 中生成 API key,或通过 Elasticsearch API 生成:

curl -X POST "https://your-es-host:9200/_security/api_key" \
  -H "Content-Type: application/json" \
  -u elastic:your-password \
  -d '{
    "name": "fscrawler-key",
    "role_descriptors": {
      "fscrawler_role": {
        "cluster": ["monitor"],
        "index": [
          {
            "names": ["job_name*"],
            "privileges": ["create_index", "write", "read", "manage"]
          }
        ]
      }
    }
  }'

响应中包含一个 encoded 字段——把它作为 api_key 的值即可。


运行爬虫

5.1 首次运行

启动 FSCrawler 并观察日志:

docker run -it --rm \
  --name fscrawler \
  -v /data/fscrawler/config:/root/.fscrawler \
  -v /data/fscrawler/documents:/tmp/es:ro \
  dadoonet/fscrawler:2.10 fscrawler job_name

启动成功后,你会看到:

INFO  [f.p.e.c.f.FsCrawlerImpl] Starting FS crawler
INFO  [f.p.e.c.f.FsCrawlerImpl] FS crawler started in watch mode.
      It will run unless you stop it with CTRL+C.
INFO  [f.p.e.c.f.c.ElasticsearchClient] Elasticsearch Client connected
      to a node running version 8.17.0
INFO  [f.p.e.c.f.FsParserAbstract] FS crawler started for [job_name]
      for [/tmp/es] every [5m]

FSCrawler 会自动创建:

  • 一个 _default/ 目录,包含面向 6、7、8 版本的默认 Elasticsearch 索引模板。
  • 一个 _status.json 文件,记录上次运行的时间戳:
{
  "name": "job_name",
  "lastrun": "2024-02-21T07:55:58.851263972",
  "indexed": 28,
  "deleted": 0
}

5.2 理解文件同步行为

有两条重要的时序规则需要理解:

  1. 初次同步 —— 在首次启动 FSCrawler之前,就把文件放进文档目录。这样能确保首次爬取时所有已有文件都被索引。
  2. 增量同步 —— 首次运行之后,FSCrawler 只索引修改时间晚于 _status.jsonlastrun 时间戳的文件。如果你需要强制重新索引所有文件,删除 _status.json 后重启即可。

提示: 如果你在首次运行之后添加历史文件却发现它们没被抓取,请检查它们的修改时间戳。你可能需要 touch 一下这些文件,或者删除 _status.json


在 Kibana 中验证

FSCrawler 运行之后,在 Kibana 中验证已索引的文档。

6.1 检查索引

在 Kibana 中进入 Stack Management > Index Management。你应该能看到两个索引:

  • job_name —— 文档索引,包含提取出的内容和元数据。
  • job_name_folder —— 文件夹索引(当 index_folders: true 时)。

6.2 通过 Dev Tools 查询文档

打开 Kibana 的 Dev Tools 并执行一次搜索:

GET job_name/_search
{
  "query": {
    "match": {
      "content": "quarterly revenue"
    }
  },
  "_source": ["file.filename", "file.content_type", "file.filesize", "content"],
  "highlight": {
    "fields": {
      "content": {
        "fragment_size": 150,
        "number_of_fragments": 3
      }
    }
  }
}

6.3 在 Kibana 中创建 Search Application

Kibana 8.8+ 内置了 Search Application 功能,无需编写任何代码就能获得一个现成的搜索 UI:

  1. 在侧边栏进入 Enterprise Search > Search Applications
  2. 点击 Create,选择你的 job_name 索引。
  3. 给应用起个名字(例如 knowledge-base)。
  4. 使用内置搜索 UI 测试查询——它开箱即用地展示文档内容、文件类型和相关度评分。

在投入开发自定义前端之前,这是向相关方演示系统的绝佳方式。


自定义索引映射

FSCrawler 的默认映射足以应对基础搜索,但生产系统往往需要自定义分析器、额外字段或不同的字段类型。下面介绍如何自定义映射。

7.1 为什么要自定义?

  • 自定义分析器 —— 使用面向特定语言的分析器(例如面向 CJK 文本的 icu_analyzer),而非默认的 standard 分析器。
  • keyword 字段 —— 将 file.extensionfile.content_type 设为 keyword 字段,以支持精确匹配过滤和聚合。
  • 额外字段 —— 添加业务元数据字段(部门、项目、密级)。
  • 禁用 source 存储 —— 对大文档不存储 _source 以节省磁盘空间(仍可搜索,但无法取回原文)。

7.2 提供自定义映射

在 job 配置目录中创建文件 _default/8/_settings_folder.json(用于 ES 8.x)。下面是一个针对英文内容配置了自定义分析器的示例:

{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "content_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "stop",
            "snowball",
            "asciifolding"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "analyzer": "content_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "file": {
        "properties": {
          "content_type": { "type": "keyword" },
          "filename": {
            "type": "text",
            "fields": {
              "keyword": { "type": "keyword" }
            }
          },
          "extension": { "type": "keyword" },
          "filesize": { "type": "long" },
          "last_modified": { "type": "date" },
          "url": { "type": "keyword" }
        }
      },
      "path": {
        "properties": {
          "virtual": { "type": "keyword" },
          "real": { "type": "keyword" }
        }
      },
      "meta": {
        "properties": {
          "author": { "type": "text" },
          "title": { "type": "text" },
          "keywords": { "type": "keyword" }
        }
      },
      "external": {
        "type": "object",
        "dynamic": true
      }
    }
  }
}

_settings.yaml 中设置 push_templates: true,FSCrawler 就会在启动时把这份映射推送到 Elasticsearch。

7.3 面向 CJK(中日韩)内容的映射

如果你的文档包含 CJK 文本,请使用 ICU 分析插件:

# Install the ICU plugin on your Elasticsearch cluster
bin/elasticsearch-plugin install analysis-icu

然后在映射中使用 icu_analyzer

{
  "content": {
    "type": "text",
    "analyzer": "icu_analyzer"
  }
}

用于文件上传的 REST API

FSCrawler 内置了一个 REST API,让你可以通过程序上传文件——当文件来自 Web 应用、CI 管线或 S3 事件触发时非常有用。

8.1 启用 REST API

启动 FSCrawler 时加上 --rest

docker run -it --rm \
  --name fscrawler \
  -p 8080:8080 \
  -v /data/fscrawler/config:/root/.fscrawler \
  -v /data/fscrawler/documents:/tmp/es:ro \
  dadoonet/fscrawler:2.10 fscrawler job_name --rest

8.2 检查状态

curl http://localhost:8080/fscrawler

响应:

{
  "ok": true,
  "version": "2.10",
  "elasticsearch": "8.17.0",
  "settings": {
    "name": "job_name",
    "fs": {
      "url": "/tmp/es",
      "update_rate": "5m"
    }
  }
}

8.3 上传文件

# Simple upload
curl -F "file=@report.pdf" "http://localhost:8080/fscrawler/_document"

响应:

{
  "ok": true,
  "filename": "report.pdf",
  "url": "https://your-es-host:9200/job_name/_doc/abc123def456"
}

8.4 上传时附带自定义标签

创建一个包含业务元数据的 tags.json 文件:

{
  "external": {
    "department": "engineering",
    "project": "knowledge-base",
    "classification": "internal",
    "uploaded_by": "api-service"
  }
}

带标签上传:

curl -F "file=@report.pdf" -F "tags=@tags.json" \
  "http://localhost:8080/fscrawler/_document"

external 对象会被合并进 Elasticsearch 文档,使其可被搜索和过滤。

8.5 Python 客户端

下面是一个可用于生产环境的 FSCrawler REST API Python 客户端:

"""FSCrawler REST API client for programmatic document upload."""

import json
import logging
from pathlib import Path

import requests

logger = logging.getLogger(__name__)


class FSCrawlerClient:
    """Client for the FSCrawler REST API."""

    def __init__(self, base_url: str = "http://localhost:8080"):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()

    def health_check(self) -> dict:
        """Check FSCrawler status and connectivity."""
        resp = self.session.get(f"{self.base_url}/fscrawler")
        resp.raise_for_status()
        return resp.json()

    def upload_document(
        self,
        file_path: str | Path,
        tags: dict | None = None,
        index: str | None = None,
    ) -> dict:
        """
        Upload a document to FSCrawler for indexing.

        Args:
            file_path: Path to the file to upload.
            tags: Optional dict of custom metadata (stored under 'external').
            index: Optional index name override (defaults to job name).

        Returns:
            Response dict with 'ok', 'filename', and 'url' fields.
        """
        file_path = Path(file_path)
        if not file_path.exists():
            raise FileNotFoundError(f"File not found: {file_path}")

        url = f"{self.base_url}/fscrawler/_document"
        if index:
            url += f"?index={index}"

        files = {"file": (file_path.name, open(file_path, "rb"))}

        if tags:
            tags_content = json.dumps({"external": tags})
            files["tags"] = ("tags.json", tags_content, "application/json")

        resp = self.session.post(url, files=files)
        resp.raise_for_status()

        result = resp.json()
        if not result.get("ok"):
            raise RuntimeError(f"Upload failed: {result}")

        logger.info("Uploaded %s -> %s", file_path.name, result.get("url"))
        return result

    def upload_directory(
        self,
        directory: str | Path,
        extensions: list[str] | None = None,
        tags: dict | None = None,
        recursive: bool = True,
    ) -> list[dict]:
        """
        Upload all matching files in a directory.

        Args:
            directory: Path to the directory.
            extensions: File extensions to include (e.g., ['.pdf', '.docx']).
                        If None, uploads all files.
            tags: Optional metadata applied to all files.
            recursive: Whether to search subdirectories.

        Returns:
            List of upload results.
        """
        directory = Path(directory)
        pattern = "**/*" if recursive else "*"
        results = []

        for file_path in sorted(directory.glob(pattern)):
            if not file_path.is_file():
                continue
            if extensions and file_path.suffix.lower() not in extensions:
                continue

            try:
                result = self.upload_document(file_path, tags=tags)
                results.append(result)
            except Exception as e:
                logger.error("Failed to upload %s: %s", file_path, e)
                results.append({"ok": False, "filename": file_path.name, "error": str(e)})

        return results


# ── Usage example ────────────────────────────────────────────
if __name__ == "__main__":
    client = FSCrawlerClient("http://localhost:8080")

    # Check connectivity
    status = client.health_check()
    print(f"FSCrawler {status['version']} connected to ES {status['elasticsearch']}")

    # Upload a single file with tags
    result = client.upload_document(
        "quarterly-report.pdf",
        tags={
            "department": "finance",
            "quarter": "Q4-2024",
            "classification": "confidential",
        },
    )
    print(f"Indexed: {result['filename']} -> {result['url']}")

    # Batch upload a directory
    results = client.upload_directory(
        "/data/incoming/reports/",
        extensions=[".pdf", ".docx", ".xlsx"],
        tags={"source": "automated-upload", "batch": "2024-02-20"},
    )
    print(f"Uploaded {sum(1 for r in results if r['ok'])} / {len(results)} files")

性能调优

FSCrawler 的默认设置偏保守。对于大规模文档集(成千上万个文件),调优必不可少。

9.1 Elasticsearch 批量写入设置

_settings.yaml 中的这些设置控制 FSCrawler 如何向 Elasticsearch 发送数据:

设置项默认值推荐值说明
bulk_size100100-500每个批量请求的文档数
flush_interval"5s""5s"-"30s"两次刷写之间的最大间隔
byte_size"10mb""10mb"-"50mb"批量请求的最大字节数
elasticsearch:
  bulk_size: 200
  flush_interval: "10s"
  byte_size: "25mb"

增大 bulk_size 会减少发往 Elasticsearch 的 HTTP 请求数,但会增加内存占用。对于大文件(数 MB 的 PDF),应保持较低的 bulk_size,以免超出 byte_size

9.2 OCR 性能

OCR 是整条管线中最慢的环节——慢一个数量级。单张扫描页面的 OCR 可能耗时 2-5 秒,而文本提取只需毫秒级。

提升 OCR 性能的策略:

  • 不需要就禁用 OCR。 如果所有文档都有内嵌文本,将 ocr.enabled 设为 false
  • 使用 ocr_and_text 策略而非 ocr_only。这样有内嵌文本的页面可以被快速提取,只有基于图像的页面才触发 OCR。
  • 限制 OCR 语言。 每增加一个语言包都会拉长处理时间。除非你真的全都需要,否则用 eng 而不是 chi_sim+eng+jpn+kor
  • 为 OCR 密集型负载的 Docker 容器分配更多内存:
docker run -it --rm \
  --memory=4g \
  -e JAVA_OPTS="-Xmx2g" \
  -v /data/fscrawler/config:/root/.fscrawler \
  -v /data/fscrawler/documents:/tmp/es:ro \
  dadoonet/fscrawler:2.10 fscrawler job_name

9.3 爬取频率与资源占用的权衡

update_rate 设置控制 FSCrawler 扫描文件目录的频率。设得太低(例如 10s)会导致文件系统被持续扫描;设得太高(例如 1h)则会延迟新文档的可用时间。

参考准则:

  • 开发环境:1m
  • 活跃的文档摄取:5m
  • 稳定且偶有更新的知识库:15m-1h
  • 结合 REST API 实现实时上传时:30m-1h(REST API 会立即索引,目录扫描只是一道兜底保险)

生产环境部署

10.1 以守护进程方式运行

在生产环境中,以后台分离的 Docker 容器方式运行 FSCrawler,并开启自动重启:

docker run -d \
  --name fscrawler \
  --restart unless-stopped \
  --memory=4g \
  -e JAVA_OPTS="-Xmx2g" \
  -p 8080:8080 \
  -v /data/fscrawler/config:/root/.fscrawler \
  -v /data/fscrawler/documents:/tmp/es:ro \
  dadoonet/fscrawler:2.10 fscrawler job_name --rest

或使用 Docker Compose:

# docker-compose.yml
services:
  fscrawler:
    image: dadoonet/fscrawler:2.10
    container_name: fscrawler
    restart: unless-stopped
    mem_limit: 4g
    environment:
      - JAVA_OPTS=-Xmx2g
    ports:
      - "8080:8080"
    volumes:
      - ./config:/root/.fscrawler
      - ./documents:/tmp/es:ro
    command: fscrawler job_name --rest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/fscrawler"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

10.2 健康检查与监控

使用 REST API 的健康检查端点进行监控:

# Simple health check for load balancers or container orchestrators
curl -sf http://localhost:8080/fscrawler | jq '.ok'

要进行更全面的监控,可跟踪以下 Elasticsearch 指标:

# Document count in the index
curl -s "https://your-es-host:9200/job_name/_count" | jq '.count'

# Index size on disk
curl -s "https://your-es-host:9200/job_name/_stats/store" | jq '.indices.job_name.total.store.size_in_bytes'

# Check _status.json for last run time
cat /data/fscrawler/config/job_name/_status.json | jq '.lastrun'

10.3 处理大规模文件集

对于数万个文件的初次索引:

  1. 在启动 FSCrawler 之前暂存好文件。 先把所有文件放进文档目录,再启动爬虫。这样可以避免批量摄取过程中增量扫描带来的开销。
  2. 在批量导入期间增大 Elasticsearch 的刷新间隔:
# Before bulk load — reduce indexing overhead
curl -X PUT "https://your-es-host:9200/job_name/_settings" \
  -H "Content-Type: application/json" \
  -d '{"index": {"refresh_interval": "60s"}}'

# After bulk load — restore normal refresh
curl -X PUT "https://your-es-host:9200/job_name/_settings" \
  -H "Content-Type: application/json" \
  -d '{"index": {"refresh_interval": "1s"}}'
  1. 为不同目录使用多个 FSCrawler job。 每个 job 独立运行,可针对不同的文件类型或 OCR 设置进行配置。

10.4 使用 Amazon OpenSearch

如果你更倾向于托管服务,FSCrawler 也能与 Amazon OpenSearch(AWS 对 Elasticsearch 的分支)配合使用。配置几乎完全相同:

elasticsearch:
  nodes:
    - url: "https://your-domain.us-east-1.es.amazonaws.com"
  api_key: "your-opensearch-api-key"
  ssl_verification: true
  push_templates: true

对于 OpenSearch Serverless 集合,你需要使用基于 IAM 的认证。通过环境变量或 IAM 角色为 FSCrawler 容器配置 AWS 凭证,并使用相应的 OpenSearch 端点。


工具对比

FSCrawler 并不是把文档索引进 Elasticsearch 的唯一方式。下面是它与各替代方案的对比:

特性FSCrawlerTika ServerIngest AttachmentUnstructured.io
部署方式独立部署(Docker)独立部署(Docker)ES 插件独立部署(Docker)
文件监控内置目录监控无(仅 API)无(仅 API)无(仅 API)
REST 上传 API通过 ES Ingest API
OCR 支持Tesseract(内置)Tesseract(内置)Tesseract + PaddleOCR
Elasticsearch 集成原生(直接索引)无(返回文本)原生(ingest pipeline)通过连接器
格式覆盖1000+(通过 Tika)1000+(通过 Tika)有限子集25+ 种格式
自定义元数据/标签有(external 对象)有(ingest pipeline)
增量同步有(基于时间戳)
搭建复杂度低(配置文件)低(API 调用)中(管线配置)中(Python SDK)
最适合文件系统索引仅文本提取小规模、集群内AI/ML 管线、RAG

何时选择 FSCrawler:

  • 你需要索引一个文件目录,并在文件变化时保持索引同步。
  • 你想要一个几乎零代码的开箱即用方案——只需 Docker 和一个 YAML 配置。
  • 你需要对扫描文档提供 OCR 支持。

何时选择替代方案:

  • Tika Server —— 你只需要文本提取,不需要 Elasticsearch 索引。索引由你的应用自行处理。
  • Ingest Attachment 插件 —— 你已经在使用 Elasticsearch ingest pipeline,希望一切都留在集群内。注意:不支持 OCR。
  • Unstructured.io —— 你在构建 RAG 管线,需要结构化的文档解析(表格、标题、章节),而非扁平的纯文本提取。

与 RAG 管线集成

FSCrawler 与 RAG 系统互补得很好。FSCrawler 负责文档摄取的”硬骨头”——格式检测、文本提取、OCR,而 Elasticsearch 存储处理结果。你的 RAG 管线随后查询 Elasticsearch,为 LLM 检索相关上下文。

一种典型的集成模式:

from elasticsearch import Elasticsearch

es = Elasticsearch(
    "https://your-es-host:9200",
    api_key="your-api-key",
)


def search_knowledge_base(query: str, top_k: int = 5) -> list[dict]:
    """Search the FSCrawler-indexed knowledge base."""
    results = es.search(
        index="job_name",
        body={
            "query": {
                "multi_match": {
                    "query": query,
                    "fields": ["content", "file.filename^2", "meta.title^3"],
                    "type": "best_fields",
                }
            },
            "size": top_k,
            "_source": ["content", "file.filename", "file.content_type", "meta.title"],
            "highlight": {
                "fields": {"content": {"fragment_size": 300, "number_of_fragments": 3}}
            },
        },
    )

    documents = []
    for hit in results["hits"]["hits"]:
        doc = {
            "filename": hit["_source"].get("file", {}).get("filename"),
            "content_type": hit["_source"].get("file", {}).get("content_type"),
            "title": hit["_source"].get("meta", {}).get("title"),
            "score": hit["_score"],
            "content": hit["_source"].get("content", ""),
            "highlights": hit.get("highlight", {}).get("content", []),
        }
        documents.append(doc)

    return documents


# Use in a RAG pipeline
context_docs = search_knowledge_base("employee onboarding policy")
context = "\n\n---\n\n".join(
    f"[{doc['filename']}]\n{doc['content'][:2000]}" for doc in context_docs
)
# Feed 'context' into your LLM prompt...

这种模式让你兼得两者之长:FSCrawler 处理解析 50 种不同文件格式的脏活累活,而你的 RAG 管线只需一次简单查询,就能从 Elasticsearch 拿到干净的文本。


结语

FSCrawler 属于那种把一件事做到极致的工具:它接收数十种格式的文件,提取其中的文本内容(包括对扫描文档进行 OCR),并把所有内容索引进 Elasticsearch。无需自定义代码,无需复杂的管线编排——只要一个 Docker 容器和一份 YAML 配置文件。

关键要点:

  1. 从 Docker 和一份简单的 _settings.yaml 起步。 先让文档流入 Elasticsearch,再谈任何优化。
  2. 只在需要时才启用 OCR。 它是最大的单一性能瓶颈。对混合型文档集使用 ocr_and_text 策略。
  3. 用 API key 而非用户名/密码。 username/password 字段在 FSCrawler 2.10 中已被弃用。
  4. 为生产环境自定义索引映射。 默认映射能用,但自定义分析器和 keyword 字段会显著提升搜索质量。
  5. 使用 REST API 进行程序化上传。与目录监控结合,就能同时覆盖批量摄取和实时摄取。
  6. 用健康检查做监控,并跟踪 _status.json 文件,以便及早发现爬取失败。

对于构建内部知识库、文档搜索系统,或 RAG 管线检索层的团队来说,FSCrawler 是一个坚实的基础,让你无需编写自定义的文档解析代码。


参考资料

参考资料

  1. FSCrawler documentation — Read the Docs
  2. Elasticsearch reference — Elastic