No description
  • Go 63.4%
  • Vue 24.2%
  • JavaScript 8.7%
  • CSS 1.7%
  • HTML 1.3%
  • Other 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tronax f652ea8010
Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine
Single-Binary Go-Server mit eingebettetem Vue-3-Frontend:

- Ingestion hybrid: MV3-Extension extrahiert gerendertes DOM per
  Mozilla Readability; Server-Fetch mit Readability-Port als Fallback
- Satzbasiertes Chunking (~350 Woerter, 60 Overlap) mit Tests
- Embeddings asynchron via Worker-Pool: Ollama, OpenAI-kompatibel
  oder eingebauter Hash-Provider (Zero-Config-Default)
- Hybrid-Suche: Kosinus-Aehnlichkeit + SQLite FTS5, fusioniert per
  Reciprocal Rank Fusion; Response mit Bookmark + Chunk-Snippets
- Vektorpersistenz als BLOB + In-Memory-Kosinusindex (CGO-frei,
  moderne Implementierung statt sqlite-vec)
- Web-UI: Suche mit Strg/Cmd+K, Snippet-Highlighting, Bibliothek,
  Detailansicht mit Chunks und Volltext
- Docker-Multi-Stage-Build, optionaler Ollama-Service via Compose-Profil
2026-09-04 10:50:30 +02:00
cmd/server Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
extension Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
internal Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
web Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
.env.example Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
.gitignore Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
docker-compose.yml Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
Dockerfile Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
go.mod Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
go.sum Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00
README.md Contexta v0.1.0: Self-hosted Context- & Bookmark-Engine 2026-09-04 10:50:30 +02:00

Contexta

Self-hosted Context- & Bookmark-Engine mit vektorbasierter Volltextsuche. Contexta extrahiert den vollständigen Fließtext gespeicherter Websites, zerlegt ihn in semantische Chunks, bettet diese als Vektoren ein und findet einzelne Textpassagen wieder – nicht nur ganze Seiten.

Single-Binary (Go + eingebettetes Vue-Frontend), SQLite als einziger Zustand, keine Abhängigkeiten zur Laufzeit außer optional einem Embedding-Endpoint.

┌────────────┐  {url, title, text, …}   ┌──────────────────────────────────┐
│ Extension   │ ───────────────────────▶ │  POST /api/bookmarks → 202       │
│ (DOM-First) │                          │    Worker-Pool:                  │
├────────────┤                          │   Fetch/Parse (Readability)      │
│ Web-UI      │  GET /api/search?q=…     │   → Chunking (satzbasiert)       │
│ Vue 3       │ ◀─────────────────────── │   → Embeddings (Ollama/OpenAI/…) │
└────────────┘  Bookmark + Snippets      │   → SQLite + Vektorindex         │
                                         │  GET /api/search: FTS5 ⨯ Vektor  │
                                         │    = Hybrid (RRF-Fusion)         │
                                         └──────────────────────────────────┘

Features

  • Ingestion (hybrid): Die Extension extrahiert das gerenderte DOM per Mozilla Readability (erfasst Inhalte hinter Logins, Paywalls, SPAs). Wird nur eine URL übermittelt, lädt und extrahiert ein Go-Worker die Seite serverseitig (Readability-Port, ohne Nav/Footer/Ads/Skripte).
  • Chunking: satzbasierte Zerlegung (~350 Wörter ≈ 400–600 Tokens, 60 Wörter Überlappung), Abkürzungs-sicher („z. B.“), konfigurierbar.
  • Embeddings: asynchron über konfigurierbare Endpoints – Ollama (/api/embed), OpenAI-kompatible APIs oder eingebauter Hash-Modus (ohne Abhängigkeiten, sofort startklar).
  • Hybrid-Suche: Kosinus-Ähnlichkeit über alle Chunks und SQLite FTS5 (bm25), fusioniert per Reciprocal Rank Fusion. Ergebnis: Bookmark + die exakt getroffenen Chunk-Snippets + Score.
  • Browser-Extension (Manifest V3): Popup, Tastenkürzel Strg/Cmd+Shift+S, Kontextmenü; Konfiguration von Server-URL und API-Token.
  • Web-UI (Vue 3 + Tailwind): Suche mit Strg/Cmd+K, Modus-Wahl (Hybrid/Semantisch/Keyword), Highlighting, Bibliothek, Detailansicht mit allen Chunks und Volltext.

