@@ -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 {
770797class _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
778804extension on List <IndexedPackageHit > {
0 commit comments