向量文档搜索系统

potatosearch是一个轻量级向量文档搜索系统,通过仅存储向量和轻量级定位器来优化资源使用,支持多种文档格式的索引和搜索,适用于本地或高度信任的网络环境。

  • by jacobsee
  • Python
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 jacobsee/potatosearch

使用说明

马铃薯研究

面向土豆硬件和真人的矢量文档搜索系统和MCP服务器-只需部署、摄取和使用。

尊重您的系统资源,只将向量和轻量级定位器嵌入到原始源文件中,而不是将所有内容复制到向量数据库中。此外,还支持IVF-PQ索引,以进一步减少占用空间并加快搜索速度。

目前支持ZIM存档、PDF、Microsoft Office和ODF格式的摄取。

为什么?

标准向量数据库存储嵌入向量 和 把全文放在一起。例如,如果你的源语料库是几百GB的压缩文本,向量数据库会解压缩并复制所有内容,迅速膨胀到数TB。如果你在自带矢量模式下使用它们来避免这种情况,你必须自己管理。

马铃薯搜索商店仅限:

  • 嵌入向量(通过FAISS乘积量化压缩)
  • 文档定位器: (backend_name, locator_string, char_start, char_end)

在查询时,它搜索精简索引,解析指针,并根据需要从原始源文件中读取实际文本。

建筑

HTTP API (FastAPI, port 8391)
    ↓
Query Engine: embed query → search all shards → merge by score → fetch text
    ↓
┌────────────────────────────────────────────────────────────┐
│  Per-Backend Shards                                        │
│                                                            │
│  data/shards/wikipedia-en/     data/shards/my-docs/        │
│    ├── faiss.index               ├── faiss.index           │
│    └── refs.sqlite               └── refs.sqlite           │
│                                                            │
│  Each shard has its own FAISS index + SQLite ref store.    │
│  Shards can be ingested, dropped, and rebuilt              │
│  independently.                                            │
└────────────────────────────────────────────────────────────┘
    ↓
Storage Backend Plugins
┌─────┬───────────┬──────┐
│ ZIM │ Plaintext │ ...  │
└─────┴───────────┴──────┘

每个配置的后端都有自己的 碎片 --一个包含FAISS索引和SQLite引用存储的隔离目录。这允许对每个源进行独立的生命周期管理:您可以摄取、删除或重建任何单个分片,而无需接触其他分片。

安全说明

这旨在在本地或 高度信任 网络,例如为此工作使用一个命名空间/分段。尚未实现任何形式的身份验证。 不要将其暴露给不受信任的网络.

快速开始

1.安装发动机

cd engine

# Using pipenv (recommended)
pipenv install

# Or with pip
pip install -e .

# With ZIM support
pip install -e ".[zim]"

2.配置后端

mkdir -p data
cp engine/backends.example.json data/backends.json
# Edit data/backends.json with your actual paths

示例 data/backends.json:

{
  "backends": [
    {
      "id": "wikipedia-en",
      "type": "zim",
      "description": "English Wikipedia — full text of all articles",
      "paths": ["/media/archive/wikipedia_en.zim"],
      "min_text_length": 200
    },
    {
      "id": "notes",
      "type": "plaintext",
      "description": "Personal notes and documentation",
      "paths": ["/home/user/notes"]
    }
  ]
}

每个条目都需要:

  • id --此后端的唯一名称。用作shard目录名称,并在CLI/API命令中使用。如果省略,则默认为 type.
  • type --后端类型(zim 或 plaintext).
  • paths --后端的源路径列表。

可选字段:

  • description --此后端包含的内容的人类可读描述。通过API和MCP公开,以便代理可以发现和选择相关后端。

您可以有多个具有不同ID的相同类型的条目(例如,不同ZIM存档的单独分片)。

3.摄入

对于小型语料库(<~1M块),平面索引可以很好地工作:

pipenv run python -m potatosearch.cli ingest --backend wikipedia-en

对于大型语料库,首先训练IVF-PQ索引(采样500K块,构建压缩的磁盘驻留索引):

pipenv run python -m potatosearch.cli ingest --backend wikipedia-en --train

4.查询(CLI)

查询在所有索引分片中搜索,并按分数合并结果:

pipenv run python -m potatosearch.cli query "How does photosynthesis work?" --top-k 5

5.运行服务器

