Skip to content

Commit 0a4c355

Browse files
authored
Promote name matches to top position + add name match badge after the package name. (#8745)
1 parent 691a8fd commit 0a4c355

18 files changed

+74
-69
lines changed

app/lib/frontend/templates/admin.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ String renderAccountPackagesPage({
5858
searchForm: null,
5959
sdkLibraryHits: [],
6060
packageHits: packageHits,
61+
nameMatches: null,
6162
),
6263
if (nextPackage != null)
6364
d.div(

app/lib/frontend/templates/listing.dart

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
import 'dart:math';
66

77
import 'package:_pub_shared/search/search_form.dart';
8-
import 'package:collection/collection.dart';
98

109
import '../../package/search_adapter.dart';
1110
import '../../search/search_service.dart';
12-
import '../../shared/urls.dart' as urls;
1311
import '../dom/dom.dart' as d;
1412

1513
import '_consts.dart';
@@ -29,6 +27,7 @@ d.Node packageList(SearchResultPage searchResultPage) {
2927
searchForm: searchResultPage.form,
3028
sdkLibraryHits: searchResultPage.sdkLibraryHits,
3129
packageHits: searchResultPage.packageHits,
30+
nameMatches: searchResultPage.nameMatches,
3231
);
3332
}
3433

@@ -50,7 +49,6 @@ String renderPkgIndexPage(
5049
title: topPackages,
5150
messageFromBackend: searchResultPage.errorMessage,
5251
),
53-
nameMatches: _nameMatches(searchForm, searchResultPage.nameMatches),
5452
packageList: packageList(searchResultPage),
5553
pagination: searchResultPage.hasHit ? paginationNode(links) : null,
5654
openSections: openSections,
@@ -123,27 +121,3 @@ class PageLinks {
123121
return min(fromSymmetry, max(currentPage!, fromCount));
124122
}
125123
}
126-
127-
d.Node? _nameMatches(SearchForm form, List<String>? matches) {
128-
if (matches == null || matches.isEmpty) {
129-
return null;
130-
}
131-
final singular = matches.length == 1;
132-
final isExactNameMatch = singular && form.parsedQuery.text == matches.single;
133-
final nameMatchLabel = isExactNameMatch
134-
? 'Exact package name match: '
135-
: 'Matching package ${singular ? 'name' : 'names'}: ';
136-
137-
return d.p(children: [
138-
d.text(nameMatchLabel),
139-
...matches.expandIndexed((i, name) {
140-
return [
141-
if (i > 0) d.text(', '),
142-
d.a(
143-
href: urls.pkgPageUrl(name),
144-
child: d.b(text: name),
145-
),
146-
];
147-
}),
148-
]);
149-
}

app/lib/frontend/templates/package_misc.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ final flutterFavoriteBadgeNode = packageBadgeNode(
2929
),
3030
);
3131

