Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,7 @@ get_non_separator_tokens_1: await client.index('articles').getNonSeparatorTokens
update_non_separator_tokens_1: "await client.index('articles').updateNonSeparatorTokens([\"@\", \"#\"]);"
reset_non_separator_tokens_1: await client.index('articles').resetNonSeparatorTokens();
export_post_1: "await client.export(\n ExportQuery(\n url: exportSinkUrl,\n apiKey: 'new_instance_api_key',\n payloadSize: \"100 MiB\",\n ),\n);"
list_dynamic_search_rules_1: "await client.listDynamicSearchRules();"
get_dynamic_search_rule_1: "await client.getDynamicSearchRule('black-friday');"
patch_dynamic_search_rule_1: "await client.updateOrCreateDynamicSearchRule(\n 'black-friday',\n DynamicSearchRule(\n uid: 'black-friday',\n description: 'Black Friday 2025 rules',\n precedence: 10,\n active: true,\n conditions: <String, Object?>{\n 'query': <String, Object?>{'isEmpty': true},\n 'time': <String, Object?>{\n 'start': '2025-11-28T00:00:00Z',\n 'end': '2025-11-28T23:59:59Z',\n },\n },\n actions: <Map<String, Object?>>[\n <String, Object?>{\n 'selector': <String, Object?>{'indexUid': 'products', 'id': '123'},\n 'action': <String, Object?>{'type': 'pin', 'position': 1},\n },\n ],\n ),\n);"
delete_dynamic_search_rule_1: "await client.deleteDynamicSearchRule('black-friday');"
64 changes: 64 additions & 0 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,68 @@ class MeiliSearchClient {
),
);
}

//
// Dynamic Search Rules endpoints (experimental, Meilisearch v1.50.0+)
//
// These endpoints require the `dynamicSearchRules` experimental feature
// flag to be enabled on the server, e.g.:
//
// await client.http.updateExperimentalFeatures(
// const UpdateExperimentalFeatures(dynamicSearchRules: true),
// );
//

/// Lists Dynamic Search Rules. The server exposes this as a POST so the
/// filter expression can travel in the body; pass [params] to paginate
/// or filter, or omit it to list rules with the server's default paging.
@RequiredMeiliServerVersion('1.50.0')
Future<Result<DynamicSearchRule>> listDynamicSearchRules({
DynamicSearchRulesQuery? params,
}) async {
final response = await http.postMethod<Map<String, Object?>>(
'/dynamic-search-rules',
data: params?.toBody() ?? const <String, Object?>{},
);

return Result<DynamicSearchRule>.fromMapWithType(
response.data!,
(model) => DynamicSearchRule.fromJson(model),
);
}

/// Retrieves a single Dynamic Search Rule by its [uid].
@RequiredMeiliServerVersion('1.50.0')
Future<DynamicSearchRule> getDynamicSearchRule(String uid) async {
final response = await http.getMethod<Map<String, Object?>>(
'/dynamic-search-rules/$uid',
);

return DynamicSearchRule.fromJson(response.data!);
}

/// Creates a new Dynamic Search Rule with the given [uid], or updates
/// the existing one (upsert). Returns the async [Task] the server
/// enqueues to apply the change.
@RequiredMeiliServerVersion('1.50.0')
Future<Task> updateOrCreateDynamicSearchRule(
String uid,
DynamicSearchRule rule,
) {
return _update(
http.patchMethod<Map<String, Object?>>(
'/dynamic-search-rules/$uid',
data: rule.toUpsertBody(),
),
);
}

/// Deletes the Dynamic Search Rule identified by [uid]. Returns the
/// async [Task] the server enqueues to apply the deletion.
@RequiredMeiliServerVersion('1.50.0')
Future<Task> deleteDynamicSearchRule(String uid) {
return _update(
http.deleteMethod<Map<String, Object?>>('/dynamic-search-rules/$uid'),
);
}
}
1 change: 1 addition & 0 deletions lib/src/query_parameters/_exports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export 'facet_search_query.dart';
export 'swap_index.dart';
export 'hybrid_search.dart';
export 'export_query.dart';
export 'dynamic_search_rules_query.dart';
53 changes: 53 additions & 0 deletions lib/src/query_parameters/dynamic_search_rules_query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/// The filter object accepted inside a
/// `POST /dynamic-search-rules` body (Meilisearch v1.50.0+).
///
/// In v1.50.0 the list-endpoint filter changed shape: instead of a bare
/// filter-expression string it takes an object. [query] searches across
/// the rule's description and its `conditions.query.words`; [active]
/// narrows the result to active/paused rules.
class DynamicSearchRulesFilter {
final String? query;
final bool? active;

const DynamicSearchRulesFilter({this.query, this.active});

Map<String, Object?> toJson() => <String, Object?>{
if (query != null) 'query': query,
if (active != null) 'active': active,
};
}

