Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/bin/tools/search_benchmark.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,6 +25,7 @@ Future<void> main(List<String> args) async {

// NOTE: please add more queries to this list, especially if there is a performance bottleneck.
final queries = [
'chart',
'json',
'camera',
'android camera',
Expand All @@ -33,7 +35,10 @@ Future<void> main(List<String> 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();
Expand Down
55 changes: 53 additions & 2 deletions app/lib/search/mem_index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class InMemoryPackageIndex {
late final TokenIndex<String> _readmeIndex;
late final TokenIndex<IndexedApiDocPage> _apiSymbolIndex;
late final _scorePool = ScorePool(_packageNameIndex._packageNames);
final _tagIds = <String, int>{};
final _documentTagIds = <List<int>>[];

/// Adjusted score takes the overall score and transforms
/// it linearly into the [0.4-1.0] range.
Expand Down Expand Up @@ -58,6 +60,14 @@ class InMemoryPackageIndex {
final doc = _documents[i];
_documentsByName[doc.package] = doc;

// transform tags into numberical IDs
final tagIds = <int>[];
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) {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions app/lib/search/search_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ class PackageDocument {

Map<String, dynamic> toJson() => _$PackageDocumentToJson(this);

@JsonKey(includeFromJson: false, includeToJson: false)
late final Set<String> tagsForLookup = Set.of(tags);

late final packageNameLowerCased = package.toLowerCase();
}

Expand Down
15 changes: 2 additions & 13 deletions pkg/_pub_shared/lib/search/search_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ class TagsPredicate {
bool get isEmpty => _values.isEmpty;
bool get isNotEmpty => _values.isNotEmpty;

Iterable<MapEntry<String, bool>> 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);
Expand Down Expand Up @@ -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<String> 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];
Expand Down
Loading