32+
/// Renders the name match badge node, used for exact package name hits.
33+
final nameMatchBadgeNode = packageBadgeNode(
34+
label: 'Name match',
35+
color: 'name-match',
36+
);
37+
3238
/// Renders the null-safe badge used by package listing and package page.
3339
d.Node nullSafeBadgeNode({String? title}) {
3440
return packageBadgeNode(

app/lib/frontend/templates/views/pkg/index.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,12 @@ import '../../../static_files.dart';
1313
d.Node packageListingNode({
1414
required SearchForm searchForm,
1515
required d.Node listingInfo,
16-
required d.Node? nameMatches,
1716
required d.Node packageList,
1817
required d.Node? pagination,
1918
required Set<String>? openSections,
2019
}) {
2120
final innerContent = d.fragment([
2221
listingInfo,
23-
if (nameMatches != null)
24-
d.div(
25-
classes: ['listing-highlight-block'],
26-
child: nameMatches,
27-
),
2822
packageList,
2923
if (pagination != null) pagination,
3024
d.markdown('Check our help page for details on '

app/lib/frontend/templates/views/pkg/package_list.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ d.Node listOfPackagesNode({
2727
required SearchForm? searchForm,
2828
required List<SdkLibraryHit> sdkLibraryHits,
2929
required List<PackageView> packageHits,
30+
required List<String>? nameMatches,
3031
}) {
32+
final bestNameMatch =
33+
(nameMatches == null || nameMatches.isEmpty) ? null : nameMatches.first;
3134
return d.div(
3235
classes: ['packages'],
3336
children: [
3437
...sdkLibraryHits.map(_sdkLibraryItem),
35-
...packageHits.map((hit) => _packageItem(hit, searchForm: searchForm)),
38+
...packageHits.map((hit) => _packageItem(
39+
hit,
40+
searchForm: searchForm,
41+
isNameMatch: hit.name == bestNameMatch,
42+
)),
3643
imageCarousel(),
3744
],
3845
);
@@ -48,6 +55,7 @@ d.Node _sdkLibraryItem(SdkLibraryHit hit) {
4855
return _item(
4956
url: hit.url!,
5057
name: hit.library!,
58+
isNameMatch: false,
5159
newTimestamp: null,
5260
labeledScoresNode: null,
5361
description: hit.description ?? '',
@@ -71,6 +79,7 @@ d.Node _sdkLibraryItem(SdkLibraryHit hit) {
7179
d.Node _packageItem(
7280
PackageView view, {
7381
required SearchForm? searchForm,
82+
required bool isNameMatch,
7483
}) {
7584
final isFlutterFavorite = view.tags.contains(PackageTags.isFlutterFavorite);
7685
final isNullSafe = view.tags.contains(PackageVersionTags.isNullSafe);
@@ -177,6 +186,7 @@ d.Node _packageItem(
177186
screenshotDescriptions: screenshotDescriptions,
178187
url: urls.pkgPageUrl(view.name),
179188
name: view.name,
189+
isNameMatch: isNameMatch,
180190
newTimestamp: view.created,
181191
labeledScoresNode: labeledScoresNodeFromPackageView(view),
182192
description: view.ellipsizedDescription ?? '',
@@ -207,6 +217,7 @@ d.Node _item({
207217
List<d.Node> topics = const [],
208218
required String url,
209219
required String name,
220+
required bool isNameMatch,
210221
required DateTime? newTimestamp,
211222
required d.Node? labeledScoresNode,
212223
required String description,
@@ -230,6 +241,8 @@ d.Node _item({
230241
], children: [
231242
d.a(href: url, text: name),
232243
if (copyIcon != null) copyIcon,
244+
d.text(' '),
245+
if (isNameMatch) nameMatchBadgeNode,
233246
]),
234247
if (age != null && age.inDays <= 30)
235248
d.div(

app/lib/search/mem_index.dart

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final _textSearchTimeout = Duration(milliseconds: 500);
2323
class InMemoryPackageIndex {
2424
final List<PackageDocument> _documents;
2525
final _documentsByName = <String, PackageDocument>{};
26+
final _nameToIndex = <String, int>{};
2627
late final PackageNameIndex _packageNameIndex;
2728
late final TokenIndex<String> _descrIndex;
2829
late final TokenIndex<String> _readmeIndex;
@@ -62,6 +63,7 @@ class InMemoryPackageIndex {
6263
for (var i = 0; i < _documents.length; i++) {
6364
final doc = _documents[i];
6465
_documentsByName[doc.package] = doc;
66+
_nameToIndex[doc.package] = i;
6567

6668
// transform tags into numberical IDs
6769
final tagIds = <int>[];
@@ -232,7 +234,9 @@ class InMemoryPackageIndex {
232234
);
233235

234236
String? bestNameMatch;
235-
if (parsedQueryText != null) {
237+
if (parsedQueryText != null &&
238+
query.parsedQuery.hasOnlyFreeText &&
239+
query.isNaturalOrder) {
236240
// exact package name
237241
if (_documentsByName.containsKey(parsedQueryText)) {
238242
bestNameMatch = parsedQueryText;
@@ -268,12 +272,18 @@ class InMemoryPackageIndex {
268272
/// it linearly into the [0.4-1.0] range, to allow better
269273
/// multiplication outcomes.
270274
packageScores.multiplyAllFromValues(_adjustedOverallScores);
271-
indexedHits = _rankWithValues(packageScores,
272-
requiredLengthThreshold: query.offset);
275+
indexedHits = _rankWithValues(
276+
packageScores,
277+
requiredLengthThreshold: query.offset,
278+
bestNameMatch: bestNameMatch,
279+
);
273280
break;
274281
case SearchOrder.text:
275-
indexedHits = _rankWithValues(packageScores,
276-
requiredLengthThreshold: query.offset);
282+
indexedHits = _rankWithValues(
283+
packageScores,
284+
requiredLengthThreshold: query.offset,
285+
bestNameMatch: bestNameMatch,
286+
);
277287
break;
278288
case SearchOrder.created:
279289
indexedHits = _createdOrderedHits.whereInScores(packageScores);
@@ -317,10 +327,14 @@ class InMemoryPackageIndex {
317327
packageHits = indexedHits.map((h) => h.hit).toList();
318328
}
319329

330+
// Only indicate name match when the first item's score is lower than the second's score.
331+
final indicateNameMatch = bestNameMatch != null &&
332+
packageHits.length > 1 &&
333+
((packageHits[0].score ?? 0) <= (packageHits[1].score ?? 0));
320334
return PackageSearchResult(
321335
timestamp: clock.now().toUtc(),
322336
totalCount: totalCount,
323-
nameMatches: bestNameMatch == null ? null : [bestNameMatch],
337+
nameMatches: indicateNameMatch ? [bestNameMatch] : null,
324338
packageHits: packageHits,
325339
errorMessage: textResults?.errorMessage,
326340
);
@@ -471,11 +485,14 @@ class InMemoryPackageIndex {
471485
IndexedScore<String> score, {
472486
// if the item count is fewer than this threshold, an empty list will be returned
473487
int? requiredLengthThreshold,
488+
String? bestNameMatch,
474489
}) {
475490
final list = <IndexedPackageHit>[];
491+
final bestNameIndex =
492+
bestNameMatch == null ? null : _nameToIndex[bestNameMatch];
476493
for (var i = 0; i < score.length; i++) {
477494
final value = score.getValue(i);
478-
if (value <= 0.0) continue;
495+
if (value <= 0.0 && i != bestNameIndex) continue;
479496
list.add(IndexedPackageHit(
480497
i, PackageHit(package: score.keys[i], score: value)));
481498
}
@@ -484,6 +501,8 @@ class InMemoryPackageIndex {
484501
return [];
485502
}
486503
list.sort((a, b) {
504+
if (a.index == bestNameIndex) return -1;
505+
if (b.index == bestNameIndex) return 1;
487506
final scoreCompare = -a.hit.score!.compareTo(b.hit.score!);
488507
if (scoreCompare != 0) return scoreCompare;
489508
// if two packages got the same score, order by last updated

app/lib/search/search_service.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ class ServiceSearchQuery {
283283
late final effectiveOrder = parsedQuery.order ?? order;
284284
bool get _hasQuery => query != null && query!.isNotEmpty;
285285
bool get _hasOnlyFreeText => _hasQuery && parsedQuery.hasOnlyFreeText;
286-
bool get _isNaturalOrder =>
286+
bool get isNaturalOrder =>
287287
effectiveOrder == null ||
288288
effectiveOrder == SearchOrder.top ||
289289
effectiveOrder == SearchOrder.text;
@@ -294,7 +294,7 @@ class ServiceSearchQuery {
294294
bool get includeSdkResults =>
295295
offset == 0 &&
296296
_hasOnlyFreeText &&
297-
_isNaturalOrder &&
297+
isNaturalOrder &&
298298
_hasNoOwnershipScope &&
299299
!_isFlutterFavorite &&
300300
(textMatchExtent ?? TextMatchExtent.api).shouldMatchApi();

app/test/search/api_doc_page_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ void main() {
5353
expect(json.decode(json.encode(result)), {
5454
'timestamp': isNotNull,
5555
'totalCount': 2,
56-
'nameMatches': ['foo'],
5756
'sdkLibraryHits': [],
5857
'packageHits': [
5958
{'package': 'foo', 'score': 1.0}, // finds package name

app/test/search/flutter_iap_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ void main() {
6969
expect(json.decode(json.encode(result)), {
7070
'timestamp': isNotNull,
7171
'totalCount': 1,
72-
'nameMatches': ['flutter_iap'],
7372
'sdkLibraryHits': [],
7473
'packageHits': [
7574
{'package': 'flutter_iap', 'score': 1.0},

app/test/search/handlers_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ void main() {
3333
await expectJsonResponse(await issueGet('/search?q=oxygen'), body: {
3434
'timestamp': isNotNull,
3535
'totalCount': 1,
36-
'nameMatches': ['oxygen'],
3736
'sdkLibraryHits': [],
3837
'packageHits': [
3938
{'package': 'oxygen', 'score': isPositive},

app/test/search/haversine_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,6 @@ MIT'''),
380380
expect(json.decode(json.encode(result)), {
381381
'timestamp': isNotNull,
382382
'totalCount': 3,
383-
'nameMatches': ['haversine'],
384383
'sdkLibraryHits': [],
385384
'packageHits': [
386385
{'package': 'haversine', 'score': 1.0},

app/test/search/json_tool_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ void main() {
2727
expect(json.decode(json.encode(result)), {
2828
'timestamp': isNotNull,
2929
'totalCount': 1,
30-
'nameMatches': ['jsontool'],
3130
'sdkLibraryHits': [],
3231
'packageHits': [
3332
{'package': 'jsontool', 'score': 1.0},
@@ -70,8 +69,8 @@ void main() {
7069
'nameMatches': ['jsontool'],
7170
'sdkLibraryHits': [],
7271
'packageHits': [
73-
{'package': 'json2entity', 'score': 1.0},
7472
{'package': 'jsontool', 'score': 1.0},
73+
{'package': 'json2entity', 'score': 1.0},
7574
{'package': 'json_to_model', 'score': closeTo(0.73, 0.01)},
7675
],
7776
});

app/test/search/maps_test.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ void main() {
2626
expect(json.decode(json.encode(result)), {
2727
'timestamp': isNotNull,
2828
'totalCount': 2,
29-
'nameMatches': ['maps'],
3029
'sdkLibraryHits': [],
3130
'packageHits': [
3231
{'package': 'maps', 'score': 1.0},
@@ -44,8 +43,8 @@ void main() {
4443
'nameMatches': ['map'],
4544
'sdkLibraryHits': [],
4645
'packageHits': [
47-
{'package': 'maps', 'score': 1.0},
4846
{'package': 'map', 'score': 1.0},
47+
{'package': 'maps', 'score': 1.0},
4948
],
5049
});
5150
});

0 commit comments

Comments
 (0)