Skip to content

feat(search): hybride Suche mit EmbeddingGemma 2 über llama-server, RRF mit Volltext, Rückfall (REQ-535) - #153

Merged
NurRobin merged 1 commit into
devfrom
feat/hybrid-search-embeddings
Oct 20, 2026
Merged

NurRobin merged 1 commit into
devfrom
feat/hybrid-search-embeddings

Conversation

@NurRobin

@NurRobin NurRobin commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Pull request

What changed?

Hybride Suche über Wissenseinträge mit EmbeddingGemma 2 (REQ-535). Mit einem Embedding-Server auf Loopback (GHOSTTREE_EMBED_URL, llama.cpp llama-server) mischt die Wissenssuche (context_search, GET /api/search, Web-Suche /ui/search und die Suche auf /ui/knowledge) die Volltext-Rangfolge und eine Vektor-Rangfolge per Reziprok-Rang-Fusion (k = 60). Ein Treffer, den nur der Vektor fand, trägt "match": "semantic" (API) bzw. match:semantic im Label des MCP-Werkzeugs. Ohne die Einstellung läuft nichts, und die Ausgabe ist die heutige, Byte für Byte (getestet gegen die Volltextsuche). Der Server bleibt cgo-frei, es kommt keine neue Abhängigkeit hinzu: Inferenz ist ein eigener Prozess, ghosttree spricht POST /v1/embeddings und GET /health per Standardbibliothek.

