Skip to content

Commit 3f2c87f

Browse files
authored
Improve name search by changing Set.contains into counting. (dart-lang#9458)
1 parent a054651 commit 3f2c87f

3 files changed

Lines changed: 87 additions & 39 deletions

File tree

app/bin/tools/search_benchmark.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,30 @@ Future<void> main(List<String> args) async {
2525

2626
// NOTE: please add more queries to this list, especially if there is a performance bottleneck.
2727
final queries = [
28+
// tag only
2829
'sdk:dart',
2930
'sdk:flutter platform:android',
3031
'is:flutter-favorite',
32+
// frequent substring
33+
'a',
34+
'er',
35+
// frequently searched for
3136
'chart',
3237
'json',
3338
'camera',
39+
// frequent words of different length
40+
'dart',
41+
'flutter',
42+
'development',
43+
// multi-word expressions
3444
'android camera',
3545
'sql database',
46+
'to help speed up',
3647
];
3748

3849
final sw = Stopwatch()..start();
3950
var count = 0;
40-
for (var i = 0; i < 100; i++) {
51+
for (var i = 0; i < 100 || sw.elapsed.inMinutes < 2; i++) {
4152
await index.search(
4253
ServiceSearchQuery.parse(
4354
query: queries[i % queries.length],

app/lib/search/mem_index.dart

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ class InMemoryPackageIndex {
465465
for (final word in words) {
466466
await _scorePool.withPoolItem(
467467
fn: (wordScore) async {
468-
_packageNameIndex.searchWord(
468+
_packageNameIndex._searchWord(
469469
word,
470470
score: wordScore,
471471
filterOnNonZeros: packageScores,
@@ -659,17 +659,30 @@ class PackageNameIndex {
659659
/// Maps the collapsed name to all the original names (e.g. `asyncmap`=> [`async_map`, `as_y_n_cmaP`]).
660660
late final Map<String, List<String>> _collapsedNameResolvesToMap;
661661

662+
/// Inverted trigram index: maps each trigram to the package indexes (ascending),
663+
/// whose collapsed name contains that trigram.
664+
late final Map<String, List<int>> _trigramPostings;
665+
666+
// TODO(https://github.com/dart-lang/pub-dev/issues/9462): consider int-based counter implementation
667+
late final _counterPool = ScorePool(_packageNames.length);
668+
662669
PackageNameIndex(this._packageNames) {
663670
_data = _packageNames.map((package) {
664671
final lowercased = package.toLowerCase();
665672
final collapsed = _removeUnderscores(lowercased);
666-
return _PkgNameData(lowercased, collapsed, trigrams(collapsed).toSet());
673+
return _PkgNameData(lowercased, collapsed);
667674
}).toList();
668675
_collapsedNameResolvesToMap = {};
676+
_trigramPostings = {};
669677
for (var i = 0; i < _data.length; i++) {
670678
_collapsedNameResolvesToMap
671679
.putIfAbsent(_data[i].collapsed, () => [])
672680
.add(_packageNames[i]);
681+
// Register only a single trigram -> document index posting.
682+
// TODO(https://github.com/dart-lang/pub-dev/issues/9462): consider posting weights
683+
for (final trigram in trigrams(_data[i].collapsed).toSet()) {
684+
_trigramPostings.putIfAbsent(trigram, () => <int>[]).add(i);
685+
}
673686
}
674687
}
675688

@@ -681,7 +694,7 @@ class PackageNameIndex {
681694
IndexedScore? score;
682695
for (final w in splitForQuery(text)) {
683696
final s = IndexedScore(_packageNames.length);
684-
searchWord(w, score: s, filterOnNonZeros: score);
697+
_searchWord(w, score: s, filterOnNonZeros: score);
685698
if (score == null) {
686699
score = s;
687700
} else {
@@ -702,59 +715,73 @@ class PackageNameIndex {
702715
///
703716
/// When [filterOnNonZeros] is present, only the indexes with an already
704717
/// non-zero value are evaluated.
705-
void searchWord(
718+
void _searchWord(
706719
String word, {
707720
required IndexedScore score,
708721
IndexedScore? filterOnNonZeros,
709722
}) {
710723
assert(score.length == _packageNames.length);
711724
final lowercasedWord = word.toLowerCase();
712725
final collapsedWord = _removeUnderscores(lowercasedWord);
713-
final parts = collapsedWord.length <= 3
714-
? [collapsedWord]
715-
: trigrams(collapsedWord);
716-
for (var i = 0; i < _data.length; i++) {
717-
if (filterOnNonZeros?.isNotPositive(i) ?? false) {
718-
continue;
719-
}
720726

721-
final entry = _data[i];
727+
bool tryScoreWithSubstringMatch(int index) {
728+
final entry = _data[index];
722729
if (entry.collapsed.length >= collapsedWord.length &&
723730
entry.collapsed.contains(collapsedWord)) {
724731
// also check for non-collapsed match
725732
if (entry.lowercased.length >= lowercasedWord.length &&
726733
entry.lowercased.contains(lowercasedWord)) {
727-
score.setValue(i, 1.0);
728-
continue;
734+
score.setValue(index, 1.0);
735+
} else {
736+
score.setValue(index, 0.99);
729737
}
738+
return true;
739+
}
740+
return false;
741+
}
730742

731-
score.setValue(i, 0.99);
732-
continue;
743+
// Note: short strings (less than a trigram) are not indexed separately,
744+
// we need to do full substring check, evaluating every package.
745+
// TODO(https://github.com/dart-lang/pub-dev/issues/9462): consider updating the index + the evaluation algorithm
746+
if (collapsedWord.length < 3) {
747+
for (var i = 0; i < _data.length; i++) {
748+
if (filterOnNonZeros?.isNotPositive(i) ?? false) continue;
749+
tryScoreWithSubstringMatch(i);
733750
}
734-
var matched = 0;
735-
var unmatched = 0;
736-
final acceptThreshold = parts.length ~/ 2;
737-
final rejectThreshold = parts.length - acceptThreshold;
738-
for (final part in parts) {
739-
if (entry.trigrams.contains(part)) {
740-
matched++;
741-
} else {
742-
unmatched++;
743-
if (unmatched > rejectThreshold) {
744-
// we have no chance to accept this hit
745-
break;
751+
return;
752+
}
753+
754+
_counterPool.withPoolItemSync(
755+
fn: (countScore) {
756+
assert(countScore.length == _packageNames.length);
757+
758+
// count the trigrams for each package
759+
final parts = trigrams(collapsedWord);
760+
for (final part in parts) {
761+
final postings = _trigramPostings[part];
762+
if (postings == null) continue;
763+
for (final i in postings) {
764+
final count = countScore.getValue(i);
765+
countScore.setValue(i, count + 1.0);
746766
}
747767
}
748-
}
749768

750-
if (matched >= acceptThreshold) {
751-
// making sure that match score is minimum 0.5
752-
final v = matched / parts.length;
753-
if (v >= 0.5) {
754-
score.setValue(i, v);
769+
final acceptThreshold = parts.length ~/ 2;
770+
for (var i = 0; i < _packageNames.length; i++) {
771+
if (filterOnNonZeros?.isNotPositive(i) ?? false) continue;
772+
if (countScore.isNotPositive(i)) continue;
773+
if (tryScoreWithSubstringMatch(i)) continue;
774+
final matched = countScore.getValue(i);
775+
if (matched >= acceptThreshold) {
776+
// making sure that match score is minimum 0.5
777+
final v = matched / parts.length;
778+
if (v >= 0.5) {
779+
score.setValue(i, v);
780+
}
781+
}
755782
}
756-
}
757-
}
783+
},
784+
);
758785
}
759786

760787
/// Returns the list of package names where the collapsed name matches.
@@ -770,9 +797,8 @@ class PackageNameIndex {
770797
class _PkgNameData {
771798
final String lowercased;
772799
final String collapsed;
773-
final Set<String> trigrams;
774800

775-
_PkgNameData(this.lowercased, this.collapsed, this.trigrams);
801+
_PkgNameData(this.lowercased, this.collapsed);
776802
}
777803

778804
extension on List<IndexedPackageHit> {

app/lib/search/token_index.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,17 @@ abstract class _AllocationPool<T> {
374374
}
375375
}
376376

377+
/// Executes synchronous [fn] and provides a pool item in the callback.
378+
/// The item will be released to the pool after [fn] completes.
379+
R withPoolItemSync<R>({required R Function(T array) fn}) {
380+
final item = _acquire();
381+
try {
382+
return fn(item);
383+
} finally {
384+
_release(item);
385+
}
386+
}
387+
377388
/// Executes [fn] and provides a getter function that can be used to
378389
/// acquire new pool items while the [fn] is being executed. The
379390
/// acquired items will be released back to the pool after [fn] completes.

0 commit comments

Comments
 (0)