189 lines
7.9 KiB
Markdown
189 lines
7.9 KiB
Markdown
# Ingest Pipeline — Filesystem Scan
|
||
|
||
One-shot pipeline that crawls a directory, converts documents to text, embeds them via
|
||
the nomic service, and stores them in Qdrant. Run it manually against any mounted path.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
┌─────────────────────────┐
|
||
│ ingest.py --path /... │ runs on CT 104 (has direct access to ai-internal network)
|
||
└──────────┬──────────────┘
|
||
│ per file
|
||
▼
|
||
┌────────────────────────────────────────────────────────────────┐
|
||
│ 1. File scanner glob recursively, filter by ext │
|
||
│ 2. Change detection sha256(path + mtime) → skip if seen │
|
||
│ 3. Docling converter POST http://docling:5001/convert │
|
||
│ (PDF/DOCX/PPTX/XLSX/HTML → Markdown) │
|
||
│ Plain text/Markdown read directly │
|
||
│ Images (jpg/png/...) send to nomic vision endpoint │
|
||
│ 4. Chunker split Markdown by headers + size │
|
||
│ chunk_size=1200 overlap=150 (matches OWUI RAG config) │
|
||
│ 5. Nomic embedder POST http://nomic:80/v1/embeddings │
|
||
│ text chunks → 768d images → 768d (same space!) │
|
||
│ 6. Qdrant upsert collection `documents`, named vectors │
|
||
└────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Qdrant: http://qdrant:6333 │
|
||
│ collection: documents │
|
||
│ vector: {size: 768, distance: Cosine} │
|
||
│ payload: {path, title, chunk_idx, text, type, mtime, hash} │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌────────────────────────────┐
|
||
│ Open WebUI RAG search │ queries `documents` collection
|
||
│ n8n / MCP tools │ same collection, same embedding space
|
||
└────────────────────────────┘
|
||
```
|
||
|
||
## Why not n8n for this
|
||
|
||
n8n is the right tool for **event-driven** pipelines (webhook on new file, Paperless
|
||
webhook, Nextcloud activity, scheduled re-sync). For a **bulk one-shot crawl**, a Python
|
||
script is better:
|
||
- direct filesystem access with `os.walk`
|
||
- proper progress bar and error recovery
|
||
- batched Qdrant upserts (n8n does one HTTP call per node)
|
||
- easy CLI: `python3 ingest.py /mnt/pve/unas/Notizen`
|
||
|
||
n8n handles the _ongoing_ layer (see [Ongoing ingestion](#ongoing-ingestion-n8n)).
|
||
|
||
## Supported file types
|
||
|
||
| Extension | Handler | Notes |
|
||
|-----------|---------|-------|
|
||
| `.pdf` | Docling | best quality, preserves tables |
|
||
| `.docx`, `.odt` | Docling | |
|
||
| `.pptx` | Docling | slides → sections |
|
||
| `.xlsx`, `.ods` | Docling | tables → Markdown |
|
||
| `.html`, `.htm` | Docling | |
|
||
| `.md`, `.txt`, `.rst` | direct read | no conversion needed |
|
||
| `.jpg`, `.jpeg`, `.png`, `.webp`, `.gif` | nomic vision | 768d image embedding, no text chunks |
|
||
|
||
## Script: `ingest.py`
|
||
|
||
Lives at `/opt/stacks/ai/ingest/ingest.py`. Runs directly on CT 104.
|
||
|
||
### Usage
|
||
|
||
```bash
|
||
# Index everything under a path
|
||
python3 /opt/stacks/ai/ingest/ingest.py --path /mnt/pve/unas/Notizen
|
||
|
||
# Different collection, force re-index
|
||
python3 /opt/stacks/ai/ingest/ingest.py \
|
||
--path /mnt/pve/unas/Dokumente \
|
||
--collection work-docs \
|
||
--force
|
||
|
||
# Dry run (print files, don't embed)
|
||
python3 /opt/stacks/ai/ingest/ingest.py --path /path/to/docs --dry-run
|
||
```
|
||
|
||
### State file
|
||
|
||
`/opt/stacks/ai/ingest/state/<collection>.json` tracks `{path: {hash, indexed_at}}`.
|
||
Subsequent runs skip unchanged files. Delete the state file to force full re-index.
|
||
|
||
### Qdrant collection schema
|
||
|
||
```python
|
||
vectors_config = VectorParams(size=768, distance=Distance.COSINE)
|
||
# payload per point:
|
||
{
|
||
"path": "/mnt/pve/unas/Notizen/someFile.md",
|
||
"title": "someFile", # filename without ext
|
||
"chunk_idx": 0, # 0-based chunk index within file
|
||
"total_chunks": 3,
|
||
"text": "…chunk content…", # empty string for images
|
||
"type": "markdown", # markdown | pdf | docx | image | …
|
||
"mtime": 1716700000.0,
|
||
"hash": "a3f…", # sha256 of file content
|
||
"source": "filesystem",
|
||
}
|
||
```
|
||
|
||
### Chunking strategy
|
||
|
||
For Markdown output from Docling (and raw .md/.txt):
|
||
1. Split on `## ` / `### ` headers first (keep header as first line of chunk)
|
||
2. If chunk > 1200 chars, split further on double-newline (`\n\n`)
|
||
3. If still > 1200 chars, hard-split with 150-char overlap
|
||
|
||
Images: single point per file, no chunking.
|
||
|
||
### Error handling
|
||
|
||
- Docling timeout (>60s): skip file, log to `ingest_errors.log`, continue
|
||
- Qdrant upsert failure: retry 3×, then log and continue
|
||
- Re-run is safe: state file prevents double-indexing
|
||
|
||
## Building and running
|
||
|
||
```bash
|
||
# On CT 104
|
||
mkdir -p /opt/stacks/ai/ingest
|
||
cd /opt/stacks/ai/ingest
|
||
|
||
# Install deps (lightweight — no torch needed, calls services via HTTP)
|
||
pip3 install qdrant-client requests tqdm
|
||
|
||
# Run
|
||
python3 ingest.py --path /mnt/pve/unas/Notizen
|
||
```
|
||
|
||
No container needed for the script itself — it runs on CT 104 bare Python and calls
|
||
`http://docling:5001`, `http://nomic:80`, `http://qdrant:6333` via `ai-internal` (all
|
||
on the same Docker network/host).
|
||
|
||
To run it from _outside_ CT 104 (e.g. the PVE host), wrap it in a container later.
|
||
|
||
## Open WebUI integration
|
||
|
||
OWUI's knowledge base already points at Qdrant (`VECTOR_DB=qdrant`). To surface
|
||
documents from the `documents` collection in chat:
|
||
|
||
1. Admin → Knowledge → Create Knowledge Base
|
||
2. Name: "Local Documents"
|
||
3. The collection is populated by `ingest.py` — OWUI will search it on `#`-prefixed
|
||
RAG queries or when the knowledge base is enabled in a chat.
|
||
|
||
> **Note**: OWUI creates its own internal collection names. To share the same `documents`
|
||
> collection between `ingest.py` and OWUI, use the OWUI API to create a knowledge base
|
||
> pointing to the pre-populated collection — or let OWUI manage its own collection and
|
||
> have `ingest.py` add documents via the OWUI knowledge API (`POST /api/v1/knowledge/{id}/file/add`).
|
||
> The OWUI API path is cleaner for OWUI search integration; the direct Qdrant path is
|
||
> better for external tools (n8n, MCP).
|
||
|
||
## Ongoing ingestion (n8n)
|
||
|
||
After the one-shot crawl, wire n8n for continuous ingestion:
|
||
|
||
| Trigger | n8n nodes | Notes |
|
||
|---------|-----------|-------|
|
||
| Nextcloud webhook (file created/modified) | HTTP → SSH → `ingest.py --path <file>` | Nextcloud admin → Webhooks app |
|
||
| Paperless post-consume webhook | HTTP → Docling → nomic → Qdrant upsert | Paperless has `POST_CONSUME_SCRIPT` hook |
|
||
| Cron re-scan | Schedule → SSH → `ingest.py --path /mnt/pve/unas/Notizen` | weekly full re-sync |
|
||
|
||
The cron re-scan is safe because `ingest.py` skips unchanged files via state hash.
|
||
|
||
## Paths available on CT 104
|
||
|
||
| Path | Contents |
|
||
|------|---------|
|
||
| `/mnt/pve/unas/Notizen/` | Obsidian vault (Markdown) |
|
||
| `/mnt/pve/unas/` | full UNAS NFS share |
|
||
| `/opt/stacks/*/` | stack configs (already in Git, lower priority) |
|
||
|
||
Nextcloud files are on CT 105 (`192.168.1.41`). Access via:
|
||
- WebDAV: `https://nc.nuclide.systems/remote.php/dav/files/fkrebs@nucli.de/`
|
||
- Or mount the NC data volume — not currently mounted on CT 104
|
||
|
||
## Status
|
||
|
||
**Not yet implemented.** Design only. Next step: write `ingest.py`.
|