Skip to content

Commit 36f5179

Browse files
authored
Name matches at the beginning of search. (#8085)
1 parent 6630cae commit 36f5179

File tree

13 files changed

+142
-73
lines changed

13 files changed

+142
-73
lines changed

app/lib/frontend/handlers/listing.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ Future<shelf.Response> _packagesHandlerHtmlCore(shelf.Request request) async {
8181
);
8282
final int totalCount = searchResult.totalCount;
8383
final errorMessage = searchResult.errorMessage;
84-
final statusCode =
85-
searchResult.statusCode ?? (errorMessage == null ? 200 : 500);
84+
final statusCode = searchResult.statusCode;
8685
if (errorMessage != null && statusCode >= 500) {
8786
_logger.severe('[pub-search-not-working] ${searchResult.errorMessage}');
8887
}
@@ -93,7 +92,6 @@ Future<shelf.Response> _packagesHandlerHtmlCore(shelf.Request request) async {
9392
searchResult,
9493
links,
9594
searchForm: searchForm,
96-
messageFromBackend: searchResult.errorMessage,
9795
openSections: openSections,
9896
),
9997
status: statusCode,

app/lib/frontend/templates/listing.dart

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

77
import 'package:_pub_shared/search/search_form.dart';
8+
import 'package:collection/collection.dart';
89

910
import '../../package/search_adapter.dart';
1011
import '../../search/search_service.dart';
12+
import '../../shared/urls.dart' as urls;
1113
import '../dom/dom.dart' as d;
1214

1315
import '_consts.dart';
@@ -35,7 +37,6 @@ String renderPkgIndexPage(
3537
SearchResultPage searchResultPage,
3638
PageLinks links, {
3739
required SearchForm searchForm,
38-
String? messageFromBackend,
3940
Set<String>? openSections,
4041
}) {
4142
final topPackages = getSdkDict(null).topSdkPackages;
@@ -47,8 +48,9 @@ String renderPkgIndexPage(
4748
searchForm: searchForm,
4849
totalCount: searchResultPage.totalCount,
4950
title: topPackages,
50-
messageFromBackend: messageFromBackend,
51+
messageFromBackend: searchResultPage.errorMessage,
5152
),
53+
nameMatches: _nameMatches(searchForm, searchResultPage.nameMatches),
5254
packageList: packageList(searchResultPage),
5355
pagination: searchResultPage.hasHit ? paginationNode(links) : null,
5456
openSections: openSections,
@@ -121,3 +123,24 @@ class PageLinks {
121123
return min(fromSymmetry, max(currentPage!, fromCount));
122124
}
123125
}
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.b(text: nameMatchLabel),
139+
...matches.expandIndexed((i, name) {
140+
return [
141+
if (i > 0) d.text(', '),
142+
d.code(child: d.a(href: urls.pkgPageUrl(name), text: name)),
143+
];
144+
}),
145+
]);
146+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import '../../../static_files.dart';
1313
d.Node packageListingNode({
1414
required SearchForm searchForm,
1515
required d.Node listingInfo,
16+
required d.Node? nameMatches,
1617
required d.Node packageList,
1718
required d.Node? pagination,
1819
required Set<String>? openSections,
1920
}) {
2021
final innerContent = d.fragment([
2122
listingInfo,
23+
if (nameMatches != null) nameMatches,
2224
packageList,
2325
if (pagination != null) pagination,
2426
d.markdown('Check our help page for details on '

app/lib/package/search_adapter.dart

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ class SearchAdapter {
4242
return SearchResultPage(
4343
form,
4444
result.totalCount,
45+
nameMatches: result.nameMatches,
4546
sdkLibraryHits: result.sdkLibraryHits,
4647
packageHits:
4748
result.packageHits.map((h) => views[h.package]).nonNulls.toList(),
4849
errorMessage: result.errorMessage,
49-
statusCode: result.statusCode,
50+
statusCode: result.statusCode ?? 200,
5051
);
5152
}
5253

@@ -84,8 +85,10 @@ class SearchAdapter {
8485
Future<PackageSearchResult> _fallbackSearch(SearchForm form) async {
8586
// Some search queries must not be served with the fallback search.
8687
if (form.parsedQuery.tagsPredicate.isNotEmpty) {
87-
return PackageSearchResult.empty(
88-
errorMessage: 'Search is temporarily unavailable.');
88+
return PackageSearchResult.error(
89+
errorMessage: 'Search is temporarily unavailable.',
90+
statusCode: 503,
91+
);
8992
}
9093

9194
final names = await nameTracker
@@ -108,11 +111,13 @@ class SearchAdapter {
108111
packageHits =
109112
packageHits.skip(form.offset).take(form.pageSize ?? 10).toList();
110113
return PackageSearchResult(
111-
timestamp: clock.now().toUtc(),
112-
packageHits: packageHits,
113-
totalCount: totalCount,
114-
errorMessage:
115-
'Search is temporarily impaired, filtering and ranking may be incorrect.');
114+
timestamp: clock.now().toUtc(),
115+
packageHits: packageHits,
116+
totalCount: totalCount,
117+
errorMessage:
118+
'Search is temporarily impaired, filtering and ranking may be incorrect.',
119+
statusCode: 503,
120+
);
116121
}
117122

118123
Future<Map<String, PackageView>> _getPackageViewsFromHits(
@@ -137,6 +142,11 @@ class SearchResultPage {
137142
/// The total number of results available for the search.
138143
final int totalCount;
139144

145+
/// Package names that are exact name matches or close to (e.g. names that
146+
/// would be considered as blocker for publishing).
147+
final List<String>? nameMatches;
148+
149+
/// The hits from the SDK libraries.
140150
final List<SdkLibraryHit> sdkLibraryHits;
141151

142152
/// The current list of packages on the page.
@@ -146,24 +156,20 @@ class SearchResultPage {
146156
/// the query was not processed entirely.
147157
final String? errorMessage;
148158

149-
/// The non-200 status code that will be used to render the [errorMessage].
150-
final int? statusCode;
159+
/// The code that will be used to render the page.
160+
final int statusCode;
151161

152162
SearchResultPage(
153163
this.form,
154164
this.totalCount, {
165+
this.nameMatches,
155166
List<SdkLibraryHit>? sdkLibraryHits,
156167
List<PackageView>? packageHits,
157168
this.errorMessage,
158-
this.statusCode,
169+
this.statusCode = 200,
159170
}) : sdkLibraryHits = sdkLibraryHits ?? <SdkLibraryHit>[],
160171
packageHits = packageHits ?? <PackageView>[];
161172

162-
SearchResultPage.empty(this.form, {this.errorMessage, this.statusCode})
163-
: totalCount = 0,
164-
sdkLibraryHits = <SdkLibraryHit>[],
165-
packageHits = [];
166-
167173
bool get hasNoHit => sdkLibraryHits.isEmpty && packageHits.isEmpty;
168174
bool get hasHit => !hasNoHit;
169175
}

app/lib/search/mem_index.dart

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class InMemoryPackageIndex {
147147
packages.removeWhere((x) => !keys.contains(x));
148148
}
149149

150+
List<String>? nameMatches;
150151
late List<PackageHit> packageHits;
151152
switch (query.effectiveOrder ?? SearchOrder.top) {
152153
case SearchOrder.top:
@@ -162,12 +163,10 @@ class InMemoryPackageIndex {
162163
.map((key, value) => value * _adjustedOverallScores[key]!);
163164
// If the search hits have an exact name match, we move it to the front of the result list.
164165
final parsedQueryText = query.parsedQuery.text;
165-
final priorityPackageName =
166-
packages.contains(parsedQueryText ?? '') ? parsedQueryText : null;
167-
packageHits = _rankWithValues(
168-
overallScore.getValues(),
169-
priorityPackageName: priorityPackageName,
170-
);
166+
if (parsedQueryText != null && _packages.containsKey(parsedQueryText)) {
167+
nameMatches = <String>[parsedQueryText];
168+
}
169+
packageHits = _rankWithValues(overallScore.getValues());
171170
break;
172171
case SearchOrder.text:
173172
final score = textResults?.pkgScore ?? Score.empty();
@@ -209,6 +208,7 @@ class InMemoryPackageIndex {
209208
return PackageSearchResult(
210209
timestamp: clock.now().toUtc(),
211210
totalCount: totalCount,
211+
nameMatches: nameMatches,
212212
packageHits: packageHits,
213213
);
214214
}
@@ -333,16 +333,11 @@ class InMemoryPackageIndex {
333333
return null;
334334
}
335335

336-
List<PackageHit> _rankWithValues(
337-
Map<String, double> values, {
338-
String? priorityPackageName,
339-
}) {
336+
List<PackageHit> _rankWithValues(Map<String, double> values) {
340337
final list = values.entries
341338
.map((e) => PackageHit(package: e.key, score: e.value))
342339
.toList();
343340
list.sort((a, b) {
344-
if (a.package == priorityPackageName) return -1;
345-
if (b.package == priorityPackageName) return 1;
346341
final int scoreCompare = -a.score!.compareTo(b.score!);
347342
if (scoreCompare != 0) return scoreCompare;
348343
// if two packages got the same score, order by last updated

app/lib/search/result_combiner.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,7 @@ class SearchResultCombiner {
4848
sdkLibraryHits.sort((a, b) => -a.score.compareTo(b.score));
4949
}
5050

51-
return PackageSearchResult(
52-
timestamp: primaryResult.timestamp,
53-
totalCount: primaryResult.totalCount,
54-
packageHits: primaryResult.packageHits,
51+
return primaryResult.change(
5552
sdkLibraryHits: sdkLibraryHits.take(3).toList(),
5653
);
5754
}

app/lib/search/search_client.dart

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class SearchClient {
5252
// check validity first
5353
final validity = query.evaluateValidity();
5454
if (validity.isRejected) {
55-
return PackageSearchResult.empty(
55+
return PackageSearchResult.error(
5656
errorMessage: 'Search query rejected. ${validity.rejectReason}',
5757
statusCode: 400,
5858
);
@@ -87,8 +87,10 @@ class SearchClient {
8787
}
8888
}
8989
if (response == null) {
90-
return PackageSearchResult.empty(
91-
errorMessage: 'Search is temporarily unavailable.');
90+
return PackageSearchResult.error(
91+
errorMessage: 'Search is temporarily unavailable.',
92+
statusCode: 503,
93+
);
9294
}
9395
if (response.statusCode == 200) {
9496
return PackageSearchResult.fromJson(
@@ -97,12 +99,16 @@ class SearchClient {
9799
}
98100
// Search request before the service initialization completed.
99101
if (response.statusCode == searchIndexNotReadyCode) {
100-
return PackageSearchResult.empty(
101-
errorMessage: 'Search is temporarily unavailable.');
102+
return PackageSearchResult.error(
103+
errorMessage: 'Search is temporarily unavailable.',
104+
statusCode: 503,
105+
);
102106
}
103107
// There has been a generic issue with the service.
104-
return PackageSearchResult.empty(
105-
errorMessage: 'Service returned status code ${response.statusCode}.');
108+
return PackageSearchResult.error(
109+
errorMessage: 'Service returned status code ${response.statusCode}.',
110+
statusCode: response.statusCode,
111+
);
106112
}
107113

108114
if (sourceIp != null) {

app/lib/search/search_service.dart

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,12 @@ class QueryValidity {
303303

304304
@JsonSerializable(includeIfNull: false, explicitToJson: true)
305305
class PackageSearchResult {
306-
final DateTime? timestamp;
306+
final DateTime timestamp;
307307
final int totalCount;
308+
309+
/// Package names that are exact name matches or close to (e.g. names that
310+
/// would be considered as blocker for publishing).
311+
final List<String>? nameMatches;
308312
final List<SdkLibraryHit> sdkLibraryHits;
309313
final List<PackageHit> packageHits;
310314

@@ -317,32 +321,46 @@ class PackageSearchResult {
317321
PackageSearchResult({
318322
required this.timestamp,
319323
required this.totalCount,
324+
this.nameMatches,
320325
List<SdkLibraryHit>? sdkLibraryHits,
321326
List<PackageHit>? packageHits,
322327
this.errorMessage,
323-
this.statusCode,
324-
}) : sdkLibraryHits = sdkLibraryHits ?? <SdkLibraryHit>[],
325-
packageHits = packageHits ?? <PackageHit>[];
326-
327-
PackageSearchResult.empty({this.errorMessage, this.statusCode})
328-
: timestamp = clock.now().toUtc(),
328+
int? statusCode,
329+
}) : packageHits = packageHits ?? <PackageHit>[],
330+
sdkLibraryHits = sdkLibraryHits ?? <SdkLibraryHit>[],
331+
statusCode = statusCode;
332+
333+
PackageSearchResult.error({
334+
required this.errorMessage,
335+
required this.statusCode,
336+
}) : timestamp = clock.now().toUtc(),
329337
totalCount = 0,
338+
nameMatches = null,
330339
sdkLibraryHits = <SdkLibraryHit>[],
331340
packageHits = <PackageHit>[];
332341

333-
factory PackageSearchResult.fromJson(Map<String, dynamic> json) {
334-
return _$PackageSearchResultFromJson({
335-
// TODO: remove fallback in the next release
336-
'errorMessage': json['message'],
337-
...json,
338-
});
339-
}
342+
factory PackageSearchResult.fromJson(Map<String, dynamic> json) =>
343+
_$PackageSearchResultFromJson(json);
340344

341-
Duration get age => clock.now().difference(timestamp!);
345+
Duration get age => clock.now().difference(timestamp);
342346

343347
Map<String, dynamic> toJson() => _$PackageSearchResultToJson(this);
344348

345349
bool get isEmpty => packageHits.isEmpty && sdkLibraryHits.isEmpty;
350+
351+
PackageSearchResult change({
352+
List<SdkLibraryHit>? sdkLibraryHits,
353+
}) {
354+
return PackageSearchResult(
355+
timestamp: timestamp,
356+
totalCount: totalCount,
357+
nameMatches: nameMatches,
358+
sdkLibraryHits: sdkLibraryHits ?? this.sdkLibraryHits,
359+
packageHits: packageHits,
360+
errorMessage: errorMessage,
361+
statusCode: statusCode,
362+
);
363+
}
346364
}
347365

348366
@JsonSerializable(includeIfNull: false, explicitToJson: true)

app/lib/search/search_service.g.dart

Lines changed: 9 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)