feat(brain): Volltext-Suche zusaetzlich zu Semantic — Default ist jetzt Wortlich

Stefan wollte ne richtige Suche statt nur "klingt aehnlich". Beide
Modi sind jetzt verfuegbar, Default ist Volltext:

- 📝 Wortlich (Substring, case-insensitive ueber Title + Content +
  Category + Tags) — neuer Endpoint /memory/search-text. Full-Scan
  via Qdrant scroll, k=50. Findet "cessna" exakt im Content. Bei
  kleiner DB (<1000 Eintraege) unkritisch performant.

- 🧠 Semantisch (Embedder + score_threshold 0.30) — bestehender
  /memory/search Endpoint. Findet konzeptuell verwandte Eintraege.

Diagnostic UI: Dropdown neben dem Suchfeld zum Modus-Wechsel.
Info-Banner zeigt klar welcher Modus aktiv ist.

Warum Wortlich Default: bei kleiner DB liefert Semantic gern False
Positives mit Score 0.30-0.45 fuer komplett unverwandte Begriffe
(z.B. "cessna" matched "Tageslog fuehren" mit 0.43). Wortlich ist
deterministisch und vermeidet das Rauschen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-12 16:59:31 +02:00
parent 3c41f11997
commit 6549fcbce8
3 changed files with 96 additions and 10 deletions
+26 -10
View File
@@ -824,9 +824,15 @@
</div>
<div class="card" style="margin-bottom:8px;">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<input type="text" id="brain-search" placeholder="Semantische Suche (z.B. 'Stefan Persönlichkeit')..."
<input type="text" id="brain-search" placeholder="Suche (z.B. 'cessna' oder 'Stefan Persönlichkeit')..."
style="flex:1;min-width:200px;background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"
onkeydown="if(event.key==='Enter') runBrainSearch()">
<select id="brain-search-mode" onchange="if(document.getElementById('brain-search').value.trim()) runBrainSearch()"
title="Wortlich = exakter Substring-Match. Semantisch = 'klingt aehnlich' via Embeddings."
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
<option value="text" selected>📝 Wortlich</option>
<option value="semantic">🧠 Semantisch</option>
</select>
<button class="btn secondary" onclick="runBrainSearch()" style="padding:4px 12px;font-size:11px;">Suchen</button>
<select id="brain-filter-type" onchange="loadBrainMemoryList()"
style="background:#080810;color:#E0E0F0;border:1px solid #1E1E2E;padding:6px;border-radius:4px;font-family:inherit;font-size:11px;">
@@ -3457,13 +3463,23 @@
return;
}
const typeFilter = document.getElementById('brain-filter-type').value;
// k=10 + Score-Threshold im Backend (0.30) → nur relevante Treffer.
// Frueher k=20 ohne Threshold: bei kleiner DB landete fast alles
// als "Treffer", egal wie unaehnlich.
const params = new URLSearchParams({ q, k: '10', include_pinned: 'true', score_threshold: '0.30' });
if (typeFilter) params.set('type', typeFilter);
const mode = (document.getElementById('brain-search-mode')?.value) || 'text';
let url, modeLabel;
if (mode === 'semantic') {
// Embedder-basiert, mit Score-Threshold gegen Rauschen
const params = new URLSearchParams({ q, k: '10', include_pinned: 'true', score_threshold: '0.30' });
if (typeFilter) params.set('type', typeFilter);
url = '/api/brain/memory/search?' + params.toString();
modeLabel = '🧠 semantisch (Score ≥ 0.30)';
} else {
// Volltext-Substring (case-insensitive) — findet exakte Begriffe
const params = new URLSearchParams({ q, k: '50', include_pinned: 'true' });
if (typeFilter) params.set('type', typeFilter);
url = '/api/brain/memory/search-text?' + params.toString();
modeLabel = '📝 wortlich (Substring)';
}
try {
const r = await fetch('/api/brain/memory/search?' + params.toString());
const r = await fetch(url);
if (!r.ok) throw new Error('HTTP ' + r.status);
const hits = await r.json();
hits.forEach(m => { brainMemoryCache[m.id] = m; });
@@ -3471,13 +3487,13 @@
if (info) {
info.style.display = 'block';
if (hits.length === 0) {
info.innerHTML = `🔍 Keine relevanten Treffer für "${escapeHtml(q)}"` +
info.innerHTML = `🔍 Keine Treffer für "${escapeHtml(q)}"` +
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
` (Score < 0.30). Versuche andere Begriffe oder klicke das ✕ rechts um die Suche zu schliessen.`;
` · ${modeLabel}. Anderen Begriff probieren oder ✕ rechts um Suche zu schliessen.`;
} else {
info.innerHTML = `🔍 ${hits.length} Treffer für "${escapeHtml(q)}"` +
(typeFilter ? ` · Typ=${escapeHtml(typeFilter)}` : '') +
` · sortiert nach Aehnlichkeit (Score &ge; 0.30)`;
` · ${modeLabel}`;
}
}
renderBrainList(hits, true);