From 33a0274d5dbdce19da4739c726026663cc8da830 Mon Sep 17 00:00:00 2001 From: NITISH-R-G <225521762+NITISH-R-G@users.noreply.github.com> Date: Thu, 14 May 2026 22:54:24 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20FirstAidReposito?= =?UTF-8?q?ry=20search=20and=20database=20seeding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `_FirstAidEntry` to strongly type the corpus map. - Pre-compute `titleLower`, `tagsLower`, and `searchHaystackLower` strings when parsing the initial corpus JSON to avoid calling `.toLowerCase()` redundantly in search loops. - Use `appDb.executeBatch` instead of sequential `appDb.execute` calls inside a loop to insert corpus into SQLite, solving the N+1 query problem during database initialization. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- lib/services/first_aid_repository.dart | 95 ++++++++++++++++---------- 1 file changed, 59 insertions(+), 36 deletions(-) diff --git a/lib/services/first_aid_repository.dart b/lib/services/first_aid_repository.dart index 7d93080..99e7bc3 100644 --- a/lib/services/first_aid_repository.dart +++ b/lib/services/first_aid_repository.dart @@ -5,6 +5,31 @@ import 'package:flutter/services.dart' show rootBundle; import '../database/app_database.dart'; +class _FirstAidEntry { + final String id; + final String title; + final String body; + final String tags; + final String source; + + // ⚡ Bolt: Cache lowercased strings at parse time to prevent redundant + // string allocations and `.toLowerCase()` calls during high-frequency + // search/autocomplete loops. + final String titleLower; + final String tagsLower; + final String searchHaystackLower; + + _FirstAidEntry({ + required this.id, + required this.title, + required this.body, + required this.tags, + required this.source, + }) : titleLower = title.toLowerCase(), + tagsLower = tags.toLowerCase(), + searchHaystackLower = '$title $body $tags'.toLowerCase(); +} + /// Evidence-based first-aid text bundled in assets and indexed with SQLite FTS5 /// on the **same** PowerSync database as the rest of the app (no separate sqflite DB). /// @@ -16,7 +41,7 @@ class FirstAidRepository { static const String assetPath = 'assets/first_aid/corpus.json'; - List>? _corpusRows; + List<_FirstAidEntry>? _corpusRows; bool _ftsReady = false; static const String medicalDisclaimer = @@ -29,7 +54,16 @@ class FirstAidRepository { if (_corpusRows == null) { final raw = await rootBundle.loadString(assetPath); final decoded = jsonDecode(raw) as List; - _corpusRows = decoded.cast>(); + _corpusRows = decoded.map((e) { + final map = e as Map; + return _FirstAidEntry( + id: map['id'] as String? ?? '', + title: map['title'] as String? ?? '', + body: map['body'] as String? ?? '', + tags: map['tags'] as String? ?? '', + source: map['source'] as String? ?? '', + ); + }).toList(); } if (kIsWeb || !isDatabaseInitialized || _ftsReady) { return; @@ -43,25 +77,22 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( ); '''); - final countRows = - await appDb.getAll('SELECT COUNT(*) AS c FROM first_aid_fts'); + final countRows = await appDb.getAll( + 'SELECT COUNT(*) AS c FROM first_aid_fts', + ); final count = (countRows.first['c'] as num?)?.toInt() ?? 0; if (count == 0 && _corpusRows!.isNotEmpty) { - for (final row in _corpusRows!) { - await appDb.execute( - ''' - INSERT INTO first_aid_fts (title, body, tags, source) - VALUES (?, ?, ?, ?) - ''', - [ - row['title'] as String? ?? '', - row['body'] as String? ?? '', - row['tags'] as String? ?? '', - row['source'] as String? ?? '', - ], - ); - } + // ⚡ Bolt: Use executeBatch instead of a loop of execute() calls + // to eliminate the N+1 query problem during DB initialization. + final parameterSets = _corpusRows!.map((row) { + return [row.title, row.body, row.tags, row.source]; + }).toList(); + + await appDb.executeBatch(''' + INSERT INTO first_aid_fts (title, body, tags, source) + VALUES (?, ?, ?, ?) + ''', parameterSets); } _ftsReady = true; } @@ -88,10 +119,8 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( final suggestions = []; for (final row in _corpusRows!) { - final title = row['title'] as String? ?? ''; - final tags = row['tags'] as String? ?? ''; - if (title.toLowerCase().contains(q) || tags.toLowerCase().contains(q)) { - suggestions.add(title); + if (row.titleLower.contains(q) || row.tagsLower.contains(q)) { + suggestions.add(row.title); } if (suggestions.length >= 6) break; } @@ -105,13 +134,11 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( } var bestScore = -1; - Map? best; + _FirstAidEntry? best; for (final row in _corpusRows!) { - final haystack = - '${row['title']} ${row['body']} ${row['tags']}'.toLowerCase(); var score = 0; for (final t in tokens) { - if (haystack.contains(t)) score += 3; + if (row.searchHaystackLower.contains(t)) score += 3; } if (score > bestScore) { bestScore = score; @@ -124,9 +151,9 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( } return _formatResult( - title: best['title'] as String? ?? 'Topic', - body: best['body'] as String? ?? '', - source: best['source'] as String? ?? '', + title: best.title, + body: best.body, + source: best.source, ); } @@ -186,19 +213,15 @@ CREATE VIRTUAL TABLE IF NOT EXISTS first_aid_fts USING fts5( } String _pickGeneralOrFirst() { - Map? row; + _FirstAidEntry? row; for (final r in _corpusRows!) { - if ((r['id'] as String?) == 'general-road-emergency-india') { + if (r.id == 'general-road-emergency-india') { row = r; break; } } row ??= _corpusRows!.first; - return _formatResult( - title: row['title'] as String? ?? '', - body: row['body'] as String? ?? '', - source: row['source'] as String? ?? '', - ); + return _formatResult(title: row.title, body: row.body, source: row.source); } List _tokenize(String q) {