← Back to Work

// 07 / Local AI Stack  ·  17 Apr 2026

Local AI Stack —
Installation & Setup

StackSearXNG · MeiliSearch · Watchdog
PlatformUbuntu 24.04 · Docker

// the stack

Three services, one offline AI workstation

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.

SearXNG
Port 8080

Privacy-respecting meta search engine. Aggregates results from Google, Bing, DuckDuckGo, GitHub and more. Runs entirely in Docker.

MeiliSearch
Port 7700

Lightning-fast local document search. Indexes your own files — PDF, DOCX, Markdown, code — for instant full-text retrieval.

Watchdog
Python service

Filesystem monitor. Watches folders for new or updated files and automatically re-indexes them into MeiliSearch in real time.

Prerequisite
Already running

Docker Engine, Compose plugin, Ollama with at least one model, Python 3.10+. See Part 0 for the base setup.

// part 1 — searxng

SearXNG — Privacy Web Search

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.

STEP 01

Create config folder and generate secret key

bash
mkdir -p ~/ai-stack/searxng && cd ~/ai-stack/searxng
python3 -c "import secrets; print(secrets.token_hex(32))"
STEP 02

Create settings.yml

yaml~/ai-stack/searxng/settings.yml
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
STEP 03

Add SearXNG to docker-compose.yml and start

yaml~/ai-stack/docker-compose.yml
  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]
bash
cd ~/ai-stack && docker compose up -d searxng
docker compose logs -f searxng
Verify

Navigate to http://localhost:8080 — you should see the SearXNG search interface.

STEP 04

Query SearXNG via API

SearXNG exposes a JSON API. This is how the FastAPI backend will query it in Part 2.

python
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 — Local Document Search

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.

STEP 05

Add to docker-compose.yml and start

yaml~/ai-stack/docker-compose.yml
  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
bash
docker compose up -d meilisearch
pip install meilisearch --break-system-packages
Note

In production set MEILI_ENV=production. Dashboard available at http://localhost:7700.

STEP 06

Create the index — run once

python
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 — Automatic Document Indexing

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.

STEP 07

Install dependencies

bash
pip install watchdog pymupdf python-docx markdown --break-system-packages
STEP 08

The indexing pipeline

python~/ai-stack/indexer/indexer.py
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()
STEP 09

Run as a systemd service

ini/etc/systemd/system/ai-indexer.service
[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
bash
sudo systemctl daemon-reload && sudo systemctl enable ai-indexer
sudo systemctl start ai-indexer && sudo systemctl status ai-indexer
local_ai_stack_guide.pdf Full guide — installation, configuration & search endpoint · v1.0 · Apr 2026
Download PDF