Skip to content

Commit 4dfd59f

Browse files
authored
Merge pull request #480 from meilisearch/feat/add-network-api
Add network API
2 parents 4a5ece3 + 180bca4 commit 4dfd59f

10 files changed

Lines changed: 758 additions & 4 deletions

lib/src/client.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,64 @@ class MeiliSearchClient {
306306
);
307307
return Task.fromMap(response.data!);
308308
}
309+
310+
/// Current network topology (requires experimental network feature).
311+
@RequiredMeiliServerVersion('1.37.0')
312+
Future<Network> getNetwork() async {
313+
final response = await http.getMethod<Map<String, Object?>>('/network');
314+
return Network.fromMap(response.data!);
315+
}
316+
317+
/// Partially update network topology via `PATCH /network`.
318+
@RequiredMeiliServerVersion('1.37.0')
319+
Future<Network> updateNetwork(UpdateNetworkOptions input) async {
320+
final data = input.toPatchMap();
321+
if (data.isEmpty) {
322+
throw ArgumentError.value(
323+
input,
324+
'input',
325+
'input must contain at least one field',
326+
);
327+
}
328+
final response =
329+
await http.patchMethod<Map<String, Object?>>('/network', data: data);
330+
return Network.fromMap(response.data!);
331+
}
332+
333+
/// Register or replace a single remote by name.
334+
@RequiredMeiliServerVersion('1.37.0')
335+
Future<Network> addRemote(String remoteName, Remote remote) {
336+
return updateNetwork(
337+
UpdateNetworkOptions(remotes: {remoteName: remote}),
338+
);
339+
}
340+
341+
/// Remove a remote by setting its entry to `null` in the patch payload.
342+
@RequiredMeiliServerVersion('1.37.0')
343+
Future<Network> removeRemote(String remoteName) {
344+
return updateNetwork(
345+
UpdateNetworkOptions(remotes: {remoteName: null}),
346+
);
347+
}
348+
349+
@RequiredMeiliServerVersion('1.37.0')
350+
Future<Network> addRemotesToShard(String shardName, List<String> remotes) {
351+
return updateNetwork(
352+
UpdateNetworkOptions(
353+
shards: {shardName: Shard(addRemotes: remotes)},
354+
),
355+
);
356+
}
357+
358+
@RequiredMeiliServerVersion('1.37.0')
359+
Future<Network> removeRemotesFromShard(
360+
String shardName,
361+
List<String> remotes,
362+
) {
363+
return updateNetwork(
364+
UpdateNetworkOptions(
365+
shards: {shardName: Shard(removeRemotes: remotes)},
366+
),
367+
);
368+
}
309369
}

lib/src/query_parameters/index_search_query.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class IndexSearchQuery extends SearchQuery {
3535
super.vector,
3636
super.showRankingScoreDetails,
3737
super.rankingScoreThreshold,
38+
super.useNetwork,
3839
});
3940

4041
@override
@@ -73,6 +74,7 @@ class IndexSearchQuery extends SearchQuery {
7374
List<dynamic /* double | List<double> */ >? vector,
7475
bool? showRankingScoreDetails,
7576
double? rankingScoreThreshold,
77+
bool? useNetwork,
7678
}) =>
7779
IndexSearchQuery(
7880
query: query ?? this.query,
@@ -103,5 +105,6 @@ class IndexSearchQuery extends SearchQuery {
103105
showRankingScoreDetails ?? this.showRankingScoreDetails,
104106
rankingScoreThreshold:
105107
rankingScoreThreshold ?? this.rankingScoreThreshold,
108+
useNetwork: useNetwork ?? this.useNetwork,
106109
);
107110
}