pipenv run potatosearch-server
# or: pipenv run python -m potatosearch.api.server

服务器从端口8391开始,同时为REST API和(可选)管理UI提供服务。

6.查询(API)

curl -X POST http://localhost:8391/api/query \
  -H "Content-Type: application/json" \
  -d '{"question": "How does photosynthesis work?", "top_k": 5}'

您可以将搜索限制在特定的后端:

curl -X POST http://localhost:8391/api/query \
  -H "Content-Type: application/json" \
  -d '{"question": "How does photosynthesis work?", "top_k": 5, "backends": ["wikipedia-en"]}'

7.管理界面(可选)

管理UI是由引擎提供服务的React SPA。要在开发中使用它:

# Build the frontend
cd ui
npm install
npm run build

# Start the engine (UI is served automatically from ui/dist/)
cd ../engine
pipenv run potatosearch-server

然后打开 http://localhost:8391 在您的浏览器中。

对于使用热重载的前端开发:

# Terminal 1: start the engine
cd engine
pipenv run potatosearch-server

# Terminal 2: start Vite dev server (proxies API calls to engine)
cd ui
npm run dev

然后打开 http://localhost:5173.

若要禁用UI服务(启用API模式),请设置 POTATOSEARCH_SERVE_UI=false.

码头工人

单级多级 Dockerfile 构建两个图像变体:

引擎+管理UI(默认)

docker build -t potatosearch .
docker run -v ./data:/data -p 8391:8391 potatosearch

打开 http://localhost:8391 对于管理UI,或使用上的API http://localhost:8391/api/*.

仅发动机(无用户界面)

docker build --target engine -t potatosearch-engine .
docker run -v ./data:/data -p 8391:8391 potatosearch-engine

在以下位置安装卷 /data 用于持久存储(backends.json、分片索引、模型缓存)。

MCP(模型上下文协议)

potatosearch为MCP端点提供服务 /mcp 使用Streamable HTTP,因此AI代理可以直接通过网络发现和搜索您的索引语料库。客户端不需要本地安装或数据。

从本地代理连接

添加到您的项目 .mcp.json:

{
  "mcpServers": {
    "potatosearch": {
      "type": "streamable-http",
      "url": "http://your-server:8391/mcp"
    }
  }
}

MCP工具

工具 说明
list_backends 列出所有后端及其描述和索引统计数据。
search 语义搜索,可选后端过滤器和top_k
get_document 通过后端ID和定位器检索文档的全文。

添加 description 将字段添加到后端 backends.json 因此,代理可以了解每个语料库包含的内容,并选择要搜索的内容。

碎片生命周期

每个后端的索引数据都存在于 data/shards/{backend_id}/.

轮换来源(例如,更新ZIM档案)

# 1. Drop the old shard
pipenv run python -m potatosearch.cli drop --backend wikipedia-en

# 2. Update backends.json to point to the new ZIM file

# 3. Reingest
pipenv run python -m potatosearch.cli ingest --backend wikipedia-en --train

其他碎片不受影响。这是相对于单片索引的主要优势——您不需要仅仅因为一个50GB的存档被更新而重新处理750GB的数据。

检查碎片

pipenv run python -m potatosearch.cli stats

输出:

  notes: 1200 chunks, 1200 vectors
  stackexchange: 5400000 chunks, 5400000 vectors
  wikipedia-en: 22000000 chunks, 22000000 vectors
Total: 27401200 chunks, 27401200 vectors

API终点

所有端点都在 /api:

方法 路径 描述
职位 /api/query 联合搜索(可选 backends 过滤器)
得到 /api/document/{id} 通过后端ID和定位器检索完整文档文本
职位 /api/ingest 从后端摄取到其分片中
得到 /api/ingest/status 活动/最近摄取作业的进展
得到 /api/backends 列出带有描述和分片信息的后端
得到 /api/stats 聚合和每个分片统计数据
删除 /api/backends/{id} 删除一个分片(删除索引+引用)
职位 /api/reload 重新加载backends.json并重新打开索引
得到 /api/config 读取backends.json配置
PUT /api/config 写backends.json配置
职位 /api/config/validate 验证配置负载
得到 /api/health 健康检查
/mcp MCP端点(可流式HTTP)

配置

所有设置都可以通过前缀为的环境变量覆盖 POTATOSEARCH_:

