Skip to content

Commit 8bd3274

Browse files
authored
Refactor search + better early return when there are no hits. (#8805)
1 parent a1e832f commit 8bd3274

File tree

3 files changed

+70
-45
lines changed

3 files changed

+70
-45
lines changed

app/lib/search/mem_index.dart

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class InMemoryPackageIndex {
138138

139139
PackageSearchResult search(ServiceSearchQuery query) {
140140
// prevent any work if offset is outside of the range
141-
if ((query.offset ?? 0) >= _documents.length) {
141+
if (query.offset >= _documents.length) {
142142
return PackageSearchResult.empty();
143143
}
144144
return _bitArrayPool.withPoolItem(fn: (array) {
@@ -150,11 +150,10 @@ class InMemoryPackageIndex {
150150
});
151151
}
152152

153-
PackageSearchResult _search(
154-
ServiceSearchQuery query,
155-
BitArray packages,
156-
IndexedScore<String> packageScores,
157-
) {
153+
/// Filters on query predicates with fast bitmap operations.
154+
/// Returns 0 if no document matches the query, or the number
155+
/// of documents that matches the predicates so far.
156+
int _filterOnPredicates(ServiceSearchQuery query, BitArray packages) {
158157
// filter on tags
159158
final combinedTagsPredicate =
160159
query.tagsPredicate.appendPredicate(query.parsedQuery.tagsPredicate);
@@ -164,7 +163,7 @@ class InMemoryPackageIndex {
164163
if (entry.value) {
165164
if (tagBits == null) {
166165
// the predicate is not matched by any document
167-
return PackageSearchResult.empty();
166+
return 0;
168167
}
169168
packages.and(tagBits);
170169
} else {
@@ -215,6 +214,19 @@ class InMemoryPackageIndex {
215214
(i) => now.difference(_documents[i].updated) > updatedDuration);
216215
}
217216

217+
return packages.cardinality;
218+
}
219+
220+
PackageSearchResult _search(
221+
ServiceSearchQuery query,
222+
BitArray packages,
223+
IndexedScore<String> packageScores,
224+
) {
225+
final predicateFilterCount = _filterOnPredicates(query, packages);
226+
if (predicateFilterCount <= query.offset) {
227+
return PackageSearchResult.empty();
228+
}
229+
218230
// TODO: find a better way to handle predicate-only filtering and scoring
219231
for (final index in packages.asIntIterable()) {
220232
if (index >= _documents.length) break;
@@ -230,32 +242,7 @@ class InMemoryPackageIndex {
230242
textMatchExtent: query.textMatchExtent ?? TextMatchExtent.api,
231243
);
232244

233-
String? bestNameMatch;
234-
if (parsedQueryText != null &&
235-
query.parsedQuery.hasOnlyFreeText &&
236-
query.isNaturalOrder) {
237-
// exact package name
238-
if (_documentsByName.containsKey(parsedQueryText)) {
239-
bestNameMatch = parsedQueryText;
240-
} else {
241-
// reduced package name match
242-
final matches = _packageNameIndex.lookupMatchingNames(parsedQueryText);
243-
if (matches != null && matches.isNotEmpty) {
244-
bestNameMatch = matches.length == 1
245-
? matches.single
246-
:
247-
// Note: to keep it simple, we select the most downloaded one from competing matches.
248-
matches.reduce((a, b) {
249-
if (_documentsByName[a]!.downloadCount >
250-
_documentsByName[b]!.downloadCount) {
251-
return a;
252-
} else {
253-
return b;
254-
}
255-
});
256-
}
257-
}
258-
}
245+
final bestNameMatch = _bestNameMatch(query);
259246

260247
List<IndexedPackageHit> indexedHits;
261248
switch (query.effectiveOrder ?? SearchOrder.top) {
@@ -337,6 +324,42 @@ class InMemoryPackageIndex {
337324
);
338325
}
339326

327+
/// Returns the package name that is considered as the best name match
328+
/// for the [query], or `null` if there is no such package name, or the
329+
/// match is not enabled for the given context (e.g. non-default ordering).
330+
String? _bestNameMatch(ServiceSearchQuery query) {
331+
final parsedQueryText = query.parsedQuery.text;
332+
if (parsedQueryText == null) {
333+
return null;
334+
}
335+
if (!query.parsedQuery.hasOnlyFreeText || !query.isNaturalOrder) {
336+
return null;
337+
}
338+
339+
// exact package name
340+
if (_documentsByName.containsKey(parsedQueryText)) {
341+
return parsedQueryText;
342+
}
343+
344+
// reduced package name match
345+
final matches = _packageNameIndex.lookupMatchingNames(parsedQueryText);
346+
if (matches == null || matches.isEmpty) {
347+
return null;
348+
}
349+
if (matches.length == 1) {
350+
return matches.single;
351+
}
352+
// Note: to keep it simple, we select the most downloaded one from competing matches.
353+
return matches.reduce((a, b) {
354+
if (_documentsByName[a]!.downloadCount >
355+
_documentsByName[b]!.downloadCount) {
356+
return a;
357+
} else {
358+
return b;
359+
}
360+
});
361+
}
362+
340363
/// Update the overall score both on [PackageDocument] and in the [_adjustedOverallScores] map.
341364
void _updateOverallScores() {
342365
_adjustedOverallScores = _documents.map((doc) {

app/lib/search/search_service.dart

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ class ServiceSearchQuery {
167167

168168
/// The value of the `sort` URL query parameter.
169169
final SearchOrder? order;
170-
final int? offset;
171-
final int? limit;
170+
final int offset;
171+
final int limit;
172172

173173
/// The scope/depth of text matching.
174174
final TextMatchExtent? textMatchExtent;
@@ -179,10 +179,12 @@ class ServiceSearchQuery {
179179
String? publisherId,
180180
required this.minPoints,
181181
this.order,
182-
this.offset,
183-
this.limit,
182+
int? offset,
183+
int? limit,
184184
this.textMatchExtent,
185-
}) : parsedQuery = ParsedQueryText.parse(query),
185+
}) : offset = max(0, offset ?? 0),
186+
limit = max(_minSearchLimit, limit ?? 10),
187+
parsedQuery = ParsedQueryText.parse(query),
186188
tagsPredicate = tagsPredicate ?? TagsPredicate(),
187189
publisherId = publisherId?.trimToNull();
188190

@@ -232,8 +234,8 @@ class ServiceSearchQuery {
232234
publisherId: publisherId,
233235
order: order,
234236
minPoints: minPoints,
235-
offset: max(0, offset),
236-
limit: max(_minSearchLimit, limit),
237+
offset: offset,
238+
limit: limit,
237239
textMatchExtent: textMatchExtent,
238240
);
239241
}
@@ -264,10 +266,10 @@ class ServiceSearchQuery {
264266
'q': query,
265267
'tags': tagsPredicate.toQueryParameters(),
266268
'publisherId': publisherId,
267-
'offset': offset?.toString(),
269+
'offset': offset.toString(),
268270
if (minPoints != null && minPoints! > 0)
269271
'minPoints': minPoints.toString(),
270-
'limit': limit?.toString(),
272+
'limit': limit.toString(),
271273
'order': order?.name,
272274
if (textMatchExtent != null) 'textMatchExtent': textMatchExtent!.name,
273275
};

app/lib/shared/utils.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,16 @@ String contentType(String name) {
155155
}
156156

157157
/// Returns a subset of the list, bounded by [offset] and [limit].
158-
List<T> boundedList<T>(List<T> list, {int? offset, int? limit}) {
158+
List<T> boundedList<T>(List<T> list, {int offset = 0, int limit = 0}) {
159159
Iterable<T> iterable = list;
160-
if (offset != null && offset > 0) {
160+
if (offset > 0) {
161161
if (offset >= list.length) {
162162
return <T>[];
163163
} else {
164164
iterable = iterable.skip(offset);
165165
}
166166
}
167-
if (limit != null && limit > 0) {
167+
if (limit > 0) {
168168
iterable = iterable.take(limit);
169169
}
170170
return iterable.toList();

0 commit comments

Comments
 (0)