refactor: lokale Nachrichtensuche als eigenes Modul (lib/features/search) + 5 Vertragstests

Verhaltensgleich aus search_modal.dart herausgeloest (gleiche Deckel 1500/60/500,
gleiche Schleife; Medien-Filter jetzt Parameter). Tests laufen ueber die echte
SDK-DB-Schicht mit synthetischen Daten (test/support/synthetic_matrix_data.dart).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Bernd Steckmeister
2026-10-07 11:55:28 +02:00
co-authored by Claude Opus 5.5
parent b2413ef627
commit d5ff7982a0
4 changed files with 377 additions and 40 deletions
@@ -0,0 +1,62 @@
import 'package:matrix/matrix.dart';
/// Lokale Nachrichtensuche über die entschlüsselten Events in der Client-DB.
///
/// Die server-seitige `/search`-API sieht bei E2EE nur Chiffretext und liefert
/// für verschlüsselte Räume nichts – deshalb wird (wie bei Element) die lokale
/// Datenbank durchsucht. Aus `search_modal.dart` herausgelöst, damit Tests und
/// `benchmark/` genau diesen Codepfad prüfen/messen können.
///
/// Bei [mediaOnly] werden nur Medien-Nachrichten geliefert, deren
/// Dateiname/Beschriftung passt. Ergebnis: neueste zuerst, höchstens
/// [searchTotalCap] Treffer; pro Raum werden höchstens
/// [searchPerRoomScanCap] Events gescannt.
Future<List<Event>> localMessageSearch(
Client client,
String query, {
bool mediaOnly = false,
}) async {
final db = client.database;
final lower = query.toLowerCase();
const mediaTypes = {'m.image', 'm.video', 'm.file', 'm.audio'};
final results = <Event>[];
for (final room in client.rooms.where((r) => !r.isSpace)) {
var start = 0;
var scanned = 0;
try {
while (scanned < searchPerRoomScanCap) {
final events = await db.getEventList(
room,
start: start,
limit: searchScanChunk,
);
if (events.isEmpty) break;
for (final e in events) {
if (e.type != EventTypes.Message || e.redacted) continue;
final msgtype = e.content.tryGet<String>('msgtype') ?? '';
if (mediaOnly && !mediaTypes.contains(msgtype)) continue;
if (!e.body.toLowerCase().contains(lower)) continue;
results.add(e);
}
start += searchScanChunk;
scanned += events.length;
final roomExhausted = events.length < searchScanChunk;
if (roomExhausted || results.length >= searchTotalCap) break;
}
} catch (_) {}
if (results.length >= searchTotalCap) break;
}
results.sort((a, b) => b.originServerTs.compareTo(a.originServerTs));
return results.take(searchTotalCap).toList();
}
/// Höchstens so viele Events werden pro Raum gescannt.
const searchPerRoomScanCap = 1500;
/// Höchstens so viele Treffer liefert die Suche insgesamt.
const searchTotalCap = 60;
/// So viele Events werden pro DB-Abfrage geladen.
const searchScanChunk = 500;