Quickstart

Voraussetzungen: Go ≥ 1.22 und Node ≥ 20 (nur zum Bauen des Frontends).

# 1) Frontend bauen
cd web && npm install && npm run build && cd ..

# 2) Server bauen und starten
go build -o contexta.exe ./cmd/server
./contexta.exe
# → http://localhost:8080

Erster Test ohne Extension:

curl -X POST localhost:8080/api/bookmarks -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/artikel","title":"Ein Artikel","text":"Langer Fließtext …","tags":["test"]}'

curl "localhost:8080/api/search?q=suchbegriff"

Docker

docker compose up -d --build contexta    # mit eingebautem Hash-Embedding
docker compose --profile ollama up -d   # zusätzlich Ollama
docker compose exec ollama ollama pull nomic-embed-text
# .env: EMBEDDINGS_PROVIDER=ollama, EMBEDDINGS_URL=http://ollama:11434

Embeddings für echte semantische Suche

EMBEDDINGS_PROVIDER Quelle Beispiel-Modell
hash (Default) eingebaut – (384d, lexikalische Näherung, Offline)
ollama EMBEDDINGS_URL (Default http://localhost:11434) nomic-embed-text, bge-m3
openai EMBEDDINGS_URL (Default https://api.openai.com/v1) text-embedding-3-small
none – nur Keyword-Suche
# Ollama lokal:
ollama pull nomic-embed-text
EMBEDDINGS_PROVIDER=ollama ./contexta.exe

Beim Wechsel des Modells (andere Dimension) alle Bookmarks neu indexieren: curl -X POST localhost:8080/api/reindex

Konfiguration (Umgebungsvariablen)

Variable Default Bedeutung
CONTEXTA_ADDR :8080 HTTP-Listen-Adresse
CONTEXTA_DB contexta.db SQLite-Datei
CONTEXTA_TOKEN – Bearer-Token für /api (leer = offen)
EMBEDDINGS_PROVIDER hash hash | ollama | openai | none
EMBEDDINGS_URL http://localhost:11434 Endpoint-Basis
EMBEDDINGS_MODEL nomic-embed-text Modellname
EMBEDDINGS_API_KEY – für openai
EMBEDDINGS_DIMS auto Dimension erzwingen
CONTEXTA_WORKERS 2 Parallel indexierende Goroutinen
CHUNK_TARGET_WORDS 350 Chunk-Zielgröße (Wörter)
CHUNK_OVERLAP_WORDS 60 Überlappung (Wörter)
FETCH_TIMEOUT 25 Sekunden für Server-Fetch
FETCH_MAX_BYTES 8388608 Max. Antwortgröße

Siehe auch .env.example.

API

Methode & Pfad Beschreibung
POST /api/bookmarks Bookmark einreihen → 202. Body: {url, title?, html?, text?, selected_text?, tags?, source?}. Gleiche URL = Update & Neuindexierung.
GET /api/bookmarks Liste (?status=&tag=&q=&limit=&offset=)
GET /api/bookmarks/{id} Detail inkl. aller Chunks
DELETE /api/bookmarks/{id} Löschen (inkl. Chunks/Vektoren)
GET /api/search?q=&mode=&limit= mode=hybrid (Default) | semantic | keyword
POST /api/reindex {} = alles, {bookmark_id} = einzeln
GET /api/status Zähler, Embedding-Modell, Dimensionen
GET /api/health Liveness (ohne Auth)

Suchantwort (gekürzt):

{
  "query": "sauerteig brot backen", "mode": "hybrid", "took_ms": 5,
  "results": [{
    "bookmark": { "id": "…", "url": "…", "title": "…", "tags": ["backen"] },
    "score": 0.0328,
    "matched_chunks": [{
      "chunk_index": 0,
      "snippet": "… Brot mit Sauerteig backen …",
      "content": "Sauerteig besteht aus …",
      "vector_score": 0.29, "keyword_rank": 0.0000039, "score": 0.0328
    }]
  }]
}

Mit gesetztem CONTEXTA_TOKEN senden Extension und Web-UI Authorization: Bearer <token>; /api/health bleibt offen.

Browser-Extension installieren

  1. chrome://extensions → Entwicklermodus → Entpackte Erweiterung laden → Ordner extension/
  2. Popup öffnen: Server-URL + ggf. Token eintragen → Testen
  3. Seite speichern per Popup-Button, Strg/Cmd+Shift+S oder Kontextmenü

Details: extension/README.md.

Datenmodell (SQLite)

CREATE TABLE bookmarks (
    id TEXT PRIMARY KEY,
    url TEXT NOT NULL UNIQUE,
    title TEXT NOT NULL,
    summary TEXT NOT NULL DEFAULT '',
    tags TEXT NOT NULL DEFAULT '[]',
    status TEXT NOT NULL DEFAULT 'pending', -- pending|indexing|ready|failed
    error TEXT NOT NULL DEFAULT '',
    source TEXT NOT NULL DEFAULT '',
    content TEXT NOT NULL DEFAULT '',       -- extrahierter Volltext (Reindex-Basis)
    content_length INTEGER NOT NULL DEFAULT 0,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    indexed_at DATETIME
);

CREATE TABLE chunks (
    id          TEXT PRIMARY KEY,
    bookmark_id TEXT NOT NULL REFERENCES bookmarks(id) ON DELETE CASCADE,
    chunk_index INTEGER NOT NULL,
    content     TEXT NOT NULL,
    embedding   BLOB,                       -- float32 little-endian
    UNIQUE (bookmark_id, chunk_index)
);

-- Keyword-Index (external content, per Trigger synchron gehalten)
CREATE VIRTUAL TABLE chunks_fts USING fts5(content, content='chunks', content_rowid='rowid');

Warum kein sqlite-vec (vec0)?

Die Spezifikation erlaubt „alternatives lokales Vektorspeicher-Modul“. Contexta speichert Embeddings als BLOB in SQLite und hält einen normalisierten In-Memory-Index mit Brute-Force-Kosinussuche. Gründe:

  • CGO-freie Single-Binary (modernc.org/sqlite): kein gcc, läuft überall (Windows/macOS/Linux) und im Scratch-Container.
  • Bei der Zielgröße (10⁴–10⁵ Chunks à 384–1536 Dimensionen) ist der lineare Scan in Go im Millisekundenbereich – schneller als der Umweg über eine virtuelle Tabelle.
  • Dimension flexibel pro Modell (vec0 bräuchte feste FLOAT[n]-Dimension).

Architektur

cmd/server/          Entrypoint, go:embed des Frontends, SPA-Fallback
internal/api/        REST-Handler, CORS, optionale Token-Auth, Logging
internal/db/         Schema, Migrationen, FTS5-Triggersync, Vektor-BLOBs
internal/chunker/    Satzbasierter Splitter (Overlap, Abkürzungssicher)
internal/parser/     Readability-Port + Fallback, Charset-Detection, Fetch
internal/embeddings/ Ollama / OpenAI / Hash-Clients, Batch-Embedding
internal/worker/     Goroutinen-Pool, Crash-Recovery (pending/indexing bei Start)
internal/search/     RRF-Fusion (FTS5 + Kosinus), Snippet-Fenster, Gruppierung
web/                 Vue 3 + Vite + Tailwind 4 (wird via go:embed eingebettet)
extension/           Manifest V3: Readability-Extraktion, Popup, Commands

Ablauf der Indexierung: POST /api/bookmarks validiert und speichert das Bookmark (pending), antwortet 202, und der Worker-Pool übernimmt: Content beschaffen (Extension-Text > Extension-HTML > gespeicherter Volltext > HTTP-Fetch) → Chunking → Chunks + FTS5 persistieren → Embeddings erzeugen und speichern → ready. Fehlschläge landen in status='failed' inklusive Fehlermeldung; die Chunks bleiben für die Keyword-Suche erhalten und ein POST /api/reindex wiederholt den Versuch. Bei Serverstart werden hängengebliebene Jobs (pending/indexing) automatisch wieder eingequeued.

Entwicklung

# Backend mit eingebettetem Hinweis-Frontend (ohne Node-Build) starten
go run ./cmd/server

# Frontend-Devserver mit API-Proxy auf :8080
cd web && npm run dev

# Tests
go test ./...

Sicherheit

  • Standardmäßig lauscht Contexta auf :8080 aller Interfaces. Im Heimnetz empfohlen: CONTEXTA_ADDR=127.0.0.1:8080 oder CONTEXTA_TOKEN setzen.
  • Der serverseitige Fetch folgt jeder öffentlich erreichbaren URL – Contexta ist als persönliches Tool gedacht, kein Multi-Tenant-Betrieb.