Skip to content

Commit e90f831

Browse files
authored
Suggest topics based on search phrases (behind experimental). (#8186)
1 parent e82c94d commit e90f831

File tree

7 files changed

+82
-3
lines changed

7 files changed

+82
-3
lines changed

app/lib/frontend/handlers/experimental.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import '../../shared/cookie_utils.dart';
88

99
const _publicFlags = <String>{
1010
'dark',
11-
'search-completion',
1211
'download-counts',
12+
'search-completion',
13+
'search-topics',
1314
};
1415

1516
const _allFlags = <String>{
@@ -86,6 +87,7 @@ class ExperimentalFlags {
8687
}
8788

8889
bool get isSearchCompletionEnabled => isEnabled('search-completion');
90+
bool get isSearchTopicsEnabled => isEnabled('search-topics');
8991

9092
bool get isDarkModeEnabled => isEnabled('dark');
9193
bool get isDarkModeDefault => isEnabled('dark-as-default');

app/lib/frontend/templates/listing.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:math';
66

77
import 'package:_pub_shared/search/search_form.dart';
88
import 'package:collection/collection.dart';
9+
import 'package:pub_dev/frontend/request_context.dart';
910

1011
import '../../package/search_adapter.dart';
1112
import '../../search/search_service.dart';
@@ -51,6 +52,9 @@ String renderPkgIndexPage(
5152
messageFromBackend: searchResultPage.errorMessage,
5253
),
5354
nameMatches: _nameMatches(searchForm, searchResultPage.nameMatches),
55+
topicMatches: requestContext.experimentalFlags.isSearchTopicsEnabled
56+
? _topicMatches(searchForm, searchResultPage.topicMatches)
57+
: null,
5458
packageList: packageList(searchResultPage),
5559
pagination: searchResultPage.hasHit ? paginationNode(links) : null,
5660
openSections: openSections,
@@ -147,3 +151,24 @@ d.Node? _nameMatches(SearchForm form, List<String>? matches) {
147151
}),
148152
]);
149153
}
154+
155+
d.Node? _topicMatches(SearchForm form, List<String>? matches) {
156+
if (matches == null || matches.isEmpty) {
157+
return null;
158+
}
159+
final singular = matches.length == 1;
160+
final isExactNameMatch = singular && form.parsedQuery.text == matches.single;
161+
final nameMatchLabel = isExactNameMatch
162+
? 'Exact topic match: '
163+
: 'Matching ${singular ? 'topic' : 'topics'}: ';
164+
165+
return d.p(children: [
166+
d.text(nameMatchLabel),
167+
...matches.expandIndexed((i, name) {
168+
return [
169+
if (i > 0) d.text(', '),
170+
d.a(href: urls.searchUrl(q: 'topic:$name'), text: '#$name'),
171+
];
172+
}),
173+
]);
174+
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,22 @@ d.Node packageListingNode({
1414
required SearchForm searchForm,
1515
required d.Node listingInfo,
1616
required d.Node? nameMatches,
17+
required d.Node? topicMatches,
1718
required d.Node packageList,
1819
required d.Node? pagination,
1920
required Set<String>? openSections,
2021
}) {
22+
final matchHighlights = [
23+
if (nameMatches != null) nameMatches,
24+
if (topicMatches != null) topicMatches,
25+
];
2126
final innerContent = d.fragment([
2227
listingInfo,
23-
if (nameMatches != null)
24-
d.div(classes: ['listing-highlight-block'], child: nameMatches),
28+
if (matchHighlights.isNotEmpty)
29+
d.div(
30+
classes: ['listing-highlight-block'],
31+
children: matchHighlights,
32+
),
2533
packageList,
2634
if (pagination != null) pagination,
2735
d.markdown('Check our help page for details on '

app/lib/package/search_adapter.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class SearchAdapter {
4343
form,
4444
result.totalCount,
4545
nameMatches: result.nameMatches,
46+
topicMatches: result.topicMatches,
4647
sdkLibraryHits: result.sdkLibraryHits,
4748
packageHits:
4849
result.packageHits.map((h) => views[h.package]).nonNulls.toList(),
@@ -146,6 +147,9 @@ class SearchResultPage {
146147
/// would be considered as blocker for publishing).
147148
final List<String>? nameMatches;
148149

150+
/// Topic names that are exact name matches or are close to a known topic.
151+
final List<String>? topicMatches;
152+
149153
/// The hits from the SDK libraries.
150154
final List<SdkLibraryHit> sdkLibraryHits;
151155

@@ -163,6 +167,7 @@ class SearchResultPage {
163167
this.form,
164168
this.totalCount, {
165169
this.nameMatches,
170+
this.topicMatches,
166171
List<SdkLibraryHit>? sdkLibraryHits,
167172
List<PackageView>? packageHits,
168173
this.errorMessage,

app/lib/search/mem_index.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:_pub_shared/search/search_form.dart';
88
import 'package:clock/clock.dart';
99
import 'package:logging/logging.dart';
1010
import 'package:meta/meta.dart';
11+
import 'package:pub_dev/service/topics/models.dart';
1112

1213
import '../shared/utils.dart' show boundedList;
1314
import 'models.dart';
@@ -37,6 +38,13 @@ class InMemoryPackageIndex {
3738
late final List<PackageHit> _likesOrderedHits;
3839
late final List<PackageHit> _pointsOrderedHits;
3940

41+
// Contains all of the topics the index had seen so far.
42+
// TODO: consider moving this into a separate index
43+
// TODO: get the list of topics from the bucket
44+
final _topics = <String>{
45+
...canonicalTopics.aliasToCanonicalMap.values,
46+
};
47+
4048
late final DateTime _lastUpdated;
4149

4250
InMemoryPackageIndex({
@@ -57,6 +65,12 @@ class InMemoryPackageIndex {
5765
}
5866
}
5967
}
68+
69+
// Note: we are not removing topics from this set, only adding them, no
70+
// need for tracking the current topic count.
71+
_topics.addAll(doc.tags
72+
.where((t) => t.startsWith('topic:'))
73+
.map((t) => t.split('topic:').last));
6074
}
6175

6276
final packageKeys = _documents.map((d) => d.package).toList();
@@ -170,7 +184,21 @@ class InMemoryPackageIndex {
170184
}
171185

172186
final nameMatches = textResults?.nameMatches;
187+
List<String>? topicMatches;
173188
List<PackageHit> packageHits;
189+
190+
if (parsedQueryText != null) {
191+
final parts = parsedQueryText
192+
.split(' ')
193+
.map((t) => canonicalTopics.aliasToCanonicalMap[t] ?? t)
194+
.toSet()
195+
.where(_topics.contains)
196+
.toList();
197+
if (parts.isNotEmpty) {
198+
topicMatches = parts;
199+
}
200+
}
201+
174202
switch (query.effectiveOrder ?? SearchOrder.top) {
175203
case SearchOrder.top:
176204
if (textResults == null) {
@@ -229,6 +257,7 @@ class InMemoryPackageIndex {
229257
timestamp: clock.now().toUtc(),
230258
totalCount: totalCount,
231259
nameMatches: nameMatches,
260+
topicMatches: topicMatches,
232261
packageHits: packageHits,
233262
);
234263
}

app/lib/search/search_service.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,9 @@ class PackageSearchResult {
312312
/// Package names that are exact name matches or close to (e.g. names that
313313
/// would be considered as blocker for publishing).
314314
final List<String>? nameMatches;
315+
316+
/// Topic names that are exact name matches or close to the queried text.
317+
final List<String>? topicMatches;
315318
final List<SdkLibraryHit> sdkLibraryHits;
316319
final List<PackageHit> packageHits;
317320

@@ -325,6 +328,7 @@ class PackageSearchResult {
325328
required this.timestamp,
326329
required this.totalCount,
327330
this.nameMatches,
331+
this.topicMatches,
328332
List<SdkLibraryHit>? sdkLibraryHits,
329333
List<PackageHit>? packageHits,
330334
this.errorMessage,
@@ -339,6 +343,7 @@ class PackageSearchResult {
339343
}) : timestamp = clock.now().toUtc(),
340344
totalCount = 0,
341345
nameMatches = null,
346+
topicMatches = null,
342347
sdkLibraryHits = <SdkLibraryHit>[],
343348
packageHits = <PackageHit>[];
344349

@@ -358,6 +363,7 @@ class PackageSearchResult {
358363
timestamp: timestamp,
359364
totalCount: totalCount,
360365
nameMatches: nameMatches,
366+
topicMatches: topicMatches,
361367
sdkLibraryHits: sdkLibraryHits ?? this.sdkLibraryHits,
362368
packageHits: packageHits,
363369
errorMessage: errorMessage,

app/lib/search/search_service.g.dart

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

0 commit comments

Comments
 (0)