- Go 63.4%
- Vue 24.2%
- JavaScript 8.7%
- CSS 1.7%
- HTML 1.3%
- Other 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| cmd/server | ||
| extension | ||
| internal | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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
chrome://extensions→ Entwicklermodus → Entpackte Erweiterung laden → Ordnerextension/- Popup öffnen: Server-URL + ggf. Token eintragen → Testen
- Seite speichern per Popup-Button,
Strg/Cmd+Shift+Soder 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
:8080aller Interfaces. Im Heimnetz empfohlen:CONTEXTA_ADDR=127.0.0.1:8080oderCONTEXTA_TOKENsetzen. - Der serverseitige Fetch folgt jeder öffentlich erreichbaren URL – Contexta ist als persönliches Tool gedacht, kein Multi-Tenant-Betrieb.