Skip to content

Commit 59af9a0

Browse files
authored
Search handler and client with HTTP POST-based request serialization. (#8892)
1 parent 26c990a commit 59af9a0

File tree

13 files changed

+255
-69
lines changed

13 files changed

+255
-69
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Important changes to data models, configuration, and migrations between each
22
AppEngine version, listed here to ease deployment and troubleshooting.
33

44
## Next Release (replace with git tag when deployed)
5+
* Note: Internal `search` instance started accepting request through `POST /search`.
56

67
## `20250812t135400-all`
78
* Bump runtimeVersion to `2025.08.12`.

app/lib/frontend/handlers/experimental.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const _publicFlags = <PublicFlag>{
1818

1919
final _allFlags = <String>{
2020
'dark-as-default',
21+
'search-post',
2122
..._publicFlags.map((x) => x.name),
2223
};
2324

@@ -92,6 +93,8 @@ class ExperimentalFlags {
9293

9394
bool get isDarkModeDefault => isEnabled('dark-as-default');
9495

96+
bool get useSearchPost => isEnabled('search-post');
97+
9598
bool get showTrending => true;
9699

97100
String encodedAsCookie() => _enabled.join(':');

app/lib/search/handlers.dart

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

8+
import 'package:_pub_shared/search/search_request_data.dart';
79
import 'package:logging/logging.dart';
810
import 'package:shelf/shelf.dart' as shelf;
11+
import 'package:shelf_router/shelf_router.dart';
912

1013
import '../shared/env_config.dart';
1114
import '../shared/handlers.dart';
@@ -18,28 +21,22 @@ final Duration _slowSearchThreshold = const Duration(milliseconds: 200);
1821

1922
/// Handlers for the search service.
2023
Future<shelf.Response> searchServiceHandler(shelf.Request request) async {
21-
final path = request.requestedUri.path;
22-
final handler = <String, shelf.Handler>{
23-
'/debug': _debugHandler,
24-
'/liveness_check': _livenessCheckHandler,
25-
'/readiness_check': _readinessCheckHandler,
26-
'/search': _searchHandler,
27-
'/robots.txt': rejectRobotsHandler,
28-
}[path];
29-
30-
if (handler != null) {
31-
return await handler(request);
32-
} else {
33-
return notFoundHandler(request);
34-
}
24+
final router = Router(notFoundHandler: notFoundHandler)
25+
..get('/debug', _debugHandler)
26+
..get('/liveness_check', _livenessCheckHandler)
27+
..get('/readiness_check', _readinessCheckHandler)
28+
..get('/search', _searchHandler)
29+
..post('/search', _searchHandler)
30+
..get('/robots.txt', rejectRobotsHandler);
31+
return await router.call(request);
3532
}
3633

37-
/// Handles /liveness_check requests.
34+
/// Handles GET /liveness_check requests.
3835
Future<shelf.Response> _livenessCheckHandler(shelf.Request request) async {
3936
return htmlResponse('OK');
4037
}
4138

42-
/// Handles /readiness_check requests.
39+
/// Handles GET /readiness_check requests.
4340
Future<shelf.Response> _readinessCheckHandler(shelf.Request request) async {
4441
if (await searchIndex.isReady()) {
4542
return htmlResponse('OK');
@@ -48,21 +45,28 @@ Future<shelf.Response> _readinessCheckHandler(shelf.Request request) async {
4845
}
4946
}
5047

51-
/// Handler /debug requests
48+
/// Handler GET /debug requests
5249
Future<shelf.Response> _debugHandler(shelf.Request request) async {
5350
final info = await searchIndex.indexInfo();
5451
return debugResponse(info.toJson());
5552
}
5653

57-
/// Handles /search requests.
54+
/// Handles GET /search requests.
55+
/// Handles POST /search requests.
5856
Future<shelf.Response> _searchHandler(shelf.Request request) async {
5957
final info = await searchIndex.indexInfo();
6058
if (!info.isReady) {
6159
return htmlResponse(searchIndexNotReadyText,
6260
status: searchIndexNotReadyCode);
6361
}
6462
final Stopwatch sw = Stopwatch()..start();
65-
final query = ServiceSearchQuery.fromServiceUrl(request.requestedUri);
63+
final query = request.method == 'POST'
64+
? ServiceSearchQuery.fromSearchRequestData(
65+
SearchRequestData.fromJson(
66+
json.decode(await request.readAsString()) as Map<String, dynamic>,
67+
),
68+
)
69+
: ServiceSearchQuery.fromServiceUrl(request.requestedUri);
6670
final result = await searchIndex.search(query);
6771
final Duration elapsed = sw.elapsed;
6872
if (elapsed > _slowSearchThreshold) {

app/lib/search/mem_index.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:math' as math;
66

77
import 'package:_pub_shared/search/search_form.dart';
8+
import 'package:_pub_shared/search/search_request_data.dart';
89
import 'package:clock/clock.dart';
910
import 'package:collection/collection.dart';
1011
import 'package:logging/logging.dart';
@@ -287,8 +288,6 @@ class InMemoryPackageIndex {
287288
case SearchOrder.updated:
288289
indexedHits = _updatedOrderedHits.whereInScores(selectFn);
289290
break;
290-
// ignore: deprecated_member_use
291-
case SearchOrder.popularity:
292291
case SearchOrder.downloads:
293292
indexedHits = _downloadsOrderedHits.whereInScores(selectFn);
294293
break;

app/lib/search/search_client.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:convert';
88
import 'package:_pub_shared/utils/http.dart';
99
import 'package:clock/clock.dart';
1010
import 'package:gcloud/service_scope.dart' as ss;
11+
import 'package:pub_dev/frontend/request_context.dart';
1112

1213
import '../../../service/rate_limit/rate_limit.dart';
1314
import '../shared/configuration.dart';
@@ -69,8 +70,33 @@ class SearchClient {
6970
Future<({int statusCode, String? body})?> doCallHttpServiceEndpoint(
7071
{String? prefix}) async {
7172
final httpHostPort = prefix ?? activeConfiguration.searchServicePrefix;
72-
final serviceUrl = '$httpHostPort/search$serviceUrlParams';
7373
try {
74+
if (requestContext.experimentalFlags.useSearchPost) {
75+
return await withRetryHttpClient(
76+
(client) async {
77+
final data = query.toSearchRequestData();
78+
// NOTE: Keeping the query parameter to help investigating logs.
79+
final uri = Uri.parse('$httpHostPort/search').replace(
80+
queryParameters: {
81+
'q': data.query,
82+
},
83+
);
84+
final rs = await client.post(
85+
uri,
86+
headers: {
87+
...?cloudTraceHeaders(),
88+
'content-type': 'application/json',
89+
},
90+
body: json.encode(data.toJson()),
91+
);
92+
return (statusCode: rs.statusCode, body: rs.body);
93+
},
94+
client: _httpClient,
95+
retryIf: (e) => (e is UnexpectedStatusException &&
96+
e.statusCode == searchIndexNotReadyCode),
97+
);
98+
}
99+
final serviceUrl = '$httpHostPort/search$serviceUrlParams';
74100
return await httpGetWithRetry(
75101
Uri.parse(serviceUrl),
76102
client: _httpClient,

app/lib/search/search_service.dart

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:math' show max;
77

88
import 'package:_pub_shared/search/search_form.dart';
9+
import 'package:_pub_shared/search/search_request_data.dart';
910
import 'package:_pub_shared/search/tags.dart';
1011
import 'package:clock/clock.dart';
1112
import 'package:collection/collection.dart';
@@ -240,6 +241,20 @@ class ServiceSearchQuery {
240241
);
241242
}
242243

244+
factory ServiceSearchQuery.fromSearchRequestData(SearchRequestData data) {
245+
final tagsPredicate = TagsPredicate.parseQueryValues(data.tags);
246+
return ServiceSearchQuery.parse(
247+
query: data.query,
248+
tagsPredicate: tagsPredicate,
249+
publisherId: data.publisherId,
250+
order: data.order,
251+
minPoints: data.minPoints,
252+
offset: data.offset ?? 0,
253+
limit: data.limit,
254+
textMatchExtent: data.textMatchExtent,
255+
);
256+
}
257+
243258
ServiceSearchQuery change({
244259
String? query,
245260
TagsPredicate? tagsPredicate,
@@ -310,38 +325,19 @@ class ServiceSearchQuery {
310325

311326
return QueryValidity.accept();
312327
}
313-
}
314-
315-
/// The scope (depth) of the text matching.
316-
enum TextMatchExtent {
317-
/// No text search is done.
318-
/// Requests with text queries will return a failure message.
319-
none,
320-
321-
/// Text search is on package names.
322-
name,
323-
324-
/// Text search is on package names, descriptions and topic tags.
325-
description,
326-
327-
/// Text search is on names, descriptions, topic tags and readme content.
328-
readme,
329-
330-
/// Text search is on names, descriptions, topic tags, readme content and API symbols.
331-
api,
332-
;
333-
334-
/// Text search is on package names.
335-
bool shouldMatchName() => index >= name.index;
336328

337-
/// Text search is on package names, descriptions and topic tags.
338-
bool shouldMatchDescription() => index >= description.index;
339-
340-
/// Text search is on names, descriptions, topic tags and readme content.
341-
bool shouldMatchReadme() => index >= readme.index;
342-
343-
/// Text search is on names, descriptions, topic tags, readme content and API symbols.
344-
bool shouldMatchApi() => index >= api.index;
329+
SearchRequestData toSearchRequestData() {
330+
return SearchRequestData(
331+
query: query,
332+
tags: tagsPredicate.toQueryParameters(),
333+
publisherId: publisherId,
334+
minPoints: minPoints,
335+
order: order,
336+
offset: offset,
337+
limit: limit,
338+
textMatchExtent: textMatchExtent,
339+
);
340+
}
345341
}
346342

347343
class QueryValidity {

app/lib/service/entrypoint/search_index.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66
import 'dart:convert';
77

8+
import 'package:_pub_shared/search/search_request_data.dart';
89
import 'package:args/args.dart';
910
import 'package:gcloud/service_scope.dart';
1011
import 'package:logging/logging.dart';

app/test/search/handlers_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:_pub_shared/search/search_request_data.dart';
78
import 'package:html/parser.dart' as html_parser;
89
import 'package:test/test.dart';
910

@@ -38,6 +39,21 @@ void main() {
3839
{'package': 'oxygen', 'score': isPositive},
3940
],
4041
});
42+
43+
await expectJsonResponse(
44+
await issuePost(
45+
'/search',
46+
body: SearchRequestData(query: 'oxygen').toJson(),
47+
),
48+
body: {
49+
'timestamp': isNotNull,
50+
'totalCount': 1,
51+
'sdkLibraryHits': [],
52+
'packageHits': [
53+
{'package': 'oxygen', 'score': isPositive},
54+
],
55+
},
56+
);
4157
});
4258

4359
testWithProfile('Finds text in description or readme', fn: () async {

pkg/_pub_shared/build.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ targets:
1616
- 'lib/data/publisher_api.dart'
1717
- 'lib/data/task_api.dart'
1818
- 'lib/data/task_payload.dart'
19+
- 'lib/search/search_form.dart'
20+
- 'lib/search/search_request_data.dart'
1921
- 'lib/utils/flutter_archive.dart'

pkg/_pub_shared/lib/search/search_form.dart

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,6 @@ enum SearchOrder {
4949
/// Search order should be in decreasing last package updated time.
5050
updated,
5151

52-
/// Search order should be in decreasing popularity score.
53-
/// WARNING: The value shouldn't be used anymore.
54-
///
55-
/// TODO: remove in a future release.
56-
@Deprecated('Popularity is no longer used.')
57-
popularity,
58-
5952
/// Search order should be in decreasing download counts.
6053
downloads,
6154

0 commit comments

Comments
 (0)