Skip to content

Commit 81a3421

Browse files
authored
Quantized score collection (dart-lang#9471)
1 parent 19ea062 commit 81a3421

7 files changed

Lines changed: 76 additions & 58 deletions

File tree

app/lib/search/mem_index.dart

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import 'token_index.dart';
2222
final _logger = Logger('search.mem_index');
2323
final _textSearchTimeout = Duration(milliseconds: 500);
2424

25+
// Quantized equivalent of the `0.01` score.
26+
const _apiScoreCutoff = scoreQuantization ~/ 100;
27+
2528
class InMemoryPackageIndex {
2629
final List<PackageDocument> _documents;
2730
final _nameToIndex = <String, int>{};
@@ -37,8 +40,8 @@ class InMemoryPackageIndex {
3740
final _tagBitArrays = <String, BitArray>{};
3841

3942
/// Adjusted score takes the overall score and transforms
40-
/// it linearly into the [0.4-1.0] range.
41-
late final List<double> _adjustedOverallScores;
43+
/// it linearly into the [0.4-1.0] range, stored with [scoreQuantization].
44+
late final List<int> _adjustedOverallScores;
4245
late final List<IndexedPackageHit> _overallOrderedHits;
4346
late final List<IndexedPackageHit> _createdOrderedHits;
4447
late final List<IndexedPackageHit> _updatedOrderedHits;
@@ -294,7 +297,7 @@ class InMemoryPackageIndex {
294297
}
295298
// Check whether the best name match will increase the total item count.
296299
if (bestNameIndex != null &&
297-
packageScores.getValue(bestNameIndex) <= 0.0) {
300+
packageScores.isNotPositive(bestNameIndex)) {
298301
totalCount++;
299302
}
300303
indexedHits = _rankWithValues(
@@ -333,7 +336,7 @@ class InMemoryPackageIndex {
333336
packageHits = indexedHits.map((ps) {
334337
final apiPages = textResults!.topApiPages?[ps.index]
335338
// TODO(https://github.com/dart-lang/pub-dev/issues/7106): extract title for the page
336-
?.map((MapEntry<String, double> e) => ApiPageRef(path: e.key))
339+
?.map((MapEntry<String, int> e) => ApiPageRef(path: e.key))
337340
.toList();
338341
return ps.hit.change(apiPages: apiPages);
339342
}).toList();
@@ -418,8 +421,8 @@ class InMemoryPackageIndex {
418421

419422
final overall = popularityScore * 0.5 + pointScore * 0.5;
420423
doc.overallScore = overall;
421-
// adding a base score prevents later multiplication with zero
422-
return 0.4 + 0.6 * overall;
424+
// adding a base score prevents later multiplication with zero (quantized)
425+
return ((0.4 + 0.6 * overall) * scoreQuantization).round();
423426
}).toList();
424427
}
425428

@@ -445,7 +448,7 @@ class InMemoryPackageIndex {
445448

446449
for (final index in packages.asIntIterable()) {
447450
if (index >= _documents.length) break;
448-
packageScores.setValue(index, 1.0);
451+
packageScores.setValue(index, scoreQuantization);
449452
}
450453

451454
bool aborted = false;
@@ -479,7 +482,7 @@ class InMemoryPackageIndex {
479482
);
480483
}
481484

482-
final topApiPages = List<List<MapEntry<String, double>>?>.filled(
485+
final topApiPages = List<List<MapEntry<String, int>>?>.filled(
483486
_documents.length,
484487
null,
485488
);
@@ -492,14 +495,13 @@ class InMemoryPackageIndex {
492495
) async {
493496
for (var i = 0; i < symbolPages.length; i++) {
494497
final value = symbolPages.getValue(i);
495-
if (value < 0.01) continue;
498+
if (value < _apiScoreCutoff) continue;
496499

497500
final doc = _apiDocPageKeys[i];
498501
if (!packages[doc.index]) continue;
499502

500503
// skip if the previously found pages are better than the current one
501-
final pages = topApiPages[doc.index] ??=
502-
<MapEntry<String, double>>[];
504+
final pages = topApiPages[doc.index] ??= <MapEntry<String, int>>[];
503505
if (pages.length >= maxApiPageCount && pages.last.value > value) {
504506
continue;
505507
}
@@ -564,7 +566,7 @@ class InMemoryPackageIndex {
564566
});
565567
for (var i = 0; i < score.length; i++) {
566568
final value = score.getValue(i);
567-
if (value <= 0.0 && i != bestNameIndex) continue;
569+
if (value <= 0 && i != bestNameIndex) continue;
568570
heap.collect(i);
569571
}
570572
return heap
@@ -574,7 +576,7 @@ class InMemoryPackageIndex {
574576
i,
575577
PackageHit(
576578
package: _documents[i].package,
577-
score: score.getValue(i),
579+
score: score.getValue(i) / scoreQuantization,
578580
),
579581
),
580582
);
@@ -640,7 +642,7 @@ class InMemoryPackageIndex {
640642

641643
class _TextResults {
642644
final bool hasNoMatch;
643-
final List<List<MapEntry<String, double>>?>? topApiPages;
645+
final List<List<MapEntry<String, int>>?>? topApiPages;
644646
final String? errorMessage;
645647

646648
factory _TextResults.empty({String? errorMessage}) {
@@ -710,12 +712,13 @@ class PackageNameIndex {
710712
}
711713

712714
/// Score assigned when the query n-gram(s) occur in the original (underscored)
713-
/// package name.
714-
static const _originalNameScore = 1.0;
715+
/// package name (1.0 quantized).
716+
static const _originalNameScore = scoreQuantization;
715717

716718
/// Score assigned when the query n-gram(s) occur only in the collapsed name,
717-
/// i.e. the match spans a removed underscore.
718-
static const _collapsedNameScore = _collapsedNameWeight / _originalNameWeight;
719+
/// i.e. the match spans a removed underscore (0.99 quantized).
720+
static final _collapsedNameScore =
721+
(_collapsedNameWeight * scoreQuantization / _originalNameWeight).round();
719722

720723
/// Posting weight bytes: the match score scaled by 100. Storing them as small
721724
/// integers lets the trigram counter sum the weights and recognize a perfect
@@ -742,7 +745,9 @@ class PackageNameIndex {
742745
final result = <String, double>{};
743746
for (var i = 0; i < _packageNames.length; i++) {
744747
final v = score.getValue(i);
745-
if (v > 0.0) result[_packageNames[i]] = v;
748+
if (v > 0) {
749+
result[_packageNames[i]] = v / scoreQuantization;
750+
}
746751
}
747752
return result;
748753
}
@@ -786,7 +791,9 @@ class PackageNameIndex {
786791
for (final part in parts) {
787792
final postings = _ngramPostings[part];
788793
if (postings == null) continue;
789-
postings.forEach((i, weight) => counts.add(i, weight));
794+
postings.forEach((i, weight) {
795+
counts.add(i, weight);
796+
});
790797
}
791798

792799
final n = parts.length;
@@ -804,7 +811,7 @@ class PackageNameIndex {
804811
score.setValue(i, _originalNameScore);
805812
} else {
806813
// Partial match: fractional relevance score in [0.5, 1.0).
807-
score.setValue(i, sum / fullMatchSum);
814+
score.setValue(i, sum * scoreQuantization ~/ fullMatchSum);
808815
}
809816
}
810817
},

app/lib/search/sdk_mem_index.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const _flutterLibraryAllowlist = {
5959
final _logger = Logger('search.dart_sdk_mem_index');
6060
final _dartUri = Uri.parse('https://api.dart.dev/stable/latest/');
6161
final _flutterUri = Uri.parse('https://api.flutter.dev/flutter/');
62+
final _top3ApiScoreCutoff = (0.05 * scoreQuantization).round();
6263

6364
/// Tries to load Dart and Flutter SDK's dartdoc `index.json` and build
6465
/// a search index from it.
@@ -198,9 +199,12 @@ class SdkMemIndex implements SdkIndex {
198199
words,
199200
(score) async => Map.fromEntries(
200201
score
201-
.topIndices(3, minValue: 0.05)
202+
.topIndices(3, minValue: _top3ApiScoreCutoff)
202203
.map(
203-
(i) => MapEntry(library.tokenIndexKeys[i], score.getValue(i)),
204+
(i) => MapEntry(
205+
library.tokenIndexKeys[i],
206+
score.getValue(i) / scoreQuantization,
207+
),
204208
),
205209
),
206210
);

app/lib/search/token_index.dart

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,14 @@ class TokenIndex {
329329
for (final entry in tokenMatch.entries) {
330330
final matchWeight = entry.value;
331331
final posting = _postings[entry.key]!;
332+
// Quantized weights:
333+
// - match weight is between 0-256 (0 <= matchWeight * weight <= 1)
334+
// - posting weight is between 0-255
335+
//
336+
// Their multiplication is between 0 and [scoreQuantization].
337+
final weightQ = (matchWeight * weight * 256).round();
332338
posting.forEach((docIndex, w) {
333-
score.setValueMaxOf(docIndex, matchWeight * (w + 1) / 256 * weight);
339+
score.setValueMaxOf(docIndex, (w + 1) * weightQ);
334340
});
335341
}
336342
}
@@ -413,11 +419,7 @@ abstract class _AllocationPool<T> {
413419
/// A reusable pool for [IndexedScore] instances to spare some memory allocation.
414420
class ScorePool extends _AllocationPool<IndexedScore> {
415421
ScorePool(int length)
416-
: super(
417-
() => IndexedScore(length),
418-
// sets all values to 0.0
419-
(score) => score._values.fillRange(0, score.length, 0.0),
420-
);
422+
: super(() => IndexedScore(length), (score) => score.reset());
421423
}
422424

423425
/// A reusable pool for [BitArray] instances to spare some memory allocation.
@@ -440,15 +442,25 @@ class IndexedCounterPool extends _AllocationPool<IndexedCounter> {
440442
);
441443
}
442444

445+
/// Fixed-point scale used by [IndexedScore] to indicate the maximum of its
446+
/// quantized value.
447+
const scoreQuantization = 65536;
448+
443449
/// Mutable score list that can accessed via integer index.
450+
///
451+
/// Values are stored as fixed-point integers at the [scoreQuantization] scale.
444452
class IndexedScore {
445-
final List<double> _values;
453+
final List<int> _values;
446454

447-
IndexedScore(int length, [double value = 0.0])
448-
: _values = List<double>.filled(length, value);
455+
IndexedScore(int length, [int value = 0])
456+
: _values = List<int>.filled(length, value);
449457

450458
late final length = _values.length;
451459

460+
void reset() {
461+
_values.fillRange(0, length, 0);
462+
}
463+
452464
int positiveCount() {
453465
var count = 0;
454466
for (var i = 0; i < length; i++) {
@@ -458,48 +470,43 @@ class IndexedScore {
458470
}
459471

460472
bool isPositive(int index) {
461-
return _values[index] > 0.0;
473+
return _values[index] > 0;
462474
}
463475

464476
bool isNotPositive(int index) {
465-
return _values[index] <= 0.0;
477+
return _values[index] <= 0;
466478
}
467479

468-
double getValue(int index) {
480+
int getValue(int index) {
469481
return _values[index];
470482
}
471483

472-
void setValue(int index, double value) {
484+
void setValue(int index, int value) {
473485
_values[index] = value;
474486
}
475487

476-
void setValueMaxOf(int index, double value) {
488+
void setValueMaxOf(int index, int value) {
477489
_values[index] = math.max(_values[index], value);
478490
}
479491

480-
/// Sets the positions greater than or equal to [start] and less than [end],
481-
/// to [fillValue].
482-
void fillRange(int start, int end, double fillValue) {
483-
assert(start <= end);
484-
if (start == end) return;
485-
_values.fillRange(start, end, fillValue);
486-
}
487-
488492
void multiplyAllFrom(IndexedScore other) {
489493
multiplyAllFromValues(other._values);
490494
}
491495

492-
void multiplyAllFromValues(List<double> values) {
496+
/// Multiplies each value with the quantized value at the same index in
497+
/// [values], keeping the result at the [scoreQuantization] scale.
498+
void multiplyAllFromValues(List<int> values) {
493499
assert(_values.length == values.length);
494500
for (var i = 0; i < _values.length; i++) {
495-
if (_values[i] == 0.0) continue;
496-
final v = values[i];
497-
_values[i] = v == 0.0 ? 0.0 : _values[i] * v;
501+
final a = _values[i];
502+
if (a == 0) continue;
503+
final b = values[i];
504+
_values[i] = b == 0 ? 0 : (a * b) ~/ scoreQuantization;
498505
}
499506
}
500507

501-
List<int> topIndices(int count, {double? minValue}) {
502-
minValue ??= 0.0;
508+
List<int> topIndices(int count, {int? minValue}) {
509+
minValue ??= 0;
503510
final heap = Heap<int>((a, b) => -_values[a].compareTo(_values[b]));
504511
for (var i = 0; i < length; i++) {
505512
final v = _values[i];

app/test/search/api_doc_page_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void main() {
6161
{'package': 'foo', 'score': 1.0}, // finds package name
6262
{
6363
'package': 'other_with_api',
64-
'score': closeTo(0.695, 0.001), // finds foo method
64+
'score': closeTo(0.694, 0.002), // finds foo method
6565
'apiPages': [
6666
{'path': 'main.html'},
6767
],

app/test/search/package_name_index_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ void main() {
8383
test('substring: entu', () {
8484
expect(index.search('entu'), {
8585
'fluent': 0.5,
86-
'fluent_ui': 0.995, // not 1.0
86+
'fluent_ui': closeTo(0.995, 0.001), // not 1.0
8787
});
8888
});
8989
});

app/test/search/stack_trace_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ void main() {
3838
'totalCount': 1,
3939
'sdkLibraryHits': [],
4040
'packageHits': [
41-
{'package': 'stack_trace', 'score': 0.9975},
41+
{'package': 'stack_trace', 'score': closeTo(0.9975, 0.001)},
4242
],
4343
});
4444
});

app/test/search/token_index_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Future<Map<String, double>> _search(
1515
final result = <String, double>{};
1616
for (var i = 0; i < score.length; i++) {
1717
final v = score.getValue(i);
18-
if (v > 0.0) result[keys[i]] = v;
18+
if (v > 0) result[keys[i]] = v / scoreQuantization;
1919
}
2020
return result;
2121
});
@@ -126,9 +126,9 @@ void main() {
126126
group('IndexedScore', () {
127127
const keys = ['a', 'b', 'c'];
128128
final score = IndexedScore(3)
129-
..setValue(0, 100.0)
130-
..setValue(1, 30.0)
131-
..setValue(2, 55.0);
129+
..setValue(0, 100)
130+
..setValue(1, 30)
131+
..setValue(2, 55);
132132

133133
test('topIndices', () {
134134
expect(score.topIndices(1).map((i) => keys[i]), ['a']);

0 commit comments

Comments
 (0)