Skip to content

Commit ee61e4f

Browse files
committed
Search index isolate without text indexing for predicate-only queries.
1 parent 25c2a5a commit ee61e4f

File tree

7 files changed

+131
-43
lines changed

7 files changed

+131
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ AppEngine version, listed here to ease deployment and troubleshooting.
99
* Bump runtimeVersion to `2025.06.03`.
1010
* Upgraded dartdoc to `8.3.4`.
1111
* Note: started to delete all `Secret` entries in Datastore.
12+
* Note: search instance uses separate isolate for predicate-only queries.
1213

1314
## `20250603t091500-all`
1415
* Bump runtimeVersion to `2025.06.02`.

app/lib/search/search_service.dart

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ class PackageDocument {
137137
Map<String, dynamic> toJson() => _$PackageDocumentToJson(this);
138138

139139
late final packageNameLowerCased = package.toLowerCase();
140+
141+
/// Removes the text-heavy content from the current object,
142+
/// making it lightweight for no-text indexing.
143+
PackageDocument removeTextContent() => PackageDocument(
144+
package: package,
145+
version: version,
146+
created: created,
147+
updated: updated,
148+
sourceUpdated: sourceUpdated,
149+
timestamp: timestamp,
150+
apiDocPages: null,
151+
dependencies: dependencies,
152+
description: null,
153+
downloadCount: downloadCount,
154+
downloadScore: downloadScore,
155+
grantedPoints: grantedPoints,
156+
likeCount: likeCount,
157+
likeScore: likeScore,
158+
maxPoints: maxPoints,
159+
readme: null,
160+
tags: tags,
161+
trendScore: trendScore,
162+
);
140163
}
141164

