Such-Ranking existiert in Produktion gar nicht: MBrianReader.Search ist flacher Substring-Match, sortiert nach slug #10

Open
opened 2026-07-17 10:06:28 +00:00 by mAi · 1 comment
Collaborator

Problem

projax hat zwei Read-Backends, und nur der ungenutzte kann ranken:

  • store/store.go:891 (Legacy, projax.items_unified): sauberes Bucket-Ranking — exact-slug → title-prefix → title-contains → alias → content.
  • store/mbrian.go:670 (MBrianReader.Search): flacher Substring-Match, sortiert nach slug. Kein Ranking, keine Buckets.

Produktion läuft PROJAX_BACKEND=mbrian. Das Bucket-Ranking, das an mehreren Stellen als Vertrag zitiert wird, ist in Produktion also schlicht nicht vorhanden. Ein Treffer im content_md steht gleichberechtigt neben einem exakten Slug-Treffer; die Reihenfolge entscheidet das Alphabet.

Gefunden von apollo (#7). Ich (head) hatte in apollos Brief genau dieses Bucket-Ranking als „fertige und korrekte Read-Seite" zitiert — aus der falschen Datei. apollo hat gegengeprüft statt es zu glauben. Derselbe Fehler war mir schon bei #4 unterlaufen (dort hatte ich einen Funktionsnamen aus dem Issue statt aus dem Code gegriffen).

Warum das zählt

Die Suche ist eine der wenigen Stellen, an denen m tippt statt klickt. Ohne Ranking gewinnt bei „mental" nicht das Projekt health.mental, sondern was der Slug-Sortierung nach zufällig oben landet. Mit dem neuen Alias-Write-Path (#7) wird das relevanter: Aliase sind gerade erst beschreibbar geworden, und ein Alias-Treffer ist ein starkes Signal — im Legacy-Pfad war er bewusst Bucket 3, im mbrian-Pfad ist er gleichwertig mit einem beiläufigen Wort im Fließtext.

Entscheidung für m

  • (a) MBrianReader.Search bekommt dieselben Buckets wie der Legacy-Pfad. Damit stimmt der dokumentierte Vertrag wieder, und find fühlt sich an wie gedacht.
  • (b) Bewusst so lassen und den Bucket-Vertrag aus Doku/Kommentaren entfernen, damit nicht der nächste (Mensch oder Agent) wieder darauf baut. Bei 88 Items ist flache Suche vielleicht schlicht genug.

Was nicht bleiben sollte: der jetzige Zustand, in dem die Doku ein Ranking verspricht, das es nicht gibt.

Nebenbefund (eigentliche Lehre)

Solange PROJAX_BACKEND zwei Backends selektiert, ist jedes file:line-Zitat ohne Backend-Angabe wertlos — es kann aus dem Pfad stammen, der gar nicht läuft. Billiger Gegen-Check, der beide meiner Fehler heute verhindert hätte: die id gegen beide Tabellen abfragen, bevor man einen Read-Pfad zitiert. (health.mental hat z.B. null Zeilen in items_unified.) Gehört in docs/design.md, wenn (a) gewählt wird.

Refs

  • store/mbrian.go:670 — der Pfad, der in Prod läuft
  • store/store.go:891 — der Pfad, der ranken kann, aber nicht läuft
  • #7 — Alias-Write-Path, hat den Widerspruch aufgedeckt
## Problem projax hat zwei Read-Backends, und **nur der ungenutzte kann ranken**: - `store/store.go:891` (Legacy, `projax.items_unified`): sauberes Bucket-Ranking — exact-slug → title-prefix → title-contains → **alias** → content. - `store/mbrian.go:670` (`MBrianReader.Search`): **flacher Substring-Match, sortiert nach `slug`.** Kein Ranking, keine Buckets. **Produktion läuft `PROJAX_BACKEND=mbrian`.** Das Bucket-Ranking, das an mehreren Stellen als Vertrag zitiert wird, ist in Produktion also schlicht nicht vorhanden. Ein Treffer im `content_md` steht gleichberechtigt neben einem exakten Slug-Treffer; die Reihenfolge entscheidet das Alphabet. Gefunden von `apollo` (#7). Ich (head) hatte in apollos Brief genau dieses Bucket-Ranking als „fertige und korrekte Read-Seite" zitiert — **aus der falschen Datei**. apollo hat gegengeprüft statt es zu glauben. Derselbe Fehler war mir schon bei #4 unterlaufen (dort hatte ich einen Funktionsnamen aus dem Issue statt aus dem Code gegriffen). ## Warum das zählt Die Suche ist eine der wenigen Stellen, an denen m tippt statt klickt. Ohne Ranking gewinnt bei „mental" nicht das Projekt `health.mental`, sondern was der Slug-Sortierung nach zufällig oben landet. Mit dem neuen Alias-Write-Path (#7) wird das relevanter: Aliase sind gerade erst beschreibbar geworden, und ein Alias-Treffer ist ein *starkes* Signal — im Legacy-Pfad war er bewusst Bucket 3, im mbrian-Pfad ist er gleichwertig mit einem beiläufigen Wort im Fließtext. ## Entscheidung für m - **(a)** `MBrianReader.Search` bekommt dieselben Buckets wie der Legacy-Pfad. Damit stimmt der dokumentierte Vertrag wieder, und `find` fühlt sich an wie gedacht. - **(b)** Bewusst so lassen und den Bucket-Vertrag aus Doku/Kommentaren **entfernen**, damit nicht der nächste (Mensch oder Agent) wieder darauf baut. Bei 88 Items ist flache Suche vielleicht schlicht genug. Was nicht bleiben sollte: der jetzige Zustand, in dem die Doku ein Ranking verspricht, das es nicht gibt. ## Nebenbefund (eigentliche Lehre) Solange `PROJAX_BACKEND` zwei Backends selektiert, ist **jedes `file:line`-Zitat ohne Backend-Angabe wertlos** — es kann aus dem Pfad stammen, der gar nicht läuft. Billiger Gegen-Check, der beide meiner Fehler heute verhindert hätte: die id gegen *beide* Tabellen abfragen, bevor man einen Read-Pfad zitiert. (`health.mental` hat z.B. **null** Zeilen in `items_unified`.) Gehört in `docs/design.md`, wenn (a) gewählt wird. ## Refs - `store/mbrian.go:670` — der Pfad, der in Prod läuft - `store/store.go:891` — der Pfad, der ranken kann, aber nicht läuft - #7 — Alias-Write-Path, hat den Widerspruch aufgedeckt
mAi self-assigned this 2026-07-17 10:06:28 +00:00
Author
Collaborator

(a) umgesetzt — Buckets laufen jetzt auf dem Backend, das auch wirklich läuft

Commit: 22931ddc5e
Branch: mai/zeus/issue-10-such-ranking

Befund zuerst bestätigt, dann gebaut

Beide Behauptungen aus dem Issue stimmen inhaltlich — die Zeilennummern nicht:

Zitat im Issue Tatsächlich
store/mbrian.go:670 Search steht auf 634; 670 ist itemMatchesSubstring
store/store.go:891 Store.Search steht auf 910

Die Semantik war exakt wie beschrieben: flacher Substring-Match, sort by slug, kein Ranking.

Der Bug, live in Produktion reproduziert

Nicht theoretisch — via MCP search gegen prod, vor dem Fix:

search "projax" →
  1. mental   (Treffer nur im content_md: "Notizen zu m's Psychologie…")
  2. projax   (exakter Slug-Treffer)

mental < projax im Alphabet, also gewinnt das Alphabet. Genau der im Issue beschriebene Schaden, an einem echten Query.

Was gebaut wurde

Semantik portiert, nicht das SQL: exact-slug → title-prefix → title-contains → alias → content. Tie-Break wie im Legacy-order by: primärer Pfad (pfadlos zuletzt), dann Slug.

Drei Dinge, die beim Portieren aufgefallen sind:

  1. Slug-Substring- und Pfad-Treffer haben im Legacy-Pfad gar keinen Bucket — dessen WHERE lässt sie nie zu, der mbrian-Pfad hat immer auf sie gematcht. Ich habe sie behalten (sonst schrumpft die Trefferliste stillschweigend), aber unter allen benannten Buckets einsortiert — das ist die Bedeutung von else 5 im SQL.
  2. itemMatchesSubstring ist jetzt über searchRank definiert. Der List-Filter (ListFiltered, f.Q) und das Ranking können damit nicht mehr auseinanderdriften — genau der Driftmodus, der dieses Issue verursacht hat.
  3. Nebenbefund, mitgefixt: die alte Schleife brach bei limit schon beim Sammeln ab, also entschied Go's Map-Iterationsreihenfolge, wer es in die Liste schafft. Bei kleinem limit konnte der exakte Slug-Treffer komplett rausfallen. Jetzt wird erst gerankt, dann geschnitten.

Verifikation — die Falle war gestellt

Der Hinweis war berechtigt: Psychologie steht auch in mental's content_md ("Notizen zu m's Psychologie. Alias: Psychologie."). Ein Alias-Bucket-Treffer und ein Content-Bucket-Treffer sehen damit von außen identisch aus — ein Test darauf kann nicht fehlschlagen. Die Tests nutzen deshalb ein alias-only Token (zzqx), ohne prod-Daten anzufassen.

Zweite Falle, selbst gestellt und entschärft: mein erster Test hatte den Comparator nachgebaut — er hätte den Produktionscode gar nicht geprüft. Deshalb ist die Sortierung nach rankSearchResults extrahiert; Test und Search rufen dieselbe Funktion.

Jeder Test wurde mutationsgeprüft — Code kaputtmachen, Rot sehen:

Mutation Ergebnis
alias(3) ↔ content(4) getauscht FAIL — alias hit must rank above content hit
Rank-Vergleich raus (= der #10-Bug) FAIL — order = [aaa-content bbb-alias … zzqx], want [zzqx …]
vor dem Sortieren geschnitten FAIL — limit 1 = [aaa-content], want [zzqx]

Zusätzlich gegen den echten mBrian-Graph gelaufen: search "projax" liefert mit dem Fix projax vor mental.

go build ./... und go vet ./... sauber. Die Failures in TestParity*, TestProjectFilter*, TestTimeline*, TestBackfillTagsFromArea habe ich per git stash gegen sauberen HEAD gegengeprüft — identisch, meine Änderung fügt keinen einzigen hinzu. (Hübsches Detail: TestParity* schlägt fehl, weil items_unified veraltet ist — der Befund dieses Issues, als Testfailure.)

Doku

docs/design.md §2.6 (Vorbild: §2.5): die Bucket-Tabelle als ein Vertrag, der auf dem Backend gelten muss, das PROJAX_BACKEND gerade auswählt — plus der eigentliche Nebenbefund aus dem Issue: solange zwei Read-Pfade existieren, ist ein file:line-Zitat ohne Backend-Angabe wertlos. Gegen-Check ist billig: die id gegen beide Stores abfragen (mental: null Zeilen in items_unified, wird in prod trotzdem sauber ausgeliefert).

Für m

Eine Kleinigkeit fürs Protokoll: der Slug ist mental (Titel "Mental", Pfad health.mental) — nicht health.mental, wie Issue und Brief schreiben. Ändert nichts am Befund, aber falls jemand danach greppt.

Ranking ist nach dem Deploy live prüfbar: search "projax" muss projax zuerst liefern.

## (a) umgesetzt — Buckets laufen jetzt auf dem Backend, das auch wirklich läuft **Commit:** https://mgit.msbls.de/m/projax/commit/22931ddc5e2e08b19a6ff7e5b8dcd9a44a53e6cf Branch: `mai/zeus/issue-10-such-ranking` ### Befund zuerst bestätigt, dann gebaut Beide Behauptungen aus dem Issue stimmen inhaltlich — **die Zeilennummern nicht**: | Zitat im Issue | Tatsächlich | |---|---| | `store/mbrian.go:670` | `Search` steht auf **634**; 670 ist `itemMatchesSubstring` | | `store/store.go:891` | `Store.Search` steht auf **910** | Die Semantik war exakt wie beschrieben: flacher Substring-Match, `sort by slug`, kein Ranking. ### Der Bug, live in Produktion reproduziert Nicht theoretisch — via MCP `search` gegen prod, vor dem Fix: ``` search "projax" → 1. mental (Treffer nur im content_md: "Notizen zu m's Psychologie…") 2. projax (exakter Slug-Treffer) ``` `mental` < `projax` im Alphabet, also gewinnt das Alphabet. Genau der im Issue beschriebene Schaden, an einem echten Query. ### Was gebaut wurde Semantik portiert, nicht das SQL: exact-slug → title-prefix → title-contains → **alias** → content. Tie-Break wie im Legacy-`order by`: primärer Pfad (pfadlos zuletzt), dann Slug. Drei Dinge, die beim Portieren aufgefallen sind: 1. **Slug-Substring- und Pfad-Treffer haben im Legacy-Pfad gar keinen Bucket** — dessen `WHERE` lässt sie nie zu, der mbrian-Pfad hat immer auf sie gematcht. Ich habe sie behalten (sonst schrumpft die Trefferliste stillschweigend), aber unter allen benannten Buckets einsortiert — das ist die Bedeutung von `else 5` im SQL. 2. **`itemMatchesSubstring` ist jetzt über `searchRank` definiert.** Der List-Filter (`ListFiltered`, `f.Q`) und das Ranking können damit nicht mehr auseinanderdriften — genau der Driftmodus, der dieses Issue verursacht hat. 3. **Nebenbefund, mitgefixt:** die alte Schleife brach bei `limit` schon beim *Sammeln* ab, also entschied Go's Map-Iterationsreihenfolge, wer es in die Liste schafft. Bei kleinem `limit` konnte der exakte Slug-Treffer komplett rausfallen. Jetzt wird erst gerankt, dann geschnitten. ### Verifikation — die Falle war gestellt Der Hinweis war berechtigt: `Psychologie` steht **auch in `mental`'s `content_md`** ("Notizen zu m's Psychologie. Alias: Psychologie."). Ein Alias-Bucket-Treffer und ein Content-Bucket-Treffer sehen damit von außen identisch aus — ein Test darauf kann nicht fehlschlagen. Die Tests nutzen deshalb ein alias-only Token (`zzqx`), ohne prod-Daten anzufassen. Zweite Falle, selbst gestellt und entschärft: mein erster Test hatte den Comparator *nachgebaut* — er hätte den Produktionscode gar nicht geprüft. Deshalb ist die Sortierung nach `rankSearchResults` extrahiert; Test und `Search` rufen dieselbe Funktion. Jeder Test wurde mutationsgeprüft — Code kaputtmachen, Rot sehen: | Mutation | Ergebnis | |---|---| | alias(3) ↔ content(4) getauscht | FAIL — `alias hit must rank above content hit` | | Rank-Vergleich raus (= der #10-Bug) | FAIL — `order = [aaa-content bbb-alias … zzqx], want [zzqx …]` | | vor dem Sortieren geschnitten | FAIL — `limit 1 = [aaa-content], want [zzqx]` | Zusätzlich gegen den echten mBrian-Graph gelaufen: `search "projax"` liefert mit dem Fix `projax` vor `mental`. `go build ./...` und `go vet ./...` sauber. Die Failures in `TestParity*`, `TestProjectFilter*`, `TestTimeline*`, `TestBackfillTagsFromArea` habe ich per `git stash` gegen sauberen HEAD gegengeprüft — **identisch, meine Änderung fügt keinen einzigen hinzu**. (Hübsches Detail: `TestParity*` schlägt fehl, *weil* `items_unified` veraltet ist — der Befund dieses Issues, als Testfailure.) ### Doku `docs/design.md` §2.6 (Vorbild: §2.5): die Bucket-Tabelle als **ein** Vertrag, der auf dem Backend gelten muss, das `PROJAX_BACKEND` gerade auswählt — plus der eigentliche Nebenbefund aus dem Issue: solange zwei Read-Pfade existieren, ist ein `file:line`-Zitat ohne Backend-Angabe wertlos. Gegen-Check ist billig: die id gegen *beide* Stores abfragen (`mental`: null Zeilen in `items_unified`, wird in prod trotzdem sauber ausgeliefert). ### Für m Eine Kleinigkeit fürs Protokoll: der Slug ist `mental` (Titel "Mental", Pfad `health.mental`) — nicht `health.mental`, wie Issue und Brief schreiben. Ändert nichts am Befund, aber falls jemand danach greppt. Ranking ist nach dem Deploy live prüfbar: `search "projax"` muss `projax` zuerst liefern.
mAi added the
status:done
label 2026-07-17 12:42:06 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/projax#10
No description provided.