/// Body for `POST /dynamic-search-rules`.
///
/// The list endpoint is a POST rather than a GET so callers can send an
/// arbitrary filter object in the body. All fields are optional; an
/// empty body lists every rule with the server's default pagination.
class DynamicSearchRulesQuery {
/// Zero-based offset into the rule list.
final int? offset;

/// Maximum number of rules to return in a single response.
final int? limit;

/// Optional filter object. See [DynamicSearchRulesFilter].
final DynamicSearchRulesFilter? filter;

const DynamicSearchRulesQuery({
this.offset,
this.limit,
this.filter,
});

/// Serializes this query to the JSON body sent to
/// `POST /dynamic-search-rules`. Nulls are omitted so callers only send
/// what they set. An empty filter object is also omitted so an
/// unconfigured [filter] doesn't send `{}` on the wire.
Map<String, Object?> toBody() {
final filterMap = filter?.toJson();
return <String, Object?>{
if (offset != null) 'offset': offset,
if (limit != null) 'limit': limit,
if (filterMap != null && filterMap.isNotEmpty) 'filter': filterMap,
};
}
}
1 change: 1 addition & 0 deletions lib/src/results/_exports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export 'all_stats.dart';
export 'network.dart';
export 'facet_stat.dart';
export 'document_container.dart';
export 'dynamic_search_rule.dart';
export 'ranking_rules/_exports.dart';
89 changes: 89 additions & 0 deletions lib/src/results/dynamic_search_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/// A Dynamic Search Rule as returned by the
/// `GET /dynamic-search-rules/{uid}` and `POST /dynamic-search-rules`
/// endpoints introduced in Meilisearch v1.50.0 (experimental).
///
/// The rule shape follows the [Meilisearch reference](https://www.meilisearch.com/docs/capabilities/search_rules/overview).
/// `conditions` and `actions` are exposed as raw maps/lists so the SDK
/// keeps working when new sub-fields are added on the server side without a
/// coordinated release. Callers that want strongly typed access can wrap
/// them at their layer.
class DynamicSearchRule {
/// The rule's unique identifier (path parameter for the single-rule
/// endpoints).
final String uid;

/// Human-readable description.
final String? description;

/// Ordering key when several rules match a given query. In v1.50.0 this
/// field was renamed from `priority` to `precedence`, and **lower**
/// numeric values apply first: a rule with `precedence: 1` is picked
/// before a rule with `precedence: 5`.
final int? precedence;

/// Whether the rule is currently active.
final bool? active;

/// Condition tree that decides when the rule fires (query patterns,
/// time windows, etc.). Exposed as a raw map so consumers see every
/// field the server sends, including ones added after this SDK ships.
final Map<String, Object?>? conditions;

/// Ordered list of actions the rule performs when it fires (typically
/// document pinning). Same raw-map rationale as [conditions].
final List<Map<String, Object?>>? actions;

/// ISO-8601 timestamp; present when the server returns it.
final DateTime? createdAt;

/// ISO-8601 timestamp; present when the server returns it.
final DateTime? updatedAt;

const DynamicSearchRule({
required this.uid,
this.description,
this.precedence,
this.active,
this.conditions,
this.actions,
this.createdAt,
this.updatedAt,
});

factory DynamicSearchRule.fromJson(Map<String, Object?> json) {
final actionsRaw = json['actions'];
final createdAtRaw = json['createdAt'];
final updatedAtRaw = json['updatedAt'];
final conditionsRaw = json['conditions'];
return DynamicSearchRule(
uid: json['uid'] as String? ?? '',
description: json['description'] as String?,
precedence: json['precedence'] as int?,
active: json['active'] as bool?,
conditions: conditionsRaw is Map
? Map<String, Object?>.from(conditionsRaw)
: null,
actions: actionsRaw is Iterable
? actionsRaw
.whereType<Map<Object?, Object?>>()
.map(Map<String, Object?>.from)
.toList()
: null,
createdAt:
createdAtRaw is String ? DateTime.tryParse(createdAtRaw) : null,
updatedAt:
updatedAtRaw is String ? DateTime.tryParse(updatedAtRaw) : null,
);
}

/// Body used when upserting a rule via
/// `PATCH /dynamic-search-rules/{uid}`. Fields that are null on this
/// instance are omitted so callers can send sparse updates.
Map<String, Object?> toUpsertBody() => <String, Object?>{
if (description != null) 'description': description,
if (precedence != null) 'precedence': precedence,
if (active != null) 'active': active,
if (conditions != null) 'conditions': conditions,
if (actions != null) 'actions': actions,
};
}
Loading