lib/src/query_parameters/search_query.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class SearchQuery extends Queryable {
3131
final double? rankingScoreThreshold;
3232
@RequiredMeiliServerVersion('1.3.0')
3333
final List<dynamic /* double | List<double> */ >? vector;
34+
@RequiredMeiliServerVersion('1.37.0')
35+
final bool? useNetwork;
3436

3537
const SearchQuery({
3638
this.offset,
@@ -56,6 +58,7 @@ class SearchQuery extends Queryable {
5658
this.showRankingScoreDetails,
5759
this.rankingScoreThreshold,
5860
this.vector,
61+
this.useNetwork,
5962
});
6063

6164
@override
@@ -83,6 +86,7 @@ class SearchQuery extends Queryable {
8386
'showRankingScoreDetails': showRankingScoreDetails,
8487
'rankingScoreThreshold': rankingScoreThreshold,
8588
'vector': vector,
89+
'useNetwork': useNetwork,
8690
};
8791
}
8892

@@ -110,6 +114,7 @@ class SearchQuery extends Queryable {
110114
List<dynamic>? vector,
111115
bool? showRankingScoreDetails,
112116
double? rankingScoreThreshold,
117+
bool? useNetwork,
113118
}) =>
114119
SearchQuery(
115120
offset: offset ?? this.offset,
@@ -138,5 +143,6 @@ class SearchQuery extends Queryable {
138143
showRankingScoreDetails ?? this.showRankingScoreDetails,
139144
rankingScoreThreshold:
140145
rankingScoreThreshold ?? this.rankingScoreThreshold,
146+
useNetwork: useNetwork ?? this.useNetwork,
141147
);
142148
}

lib/src/results/_exports.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export 'match_position.dart';
1111
export 'matching_strategy_enum.dart';
1212
export 'index_stats.dart';
1313
export 'all_stats.dart';
14+
export 'network.dart';
1415
export 'facet_stat.dart';
1516
export 'document_container.dart';
1617
export 'ranking_rules/_exports.dart';