变量 默认值 描述
POTATOSEARCH_DATA_DIR ./data 分片、配置、模型缓存的根
POTATOSEARCH_EMBEDDING_MODEL all-MiniLM-L6-v2 句子转换模型
POTATOSEARCH_EMBEDDING_DEVICE cpu cpu, cuda,或 mps
POTATOSEARCH_EMBEDDING_BATCH_SIZE 512 每个嵌入批次的块数
POTATOSEARCH_CHUNK_TARGET_WORDS 500 每个块的目标单词
POTATOSEARCH_CHUNK_OVERLAP_WORDS 75 连续块之间的重叠
POTATOSEARCH_FAISS_USE_MMAP true 内存映射索引,而不是加载到RAM
POTATOSEARCH_FAISS_NPROBE 32 IVF集群搜索(召回率与速度)
POTATOSEARCH_FAISS_NLIST 4096 IVF集群数量
POTATOSEARCH_FAISS_PQ_M 48 PQ子量化器(必须分割矢量暗)
POTATOSEARCH_QUERY_TOP_K 10 每个查询的默认结果
POTATOSEARCH_API_PORT 8391 服务器端口
POTATOSEARCH_SERVE_UI true 提供React管理UI
POTATOSEARCH_UI_DIST_DIR ./ui/dist 构建前端dist目录的路径

编写新后端

后端是一个类,它知道如何迭代某种格式的文档,并通过定位器检索特定的文本片段。该工具处理所有嵌入、分块、索引和API问题。

步骤1:实现接口

# engine/potatosearch/backends/my_backend.py
from pathlib import Path
from typing import Iterator
from potatosearch.core import Document, StorageBackend

class MyBackend(StorageBackend):

    def __init__(self, some_config: str, *, backend_id: str = "myformat"):
        self._backend_id = backend_id
        self._config = some_config

    @property
    def name(self) -> str:
        return self._backend_id

    def iterate_documents(self) -> Iterator[Document]:
        """
        Yield every document from your source. Called during ingestion.
        Each Document has:
          - locator: opaque string YOU define, used to re-fetch later
          - title: human-readable title
          - text: full plaintext content (harness handles chunking)
          - metadata: optional dict
        """
        for item in your_format_reader(self._config):
            yield Document(
                locator=item.unique_id,   # you define the format
                title=item.title,
                text=item.get_text(),
                metadata={"source": self._config},
            )

    def retrieve_text(self, locator: str, char_start: int, char_end: int) -> str:
        """
        Re-read a document and return text[char_start:char_end].
        Called at query time. Must be fast (the LLM call is the bottleneck,
        but don't do anything gratuitously slow here).
        """
        return self.retrieve_document(locator)[char_start:char_end]

    def retrieve_document(self, locator: str) -> str:
        """Return the full text of a document. Called by the get_document
        MCP tool and REST endpoint."""
        return your_format_reader.get_by_id(locator)

第二步:注册

在中添加块 engine/potatosearch/cli.pys _register_backends_from_config():

elif btype == "myformat":
    from potatosearch.backends.my_backend import MyBackend
    backend = MyBackend(
        some_config=entry["config_value"],
        backend_id=backend_id,
    )

在 backends.json:

{
  "id": "my-data",
  "type": "myformat",
  "description": "My custom data source",
  "config_value": "/path/to/data"
}

后端设计规则

  1. 后端是无状态读取器。 它们不会嵌入、块化或索引任何东西。
  2. 后端名称来自配置。 接受a backend_id 关键字参数,并从 name 财产。这允许同一后端类型的多个实例。
  3. 定位器是不透明的字符串。 线束按原样存储它们,并在查询时将其传递回去。这样设计它们 retrieve_text() 可以快速找到文档。
  4. iterate_documents() 一定很懒。 一次生成一个文件。不要将整个语料库加载到内存中。
  5. retrieve_text() 和 retrieve_document() 必须返回摄入过程中产生的相同文本,否则char偏移量将出错。如果您的格式涉及任何文本转换(HTML剥离、编码规范化),请在所有方法中应用相同的转换。最简单的模式是实现 retrieve_document() 并有 retrieve_text() 给它一片。

测试

cd engine
pipenv run pytest tests/ -v

开发完整性/AI使用

Jacob See的发展诚信声明v1.0

许可证

仅限AGPL-3.0