From 002d3830b60d9f504023e37c0a206568f7f9f4d8 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 06:33:35 +0100 Subject: [PATCH 1/9] feat(database): add text index for countries collection - Implement text index on countries collection for case-insensitive name searching - Enhance search functionality for better performance and user experience --- lib/src/services/database_seeding_service.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 9f6f416..20f5cc4 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -118,6 +118,11 @@ class DatabaseSeedingService { .collection('sources') .createIndex(keys: {'name': 'text'}, name: 'sources_text_index'); + // Text index for searching countries by name (case-insensitive) + await _db + .collection('countries') + .createIndex(keys: {'name': 'text'}, name: 'countries_text_index'); + // Indexes for country aggregation queries await _db .collection('headlines') From b2cc14761543cb9da59e903f84623d0074f6398a Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 06:35:05 +0100 Subject: [PATCH 2/9] feat(country): add name filter to distinct countries aggregation - Add optional nameFilter parameter to _getDistinctCountriesFromAggregation method - Implement regex filter for country name in aggregation pipeline - Update logging to include nameFilter information - Adjust error messages to reflect new parameter --- lib/src/services/country_service.dart | 28 ++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index ababdbc..1cd53c5 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -202,21 +202,38 @@ class CountryService { /// - [repository]: The [DataRepository] to perform the aggregation on. /// - [fieldName]: The name of the field within the documents that contains /// the country object (e.g., 'eventCountry', 'headquarters'). + /// - [nameFilter]: An optional map containing a regex filter for the country name. /// /// Throws [OperationFailedException] for internal errors during data fetch. Future> _getDistinctCountriesFromAggregation({ required DataRepository repository, required String fieldName, + Map? nameFilter, }) async { - _log.finer('Fetching distinct countries for field "$fieldName" via aggregation.'); + _log.finer( + 'Fetching distinct countries for field "$fieldName" via aggregation ' + 'with nameFilter: $nameFilter.', + ); try { - final pipeline = [ + final pipeline = >[ { r'$match': { 'status': ContentStatus.active.name, '$fieldName.id': {r'$exists': true}, }, }, + ]; + + // Add name filter if provided + if (nameFilter != null && nameFilter.isNotEmpty) { + pipeline.add({ + r'$match': { + '$fieldName.name': nameFilter, + }, + }); + } + + pipeline.addAll([ { r'$group': { '_id': '\$$fieldName.id', @@ -226,7 +243,7 @@ class CountryService { { r'$replaceRoot': {'newRoot': r'$country'}, }, - ]; + ]); final distinctCountriesJson = await repository.aggregate( pipeline: pipeline, @@ -238,12 +255,13 @@ class CountryService { _log.info( 'Successfully fetched ${distinctCountries.length} distinct countries ' - 'for field "$fieldName".', + 'for field "$fieldName" with nameFilter: $nameFilter.', ); return distinctCountries; } catch (e, s) { _log.severe( - 'Failed to fetch distinct countries for field "$fieldName".', + 'Failed to fetch distinct countries for field "$fieldName" ' + 'with nameFilter: $nameFilter.', e, s, ); From 3555cb41ee39191f892f0c35977f08e9bd7325b2 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 06:42:59 +0100 Subject: [PATCH 3/9] feat(country): enhance country fetching with name filtering and caching improvements - Add support for filtering countries by name (full or partial match) - Implement case-insensitive regex filtering for country names - Introduce caching based on usage type and name filter - Refactor country fetching methods to support name filtering - Update in-memory caches to use a map structure for improved caching --- lib/src/services/country_service.dart | 137 ++++++++++++++++++-------- 1 file changed, 98 insertions(+), 39 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 1cd53c5..80950b4 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -48,8 +48,8 @@ class CountryService { static const Duration _cacheDuration = Duration(hours: 1); // In-memory caches for frequently accessed lists with time-based invalidation. - _CacheEntry>? _cachedEventCountries; - _CacheEntry>? _cachedHeadquarterCountries; + final Map>> _cachedEventCountries = {}; + final Map>> _cachedHeadquarterCountries = {}; // Futures to hold in-flight aggregation requests to prevent cache stampedes. Future>? _eventCountriesFuture; @@ -59,11 +59,12 @@ class CountryService { /// /// Supports filtering by 'usage' to get countries that are either /// 'eventCountry' in headlines or 'headquarters' in sources. - /// If no specific usage filter is provided, it returns all active countries. + /// It also supports filtering by 'name' (full or partial match). /// /// - [filter]: An optional map containing query parameters. /// Expected keys: /// - `'usage'`: String, can be 'eventCountry' or 'headquarters'. + /// - `'name'`: String, a full or partial country name for search. /// /// Throws [BadRequestException] if an unsupported usage filter is provided. /// Throws [OperationFailedException] for internal errors during data fetch. @@ -71,19 +72,35 @@ class CountryService { _log.info('Fetching countries with filter: $filter'); final usage = filter?['usage'] as String?; + final name = filter?['name'] as String?; + + Map? nameFilter; + if (name != null && name.isNotEmpty) { + // Create a case-insensitive regex filter for the name. + nameFilter = {r'$regex': name, r'$options': 'i'}; + } if (usage == null || usage.isEmpty) { - _log.fine('No usage filter provided. Fetching all active countries.'); - return _getAllCountries(); + _log.fine( + 'No usage filter provided. Fetching all active countries ' + 'with nameFilter: $nameFilter.', + ); + return _getAllCountries(nameFilter: nameFilter); } switch (usage) { case 'eventCountry': - _log.fine('Fetching countries used as event countries in headlines.'); - return _getEventCountries(); + _log.fine( + 'Fetching countries used as event countries in headlines ' + 'with nameFilter: $nameFilter.', + ); + return _getEventCountries(nameFilter: nameFilter); case 'headquarters': - _log.fine('Fetching countries used as headquarters in sources.'); - return _getHeadquarterCountries(); + _log.fine( + 'Fetching countries used as headquarters in sources ' + 'with nameFilter: $nameFilter.', + ); + return _getHeadquarterCountries(nameFilter: nameFilter); default: _log.warning('Unsupported country usage filter: "$usage"'); throw BadRequestException( @@ -94,15 +111,28 @@ class CountryService { } /// Fetches all active countries from the repository. - Future> _getAllCountries() async { - _log.finer('Retrieving all active countries from repository.'); + /// + /// - [nameFilter]: An optional map containing a regex filter for the country name. + Future> _getAllCountries({ + Map? nameFilter, + }) async { + _log.finer( + 'Retrieving all active countries from repository with nameFilter: $nameFilter.', + ); try { + final combinedFilter = { + 'status': ContentStatus.active.name, + }; + if (nameFilter != null && nameFilter.isNotEmpty) { + combinedFilter.addAll({'name': nameFilter}); + } + final response = await _countryRepository.readAll( - filter: {'status': ContentStatus.active.name}, + filter: combinedFilter, ); return response.items; } catch (e, s) { - _log.severe('Failed to fetch all countries.', e, s); + _log.severe('Failed to fetch all countries with nameFilter: $nameFilter.', e, s); throw OperationFailedException('Failed to retrieve all countries: $e'); } } @@ -112,14 +142,20 @@ class CountryService { /// /// Uses MongoDB aggregation to efficiently get distinct country IDs /// and then fetches the full Country objects. Results are cached. - Future> _getEventCountries() async { - if (_cachedEventCountries != null && _cachedEventCountries!.isValid()) { - _log.finer('Returning cached event countries.'); - return _cachedEventCountries!.data; + /// + /// - [nameFilter]: An optional map containing a regex filter for the country name. + Future> _getEventCountries({ + Map? nameFilter, + }) async { + final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter'}'; + if (_cachedEventCountries.containsKey(cacheKey) && + _cachedEventCountries[cacheKey]!.isValid()) { + _log.finer('Returning cached event countries for key: $cacheKey.'); + return _cachedEventCountries[cacheKey]!.data; } // Atomically assign the future if no fetch is in progress, // and clear it when the future completes. - _eventCountriesFuture ??= _fetchAndCacheEventCountries() + _eventCountriesFuture ??= _fetchAndCacheEventCountries(nameFilter: nameFilter) .whenComplete(() => _eventCountriesFuture = null); return _eventCountriesFuture!; } @@ -129,39 +165,54 @@ class CountryService { /// /// Uses MongoDB aggregation to efficiently get distinct country IDs /// and then fetches the full Country objects. Results are cached. - Future> _getHeadquarterCountries() async { - if (_cachedHeadquarterCountries != null && - _cachedHeadquarterCountries!.isValid()) { - _log.finer('Returning cached headquarter countries.'); - return _cachedHeadquarterCountries!.data; + /// + /// - [nameFilter]: An optional map containing a regex filter for the country name. + Future> _getHeadquarterCountries({ + Map? nameFilter, + }) async { + final cacheKey = 'headquarters_${nameFilter ?? 'noFilter'}'; + if (_cachedHeadquarterCountries.containsKey(cacheKey) && + _cachedHeadquarterCountries[cacheKey]!.isValid()) { + _log.finer('Returning cached headquarter countries for key: $cacheKey.'); + return _cachedHeadquarterCountries[cacheKey]!.data; } // Atomically assign the future if no fetch is in progress, // and clear it when the future completes. - _headquarterCountriesFuture ??= _fetchAndCacheHeadquarterCountries() - .whenComplete(() => _headquarterCountriesFuture = null); + _headquarterCountriesFuture ??= + _fetchAndCacheHeadquarterCountries(nameFilter: nameFilter) + .whenComplete(() => _headquarterCountriesFuture = null); return _headquarterCountriesFuture!; } /// Helper method to fetch and cache distinct event countries. - Future> _fetchAndCacheEventCountries() async { - _log.finer('Fetching distinct event countries via aggregation.'); + /// + /// - [nameFilter]: An optional map containing a regex filter for the country name. + Future> _fetchAndCacheEventCountries({ + Map? nameFilter, + }) async { + _log.finer( + 'Fetching distinct event countries via aggregation with nameFilter: $nameFilter.', + ); try { final distinctCountries = await _getDistinctCountriesFromAggregation( repository: _headlineRepository, fieldName: 'eventCountry', + nameFilter: nameFilter, ); - _cachedEventCountries = _CacheEntry( + final cacheKey = 'eventCountry_${nameFilter ?? 'noFilter'}'; + _cachedEventCountries[cacheKey] = _CacheEntry( distinctCountries, DateTime.now().add(_cacheDuration), ); _log.info( 'Successfully fetched and cached ${distinctCountries.length} ' - 'event countries.', + 'event countries for key: $cacheKey.', ); return distinctCountries; } catch (e, s) { _log.severe( - 'Failed to fetch distinct event countries via aggregation.', + 'Failed to fetch distinct event countries via aggregation ' + 'with nameFilter: $nameFilter.', e, s, ); @@ -170,25 +221,34 @@ class CountryService { } /// Helper method to fetch and cache distinct headquarter countries. - Future> _fetchAndCacheHeadquarterCountries() async { - _log.finer('Fetching distinct headquarter countries via aggregation.'); + /// + /// - [nameFilter]: An optional map containing a regex filter for the country name. + Future> _fetchAndCacheHeadquarterCountries({ + Map? nameFilter, + }) async { + _log.finer( + 'Fetching distinct headquarter countries via aggregation with nameFilter: $nameFilter.', + ); try { final distinctCountries = await _getDistinctCountriesFromAggregation( repository: _sourceRepository, fieldName: 'headquarters', + nameFilter: nameFilter, ); - _cachedHeadquarterCountries = _CacheEntry( + final cacheKey = 'headquarters_${nameFilter ?? 'noFilter'}'; + _cachedHeadquarterCountries[cacheKey] = _CacheEntry( distinctCountries, DateTime.now().add(_cacheDuration), ); _log.info( 'Successfully fetched and cached ${distinctCountries.length} ' - 'headquarter countries.', + 'headquarter countries for key: $cacheKey.', ); return distinctCountries; } catch (e, s) { _log.severe( - 'Failed to fetch distinct headquarter countries via aggregation.', + 'Failed to fetch distinct headquarter countries via aggregation ' + 'with nameFilter: $nameFilter.', e, s, ); @@ -205,7 +265,8 @@ class CountryService { /// - [nameFilter]: An optional map containing a regex filter for the country name. /// /// Throws [OperationFailedException] for internal errors during data fetch. - Future> _getDistinctCountriesFromAggregation({ + Future> + _getDistinctCountriesFromAggregation({ required DataRepository repository, required String fieldName, Map? nameFilter, @@ -227,9 +288,7 @@ class CountryService { // Add name filter if provided if (nameFilter != null && nameFilter.isNotEmpty) { pipeline.add({ - r'$match': { - '$fieldName.name': nameFilter, - }, + r'$match': {'$fieldName.name': nameFilter}, }); } From ba6185f78a675504aa09d09d488ccdf319a5b429 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 06:44:24 +0100 Subject: [PATCH 4/9] feat(country): extend country query to support name filter - Add support for 'name' filter in country queries - Delegate queries with either 'usage' or 'name' filter to CountryService - Update documentation to reflect that sorting and pagination are handled by CountryService for these specialized queries --- lib/src/registry/data_operation_registry.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index 95793eb..0015501 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -131,9 +131,11 @@ class DataOperationRegistry { ), 'country': (c, uid, f, s, p) async { final usage = f?['usage'] as String?; - if (usage != null && usage.isNotEmpty) { - // For 'country' model with 'usage' filter, delegate to CountryService. - // Sorting and pagination are not supported for this specialized query. + final name = f?['name'] as String?; + + // If either 'usage' or 'name' filter is present, delegate to CountryService. + // Sorting and pagination are handled by CountryService for these specialized queries. + if ((usage != null && usage.isNotEmpty) || (name != null && name.isNotEmpty)) { final countryService = c.read(); final countries = await countryService.getCountries(f); return PaginatedResponse( @@ -142,7 +144,8 @@ class DataOperationRegistry { hasMore: false, // No more items as it's a complete filtered set ); } else { - // For standard requests, use the repository which supports pagination/sorting. + // For standard requests without specialized filters, use the repository + // which supports pagination/sorting. return c.read>().readAll( userId: uid, filter: f, From 384f2aa91d6320494ba09cd65f2192af6001da17 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 06:51:53 +0100 Subject: [PATCH 5/9] refactor(country): improve type safety in aggregate pipeline - Update pipeline variable type from Map to Map - Ensure consistent use of generic type for pipeline stages - Apply proper casting for nested map objects in match and group stages --- lib/src/services/country_service.dart | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 80950b4..48734bd 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -276,31 +276,33 @@ class CountryService { 'with nameFilter: $nameFilter.', ); try { - final pipeline = >[ - { - r'$match': { + final pipeline = >[ + { + r'$match': { 'status': ContentStatus.active.name, - '$fieldName.id': {r'$exists': true}, + '$fieldName.id': {r'$exists': true}, }, }, ]; // Add name filter if provided if (nameFilter != null && nameFilter.isNotEmpty) { - pipeline.add({ - r'$match': {'$fieldName.name': nameFilter}, - }); + pipeline.add( + { + r'$match': {'$fieldName.name': nameFilter}, + }, + ); } pipeline.addAll([ - { - r'$group': { + { + r'$group': { '_id': '\$$fieldName.id', - 'country': {r'$first': '\$$fieldName'}, + 'country': {r'$first': '\$$fieldName'}, }, }, - { - r'$replaceRoot': {'newRoot': r'$country'}, + { + r'$replaceRoot': {'newRoot': r'$country'}, }, ]); From 6c59ccc5959ce9917ea539475c5301e07cbdaa8e Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 07:14:18 +0100 Subject: [PATCH 6/9] style: format misc --- lib/src/registry/data_operation_registry.dart | 13 ++++---- lib/src/services/country_service.dart | 32 ++++++++++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/src/registry/data_operation_registry.dart b/lib/src/registry/data_operation_registry.dart index 0015501..f649179 100644 --- a/lib/src/registry/data_operation_registry.dart +++ b/lib/src/registry/data_operation_registry.dart @@ -135,7 +135,8 @@ class DataOperationRegistry { // If either 'usage' or 'name' filter is present, delegate to CountryService. // Sorting and pagination are handled by CountryService for these specialized queries. - if ((usage != null && usage.isNotEmpty) || (name != null && name.isNotEmpty)) { + if ((usage != null && usage.isNotEmpty) || + (name != null && name.isNotEmpty)) { final countryService = c.read(); final countries = await countryService.getCountries(f); return PaginatedResponse( @@ -147,11 +148,11 @@ class DataOperationRegistry { // For standard requests without specialized filters, use the repository // which supports pagination/sorting. return c.read>().readAll( - userId: uid, - filter: f, - sort: s, - pagination: p, - ); + userId: uid, + filter: f, + sort: s, + pagination: p, + ); } }, 'language': (c, uid, f, s, p) => c diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 48734bd..2bea5b2 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -49,7 +49,8 @@ class CountryService { // In-memory caches for frequently accessed lists with time-based invalidation. final Map>> _cachedEventCountries = {}; - final Map>> _cachedHeadquarterCountries = {}; + final Map>> _cachedHeadquarterCountries = + {}; // Futures to hold in-flight aggregation requests to prevent cache stampedes. Future>? _eventCountriesFuture; @@ -127,12 +128,14 @@ class CountryService { combinedFilter.addAll({'name': nameFilter}); } - final response = await _countryRepository.readAll( - filter: combinedFilter, - ); + final response = await _countryRepository.readAll(filter: combinedFilter); return response.items; } catch (e, s) { - _log.severe('Failed to fetch all countries with nameFilter: $nameFilter.', e, s); + _log.severe( + 'Failed to fetch all countries with nameFilter: $nameFilter.', + e, + s, + ); throw OperationFailedException('Failed to retrieve all countries: $e'); } } @@ -155,8 +158,9 @@ class CountryService { } // Atomically assign the future if no fetch is in progress, // and clear it when the future completes. - _eventCountriesFuture ??= _fetchAndCacheEventCountries(nameFilter: nameFilter) - .whenComplete(() => _eventCountriesFuture = null); + _eventCountriesFuture ??= _fetchAndCacheEventCountries( + nameFilter: nameFilter, + ).whenComplete(() => _eventCountriesFuture = null); return _eventCountriesFuture!; } @@ -178,9 +182,9 @@ class CountryService { } // Atomically assign the future if no fetch is in progress, // and clear it when the future completes. - _headquarterCountriesFuture ??= - _fetchAndCacheHeadquarterCountries(nameFilter: nameFilter) - .whenComplete(() => _headquarterCountriesFuture = null); + _headquarterCountriesFuture ??= _fetchAndCacheHeadquarterCountries( + nameFilter: nameFilter, + ).whenComplete(() => _headquarterCountriesFuture = null); return _headquarterCountriesFuture!; } @@ -287,11 +291,9 @@ class CountryService { // Add name filter if provided if (nameFilter != null && nameFilter.isNotEmpty) { - pipeline.add( - { - r'$match': {'$fieldName.name': nameFilter}, - }, - ); + pipeline.add({ + r'$match': {'$fieldName.name': nameFilter}, + }); } pipeline.addAll([ From 5b60dd551dd38dbf7e66a8e65542f343921a9af0 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 07:46:50 +0100 Subject: [PATCH 7/9] fix(country): prevent cache stampede by caching futures per request - Replace single Future fields with Maps to store futures per cache key - Implement logic to atomically retrieve or create futures for specific cache keys - Ensure futures are removed from the Map after completion to prevent stale data - Refactor match stage in aggregation pipeline to handle name filter --- lib/src/services/country_service.dart | 55 ++++++++++++++------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index 2bea5b2..a0ed46e 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -53,8 +53,8 @@ class CountryService { {}; // Futures to hold in-flight aggregation requests to prevent cache stampedes. - Future>? _eventCountriesFuture; - Future>? _headquarterCountriesFuture; + final Map>> _eventCountriesFutures = {}; + final Map>> _headquarterCountriesFutures = {}; /// Retrieves a list of countries based on the provided filter. /// @@ -156,12 +156,14 @@ class CountryService { _log.finer('Returning cached event countries for key: $cacheKey.'); return _cachedEventCountries[cacheKey]!.data; } - // Atomically assign the future if no fetch is in progress, - // and clear it when the future completes. - _eventCountriesFuture ??= _fetchAndCacheEventCountries( - nameFilter: nameFilter, - ).whenComplete(() => _eventCountriesFuture = null); - return _eventCountriesFuture!; + // Atomically retrieve or create the future for the specific cache key. + var future = _eventCountriesFutures[cacheKey]; + if (future == null) { + future = _fetchAndCacheEventCountries(nameFilter: nameFilter) + .whenComplete(() => _eventCountriesFutures.remove(cacheKey)); + _eventCountriesFutures[cacheKey] = future; + } + return future!; } /// Fetches a distinct list of countries that are referenced as @@ -180,12 +182,14 @@ class CountryService { _log.finer('Returning cached headquarter countries for key: $cacheKey.'); return _cachedHeadquarterCountries[cacheKey]!.data; } - // Atomically assign the future if no fetch is in progress, - // and clear it when the future completes. - _headquarterCountriesFuture ??= _fetchAndCacheHeadquarterCountries( - nameFilter: nameFilter, - ).whenComplete(() => _headquarterCountriesFuture = null); - return _headquarterCountriesFuture!; + // Atomically retrieve or create the future for the specific cache key. + var future = _headquarterCountriesFutures[cacheKey]; + if (future == null) { + future = _fetchAndCacheHeadquarterCountries(nameFilter: nameFilter) + .whenComplete(() => _headquarterCountriesFutures.remove(cacheKey)); + _headquarterCountriesFutures[cacheKey] = future; + } + return future!; } /// Helper method to fetch and cache distinct event countries. @@ -280,23 +284,20 @@ class CountryService { 'with nameFilter: $nameFilter.', ); try { - final pipeline = >[ - { - r'$match': { - 'status': ContentStatus.active.name, - '$fieldName.id': {r'$exists': true}, - }, - }, - ]; + final matchStage = { + 'status': ContentStatus.active.name, + '$fieldName.id': {r'$exists': true}, + }; // Add name filter if provided if (nameFilter != null && nameFilter.isNotEmpty) { - pipeline.add({ - r'$match': {'$fieldName.name': nameFilter}, - }); + matchStage['$fieldName.name'] = nameFilter; } - pipeline.addAll([ + final pipeline = >[ + { + r'$match': matchStage, + }, { r'$group': { '_id': '\$$fieldName.id', @@ -306,7 +307,7 @@ class CountryService { { r'$replaceRoot': {'newRoot': r'$country'}, }, - ]); + ]; final distinctCountriesJson = await repository.aggregate( pipeline: pipeline, From be42603f9160abacc065e8088201fbaaccee4897 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 07:46:59 +0100 Subject: [PATCH 8/9] perf(database): optimize country search and indexing - Replace text index with name-based index for countries collection - Update index naming for improved clarity --- lib/src/services/database_seeding_service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/services/database_seeding_service.dart b/lib/src/services/database_seeding_service.dart index 20f5cc4..26007f1 100644 --- a/lib/src/services/database_seeding_service.dart +++ b/lib/src/services/database_seeding_service.dart @@ -118,10 +118,10 @@ class DatabaseSeedingService { .collection('sources') .createIndex(keys: {'name': 'text'}, name: 'sources_text_index'); - // Text index for searching countries by name (case-insensitive) + // Index for searching countries by name (case-insensitive friendly) await _db .collection('countries') - .createIndex(keys: {'name': 'text'}, name: 'countries_text_index'); + .createIndex(keys: {'name': 1}, name: 'countries_name_index'); // Indexes for country aggregation queries await _db From 228cca9980ac54318826e20a766f526e1d968424 Mon Sep 17 00:00:00 2001 From: fulleni Date: Fri, 22 Aug 2025 07:54:16 +0100 Subject: [PATCH 9/9] style: format --- lib/src/services/country_service.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/services/country_service.dart b/lib/src/services/country_service.dart index a0ed46e..2cb4c79 100644 --- a/lib/src/services/country_service.dart +++ b/lib/src/services/country_service.dart @@ -159,11 +159,12 @@ class CountryService { // Atomically retrieve or create the future for the specific cache key. var future = _eventCountriesFutures[cacheKey]; if (future == null) { - future = _fetchAndCacheEventCountries(nameFilter: nameFilter) - .whenComplete(() => _eventCountriesFutures.remove(cacheKey)); + future = _fetchAndCacheEventCountries( + nameFilter: nameFilter, + ).whenComplete(() => _eventCountriesFutures.remove(cacheKey)); _eventCountriesFutures[cacheKey] = future; } - return future!; + return future; } /// Fetches a distinct list of countries that are referenced as @@ -185,11 +186,12 @@ class CountryService { // Atomically retrieve or create the future for the specific cache key. var future = _headquarterCountriesFutures[cacheKey]; if (future == null) { - future = _fetchAndCacheHeadquarterCountries(nameFilter: nameFilter) - .whenComplete(() => _headquarterCountriesFutures.remove(cacheKey)); + future = _fetchAndCacheHeadquarterCountries( + nameFilter: nameFilter, + ).whenComplete(() => _headquarterCountriesFutures.remove(cacheKey)); _headquarterCountriesFutures[cacheKey] = future; } - return future!; + return future; } /// Helper method to fetch and cache distinct event countries. @@ -295,9 +297,7 @@ class CountryService { } final pipeline = >[ - { - r'$match': matchStage, - }, + {r'$match': matchStage}, { r'$group': { '_id': '\$$fieldName.id',