diff --git a/app/bin/tools/search_benchmark.dart b/app/bin/tools/search_benchmark.dart index 1b4394fb79..c13e67f3ae 100644 --- a/app/bin/tools/search_benchmark.dart +++ b/app/bin/tools/search_benchmark.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:_pub_shared/search/search_form.dart'; import 'package:pub_dev/package/overrides.dart'; import 'package:pub_dev/search/mem_index.dart'; import 'package:pub_dev/search/models.dart'; @@ -24,6 +25,7 @@ Future main(List args) async { // NOTE: please add more queries to this list, especially if there is a performance bottleneck. final queries = [ + 'chart', 'json', 'camera', 'android camera', @@ -33,7 +35,10 @@ Future main(List args) async { final sw = Stopwatch()..start(); var count = 0; for (var i = 0; i < 100; i++) { - index.search(ServiceSearchQuery.parse(query: queries[i % queries.length])); + index.search(ServiceSearchQuery.parse( + query: queries[i % queries.length], + tagsPredicate: TagsPredicate.regularSearch(), + )); count++; } sw.stop(); diff --git a/app/lib/search/mem_index.dart b/app/lib/search/mem_index.dart index 6a32afc9ca..dbc29214fa 100644 --- a/app/lib/search/mem_index.dart +++ b/app/lib/search/mem_index.dart @@ -28,6 +28,8 @@ class InMemoryPackageIndex { late final TokenIndex _readmeIndex; late final TokenIndex _apiSymbolIndex; late final _scorePool = ScorePool(_packageNameIndex._packageNames); + final _tagIds = {}; + final _documentTagIds = >[]; /// Adjusted score takes the overall score and transforms /// it linearly into the [0.4-1.0] range. @@ -58,6 +60,14 @@ class InMemoryPackageIndex { final doc = _documents[i]; _documentsByName[doc.package] = doc; + // transform tags into numberical IDs + final tagIds = []; + for (final tag in doc.tags) { + tagIds.add(_tagIds.putIfAbsent(tag, () => _tagIds.length)); + } + tagIds.sort(); + _documentTagIds.add(tagIds); + final apiDocPages = doc.apiDocPages; if (apiDocPages != null) { for (final page in apiDocPages) { @@ -144,8 +154,49 @@ class InMemoryPackageIndex { final combinedTagsPredicate = query.tagsPredicate.appendPredicate(query.parsedQuery.tagsPredicate); if (combinedTagsPredicate.isNotEmpty) { - packageScores.retainWhere( - (i, _) => combinedTagsPredicate.matches(_documents[i].tagsForLookup)); + // The list of predicate tag entries, converted to tag IDs (or -1 if there is no indexed tag), + // sorted by their id. + final entriesToCheck = combinedTagsPredicate.entries + .map((e) => MapEntry(_tagIds[e.key] ?? -1, e.value)) + .toList() + ..sort((a, b) => a.key.compareTo(b.key)); + + packageScores.retainWhere((docIndex, _) { + // keeping track of tag id iteration with the `nextTagIndex` + final tagIds = _documentTagIds[docIndex]; + var nextTagIndex = 0; + + for (final entry in entriesToCheck) { + if (entry.key == -1) { + // no tag id is present for this predicate + if (entry.value) { + // the predicate is required, no document will match it + return false; + } else { + // the predicate is prohibited, no document has it, always a match + continue; + } + } + + // skipping the present tag ids until the currently matched predicate tag id + while (nextTagIndex < tagIds.length && + tagIds[nextTagIndex] < entry.key) { + nextTagIndex++; + } + + // checking presence + late bool present; + if (nextTagIndex == tagIds.length) { + present = false; + } else { + present = tagIds[nextTagIndex] == entry.key; + } + + if (entry.value && !present) return false; + if (!entry.value && present) return false; + } + return true; + }); } // filter on dependency diff --git a/app/lib/search/search_service.dart b/app/lib/search/search_service.dart index 9a5eaf83d7..74e50da4f2 100644 --- a/app/lib/search/search_service.dart +++ b/app/lib/search/search_service.dart @@ -135,9 +135,6 @@ class PackageDocument { Map toJson() => _$PackageDocumentToJson(this); - @JsonKey(includeFromJson: false, includeToJson: false) - late final Set tagsForLookup = Set.of(tags); - late final packageNameLowerCased = package.toLowerCase(); } diff --git a/pkg/_pub_shared/lib/search/search_form.dart b/pkg/_pub_shared/lib/search/search_form.dart index a773152e35..dbabd3cb6d 100644 --- a/pkg/_pub_shared/lib/search/search_form.dart +++ b/pkg/_pub_shared/lib/search/search_form.dart @@ -131,6 +131,8 @@ class TagsPredicate { bool get isEmpty => _values.isEmpty; bool get isNotEmpty => _values.isNotEmpty; + Iterable> get entries => _values.entries; + bool isRequiredTag(String tag) => _values[tag] == true; bool isProhibitedTag(String tag) => _values[tag] == false; bool hasTag(String tag) => _values.containsKey(tag); @@ -187,19 +189,6 @@ class TagsPredicate { return p; } - /// Evaluate this predicate against the list of supplied [tags]. - /// Returns true if the predicate matches the [tags], false otherwise. - bool matches(Iterable tags) { - for (final entry in _values.entries) { - final tag = entry.key; - final required = entry.value; - final present = tags.contains(tag); - if (required && !present) return false; - if (!required && present) return false; - } - return true; - } - /// Toggles [tag] between required and absent status. TagsPredicate toggleRequired(String tag) { final current = _values[tag];