@@ -12,7 +12,6 @@ import 'package:meta/meta.dart';
12
12
import 'package:pub_dev/service/topics/models.dart' ;
13
13
import 'package:pub_dev/third_party/bit_array/bit_array.dart' ;
14
14
15
- import '../shared/utils.dart' show boundedList;
16
15
import 'models.dart' ;
17
16
import 'search_service.dart' ;
18
17
import 'text_utils.dart' ;
@@ -142,9 +141,9 @@ class InMemoryPackageIndex {
142
141
return PackageSearchResult .empty ();
143
142
}
144
143
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 );
148
147
},
149
148
);
150
149
});
@@ -220,88 +219,107 @@ class InMemoryPackageIndex {
220
219
PackageSearchResult _search (
221
220
ServiceSearchQuery query,
222
221
BitArray packages,
223
- IndexedScore <String > packageScores ,
222
+ IndexedScore <String > Function () scoreFn ,
224
223
) {
225
224
final predicateFilterCount = _filterOnPredicates (query, packages);
226
225
if (predicateFilterCount <= query.offset) {
227
226
return PackageSearchResult .empty ();
228
227
}
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];
235
231
236
232
// do text matching
237
233
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
+ }
244
254
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;
246
265
247
- List <IndexedPackageHit > indexedHits;
248
- switch (query.effectiveOrder ?? SearchOrder .top ) {
266
+ Iterable <IndexedPackageHit > indexedHits;
267
+ switch (query.effectiveOrder) {
249
268
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);
252
272
break ;
253
273
}
254
274
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
+ }
266
286
indexedHits = _rankWithValues (
267
287
packageScores,
268
288
requiredLengthThreshold: query.offset,
269
- bestNameMatch : bestNameMatch ,
289
+ bestNameIndex : bestNameIndex ?? - 1 ,
270
290
);
271
291
break ;
272
292
case SearchOrder .created:
273
- indexedHits = _createdOrderedHits.whereInScores (packageScores );
293
+ indexedHits = _createdOrderedHits.whereInScores (selectFn );
274
294
break ;
275
295
case SearchOrder .updated:
276
- indexedHits = _updatedOrderedHits.whereInScores (packageScores );
296
+ indexedHits = _updatedOrderedHits.whereInScores (selectFn );
277
297
break ;
278
298
// ignore: deprecated_member_use
279
299
case SearchOrder .popularity:
280
300
case SearchOrder .downloads:
281
- indexedHits = _downloadsOrderedHits.whereInScores (packageScores );
301
+ indexedHits = _downloadsOrderedHits.whereInScores (selectFn );
282
302
break ;
283
303
case SearchOrder .like:
284
- indexedHits = _likesOrderedHits.whereInScores (packageScores );
304
+ indexedHits = _likesOrderedHits.whereInScores (selectFn );
285
305
break ;
286
306
case SearchOrder .points:
287
- indexedHits = _pointsOrderedHits.whereInScores (packageScores );
307
+ indexedHits = _pointsOrderedHits.whereInScores (selectFn );
288
308
break ;
289
309
case SearchOrder .trending:
290
- indexedHits = _trendingOrderedHits.whereInScores (packageScores );
310
+ indexedHits = _trendingOrderedHits.whereInScores (selectFn );
291
311
break ;
292
312
}
293
313
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);
298
316
299
317
late List <PackageHit > packageHits;
300
318
if ((query.textMatchExtent ?? TextMatchExtent .api).shouldMatchApi () &&
301
319
textResults != null &&
302
320
(textResults.topApiPages? .isNotEmpty ?? false )) {
303
321
packageHits = indexedHits.map ((ps) {
304
- final apiPages = textResults.topApiPages? [ps.index]
322
+ final apiPages = textResults! .topApiPages? [ps.index]
305
323
// TODO(https://github.com/dart-lang/pub-dev/issues/7106): extract title for the page
306
324
? .map ((MapEntry <String , double > e) => ApiPageRef (path: e.key))
307
325
.toList ();
@@ -380,33 +398,30 @@ class InMemoryPackageIndex {
380
398
}).toList ();
381
399
}
382
400
383
- _TextResults ? _searchText (
401
+ _TextResults _searchText (
384
402
IndexedScore <String > packageScores,
385
403
BitArray packages,
386
- String ? text, {
404
+ String text, {
387
405
required TextMatchExtent textMatchExtent,
388
406
}) {
389
- if (text == null || text.isEmpty) {
390
- return null ;
391
- }
392
-
393
407
final sw = Stopwatch ()..start ();
394
408
final words = splitForQuery (text);
395
409
if (words.isEmpty) {
396
- // packages.clearAll();
397
- packageScores.fillRange (0 , packageScores.length, 0 );
398
410
return _TextResults .empty ();
399
411
}
400
412
401
413
final matchName = textMatchExtent.shouldMatchName ();
402
414
if (! matchName) {
403
- // packages.clearAll();
404
- packageScores.fillRange (0 , packageScores.length, 0 );
405
415
return _TextResults .empty (
406
416
errorMessage:
407
417
'Search index in reduced mode: unable to match query text.' );
408
418
}
409
419
420
+ for (final index in packages.asIntIterable ()) {
421
+ if (index >= _documents.length) break ;
422
+ packageScores.setValue (index, 1.0 );
423
+ }
424
+
410
425
bool aborted = false ;
411
426
bool checkAborted () {
412
427
if (! aborted && sw.elapsed > _textSearchTimeout) {
@@ -500,19 +515,18 @@ class InMemoryPackageIndex {
500
515
List <IndexedPackageHit > _rankWithValues (
501
516
IndexedScore <String > score, {
502
517
// 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,
505
521
}) {
506
522
final list = < IndexedPackageHit > [];
507
- final bestNameIndex =
508
- bestNameMatch == null ? null : _nameToIndex[bestNameMatch];
509
523
for (var i = 0 ; i < score.length; i++ ) {
510
524
final value = score.getValue (i);
511
525
if (value <= 0.0 && i != bestNameIndex) continue ;
512
526
list.add (IndexedPackageHit (
513
527
i, PackageHit (package: score.keys[i], score: value)));
514
528
}
515
- if (( requiredLengthThreshold ?? 0 ) > list.length) {
529
+ if (requiredLengthThreshold > list.length) {
516
530
// There is no point to sort or even keep the results, as the search query offset ignores these anyway.
517
531
return [];
518
532
}
@@ -582,19 +596,22 @@ class InMemoryPackageIndex {
582
596
}
583
597
584
598
class _TextResults {
599
+ final bool hasNoMatch;
585
600
final List <List <MapEntry <String , double >>?>? topApiPages;
586
601
final String ? errorMessage;
587
602
588
603
factory _TextResults .empty ({String ? errorMessage}) {
589
604
return _TextResults (
590
605
null ,
591
606
errorMessage: errorMessage,
607
+ hasNoMatch: true ,
592
608
);
593
609
}
594
610
595
611
_TextResults (
596
612
this .topApiPages, {
597
613
this .errorMessage,
614
+ this .hasNoMatch = false ,
598
615
});
599
616
}
600
617
@@ -713,8 +730,8 @@ class _PkgNameData {
713
730
}
714
731
715
732
extension 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));
718
735
}
719
736
}
720
737
0 commit comments