@@ -12,7 +12,6 @@ import 'package:meta/meta.dart';
1212import 'package:pub_dev/service/topics/models.dart' ;
1313import 'package:pub_dev/third_party/bit_array/bit_array.dart' ;
1414
15- import '../shared/utils.dart' show boundedList;
1615import 'models.dart' ;
1716import 'search_service.dart' ;
1817import 'text_utils.dart' ;
@@ -142,9 +141,9 @@ class InMemoryPackageIndex {
142141 return PackageSearchResult .empty ();
143142 }
144143 return _bitArrayPool.withPoolItem (fn: (array) {
145- return _scorePool.withPoolItem (
146- fn : (score ) {
147- return _search (query, array, score );
144+ return _scorePool.withItemGetter (
145+ (scoreFn ) {
146+ return _search (query, array, scoreFn );
148147 },
149148 );
150149 });
@@ -220,88 +219,107 @@ class InMemoryPackageIndex {
220219 PackageSearchResult _search (
221220 ServiceSearchQuery query,
222221 BitArray packages,
223- IndexedScore <String > packageScores ,
222+ IndexedScore <String > Function () scoreFn ,
224223 ) {
225224 final predicateFilterCount = _filterOnPredicates (query, packages);
226225 if (predicateFilterCount <= query.offset) {
227226 return PackageSearchResult .empty ();
228227 }
229-
230- // TODO: find a better way to handle predicate-only filtering and scoring
231- for (final index in packages.asIntIterable ()) {
232- if (index >= _documents.length) break ;
233- packageScores.setValue (index, 1.0 );
234- }
228+ final bestNameMatch = _bestNameMatch (query);
229+ final bestNameIndex =
230+ bestNameMatch == null ? null : _nameToIndex[bestNameMatch];
235231
236232 // do text matching
237233 final parsedQueryText = query.parsedQuery.text;
238- final textResults = _searchText (
239- packageScores,
240- packages,
241- parsedQueryText,
242- textMatchExtent: query.textMatchExtent ?? TextMatchExtent .api,
243- );
234+ _TextResults ? textResults;
235+ IndexedScore <String >? packageScores;
236+
237+ if (parsedQueryText != null && parsedQueryText.isNotEmpty) {
238+ packageScores = scoreFn ();
239+ textResults = _searchText (
240+ packageScores,
241+ packages,
242+ parsedQueryText,
243+ textMatchExtent: query.textMatchExtent ?? TextMatchExtent .api,
244+ );
245+ if (textResults.hasNoMatch) {
246+ return textResults.errorMessage == null
247+ ? PackageSearchResult .empty ()
248+ : PackageSearchResult .error (
249+ errorMessage: textResults.errorMessage,
250+ statusCode: 500 ,
251+ );
252+ }
253+ }
244254
245- final bestNameMatch = _bestNameMatch (query);
255+ // The function takes the document index as parameter and returns whether
256+ // it should be in the result set. When text search is applied, the
257+ // [packageScores] contains the scores of the results, otherwise we are
258+ // using the bitarray index of the filtering.
259+ final selectFn = packageScores? .isPositive ?? packages.isSet;
260+
261+ // We know the total count at this point, we don't need to build the fully
262+ // sorted result list to get the number. The best name match may insert an
263+ // extra item, that will be addressed after the ranking score is determined.
264+ var totalCount = packageScores? .positiveCount () ?? predicateFilterCount;
246265
247- List <IndexedPackageHit > indexedHits;
248- switch (query.effectiveOrder ?? SearchOrder .top ) {
266+ Iterable <IndexedPackageHit > indexedHits;
267+ switch (query.effectiveOrder) {
249268 case SearchOrder .top:
250- if (textResults == null ) {
251- indexedHits = _overallOrderedHits.whereInScores (packageScores);
269+ case SearchOrder .text:
270+ if (packageScores == null ) {
271+ indexedHits = _overallOrderedHits.whereInScores (selectFn);
252272 break ;
253273 }
254274
255- /// Adjusted score takes the overall score and transforms
256- /// it linearly into the [0.4-1.0] range, to allow better
257- /// multiplication outcomes.
258- packageScores. multiplyAllFromValues (_adjustedOverallScores);
259- indexedHits = _rankWithValues (
260- packageScores,
261- requiredLengthThreshold : query.offset,
262- bestNameMatch : bestNameMatch,
263- );
264- break ;
265- case SearchOrder .text :
275+ if (query.effectiveOrder == SearchOrder .top) {
276+ /// Adjusted score takes the overall score and transforms
277+ /// it linearly into the [0.4-1.0] range, to allow better
278+ /// multiplication outcomes.
279+ packageScores. multiplyAllFromValues (_adjustedOverallScores);
280+ }
281+ // Check whether the best name match will increase the total item count.
282+ if (bestNameIndex != null &&
283+ packageScores. getValue (bestNameIndex) <= 0.0 ) {
284+ totalCount ++ ;
285+ }
266286 indexedHits = _rankWithValues (
267287 packageScores,
268288 requiredLengthThreshold: query.offset,
269- bestNameMatch : bestNameMatch ,
289+ bestNameIndex : bestNameIndex ?? - 1 ,
270290 );
271291 break ;
272292 case SearchOrder .created:
273- indexedHits = _createdOrderedHits.whereInScores (packageScores );
293+ indexedHits = _createdOrderedHits.whereInScores (selectFn );
274294 break ;
275295 case SearchOrder .updated:
276- indexedHits = _updatedOrderedHits.whereInScores (packageScores );
296+ indexedHits = _updatedOrderedHits.whereInScores (selectFn );
277297 break ;
278298 // ignore: deprecated_member_use
279299 case SearchOrder .popularity:
280300 case SearchOrder .downloads:
281- indexedHits = _downloadsOrderedHits.whereInScores (packageScores );
301+ indexedHits = _downloadsOrderedHits.whereInScores (selectFn );
282302 break ;
283303 case SearchOrder .like:
284- indexedHits = _likesOrderedHits.whereInScores (packageScores );
304+ indexedHits = _likesOrderedHits.whereInScores (selectFn );
285305 break ;
286306 case SearchOrder .points:
287- indexedHits = _pointsOrderedHits.whereInScores (packageScores );
307+ indexedHits = _pointsOrderedHits.whereInScores (selectFn );
288308 break ;
289309 case SearchOrder .trending:
290- indexedHits = _trendingOrderedHits.whereInScores (packageScores );
310+ indexedHits = _trendingOrderedHits.whereInScores (selectFn );
291311 break ;
292312 }
293313
294- // bound by offset and limit (or randomize items)
295- final totalCount = indexedHits.length;
296- indexedHits =
297- boundedList (indexedHits, offset: query.offset, limit: query.limit);
314+ // bound by offset and limit
315+ indexedHits = indexedHits.skip (query.offset).take (query.limit);
298316
299317 late List <PackageHit > packageHits;
300318 if ((query.textMatchExtent ?? TextMatchExtent .api).shouldMatchApi () &&
301319 textResults != null &&
302320 (textResults.topApiPages? .isNotEmpty ?? false )) {
303321 packageHits = indexedHits.map ((ps) {
304- final apiPages = textResults.topApiPages? [ps.index]
322+ final apiPages = textResults! .topApiPages? [ps.index]
305323 // TODO(https://github.com/dart-lang/pub-dev/issues/7106): extract title for the page
306324 ? .map ((MapEntry <String , double > e) => ApiPageRef (path: e.key))
307325 .toList ();
@@ -380,33 +398,30 @@ class InMemoryPackageIndex {
380398 }).toList ();
381399 }
382400
383- _TextResults ? _searchText (
401+ _TextResults _searchText (
384402 IndexedScore <String > packageScores,
385403 BitArray packages,
386- String ? text, {
404+ String text, {
387405 required TextMatchExtent textMatchExtent,
388406 }) {
389- if (text == null || text.isEmpty) {
390- return null ;
391- }
392-
393407 final sw = Stopwatch ()..start ();
394408 final words = splitForQuery (text);
395409 if (words.isEmpty) {
396- // packages.clearAll();
397- packageScores.fillRange (0 , packageScores.length, 0 );
398410 return _TextResults .empty ();
399411 }
400412
401413 final matchName = textMatchExtent.shouldMatchName ();
402414 if (! matchName) {
403- // packages.clearAll();
404- packageScores.fillRange (0 , packageScores.length, 0 );
405415 return _TextResults .empty (
406416 errorMessage:
407417 'Search index in reduced mode: unable to match query text.' );
408418 }
409419
420+ for (final index in packages.asIntIterable ()) {
421+ if (index >= _documents.length) break ;
422+ packageScores.setValue (index, 1.0 );
423+ }
424+
410425 bool aborted = false ;
411426 bool checkAborted () {
412427 if (! aborted && sw.elapsed > _textSearchTimeout) {
@@ -500,19 +515,18 @@ class InMemoryPackageIndex {
500515 List <IndexedPackageHit > _rankWithValues (
501516 IndexedScore <String > score, {
502517 // if the item count is fewer than this threshold, an empty list will be returned
503- int ? requiredLengthThreshold,
504- String ? bestNameMatch,
518+ required int requiredLengthThreshold,
519+ // When no best name match is applied, this parameter will be `-1`
520+ required int bestNameIndex,
505521 }) {
506522 final list = < IndexedPackageHit > [];
507- final bestNameIndex =
508- bestNameMatch == null ? null : _nameToIndex[bestNameMatch];
509523 for (var i = 0 ; i < score.length; i++ ) {
510524 final value = score.getValue (i);
511525 if (value <= 0.0 && i != bestNameIndex) continue ;
512526 list.add (IndexedPackageHit (
513527 i, PackageHit (package: score.keys[i], score: value)));
514528 }
515- if (( requiredLengthThreshold ?? 0 ) > list.length) {
529+ if (requiredLengthThreshold > list.length) {
516530 // There is no point to sort or even keep the results, as the search query offset ignores these anyway.
517531 return [];
518532 }
@@ -582,19 +596,22 @@ class InMemoryPackageIndex {
582596}
583597
584598class _TextResults {
599+ final bool hasNoMatch;
585600 final List <List <MapEntry <String , double >>?>? topApiPages;
586601 final String ? errorMessage;
587602
588603 factory _TextResults .empty ({String ? errorMessage}) {
589604 return _TextResults (
590605 null ,
591606 errorMessage: errorMessage,
607+ hasNoMatch: true ,
592608 );
593609 }
594610
595611 _TextResults (
596612 this .topApiPages, {
597613 this .errorMessage,
614+ this .hasNoMatch = false ,
598615 });
599616}
600617
@@ -713,8 +730,8 @@ class _PkgNameData {
713730}
714731
715732extension on List <IndexedPackageHit > {
716- List <IndexedPackageHit > whereInScores (IndexedScore scores ) {
717- return where ((h) => scores. isPositive (h.index)). toList ( );
733+ Iterable <IndexedPackageHit > whereInScores (bool Function ( int index) select ) {
734+ return where ((h) => select (h.index));
718735 }
719736}
720737
0 commit comments