lib/src/results/experimental_features.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ part 'experimental_features.g.dart';
1111
createToJson: false,
1212
)
1313
class ExperimentalFeatures {
14-
const ExperimentalFeatures();
14+
const ExperimentalFeatures({this.network});
15+
16+
final bool? network;
1517

1618
factory ExperimentalFeatures.fromJson(Map<String, dynamic> src) {
1719
return _$ExperimentalFeaturesFromJson(src);
@@ -24,7 +26,9 @@ class ExperimentalFeatures {
2426
createFactory: false,
2527
)
2628
class UpdateExperimentalFeatures {
27-
const UpdateExperimentalFeatures();
29+
const UpdateExperimentalFeatures({this.network});
30+
31+
final bool? network;
2832

2933
Map<String, dynamic> toJson() => _$UpdateExperimentalFeaturesToJson(this);
3034
}

lib/src/results/experimental_features.g.dart

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

lib/src/results/network.dart

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import '../annotations.dart';
2+
3+
class _Unset {
4+
const _Unset();
5+
}
6+
7+
const Object _unset = _Unset();
8+
9+
List<String>? _stringList(Object? raw) {
10+
if (raw == null) {
11+
return null;
12+
}
13+
if (raw is Iterable) {
14+
return raw.map((e) => e.toString()).toList();
15+
}
16+
return null;
17+
}
18+
19+
Map<String, Object?>? _asObjectMap(Object? raw) {
20+
if (raw == null) {
21+
return null;
22+
}
23+
if (raw is Map) {
24+
return raw.map((k, v) => MapEntry(k.toString(), v));
25+
}
26+
return null;
27+
}
28+
29+
/// Remote Meilisearch instance in a [Network] topology.
30+
@RequiredMeiliServerVersion('1.37.0')
31+
class Remote {
32+
const Remote({
33+
this.url,
34+
this.searchApiKey,
35+
this.writeApiKey,
36+
});
37+
38+
final String? url;
39+
final String? searchApiKey;
40+
final String? writeApiKey;
41+
42+
factory Remote.fromMap(Map<String, Object?> map) {
43+
return Remote(
44+
url: map['url'] as String?,
45+
searchApiKey: map['searchApiKey'] as String?,
46+
writeApiKey: map['writeApiKey'] as String?,
47+
);
48+
}
49+
50+
/// Fields to send in a PATCH body (sparse).
51+
Map<String, Object?> toPatchMap() {
52+
return <String, Object?>{
53+
if (url != null) 'url': url,
54+
if (searchApiKey != null) 'searchApiKey': searchApiKey,
55+
if (writeApiKey != null) 'writeApiKey': writeApiKey,
56+
};
57+
}
58+
}
59+
60+
/// Shard ownership configuration within a [Network].
61+
@RequiredMeiliServerVersion('1.37.0')
62+
class Shard {
63+
const Shard({
64+
this.remotes,
65+
this.addRemotes,
66+
this.removeRemotes,
67+
});
68+
69+
final List<String>? remotes;
70+
final List<String>? addRemotes;
71+
final List<String>? removeRemotes;
72+
73+
factory Shard.fromMap(Map<String, Object?> map) {
74+
return Shard(
75+
remotes: _stringList(map['remotes']),
76+
addRemotes: _stringList(map['addRemotes']),
77+
removeRemotes: _stringList(map['removeRemotes']),
78+
);
79+
}
80+
81+
Map<String, Object?> toPatchMap() {
82+
return <String, Object?>{
83+
if (remotes != null) 'remotes': remotes,
84+
if (addRemotes != null) 'addRemotes': addRemotes,
85+
if (removeRemotes != null) 'removeRemotes': removeRemotes,
86+
};
87+
}
88+
}
89+
90+
/// Network topology returned by `GET /network` / `PATCH /network`.
91+
@RequiredMeiliServerVersion('1.37.0')
92+
class Network {
93+
const Network({
94+
this.self,
95+
this.leader,
96+
this.remotes,
97+
this.shards,
98+
});
99+
100+
final String? self;
101+
final String? leader;
102+
final Map<String, Remote>? remotes;
103+
final Map<String, Shard>? shards;
104+
105+
factory Network.fromMap(Map<String, Object?> json) {
106+
Map<String, Remote>? remotes;
107+
final remotesRaw = _asObjectMap(json['remotes']);
108+
if (remotesRaw != null) {
109+
remotes = remotesRaw.map(
110+
(k, v) => MapEntry(
111+
k,
112+
Remote.fromMap(Map<String, Object?>.from(v! as Map)),
113+
),
114+
);
115+
}
116+
117+
Map<String, Shard>? shards;
118+
final shardsRaw = _asObjectMap(json['shards']);
119+
if (shardsRaw != null) {
120+
shards = shardsRaw.map(
121+
(k, v) => MapEntry(
122+
k,
123+
Shard.fromMap(Map<String, Object?>.from(v! as Map)),
124+
),
125+
);
126+
}
127+
128+
return Network(
129+
self: json['self'] as String?,
130+
leader: json['leader'] as String?,
131+
remotes: remotes,
132+
shards: shards,
133+
);
134+
}
135+
}
136+
137+
/// Partial update payload for `PATCH /network`.
138+
///
139+
/// Omit a field to leave it unchanged. Pass [leader] or [self] as `null` to
140+
/// clear. For [remotes] / [shards], pass a map; use `null` values for entries
141+
/// to remove a remote or shard.
142+
@RequiredMeiliServerVersion('1.37.0')
143+
class UpdateNetworkOptions {
144+
UpdateNetworkOptions({
145+
Object? self = _unset,
146+
Object? leader = _unset,
147+
Object? remotes = _unset,
148+
Object? shards = _unset,
149+
}) : _self = self,
150+
_leader = leader,
151+
_remotes = remotes,
152+
_shards = shards;
153+
154+
final Object? _self;
155+
final Object? _leader;
156+
final Object? _remotes;
157+
final Object? _shards;
158+
159+
Map<String, Object?> toPatchMap() {
160+
final out = <String, Object?>{};
161+
if (!identical(_self, _unset)) {
162+
out['self'] = _self as String?;
163+
}
164+
if (!identical(_leader, _unset)) {
165+
out['leader'] = _leader as String?;
166+
}
167+
if (!identical(_remotes, _unset)) {
168+
final m = _remotes as Map<String, Remote?>?;
169+
if (m != null) {
170+
out['remotes'] = m.map(
171+
(k, v) => MapEntry(
172+
k,
173+
v?.toPatchMap(),
174+
),
175+
);
176+
} else {
177+
out['remotes'] = null;
178+
}
179+
}
180+
if (!identical(_shards, _unset)) {
181+
final m = _shards as Map<String, Shard?>?;
182+
if (m != null) {
183+
out['shards'] = m.map(
184+
(k, v) => MapEntry(
185+
k,
186+
v?.toPatchMap(),
187+
),
188+
);
189+
} else {
190+
out['shards'] = null;
191+
}
192+
}
193+
return out;
194+
}
195+
}

0 commit comments

Comments
 (0)