Aufbau

  • internal/embed (nur Standardbibliothek): Texte nach der Modellkarte (Anfrage task: search result | query: {q}, Dokument title: {Titel} | text: {Body}, ohne Titel title: none) mit fester PrefixVersion; HTTP-Client; Normieren, int8-Quantisierung (je Vektor symmetrisch, vorher L2-normiert), Kosinus, Rangfusion. embed/embedtest ist ein Fake-Server mit deterministischen Vektoren für die Tests. Dokumenttext höchstens 2000 Zeichen (ohne Tokenizer nur zählbar; auch ein Token je Zeichen bleibt unter dem Kontext von 2048 je Slot, für Prosa sind es 500 bis 700 Token).
  • Tabellen, alle neu und additiv (CREATE TABLE IF NOT EXISTS, kein upgrade-schema, Pitfall #432): knowledge_embeddings(knowledge_id PK, model, prefix_version, dim, text_hash, scale, vec BLOB, read_at, updated_at), knowledge_embedding_failures (geparkte Einträge) und knowledge_embedding_state (eine Zeile für ctx embed status). Schreiben über den begrenzten Runtime-Writer (UpsertKnowledgeEmbedding, TouchKnowledgeEmbeddings, RecordEmbeddingFailure, SetEmbedJobState mit queueWrite); alle neuen Store-Methoden stehen im Writer-Inventar.
  • internal/embedjob: Hintergrundjob im Server, startet 10 s nach dem Server in einer eigenen Goroutine. Fällig sind aktive Einträge ohne Vektor, mit anderem Modell oder Präfix oder mit geändertem text_hash (Neueste zuerst). Höchstens --embed-batch (8) Dokumente je Anfrage, --embed-pause (2 s) zwischen den Anfragen, nie mehr als ein Aufruf zugleich, bei Fehlern 5 s, 10 s, 20 s … bis --embed-max-backoff (5 min). Fortsetzbar, weil der Zustand die Tabellen sind.
  • Suche (internal/store/search_hybrid.go): die gemeinsame Stelle ist Store.searchKnowledgeWith, hinter SearchKnowledge, SearchKnowledgeForContext, SearchAllKnowledge und dem neuen SearchKnowledgeWith. Anfrage einbetten (synchron, --embed-query-timeout 1500 ms), Kosinus über die int8-Vektoren in Go (Brute Force), je Liste die ersten 100, RRF, dann limit.
  • ctx embed status --db <pfad>: aktive Einträge, eingebettet, fällig (davon geparkt), Zustand des Jobs, Probe des Servers (Health plus eine echte Einbettung mit Zeit).
  • ctx eval search --db <pfad> …: Messwerkzeug (siehe unten).
  • deploy/ghosttree-embed.service und der Abschnitt „Embeddings for hybrid search“ in deploy/README.md; CHANGELOG (Unreleased); eine Zeile in der README.

Konfiguration (Standardwerte)

Umgebung Flag Standard
GHOSTTREE_EMBED_URL --embed-url leer = aus
GHOSTTREE_EMBED_MODEL --embed-model embeddinggemma-2-q8_0
GHOSTTREE_EMBED_QUERY_TIMEOUT --embed-query-timeout 1500ms
GHOSTTREE_EMBED_BATCH --embed-batch 8
GHOSTTREE_EMBED_PAUSE --embed-pause 2s
GHOSTTREE_EMBED_IDLE --embed-idle 30s
GHOSTTREE_EMBED_MAX_BACKOFF --embed-max-backoff 5m
GHOSTTREE_EMBED_MIN_SIMILARITY --embed-min-similarity 0.55 (0 = kein Minimum)

Sichtbarkeit im Vektorteil

Der Vektorteil rechnet nur über Zeilen, die der Leser sehen darf, und dasselbe gilt für die Rangfolgen, bevor sie gemischt werden:

  1. In der SQL-Abfrage selbst: Scope (FilterWhere/UnionWhere), withGlobalScope (globale Einträge anderer Autoren fallen für erzwingende Betrachter heraus), status = 'active' und die Ausschlüsse der Sicht (Agentensicht: nicht quarantäniert; Betreibersicht: quarantänierte bleiben).
  2. Ein Vorfilter nach Projektrolle (ProjectAccess.KnowledgePrefilter): Einträge ohne Projekt, Einträge der Projekte, in denen das Konto eine Rolle hat, und Einträge unbeanspruchter Projekte (nur Autor oder Instanz-Admin). Er ist nie enger als die Zeilenregel (Test: gleiche Antwort mit und ohne Vorfilter für Gast, Mitglied, Owner, Fremden, unbeanspruchtes Projekt), aber Einträge fremder Projekte werden weder verglichen noch der Zeilenregel übergeben.
  3. Die Zeilenregel der Aufrufer (CanSeeKnowledge und CanDeliverKnowledge in /api/search; CanSeeKnowledge plus Status in der Web-Suche) wird an die Suche übergeben (KnowledgeSearch.Keep) und auf beide Listen angewandt, bevor Ränge vergeben werden. Gäste sehen so nur trusted/verified, Fremde nichts, ein anderes Projekt nichts. Eine verborgene Zeile kann weder erscheinen noch die Reihenfolge sichtbarer verändern (Pitfall #2447). Der Test baut das so, dass Fusion vor Filter eine andere Reihenfolge ergäbe (40 verborgene Zeilen vor den sichtbaren) und prüft Gleichheit mit der Welt ohne verborgene Zeilen.
  4. Die Zeilenregel läuft höchstens über die ersten 1000 Kandidaten nach Ähnlichkeit (wie beim Volltextteil), nicht mehr über den ganzen Bestand. Test: Gast mit drei sichtbaren Einträgen und 1200 Einträgen eines fremden Projekts, mit Vorfilter höchstens 3 Prüfungen, ohne höchstens 1000. Restrisiko wie bei der Volltextsuche (#2483): die Laufzeit hängt von der Zahl der Kandidaten ab, die erst die Zeilenregel verwirft.

Rückfall

Zeitgrenze überschritten, Server aus, HTTP-Fehler, kaputte Antwort oder ein Fehler im Vektorteil selbst: Ergebnis der reinen Volltextsuche, kein Fehler an den Aufrufer, ein Log embed_search_fallback je Minute (ohne Anfragetext). Eine Anfrage nur aus Füllwörtern („was ist noch zu tun“) wird nie an den Server geschickt (Pitfall #732).

Keine Auffüller, und was als genutzt zählt (Review-Befunde 1 bis 4)

  • Mindestähnlichkeit 0,55 (vorher 0) und höchstens fünf rein semantische Treffer je Suche, höchstens die Hälfte eines kurzen Ergebnisses, hinter den Wortmatches. Die Fusion füllt damit nicht mehr bis limit auf, der „cut at …“-Hinweis stimmt wieder. Der Wert 0,55 stützt sich auf die Messung: der beste Nachbar jeder der 398 Zitat-Anfragen hatte bei EmbeddingGemma 2 einen Kosinus von mindestens 0,66, der mittlere etwa 0,78; Zitate sind länger als Fragen von Menschen und erreichen höhere Werte, daher ist 0,55 ein Startwert, der auf dem Server mit ctx eval search --embed-min-similarity … nachzujustieren ist (Test mit dem Fake-Raum: Nähe etwa 0,71 drin, Ferne 0 draußen). Die reine Vektor-Rangfolge der Messung hat weder Deckel noch Auffüllung.
  • Gezählt wird, was ausgeliefert wird, einmal. GET /api/search ruft die Suche mit NoRecord und zählt danach (RecordKnowledgeSearchHits, best-effort über den Bookkeeper) nur die Treffer, die nach Zeilenregel und limit wirklich in der Antwort stehen. Das gilt auch ohne Embeddings und ist dort eine Verhaltensänderung bei den Zählern, nicht bei der Ausgabe: bisher zählte search_hits auch den Überhang (bei erzwungenem Zugriff das Vierfache von limit) und Einträge, die die Regel danach verwirft. Die Zähler ordnen den Bootstrap und speisen ctx usage; sie bewegen sich jetzt nur noch durch tatsächlich Gelieferte. Test: zwei Wortmatches und dreißig Nachbarn (7 geliefert, 7 gezählt, je einmal), und Volltext allein mit limit=1.
  • Der Nachlauf hängt nicht an einem Text. Scheitert dieselbe Batch zum zweiten Mal, gehen die Texte einzeln an den Server; die beantworteten werden gespeichert, der Verursacher (500, unbrauchbarer Vektor) wird in knowledge_embedding_failures geparkt: 5 min, dann 10, 20 … bis 6 h, mit Fehlerzahl; ein dauerhaft abgelehnter Text (400/413/422) bis zur Änderung des Eintrags, des Modells oder des Formats. Beantwortet der Server keinen einzigen Text, ist es der Server: nichts wird geparkt, es bleibt beim Rückweichen. 404/401/5xx schreiben nie ab.
  • Der Leerlauf liest nicht mehr den ganzen Bestand. Texte werden seitenweise (64) und nur für Kandidaten gelesen: Einträge ohne aktuellen Vektor, und Einträge, die zu oder nach read_at des Vektors geändert wurden (jede Textänderung läuft über updated_at, ich habe alle UPDATE knowledge geprüft). Eine Änderung ohne Textänderung (neue Vertrauensstufe) kostet keine Anfrage an den Server, nur einen Schreibzugriff. read_at ist die Zeit vor dem Lesen des Textes, damit eine Änderung während der Berechnung nicht verloren geht (Test). Ein vollständiger Blick auf alle Einträge geschieht einmal beim Start. Bekannte Grenze: ein Text, der ohne updated_at geändert wird (kein Pfad im Code tut das), fällt erst beim nächsten Start auf.
  • -np 2 im README und in der Unit ist jetzt vorsichtig formuliert („gedacht“, auf dem Server zu messen).

Requests: bewusst nicht umgesetzt

Die Auftragssuche (SearchRequests) ist keine Rangfolge, sondern ein Filter in id-Reihenfolge mit id-Cursor (ORDER BY r.id DESC, NextCursor) und eigenen Sichtbarkeitsfiltern (Restrict, Projects, Unclaimed*, HideHeld). Eine Fusion würde sie in eine Relevanzsuche mit neuem Cursor verwandeln, dazu käme request_embeddings samt Job-Zweig und Status. Das bleibt mit denselben Bausteinen nicht klein. Der Bericht misst Requests zudem nur an 51 Anfragen. Die Bausteine (internal/embed, Fusion, Job) sind wiederverwendbar; ein eigener Auftrag kann sie nutzen. Sessions und Ghost-Dateien sind ausgenommen, Hook (relevance.go, /api/context/relevant) und Gärtner sind unverändert.

Messwerkzeug ctx eval search

ctx eval search --db <pfad> [--sample 1500] [--seed 20261020] [--embed-url …] [--min-chars 30] [--embed-min-similarity 0.55]: Anfragen sind die Evidenzzitate (knowledge_evidence.quote, mindestens 30 Zeichen, nur aktive Einträge), das Ziel ist deren knowledge_id. Es ruft SearchKnowledgeWith (dieselbe Funktion wie der Server) einmal je Rangfolge: Volltext allein (= vorher), Vektor allein, hybrid (= nachher; ohne Rückfall, ein Ausfall des Servers bricht den Lauf ab), in der Betreibersicht (alle aktiven Einträge, quarantänierte eingeschlossen). Ausgabe nur Aggregate: n, R@1, R@3, R@10, MRR@10, für alle Ziele, für die nicht quarantänierten Ziele und für die Agentensicht ohne quarantänierte Einträge. Nie ein Zitat oder Text (getestet). Die Datenbank wird nur lesend geöffnet (OpenReadOnly, keine Migration, die Suche zählt keine Nutzung); gelesen werden die gespeicherten Vektoren, eingebettet werden nur die Anfragen: nacheinander, --pause (50 ms) auseinander, mit Fortschrittszeile alle 10 s auf stderr. Hinweis, wenn nicht alles eingebettet ist. Mit mehreren Werten für --embed-min-similarity lässt sich messen, was die Untergrenze kostet.

Verification

Lokal, im Worktree auf feat/hybrid-search-embeddings:

  • gofmt -l .: leer
  • go vet ./...: sauber
  • go build ./...: sauber
  • go test ./internal/store/... -timeout 30m -count=1: ok (876 s)
  • go test ./internal/web/... -timeout 55m -count=1: ok (1245 s)
  • go test ./internal/server/... ./internal/mcpserver/... ./internal/embed/... ./internal/embedjob/... ./internal/eval/... ./internal/client/... ./cmd/... -count=1: ok
  • go test -race auf den neuen Paketen (internal/embed, internal/embedjob, internal/eval/searchquality) und den neuen Tests in internal/store, internal/server, cmd/ctx: ok

Neue Tests: Fake-Embedding-Server (httptest) mit deterministischen Vektoren.

  • Hybrid-Reihenfolge ist RRF, Kennzeichnung nur bei Treffern, die der Volltext nicht fand; Limit schneidet die gemischte Liste; keine Auffüllung auf limit (Mindestähnlichkeit, Deckel).
  • Sichtbarkeit: Gast, Fremder, Mitglied, Owner, anderes Projekt, globaler Eintrag eines anderen Autors, unbeanspruchtes Projekt, jeweils im Store (mit und ohne Vorfilter, mit und ohne Zeilenregel: gleiche Antwort), über /api/search und über /ui/search; Reihenfolge mit und ohne verborgene Zeilen identisch; Gegenprobe, dass Fusion vor Filter anders ausginge; Prüfzahl der Zeilenregel bei großem fremdem Bestand.
  • Inaktive (archiviert, ersetzt, stale, deprecated) und quarantänierte Einträge liefern in der Agentensicht nie Treffer, auch wenn ihr Vektor noch liegt.
  • Rückfall bei Server aus, 500, hängendem Server (Zeitgrenze), ohne Konfiguration (Byte-Vergleich mit der Volltextsuche), kaputtem Vektorteil; ein Logeintrag für drei Rückfälle; kein Anfragetext im Log; Warnung bei Vektoren anderer Dimension.
  • Zählung: ausgelieferte Treffer einmal, mit und ohne Embeddings.
  • Job: Batchgröße und Pausen (Start 7 s, 2 s, 2 s, 2 s, Idle 30 s), nie zwei Aufrufe zugleich, Fortsetzen nach Neustart ohne doppelte Arbeit, Neueinbetten bei Textänderung, Modellwechsel und neuem Präfix, Rückweichen 5/10/20/40/80/160 s und Erholung, inaktive Einträge, Parken eines Textes mit 500 oder Nullvektor (die anderen werden gespeichert, Verdopplung, Wiederaufnahme, Überleben eines Neustarts), dauerhaft abgelehnter Text, Server ganz aus parkt nichts, falsche URL (404) wird nicht abgeschrieben, Änderung ohne Textänderung kostet keine Anfrage, stille Umschreibung nur im ersten Blick, Änderung während der Berechnung geht nicht verloren, Abbruch durch den Context.
  • ctx eval search und ctx embed status an kleinen Test-Datenbanken: Aggregate, kein Text in der Ausgabe, Strenge gegenüber dem Rückfall, Warnung bei unvollständiger Einbettung, Probe eines kaputten Servers, geparkte Einträge.
  • Writer-Inventar (TestRuntimeStoreWriterInventory) mit den neuen Methoden.

Nicht geprüft: ein Lauf gegen einen echten llama-server (der Dienst wird getrennt eingerichtet) und die Qualitätszahlen auf echten Daten (die liefert ctx eval search nach dem Nachrechnen), ebenso der endgültige Wert für --embed-min-similarity.

Release impact

  • major — incompatible after 1.0
  • minor — compatible feature, or incompatible before 1.0
  • patch — compatible fix
  • none — no release impact

Checklist

  • This pull request targets dev (or is an approved release/hotfix promotion).
  • Tests cover changed behavior.
  • I did not add credentials, private infrastructure details, or internal planning documents.
  • User-facing behavior and the changelog are updated where needed.
  • I have read and agree to the Contributor License Agreement, and I am authorized to make this contribution.

…RF mit Volltext, Rückfall (REQ-535)

Mit GHOSTTREE_EMBED_URL mischt die Wissenssuche (context_search, /api/search,
Web-Suche) die Volltext-Rangfolge und eine Vektor-Rangfolge per
Reziprok-Rang-Fusion (k = 60). Ohne die Einstellung läuft nichts und die
Ausgabe bleibt die heutige.

- internal/embed: Texte nach der Modellkarte mit Präfixversion, HTTP-Client
  für /v1/embeddings, int8-Quantisierung, Kosinus, Rangfusion.
- Tabellen knowledge_embeddings (Modell, Präfixversion, Dimension, Texthash,
  Skalierung, int8-Vektor, Lesezeit), knowledge_embedding_failures und
  knowledge_embedding_state; Schreiben über den Runtime-Writer.
- internal/embedjob: gedrosselter Hintergrundjob (Batch 8, Pause 2 s, ein
  Aufruf zugleich, exponentielles Zurückweichen, fortsetzbar). Ein Text, an dem
  der Dienst scheitert, wird einzeln ermittelt und geparkt; im Leerlauf werden
  nur seit dem Vektor geänderte Einträge gelesen.
- Sichtbarkeit: Scope und Globalregel in der Abfrage, dazu ein Vorfilter nach
  Projektrolle, die Zeilenregel der Aufrufer vor der Fusion; verborgene Zeilen
  verschieben keine sichtbaren, und die Zeilenregel läuft höchstens 1000 Mal.
- Keine Auffüller: Mindestähnlichkeit 0,55 und höchstens fünf rein semantische
  Treffer je Suche; als genutzt zählt, was die API ausliefert, einmal.
- Rückfall auf die Volltextsuche bei Zeitüberschreitung, Ausfall oder Fehler,
  ein Log je Minute.
- ctx embed status, ctx eval search, deploy/ghosttree-embed.service und der
  Abschnitt in deploy/README.md.
@NurRobin
NurRobin force-pushed the feat/hybrid-search-embeddings branch from ddef815 to 30ee53e Compare October 20, 2026 13:18
@NurRobin
NurRobin merged commit 258839a into dev Oct 20, 2026
7 checks passed
@NurRobin
NurRobin deleted the feat/hybrid-search-embeddings branch October 20, 2026 14:04
NurRobin added a commit that referenced this pull request Oct 20, 2026
…nicht hinter dem Nachrechnen wartet (REQ-535) (#154)

# Pull request

## What changed?

**Eigener Embedding-Server für Suchanfragen (REQ-535, Folgeauftrag zu
#153).** Solange der Hintergrund-Job Dokumente einbettet (Batch 8, auf
zwei Kernen etwa 5 s je Dokument), wartet die Einbettung einer
Suchanfrage im selben `llama-server` hinter dem Batch. Gemessen: jede
Suche brauchte 1,59 s, lief damit in die Zeitgrenze von 1,5 s und fiel
auf Volltext zurück. `-np 2` hilft nicht, weil beide Slots dieselben
Rechenthreads teilen; `-ub`/`-b` lassen sich nicht verkleinern, weil
eine Einbettung die ganze Sequenz in einem ubatch braucht. Beim Erstlauf
über rund 2800 Einträge dauert das Stunden, danach kurz bei jedem neuen
oder geänderten Eintrag. Anfragen und Hintergrund-Job dürfen deshalb
getrennte Server nutzen.

### Option

`GHOSTTREE_EMBED_QUERY_URL` / `--embed-query-url` (`ctx serve`, `ctx
embed status`, `ctx eval search`): Basis-URL des Servers, der
Suchanfragen einbettet.

- Leer heißt: der Server von `--embed-url`. Das ist das heutige
Verhalten, ein Client, keine Prüfung, dieselbe `embed_config`-Logzeile
(getestet). Dieselbe Adresse zweimal (auch mit Schrägstrich oder
`/v1/embeddings`) zählt als ein Server.
- Der Job nimmt immer `--embed-url`.
- Dieselben URL-Prüfungen wie bei `--embed-url` (`embed.ParseURL`: kein
Zugang, keine Query, kein Fragment; Fehlermeldung nennt
`--embed-query-url`), und `ctx serve` warnt bei einer Adresse, die nicht
Loopback ist, weil dann jede Suchanfrage die Maschine verlässt.
- Nur die Query-URL ohne `--embed-url` ist ein Konfigurationsfehler
(`--embed-query-url needs --embed-url: …`): `ctx serve` startet nicht,
`embed status` und `eval search` enden mit Exit 2. Ohne Vektoren gibt es
nichts zu durchsuchen.

### Dasselbe Modell auf beiden Servern

Ein Anfragevektor ist nur neben Vektoren desselben Modells etwas wert.
`internal/embed/modelcheck.go`: `ctx serve` schickt im Hintergrund
(blockiert nichts) einen festen kurzen Text im Anfrageformat an beide
Server und vergleicht die Vektoren. Gleiche Dimension und Kosinus
mindestens 0,999 heißt „gleich“ (dieselbe Datei auf anderen Threads oder
in anderem Build weicht nur durch Rundung ab, weit darüber; anderes
Modell oder andere Quantisierung liegt weit darunter).

- Weichen sie ab: `embed_models_differ` als **Error** mit dem, was
abweicht, und die Suche nutzt den Vektorteil nicht
(`EmbedSearchConfig.Ready`, die Suche antwortet aus Volltext wie bei
jedem anderen Ausfall, kein Fehler an den Aufrufer, die Anfrage wird
nicht mehr gesendet), bis eine Prüfung Gleichheit findet
(`embed_models_agree`). Der strenge Modus der Messung meldet es als
Fehler. Der Job läuft weiter, die gespeicherten Vektoren sind die des
Job-Servers.
- Wie oft: alle **10 Minuten**, solange sie übereinstimmen. Ein Modell
wird getauscht, indem jemand einen Server neu startet (selten); ein
Anfragevektor eines anderen Modells macht das Ranking schlechter, nicht
falsch, und eine Prüfung kostet zwei kurze Texte, einen davon im Slot
des Servers, der gerade den Job rechnet. Bei Abweichung **jede Minute**,
damit die Suche eine Prüfung nach der Korrektur zurück ist.
- Antwortet ein Server nicht (aus, lädt das Modell noch, zu
beschäftigt), wiederholt die Prüfung nach 5 s, 10 s, … bis 1 min
(`embed_models_unverified`) und ändert nichts: das letzte Urteil gilt,
und vor dem ersten läuft die Suche wie mit einem Server. Ein Job-Server,
der aus oder ausgelastet ist, schaltet den Vektorteil also nicht ab. Je
Zustand höchstens eine Logzeile alle 10 Minuten.

### Weitere Stellen

- `ctx embed status`: bei verschiedenen Servern `job server`, `query
server` und eine Zeile `same model` (ja mit Kosinus, NEIN mit
Abweichung, oder „not checked“). Ohne zweiten Server unverändert.
- `ctx eval search --embed-query-url`: die Anfragen gehen an den
Anfrage-Server; vorher eine Prüfung auf dasselbe Modell, bei Abweichung
Abbruch (die Zahlen wären wertlos), bei einem Server, der nicht
antwortet, Hinweis auf stderr und Weiterlauf.
- `deploy/ghosttree-embed-query.service` (neu, Beispiel): zweiter
`llama-server` mit derselben GGUF-Datei, Port 8476, `-t 1 -np 1 -c 1024
-ub 1024 -b 1024`, Container mit `--cpus 1 --memory 768m --memory-swap
768m`, read-only, `--cap-drop ALL`, nur auf `127.0.0.1`.
`deploy/ghosttree-embed.service` bleibt der Dienst des Hintergrund-Jobs;
sein Kommentar zu `-np 2` ist korrigiert.
- `deploy/README.md`: Abschnitt „A second server for queries“ (warum
zwei Prozesse, mit der Messung, Konfiguration, Modellprüfung und Takt,
ein Server allein funktioniert weiter, mit dem Nachteil, dass eine Suche
während des Jobs aus Volltext antwortet). Die frühere Aussage, zwei
Slots ließen eine Suche ihre Anfrage einbetten, während ein Batch läuft,
ist durch die Messung ersetzt.
- CHANGELOG (Unreleased).

## Verification

- `gofmt -l .` leer, `go vet ./cmd/ctx ./internal/embed
./internal/embedjob ./internal/store ./internal/eval/...`, `go build
./...`: sauber.
- `go test ./cmd/ctx ./internal/embed/... ./internal/embedjob
./internal/eval/...`: ok. `internal/embed` und die neuen Tests in
`cmd/ctx` zusätzlich mit `-race` und mehrfach (`-count=8`/`-count=15`).
- `go test ./internal/store -run 'Embed|Hybrid|Search'`: ok.
- `go test ./internal/store` (ganze Suite): ok (522 s).
- Neue Tests: zwei Fake-Server mit Zähler je Server (Anfrage-Text nur am
Anfrage-Server, Dokumente nur am Job-Server, Probe an beiden); leere
Query-URL = ein Server ohne Probe und ohne neue Logzeile; Abweichung der
Modelle (andere Dimension, anderer Vektor) = Volltext-Antwort, kein
Fehler, keine weitere Anfrage an den Anfrage-Server, der Job läuft
weiter; Modellprüfung mit Wechsel auf abweichend und zurück, nicht
antwortender Server hebt das Urteil nicht auf, Ende mit dem Kontext;
`Ready` im Store (Fallback, strenger Modus, Rückkehr);
Konfigurationsfehler und URL-Prüfungen für `serve`, `embed status`,
`eval search`; Warnung bei Nicht-Loopback; `embed status` und `eval
search` mit zwei Servern.
- Nicht geprüft: der Betrieb mit echten `llama-server`-Prozessen (die
neue Unit ist ein Beispiel und nicht gestartet worden).

## Release impact

- [ ] `major` — incompatible after 1.0
- [x] `minor` — compatible feature, or incompatible before 1.0
- [ ] `patch` — compatible fix
- [ ] `none` — no release impact

## Checklist

- [x] This pull request targets `dev` (or is an approved release/hotfix
promotion).
- [x] Tests cover changed behavior.
- [x] I did not add credentials, private infrastructure details, or
internal planning documents.
- [x] User-facing behavior and the changelog are updated where needed.
- [x] I have read and agree to the [Contributor License
Agreement](../CLA.md), and I am authorized to make this contribution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant