// 07 / Local AI Stack · 17 Apr 2026
// the stack
This guide covers the installation and configuration of three core components of the Cyber-Wyse local AI workstation. Together they give you private, offline-capable search across the web and your own documents — no data sent to the cloud.
Privacy-respecting meta search engine. Aggregates results from Google, Bing, DuckDuckGo, GitHub and more. Runs entirely in Docker.
Lightning-fast local document search. Indexes your own files — PDF, DOCX, Markdown, code — for instant full-text retrieval.
Filesystem monitor. Watches folders for new or updated files and automatically re-indexes them into MeiliSearch in real time.
Docker Engine, Compose plugin, Ollama with at least one model, Python 3.10+. See Part 0 for the base setup.
// part 1 — searxng
SearXNG simultaneously queries multiple search engines, de-duplicates results, and returns them with no tracking and no API keys required. In this stack it is both a standalone search UI at localhost:8080 and a live web data source for Ollama.
mkdir -p ~/ai-stack/searxng && cd ~/ai-stack/searxng
python3 -c "import secrets; print(secrets.token_hex(32))"
use_default_settings: true
server:
secret_key: "your-generated-key-here"
limiter: false
image_proxy: true
ui:
default_theme: simple
default_lang: en
search:
safe_search: 0
default_lang: en
searxng:
image: searxng/searxng:latest
container_name: searxng
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- ./searxng:/etc/searxng:rw
environment:
- SEARXNG_BASE_URL=http://localhost:8080/
cap_drop: [ALL]
cap_add: [CHOWN, SETGID, SETUID]
cd ~/ai-stack && docker compose up -d searxng
docker compose logs -f searxng
Navigate to http://localhost:8080 — you should see the SearXNG search interface.
SearXNG exposes a JSON API. This is how the FastAPI backend will query it in Part 2.
import httpx
async def web_search(query: str, num_results: int = 10):
url = "http://localhost:8080/search"
params = {"q": query, "format": "json",
"categories": "general", "language": "en"}
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params, timeout=10)
data = response.json()
results = data.get("results", [])
return [{"title": r["title"], "url": r["url"],
"snippet": r.get("content", "")}
for r in results[:num_results]]
// part 2 — meilisearch
MeiliSearch indexes your own documents and returns highly relevant results with typo tolerance and sub-50ms response times. Relevant chunks are retrieved and passed to Ollama as context — the RAG pattern.
meilisearch:
image: getmeili/meilisearch:latest
container_name: meilisearch
restart: unless-stopped
ports:
- "7700:7700"
environment:
- MEILI_MASTER_KEY=your-master-key-here
- MEILI_ENV=development
volumes:
- ./meilisearch_data:/meili_data
docker compose up -d meilisearch
pip install meilisearch --break-system-packages
In production set MEILI_ENV=production. Dashboard available at http://localhost:7700.
import meilisearch
client = meilisearch.Client("http://localhost:7700", "your-master-key-here")
index = client.create_index("documents", {"primaryKey": "id"})
client.index("documents").update_searchable_attributes(
["title", "content", "filename", "path"])
client.index("documents").update_filterable_attributes(
["type", "folder", "date_modified"])
// part 3 — watchdog
Watchdog monitors the filesystem for changes. When a file is created, modified, or deleted in a watched folder it is automatically parsed and re-indexed into MeiliSearch — no manual re-indexing required.
pip install watchdog pymupdf python-docx markdown --break-system-packages
import time, hashlib, logging, meilisearch
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import fitz
from docx import Document as DocxDoc
WATCH_PATHS = ["/home/eamon/docs", "/home/eamon/dev/wysedsp"]
EXTENSIONS = {".pdf", ".docx", ".md", ".txt", ".py", ".cpp", ".h"}
client = meilisearch.Client("http://localhost:7700", "your-key")
def extract_text(path):
ext = path.suffix.lower()
if ext == ".pdf": return " ".join(p.get_text() for p in fitz.open(str(path)))
if ext == ".docx": return " ".join(p.text for p in DocxDoc(str(path)).paragraphs)
return path.read_text(errors="ignore")
def index_file(path):
if path.suffix.lower() not in EXTENSIONS: return
chunks = extract_text(path).split()
docs = [{"id": hashlib.md5(f"{path}:{i}".encode()).hexdigest(),
"title": path.stem, "filename": path.name, "path": str(path),
"content": " ".join(chunks[i:i+100])}
for i in range(0, len(chunks), 80)]
client.index("documents").add_documents(docs)
class IndexHandler(FileSystemEventHandler):
def on_created(self, e):
if not e.is_directory: index_file(Path(e.src_path))
def on_modified(self, e):
if not e.is_directory: index_file(Path(e.src_path))
if __name__ == "__main__":
observer = Observer()
for p in WATCH_PATHS:
observer.schedule(IndexHandler(), p, recursive=True)
observer.start()
try:
while True: time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
[Unit]
Description=AI Document Indexer
After=network.target
[Service]
ExecStart=/usr/bin/python3 /home/eamon/ai-stack/indexer/indexer.py
User=eamon
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable ai-indexer
sudo systemctl start ai-indexer && sudo systemctl status ai-indexer