From 318a871bb61bd897a611365de80ea5ae91a1015b Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Wed, 20 Aug 2025 12:59:07 +0200 Subject: [PATCH 1/3] Allow searching and filtering on packages that are provided in the query. --- app/lib/search/mem_index.dart | 16 ++++++++++++++ app/lib/search/search_service.dart | 1 + app/lib/search/token_index.dart | 6 ++--- app/test/search/mem_index_test.dart | 22 +++++++++++++++++++ .../lib/search/search_request_data.dart | 8 ++++++- .../lib/search/search_request_data.g.dart | 4 ++++ 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/app/lib/search/mem_index.dart b/app/lib/search/mem_index.dart index 4dbec6e02a..9debfbbaab 100644 --- a/app/lib/search/mem_index.dart +++ b/app/lib/search/mem_index.dart @@ -207,6 +207,7 @@ class InMemoryPackageIndex { BitArray packages, IndexedScore Function() scoreFn, ) { + _resetBitArray(packages, query.packages); final predicateFilterCount = _filterOnPredicates(query, packages); if (predicateFilterCount <= query.offset) { return PackageSearchResult.empty(); @@ -333,6 +334,21 @@ class InMemoryPackageIndex { ); } + /// The [BitArrayPool] does not resets the reused pool items, because initialization + /// depends on the presence of the [filterOnPackages] list. + void _resetBitArray(BitArray selected, List? filterOnPackages) { + if (filterOnPackages != null && filterOnPackages.isNotEmpty) { + selected.clearAll(); + for (final package in filterOnPackages) { + final index = _nameToIndex[package]; + if (index == null) continue; + selected.setBit(index); + } + } else { + selected.setRange(0, _documents.length); + } + } + /// Returns the package name that is considered as the best name match /// for the [query], or `null` if there is no such package name, or the /// match is not enabled for the given context (e.g. non-default ordering). diff --git a/app/lib/search/search_service.dart b/app/lib/search/search_service.dart index 7d84342d39..dc19b40fa5 100644 --- a/app/lib/search/search_service.dart +++ b/app/lib/search/search_service.dart @@ -211,6 +211,7 @@ class ServiceSearchQuery { int get offset => max(0, _data.offset ?? 0); int get limit => max(_minSearchLimit, _data.limit ?? 10); TextMatchExtent? get textMatchExtent => _data.textMatchExtent; + List? get packages => _data.packages; Map toUriQueryParameters() { return _data.toUriQueryParameters(); diff --git a/app/lib/search/token_index.dart b/app/lib/search/token_index.dart index 5881f4f2c3..5e6cc81d9c 100644 --- a/app/lib/search/token_index.dart +++ b/app/lib/search/token_index.dart @@ -249,10 +249,8 @@ class ScorePool extends _AllocationPool> { class BitArrayPool extends _AllocationPool { BitArrayPool(int length) : super( - // sets all bits to 1 - () => BitArray(length)..setRange(0, length), - // sets all bits to 1 - (array) => array.setRange(0, length), + () => BitArray(length), + (array) {}, // keeping the array as-is, reset happens at the beginning of the processing ); } diff --git a/app/test/search/mem_index_test.dart b/app/test/search/mem_index_test.dart index f65c0649f9..9e1b76d749 100644 --- a/app/test/search/mem_index_test.dart +++ b/app/test/search/mem_index_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:_pub_shared/search/search_form.dart'; +import 'package:_pub_shared/search/search_request_data.dart'; import 'package:clock/clock.dart'; import 'package:pub_dev/search/mem_index.dart'; import 'package:pub_dev/search/search_service.dart'; @@ -543,6 +544,27 @@ server.dart adds a small, prescriptive server (PicoServer) that can be configure ServiceSearchQuery.parse(query: '=', order: SearchOrder.text)); expect(rs.isEmpty, isTrue); }); + + test('only packages list filter', () { + final rs = index + .search(ServiceSearchQuery(SearchRequestData(packages: ['http']))); + expect(rs.packageHits.map((e) => e.package).toList(), ['http']); + }); + + test('query + packages list filter', () { + // library itself would return `http` too + final rs = index.search(ServiceSearchQuery(SearchRequestData( + query: 'library', + packages: ['async'], + ))); + expect(rs.packageHits.map((e) => e.package).toList(), ['async']); + }); + + test('non-existent package name', () { + final rs = index.search( + ServiceSearchQuery(SearchRequestData(packages: ['not-a-package']))); + expect(rs.packageHits, isEmpty); + }); }); group('special cases', () { diff --git a/pkg/_pub_shared/lib/search/search_request_data.dart b/pkg/_pub_shared/lib/search/search_request_data.dart index 254afc2756..9a8a5f9f78 100644 --- a/pkg/_pub_shared/lib/search/search_request_data.dart +++ b/pkg/_pub_shared/lib/search/search_request_data.dart @@ -17,6 +17,7 @@ class SearchRequestData { final int? offset; final int? limit; final TextMatchExtent? textMatchExtent; + final List? packages; SearchRequestData({ String? query, @@ -27,8 +28,10 @@ class SearchRequestData { this.offset, this.limit, this.textMatchExtent, + List? packages, }) : query = _trimToNull(query), - publisherId = _trimToNull(publisherId); + publisherId = _trimToNull(publisherId), + packages = packages != null && packages.isNotEmpty ? packages : null; factory SearchRequestData.fromJson(Map json) => _$SearchRequestDataFromJson(json); @@ -54,6 +57,7 @@ class SearchRequestData { break; } } + final packages = uri.queryParametersAll['packages']; return SearchRequestData( query: q, @@ -64,6 +68,7 @@ class SearchRequestData { offset: offset, limit: limit, textMatchExtent: textMatchExtent, + packages: packages, ); } @@ -78,6 +83,7 @@ class SearchRequestData { 'limit': (limit ?? 10).toString(), 'order': order?.name, if (textMatchExtent != null) 'textMatchExtent': textMatchExtent!.name, + if (packages != null && packages!.isNotEmpty) 'packages': packages, }; map.removeWhere((k, v) => v == null); return map; diff --git a/pkg/_pub_shared/lib/search/search_request_data.g.dart b/pkg/_pub_shared/lib/search/search_request_data.g.dart index a430707bb1..0b2daf791a 100644 --- a/pkg/_pub_shared/lib/search/search_request_data.g.dart +++ b/pkg/_pub_shared/lib/search/search_request_data.g.dart @@ -17,6 +17,9 @@ SearchRequestData _$SearchRequestDataFromJson(Map json) => limit: (json['limit'] as num?)?.toInt(), textMatchExtent: $enumDecodeNullable( _$TextMatchExtentEnumMap, json['textMatchExtent']), + packages: (json['packages'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$SearchRequestDataToJson(SearchRequestData instance) => @@ -29,6 +32,7 @@ Map _$SearchRequestDataToJson(SearchRequestData instance) => 'offset': instance.offset, 'limit': instance.limit, 'textMatchExtent': _$TextMatchExtentEnumMap[instance.textMatchExtent], + 'packages': instance.packages, }; const _$SearchOrderEnumMap = { From 22a9c4cb99b47474912951b05aeae4bb212a6e88 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 21 Aug 2025 10:10:38 +0200 Subject: [PATCH 2/3] Updated test --- app/test/search/mem_index_test.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/test/search/mem_index_test.dart b/app/test/search/mem_index_test.dart index 9e1b76d749..b0d65e0177 100644 --- a/app/test/search/mem_index_test.dart +++ b/app/test/search/mem_index_test.dart @@ -546,9 +546,10 @@ server.dart adds a small, prescriptive server (PicoServer) that can be configure }); test('only packages list filter', () { - final rs = index - .search(ServiceSearchQuery(SearchRequestData(packages: ['http']))); - expect(rs.packageHits.map((e) => e.package).toList(), ['http']); + final rs = index.search( + ServiceSearchQuery(SearchRequestData(packages: ['async', 'http']))); + // returns two packages, ordered by overall score + expect(rs.packageHits.map((e) => e.package).toList(), ['http', 'async']); }); test('query + packages list filter', () { From 6ed560cb0bdc03d1756940a04b20233dd155d66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Thu, 21 Aug 2025 09:34:18 +0200 Subject: [PATCH 3/3] Update app/lib/search/mem_index.dart Co-authored-by: Sigurd Meldgaard --- app/lib/search/mem_index.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/search/mem_index.dart b/app/lib/search/mem_index.dart index 9debfbbaab..4ff0e2eff8 100644 --- a/app/lib/search/mem_index.dart +++ b/app/lib/search/mem_index.dart @@ -334,7 +334,7 @@ class InMemoryPackageIndex { ); } - /// The [BitArrayPool] does not resets the reused pool items, because initialization + /// The [BitArrayPool] does not reset the reused pool items, because initialization /// depends on the presence of the [filterOnPackages] list. void _resetBitArray(BitArray selected, List? filterOnPackages) { if (filterOnPackages != null && filterOnPackages.isNotEmpty) {