@@ -28,6 +28,8 @@ class InMemoryPackageIndex {
2828 late final TokenIndex <String > _readmeIndex;
2929 late final TokenIndex <IndexedApiDocPage > _apiSymbolIndex;
3030 late final _scorePool = ScorePool (_packageNameIndex._packageNames);
31+ final _tagIds = < String , int > {};
32+ final _documentTagIds = < List <int >> [];
3133
3234 /// Adjusted score takes the overall score and transforms
3335 /// it linearly into the [0.4-1.0] range.
@@ -58,6 +60,14 @@ class InMemoryPackageIndex {
5860 final doc = _documents[i];
5961 _documentsByName[doc.package] = doc;
6062
63+ // transform tags into numberical IDs
64+ final tagIds = < int > [];
65+ for (final tag in doc.tags) {
66+ tagIds.add (_tagIds.putIfAbsent (tag, () => _tagIds.length));
67+ }
68+ tagIds.sort ();
69+ _documentTagIds.add (tagIds);
70+
6171 final apiDocPages = doc.apiDocPages;
6272 if (apiDocPages != null ) {
6373 for (final page in apiDocPages) {
@@ -122,6 +132,14 @@ class InMemoryPackageIndex {
122132 }
123133
124134 PackageSearchResult search (ServiceSearchQuery query) {
135+ // prevent any work if offset is outside of the range
136+ if ((query.offset ?? 0 ) > _documents.length) {
137+ return PackageSearchResult (
138+ timestamp: clock.now (),
139+ totalCount: 0 ,
140+ packageHits: [],
141+ );
142+ }
125143 return _scorePool.withScore (
126144 value: 1.0 ,
127145 fn: (score) {
@@ -144,8 +162,49 @@ class InMemoryPackageIndex {
144162 final combinedTagsPredicate =
145163 query.tagsPredicate.appendPredicate (query.parsedQuery.tagsPredicate);
146164 if (combinedTagsPredicate.isNotEmpty) {
147- packageScores.retainWhere (
148- (i, _) => combinedTagsPredicate.matches (_documents[i].tagsForLookup));
165+ // The list of predicate tag entries, converted to tag IDs (or -1 if there is no indexed tag),
166+ // sorted by their id.
167+ final entriesToCheck = combinedTagsPredicate.entries
168+ .map ((e) => MapEntry (_tagIds[e.key] ?? - 1 , e.value))
169+ .toList ()
170+ ..sort ((a, b) => a.key.compareTo (b.key));
171+
172+ packageScores.retainWhere ((docIndex, _) {
173+ // keeping track of tag id iteration with the `nextTagIndex`
174+ final tagIds = _documentTagIds[docIndex];
175+ var nextTagIndex = 0 ;
176+
177+ for (final entry in entriesToCheck) {
178+ if (entry.key == - 1 ) {
179+ // no tag id is present for this predicate
180+ if (entry.value) {
181+ // the predicate is required, no document will match it
182+ return false ;
183+ } else {
184+ // the predicate is prohibited, no document has it, always a match
185+ continue ;
186+ }
187+ }
188+
189+ // skipping the present tag ids until the currently matched predicate tag id
190+ while (nextTagIndex < tagIds.length &&
191+ tagIds[nextTagIndex] < entry.key) {
192+ nextTagIndex++ ;
193+ }
194+
195+ // checking presence
196+ late bool present;
197+ if (nextTagIndex == tagIds.length) {
198+ present = false ;
199+ } else {
200+ present = tagIds[nextTagIndex] == entry.key;
201+ }
202+
203+ if (entry.value && ! present) return false ;
204+ if (! entry.value && present) return false ;
205+ }
206+ return true ;
207+ });
149208 }
150209
151210 // filter on dependency
@@ -213,10 +272,12 @@ class InMemoryPackageIndex {
213272 /// it linearly into the [0.4-1.0] range, to allow better
214273 /// multiplication outcomes.
215274 packageScores.multiplyAllFromValues (_adjustedOverallScores);
216- indexedHits = _rankWithValues (packageScores);
275+ indexedHits = _rankWithValues (packageScores,
276+ requiredLengthThreshold: query.offset);
217277 break ;
218278 case SearchOrder .text:
219- indexedHits = _rankWithValues (packageScores);
279+ indexedHits = _rankWithValues (packageScores,
280+ requiredLengthThreshold: query.offset);
220281 break ;
221282 case SearchOrder .created:
222283 indexedHits = _createdOrderedHits.whereInScores (packageScores);
@@ -395,21 +456,29 @@ class InMemoryPackageIndex {
395456 return null ;
396457 }
397458
398- List <IndexedPackageHit > _rankWithValues (IndexedScore <String > score) {
459+ List <IndexedPackageHit > _rankWithValues (
460+ IndexedScore <String > score, {
461+ // if the item count is fewer than this threshold, an empty list will be returned
462+ int ? requiredLengthThreshold,
463+ }) {
399464 final list = < IndexedPackageHit > [];
400465 for (var i = 0 ; i < score.length; i++ ) {
401466 final value = score.getValue (i);
402467 if (value <= 0.0 ) continue ;
403468 list.add (IndexedPackageHit (
404469 i, PackageHit (package: score.keys[i], score: value)));
405470 }
471+ if ((requiredLengthThreshold ?? 0 ) > list.length) {
472+ // There is no point to sort or even keep the results, as the search query offset ignores these anyway.
473+ return [];
474+ }
406475 list.sort ((a, b) {
407476 final scoreCompare = - a.hit.score! .compareTo (b.hit.score! );
408477 if (scoreCompare != 0 ) return scoreCompare;
409478 // if two packages got the same score, order by last updated
410479 return _compareUpdated (_documents[a.index], _documents[b.index]);
411480 });
412- return list. toList () ;
481+ return list;
413482 }
414483
415484 List <IndexedPackageHit > _rankWithComparator (
0 commit comments