Guaipeca
README.md
# Guaipeca š§
**Lightweight configurable RAG MCP server ā FAISS + fastembed + markitdown**
> **Why "Guaipeca"?** ā _Guaipeca_ is southern Brazilian slang (Tupi-Guarani origin) for a scrappy mutt ā a _cusco_, no pedigree, no frills, but loyal and gets the job done. Seemed fitting for a lightweight RAG server with no API keys, no GPU, and no cloud dependencies.
Guaipeca is a self-hosted retrieval-augmented generation (RAG) server that exposes semantic search over your document corpora via the Model Context Protocol (MCP). It uses a lightweight tech stack ā FAISS indexes, fastembed (ONNX Runtime) embeddings, structure-aware chunking ā and is fully configurable and decoupled from any specific memory system.
## Features
- **Multi-corpus** ā define multiple document folders, each with its own weight and topic label
- **MCP-native** ā exposes `search`, `get_chunk`, `get_document`, `list_documents`, `index`, `status`, `list_folders`, and `upload` tools via MCP
- **Dual transport** ā stdio (for local LLM integration) + HTTP (for remote/network access)
- **Hybrid search** ā optional BM25 sparse keyword search fused with FAISS dense vectors via Reciprocal Rank Fusion (RRF)
- **Any document format** ā pymupdf (PDFs with font-based heading detection) + markitdown (DOCX, PPTX, XLSX, HTML ā markdown)
- **Structure-aware chunking** ā splits on headings, treats tables as atomic units, zero LLM calls
- **Local embeddings** ā fastembed (ONNX Runtime) on CPU, no GPU or API keys needed
- **Incremental indexing** ā SHA256 file hashing, only re-indexes changed files
- **FAISS backup + recovery** ā automatic `.fai.bak` backup before writes, fallback chain on load (primary ā backup ā fresh)
- **Embedding cache (LRU + TTL)** ā 10K-entry LRU cache with 1hr TTL, memory-pressure aware eviction via psutil
- **Content preprocessing** ā heading + first 500 chars sent to embedder for better semantic signal; full text stored in metadata
- **Concurrent** ā read-write lock allows multiple simultaneous searches, exclusive indexing
- **Runs on Pi 5** ā ARM64, CPU-only, ~90MB model + ~20MB FAISS
- **Optional auth** ā Bearer token authentication for HTTP transport
## Quick Start
```bash
# Clone and install
git clone https://github.com/LuisEduardoAvila/guaipeca.git
cd guaipeca
pip install -e ".[all]"
# pymupdf is included as a core dependency for PDF heading detection
# Create config
cp config/guaipeca.yaml ~/.guaipeca/guaipeca.yaml
# Edit config to point to your document folders
# Check/download embedding model
guaipeca --check-model
# Index your corpora
guaipeca index
# Search from CLI
guaipeca search "HFM data audit"
# Start MCP server (stdio + HTTP on port 8090)
guaipeca serve
```
## Configuration
Create `~/.guaipeca/guaipeca.yaml`:
```yaml
corpora:
- name: epm-docs
path: /path/to/your/docs
topic: "EPM & Oracle HFM/ARCS"
weight: 1.5
extensions: [.md, .txt, .pdf, .docx]
- name: finance-reference
path: /path/to/your/finance-docs
topic: "Finance & Accounting Standards"
weight: 1.3
extensions: [.md, .txt, .pdf]
embedding:
model: all-MiniLM-L6-v2
dimensions: 384
preprocess: true # heading + first 500 chars for better embeddings
cache_ttl_seconds: 3600 # embedding cache entry TTL (1 hour)
cache_max_entries: 10000 # max cached embeddings (LRU eviction)
chunking:
max_size: 2000
overlap: 200
table_aware: true
split_on_headings: true
server:
transport: both # stdio | http | both
port: 8090
host: 127.0.0.1 # Use 0.0.0.0 for network access
# auth_token: "your-secret-token" # Optional: require Bearer token auth
upload:
allow: ["epm-docs"] # corpora that accept file uploads
max_file_size: 52428800 # 50MB
allowed_extensions: [.md, .txt, .pdf, .docx]
auto_index: true # index after upload
# Search configuration (optional ā all values have safe defaults)
search:
hybrid: false # Enable BM25 + FAISS hybrid search (default: false)
bm25_weight: 1.0 # BM25 contribution weight in RRF fusion
dense_weight: 1.0 # Dense (FAISS) contribution weight in RRF fusion
rrf_k: 60 # RRF constant (higher = smoother ranking)
max_chars: 8000 # Threshold for auto return_mode (total result chars)
```
## Embedding Models
Guaipeca uses [FastEmbed](https://github.com/qdrant/fastembed) (ONNX Runtime) for local embeddings ā no torch, no GPU, no API keys. Models are downloaded automatically on first use and cached locally.
**Content preprocessing** is enabled by default (`embedding.preprocess: true`): before embedding, each chunk is preprocessed to its heading + first 500 characters, giving the model better semantic signal. The full text is still stored in metadata for retrieval. Disable with `preprocess: false` if you want raw chunk text sent to the embedder.
### Choosing a Model
Pick based on your use case and hardware:
#### Speed-optimized (384-dim, <100MB) ā Pi 5, resource-constrained
| Model | Dimensions | Size | Best for |
|-------|-----------|------|----------|
| `all-MiniLM-L6-v2` | 384 | 90MB | General use, fast (default) |
| `BAAI/bge-small-en-v1.5` | 384 | 67MB | Better quality, same speed |
| `snowflake/arctic-embed-xs` | 384 | 90MB | General use, compact |
#### Balanced (512ā768 dim, 120ā520MB) ā desktop, small VM
| Model | Dimensions | Size | Best for |
|-------|-----------|------|----------|
| `jinaai/jina-embeddings-v2-small-en` | 512 | 120MB | Long documents (8192 tokens) |
| `BAAI/bge-base-en-v1.5` | 768 | 210MB | Higher quality, moderate speed |
| `nomic-ai/nomic-embed-text-v1.5-Q` | 768 | 130MB | Quantized, good balance |
| `snowflake/arctic-embed-s` | 384 | 130MB | Good quality, compact |
#### Quality-optimized (768ā1024 dim, 420MB+) ā server, dedicated hardware
| Model | Dimensions | Size | Best for |
|-------|-----------|------|----------|
| `BAAI/bge-large-en-v1.5` | 1024 | 1.2GB | Best quality, slowest |
| `mixedbread-ai/mxbai-embed-large-v1` | 1024 | 640MB | High quality |
| `jinaai/jina-embeddings-v2-base-en` | 768 | 520MB | Long context (8192 tokens) |
#### Multilingual
| Model | Dimensions | Size | Best for |
|-------|-----------|------|----------|
| `paraphrase-multilingual-MiniLM-L12-v2` | 384 | 220MB | ~50 languages, fast |
| `paraphrase-multilingual-mpnet-base-v2` | 768 | 1.0GB | ~50 languages, better quality |
| `intfloat/multilingual-e5-large` | 1024 | 2.2GB | ~100 languages, best quality |
#### Code
| Model | Dimensions | Size | Best for |
|-------|-----------|------|----------|
| `jinaai/jina-embeddings-v2-base-code` | 768 | 640MB | Code + docs, 30+ programming languages |
### Switching Models
1. Update `embedding.model` and `embedding.dimensions` in your config
2. Reindex with `--force` (FAISS vectors must match the new dimensions):
```bash
guaipeca index --force
```
> ā ļø Changing dimensions (e.g. 384 ā 768) requires a full reindex. The old index is incompatible with the new model's vectors.
### Recommendations
| Use case | Recommended model | Why |
|----------|-------------------|-----|
| **Pi 5 / ARM64 SBC** | `all-MiniLM-L6-v2` | Fast, 90MB, good enough quality |
| **Small VM (2GB RAM)** | `BAAI/bge-small-en-v1.5` | Better quality, still 384-dim |
| **Dedicated server** | `BAAI/bge-base-en-v1.5` | 768-dim, noticeable quality gain |
| **Long documents** | `jinaai/jina-embeddings-v2-base-en` | 8192 token context |
| **Multilingual** | `paraphrase-multilingual-MiniLM-L12-v2` | 50+ languages, 384-dim |
| **Code search** | `jinaai/jina-embeddings-v2-base-code` | Code + docs, 30+ languages |
## MCP Tools
### search
Search across indexed corpora. Returns summary + location + score.
```
search(query="HFM data audit", top_k=5, corpora=["epm-docs"], hybrid=true, return_mode="chunks")
```
**return_mode** controls result granularity:
- `chunks` (default) ā returns individual chunks with summary, location, and score
- `documents` ā returns full source documents, deduplicated by path, with best chunk score as document score
- `auto` ā returns chunks if total result size < `search.max_chars` (default 8000), otherwise collapses to documents
### get_chunk
Retrieve full chunk text by chunk_id from search results.
```
get_chunk(chunk_id="epm-docs:a1b2c3d4e5f67890")
```
### get_document
Retrieve the full converted text (markdown) of a document by corpus and source_path. Returns text content plus metadata (filename, file size, chunk count).
```
get_document(corpus="epm-docs", source_path="/path/to/docs/report.pdf")
```
### list_documents
List all indexed documents in a corpus (or all corpora if no corpus specified). Returns source_path, filename, chunk count, file size, and last_indexed timestamp.
```
list_documents(corpus="epm-docs")
list_documents() # all corpora
```
### index
Trigger indexing for a corpus or all corpora.
```
index(corpus="all", force=false)
```
### status
Get corpus statistics: chunk counts, indexed files.
```
status()
```
### list_folders
List corpora that accept file uploads via the upload tool. Returns corpus name, path, topic, and allowed extensions.
```
list_folders()
```
### upload
Upload a file to a corpus for conversion and indexing. File content must be base64-encoded. Only corpora listed in `upload.allow` can receive files.
```
upload(corpus="epm-docs", filename="report.pdf", content="<base64>", index=true)
```
## CLI
```bash
guaipeca --check-model # Check/download embedding model
guaipeca index [corpus] [--force] # Index corpora
guaipeca search "query" [--top-k N] # Search
guaipeca search "query" --hybrid # Hybrid search (BM25 + FAISS)
guaipeca status # Show stats
guaipeca serve [--transport both] [--port 8090] # Start MCP server
```
## Running as a systemd Service
For always-on deployment on Linux (e.g. Pi 5), install Guaipeca as a systemd service.
### 1. Create the service file
```bash
# Copy the service file (or create manually ā see below)
sudo cp guaipeca.service /etc/systemd/system/guaipeca.service
```
Or create it manually at `/etc/systemd/system/guaipeca.service`:
```ini
[Unit]
Description=Guaipeca RAG MCP Server
After=network.target
[Service]
Type=simple
User=<your-user>
Group=<your-group>
WorkingDirectory=/path/to/guaipeca
ExecStart=/path/to/guaipeca --config /home/<user>/.guaipeca/guaipeca.yaml serve --transport both --port 8090
Restart=on-failure
RestartSec=10
Environment=PYTHONUNBUFFERED=1
Environment=HOME=/home/<user>
# Optional resource limits (prevent starving other services)
MemoryMax=1G
CPUQuota=200%
[Install]
WantedBy=multi-user.target
```
### 2. Enable and start
```bash
sudo systemctl daemon-reload
sudo systemctl enable guaipeca # start on boot
sudo systemctl start guaipeca # start now
```
### 3. Verify
```bash
systemctl status guaipeca
curl http://127.0.0.1:8090/health
# ā {"status": "ok", "server": "guaipeca", "version": "0.1.0"}
```
### 4. Check logs
```bash
journalctl -u guaipeca -f # follow logs
journalctl -u guaipeca --since "1 hour ago"
```
### Management
```bash
sudo systemctl stop guaipeca # stop
sudo systemctl restart guaipeca # restart
sudo systemctl disable guaipeca # stop starting on boot
```
### Notes
- The service runs `guaipeca serve` with `--transport both` (stdio + HTTP/SSE)
- Port 8090 is bound to `127.0.0.1` by default (config `server.host`). Use `0.0.0.0` for network access
- Resource limits (`MemoryMax`, `CPUQuota`) prevent Guaipeca from starving other services on resource-constrained hosts like Pi 5
- The embedding model loads at startup (~1s with fastembed/ONNX). First search/index may take slightly longer if the model needs to download
- Config changes require a restart: `sudo systemctl restart guaipeca`
- Re-index after adding files: `guaipeca --config ~/.guaipeca/guaipeca.yaml index`
## Architecture
```
guaipeca.yaml (config)
ā
Config Loader
ā
āāāāāāāāāāāāāā¬āāāāāāāāāāāāā
ā ā ā
Indexer Searcher MCP Server
ā ā ā
ā ā āāā stdio transport
ā ā āāā HTTP/SSE transport (port 8090)
ā ā
ā āāā FAISS per-corpus + weighted merge
ā
āāā pymupdf (PDF) āā
āāā markitdown (other) āā¤
ā ā
chunk ā embed ā FAISS
(read-write lock: concurrent readers, exclusive writer)
```
## Data Layout
```
~/.guaipeca/
āāā guaipeca.yaml # Config
āāā data/
ā āāā corpora/
ā ā āāā epm-docs/
ā ā ā āāā faiss_index.fai
ā ā ā āāā faiss_index.fai.bak # backup (auto-generated)
ā ā ā āāā metadata.json
ā ā ā āāā file_hashes.json
ā ā ā āāā bm25_index/ # BM25 sparse keyword index
ā ā āāā finance-reference/
ā ā āāā faiss_index.fai
ā ā āāā faiss_index.fai.bak # backup (auto-generated)
ā ā āāā metadata.json
ā ā āāā file_hashes.json
ā ā āāā bm25_index/ # BM25 sparse keyword index
ā āāā converted/ # conversion cache (pymupdf + markitdown)
āāā models/
āāā all-MiniLM-L6-v2/ # cached embedding model
```
## Dependencies
| Package | Purpose | Size |
|---------|---------|------|
| fastembed | Local embeddings (ONNX Runtime) | ~46MB (onnxruntime) + ~90MB (model) |
| faiss-cpu | Vector index | ~20MB |
| pymupdf | PDF heading detection | ~25MB |
| markitdown | Document conversion (DOCX, PPTX, XLSX, HTML) | ~30MB |
| bm25s | BM25 sparse keyword search | <1MB |
| pyyaml | Config parsing | <1MB |
| numpy | Array operations | ~15MB |
## ARM64 / Pi 5 System Dependencies
markitdown relies on libraries that require system-level packages on ARM64 (Pi 5).
Install these before `pip install`:
```bash
# Debian/Ubuntu/Raspberry Pi OS
sudo apt install libxml2-dev libxslt-dev libffi-dev libjpeg-dev
# If using PDF conversion:
sudo apt install poppler-utils
# If using DOCX conversion:
sudo apt install antiword
```
If you encounter build errors with `lxml` or `charset-normalizer` on ARM64,
ensure you have `build-essential` and `python3-dev` installed:
```bash
sudo apt install build-essential python3-dev
```
## Container Deployment (Docker)
Guaipeca can run in a Docker container for VM deployment (ARM64 or x86_64). The Pi 5 stays bare-metal; containers are for VMs.
### Quick Start
```bash
# 1. Prepare data directory
mkdir -p data/{config,corpora,index,models}
cp config/guaipeca.container.yaml data/config/guaipeca.yaml
# Edit data/config/guaipeca.yaml as needed
# Copy your documents to data/corpora/
# 2. Start the container
docker compose up -d
# 3. Check health
curl http://localhost:8090/health
# 4. Index corpora (first time)
docker compose exec guaipeca guaipeca --config /data/config/guaipeca.yaml index
```
### Building from Source
```bash
docker build -t guaipeca .
docker run -d --name guaipeca \
-p 8090:8090 \
-v ./data:/data \
--restart unless-stopped \
guaipeca
```
> **Note:** Pre-built images will be available on ghcr.io after the first
> push to `main` triggers the CI/CD pipeline. Until then, build from source
> using the command above.
### Container Data Layout
```
data/ ā /data (mounted volume)
āāā config/ ā /data/config (guaipeca.yaml)
āāā corpora/ ā /data/corpora (source documents)
āāā index/ ā /data/index (FAISS + BM25 + metadata)
āāā models/ ā /data/models (embedding model cache)
```
The embedding model (~90MB) downloads to the volume on first run. Subsequent starts load from cache ā no network needed.
### Automated Rebuilds
Push to `main` triggers a GitHub Actions workflow that:
- Builds multi-arch images (arm64 + amd64)
- Tags with `latest` and commit SHA
- Pushes to `ghcr.io/luis-eduardo-avila/guaipeca`
See `docs/container-deployment.md` for detailed VM deployment instructions.
## Security
- **Default binding:** The HTTP server binds to `127.0.0.1` (localhost) by default.
Change `host: 0.0.0.0` in config only if you need network access.
- **Authentication:** Set `auth_token` in the server config to require
`Authorization: Bearer <token>` headers on HTTP requests. When auth is enabled,
CORS is restricted (no wildcard origin).
- **HTTP endpoints:**
- `GET /sse` ā SSE stream for MCP transport
- `POST /messages` ā JSON-RPC messages
- `GET /health` ā health check
- `GET /tools` ā list available tools
- `GET /download/{corpus}/{filename}` ā download original file (requires auth, path traversal protected)
- **Error messages:** Client error responses are sanitized to avoid leaking
internal file paths or library details.
## License
MIT