142165
/// A reference to an API doc page
@@ -299,9 +322,6 @@ class ServiceSearchQuery {
299322
!_isFlutterFavorite &&
300323
(textMatchExtent ?? TextMatchExtent.api).shouldMatchApi();
301324

302-
bool get considerHighlightedHit => _hasOnlyFreeText && _hasNoOwnershipScope;
303-
bool get includeHighlightedHit => considerHighlightedHit && offset == 0;
304-
305325
/// Returns the validity status of the query.
306326
QueryValidity evaluateValidity() {
307327
// Block search on unreasonably long search queries (when the free-form

app/lib/search/updater.dart

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,19 @@ IndexUpdater get indexUpdater => ss.lookup(#_indexUpdater) as IndexUpdater;
3030

3131
/// Loads a local search snapshot file and builds an in-memory package index from it.
3232
Future<InMemoryPackageIndex> loadInMemoryPackageIndexFromFile(
33-
String path) async {
33+
String path, {
34+
bool removeTextContent = false,
35+
}) async {
3436
final file = File(path);
3537
final content =
3638
json.decode(utf8.decode(gzip.decode(await file.readAsBytes())))
3739
as Map<String, Object?>;
3840
final snapshot = SearchSnapshot.fromJson(content);
3941
return InMemoryPackageIndex(
40-
documents:
41-
snapshot.documents!.values.where((d) => !isSdkPackage(d.package)));
42+
documents: snapshot.documents!.values
43+
.where((d) => !isSdkPackage(d.package))
44+
.map((d) => removeTextContent ? d.removeTextContent() : d),
45+
);
4246
}
4347

4448
/// Saves the provided [documents] into a local search snapshot file.
@@ -60,8 +64,8 @@ class IndexUpdater {
6064

6165
/// Loads the package index snapshot, or if it fails, creates a minimal
6266
/// package index with only package names and minimal information.
63-
Future<void> init() async {
64-
final isReady = await _initSnapshot();
67+
Future<void> init({bool removeTextContent = false}) async {
68+
final isReady = await _initSnapshot(removeTextContent);
6569
if (!isReady) {
6670
_logger.info('Loading minimum package index...');
6771
final documents = await searchBackend.loadMinimumPackageIndex().toList();
@@ -85,14 +89,17 @@ class IndexUpdater {
8589
}
8690

8791
/// Returns whether the snapshot was initialized and loaded properly.
88-
Future<bool> _initSnapshot() async {
92+
Future<bool> _initSnapshot(bool removeTextContent) async {
8993
try {
9094
_logger.info('Loading snapshot...');
9195
final documents = await searchBackend.fetchSnapshotDocuments();
9296
if (documents == null) {
9397
return false;
9498
}
95-
updatePackageIndex(InMemoryPackageIndex(documents: documents));
99+
updatePackageIndex(InMemoryPackageIndex(
100+
documents:
101+
documents.map((d) => removeTextContent ? d.removeTextContent() : d),
102+
));
96103
// Arbitrary sanity check that the snapshot is not entirely bogus.
97104
// Index merge will enable search.
98105
if (documents.length > 10) {

app/lib/service/entrypoint/search.dart

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@ class SearchCommand extends Command {
3838

3939
envConfig.checkServiceEnvironment(name);
4040
await withServices(() async {
41-
final packageIsolate = await startQueryIsolate(
41+
final primaryIsolate = await startSearchIsolate(logger: _logger);
42+
registerScopeExitCallback(primaryIsolate.close);
43+
44+
final reducedIsolate = await startSearchIsolate(
4245
logger: _logger,
43-
kind: 'package',
44-
spawnUri:
45-
Uri.parse('package:pub_dev/service/entrypoint/search_index.dart'),
46+
removeTextContent: true,
4647
);
47-
registerScopeExitCallback(packageIsolate.close);
48+
registerScopeExitCallback(reducedIsolate.close);
4849

4950
final sdkIsolate = await startQueryIsolate(
5051
logger: _logger,
@@ -56,8 +57,9 @@ class SearchCommand extends Command {
5657

5758
registerSearchIndex(
5859
SearchResultCombiner(
59-
primaryIndex:
60-
LatencyAwareSearchIndex(IsolateSearchIndex(packageIsolate)),
60+
primaryIndex: LatencyAwareSearchIndex(
61+
IsolateSearchIndex(primaryIsolate, reducedIsolate),
62+
),
6163
sdkIndex: SdkIsolateIndex(sdkIsolate),
6264
),
6365
);
@@ -70,7 +72,10 @@ class SearchCommand extends Command {
7072
await Future.delayed(delay);
7173

7274
// create a new index and handover with a 2-minute maximum wait
73-
await packageIsolate.renew(count: 1, wait: Duration(minutes: 2));
75+
await Future.wait([
76+
primaryIsolate.renew(count: 1, wait: Duration(minutes: 2)),
77+
reducedIsolate.renew(count: 1, wait: Duration(minutes: 2)),
78+
]);
7479

7580
// schedule the renewal again
7681
scheduleRenew();

app/lib/service/entrypoint/search_index.dart

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import 'package:pub_dev/shared/utils.dart';
2222
final _logger = Logger('search_index');
2323

2424
final _argParser = ArgParser()
25+
..addFlag(
26+
'remove-text-content',
27+
defaultsTo: false,
28+
help: 'When set, the text content of the index will be removed.',
29+
)
2530
..addOption(
2631
'snapshot',
2732
help:
@@ -34,6 +39,7 @@ Future<void> main(List<String> args, var message) async {
3439

3540
final argv = _argParser.parse(args);
3641
final snapshot = argv['snapshot'] as String?;
42+
final removeTextContent = (argv['remove-text-content'] as bool?) ?? false;
3743

3844
final ServicesWrapperFn servicesWrapperFn;
3945
if (envConfig.isRunningInAppengine) {
@@ -46,9 +52,10 @@ Future<void> main(List<String> args, var message) async {
4652
await fork(() async {
4753
await servicesWrapperFn(() async {
4854
if (snapshot == null) {
49-
await indexUpdater.init();
55+
await indexUpdater.init(removeTextContent: removeTextContent);
5056
} else {
51-
updatePackageIndex(await loadInMemoryPackageIndexFromFile(snapshot));
57+
updatePackageIndex(await loadInMemoryPackageIndexFromFile(snapshot,
58+
removeTextContent: removeTextContent));
5259
}
5360

5461
await runIsolateFunctions(
@@ -75,12 +82,29 @@ Future<void> main(List<String> args, var message) async {
7582
timer.cancel();
7683
}
7784

85+
/// Starts a new search isolate with optional overrides.
86+
Future<IsolateRunner> startSearchIsolate({
87+
Logger? logger,
88+
bool removeTextContent = false,
89+
String? snapshot,
90+
}) async {
91+
return await startQueryIsolate(
92+
logger: logger ?? _logger,
93+
kind: removeTextContent ? 'reduced' : 'primary',
94+
spawnUri: Uri.parse('package:pub_dev/service/entrypoint/search_index.dart'),
95+
spawnArgs: [
96+
if (snapshot != null) ...['--snapshot', snapshot],
97+
],
98+
);
99+
}
100+
78101
/// Implementation of [SearchIndex] that uses [RequestMessage]s to send requests
79102
/// across isolate boundaries. The instance should be registered inside the
80103
/// `frontend` isolate, and it calls the `index` isolate as a delegate.
81104
class IsolateSearchIndex implements SearchIndex {
82-
final IsolateRunner _runner;
83-
IsolateSearchIndex(this._runner);
105+
final IsolateRunner _primary;
106+
final IsolateRunner _reduced;
107+
IsolateSearchIndex(this._primary, this._reduced);
84108
var _isReady = false;
85109

86110
@override
@@ -96,7 +120,7 @@ class IsolateSearchIndex implements SearchIndex {
96120
@override
97121
FutureOr<IndexInfo> indexInfo() async {
98122
try {
99-
final info = await _runner.sendRequest(
123+
final info = await _primary.sendRequest(
100124
'info',
101125
timeout: Duration(seconds: 5),
102126
);
@@ -114,7 +138,8 @@ class IsolateSearchIndex implements SearchIndex {
114138
@override
115139
FutureOr<PackageSearchResult> search(ServiceSearchQuery query) async {
116140
try {
117-
final rs = await _runner.sendRequest(
141+
final runner = query.parsedQuery.hasFreeText ? _primary : _reduced;
142+
final rs = await runner.sendRequest(
118143
Uri(queryParameters: query.toUriQueryParameters()).toString(),
119144
timeout: Duration(minutes: 1),
120145
);

app/test/service/entrypoint/search_index_test.dart

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
import 'package:logging/logging.dart';
65
import 'package:path/path.dart' as p;
76
import 'package:pub_dev/search/search_service.dart';
87
import 'package:pub_dev/search/updater.dart';
@@ -11,44 +10,59 @@ import 'package:pub_dev/service/entrypoint/search_index.dart';
1110
import 'package:pub_dev/shared/utils.dart';
1211
import 'package:test/test.dart';
1312

14-
final _logger = Logger('search_index_test');
15-
1613
void main() {
1714
group('Search index inside an isolate', () {
18-
late IsolateRunner indexRunner;
15+
late IsolateRunner primaryRunner;
16+
late IsolateRunner reducedRunner;
1917

2018
tearDown(() async {
21-
await indexRunner.close();
19+
await primaryRunner.close();
20+
await reducedRunner.close();
2221
});
2322

2423
test('start and work with local index', () async {
2524
await withTempDirectory((tempDir) async {
26-
final snapshotPath = p.join(tempDir.path, 'index.json.gz');
25+
// NOTE: The primary and the reduced index loads two different dataset,
26+
// in order to make the testing of the executation path unambigious.
27+
final primaryPath = p.join(tempDir.path, 'primary.json.gz');
2728
await saveInMemoryPackageIndexToFile(
2829
[
2930
PackageDocument(
3031
package: 'json_annotation',
3132
description: 'Annotation metadata for JSON serialization.',
33+
tags: ['sdk:dart'],
3234
),
3335
],
34-
snapshotPath,
36+
primaryPath,
3537
);
3638

37-
indexRunner = IsolateRunner.uri(
38-
kind: 'index',
39-
logger: _logger,
40-
spawnUri:
41-
Uri.parse('package:pub_dev/service/entrypoint/search_index.dart'),
42-
spawnArgs: ['--snapshot', snapshotPath],
39+
primaryRunner = await startSearchIsolate(snapshot: primaryPath);
40+
41+
final reducedPath = p.join(tempDir.path, 'reduced.json.gz');
42+
await saveInMemoryPackageIndexToFile(
43+
[
44+
PackageDocument(
45+
package: 'reduced_json_annotation',
46+
description: 'Annotation metadata for JSON serialization.',
47+
tags: ['sdk:dart'],
48+
downloadScore: 1.0,
49+
maxPoints: 100,
50+
grantedPoints: 100,
51+
),
52+
],
53+
reducedPath,
4354
);
4455

45-
await indexRunner.start(1);
56+
reducedRunner = await startSearchIsolate(snapshot: reducedPath);
57+
58+
await primaryRunner.start(1);
59+
await reducedRunner.start(1);
4660

4761
// index calling the sendport
48-
final searchIndex = IsolateSearchIndex(indexRunner);
62+
final searchIndex = IsolateSearchIndex(primaryRunner, reducedRunner);
4963
expect(await searchIndex.isReady(), true);
5064

51-
// returns package hit
65+
// text query - result from primary index
5266
final rs =
5367
await searchIndex.search(ServiceSearchQuery.parse(query: 'json'));
5468
expect(rs.toJson(), {
@@ -62,6 +76,21 @@ void main() {
6276
},
6377
],
6478
});
79+
80+
// predicate query - result from reduced index
81+
final rs2 = await searchIndex
82+
.search(ServiceSearchQuery.parse(query: 'sdk:dart'));
83+
expect(rs2.toJson(), {
84+
'timestamp': isNotEmpty,
85+
'totalCount': 1,
86+
'sdkLibraryHits': [],
87+
'packageHits': [
88+
{
89+
'package': 'reduced_json_annotation',
90+
'score': greaterThan(0.5),
91+
},
92+
],
93+
});
6594
});
6695
}, timeout: Timeout(Duration(minutes: 5)));
6796
});

pkg/_pub_shared/lib/search/search_form.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,15 +320,16 @@ class ParsedQueryText {
320320
bool get hasAnyDependency =>
321321
refDependencies.isNotEmpty || allDependencies.isNotEmpty;
322322

323+
late final hasFreeText = text != null && text!.isNotEmpty;
324+
323325
bool get hasOnlyFreeText =>
324-
text != null &&
325-
text!.isNotEmpty &&
326+
hasFreeText &&
326327
packagePrefix == null &&
327328
!hasAnyDependency &&
328329
tagsPredicate.isEmpty;
329330

330331
int get componentCount =>
331-
(text == null || text!.isEmpty ? 0 : 1) +
332+
(hasFreeText ? 1 : 0) +
332333
(packagePrefix == null ? 0 : 1) +
333334
refDependencies.length +
334335
allDependencies.length +

0 commit comments

Comments
 (0)