-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
350: Add Result container r=brunoocasali a=ahmednfwela # Pull Request ## Related issue Fixes #335 Fixes #337 Fixes #338 Starts work on #276 ## What does this PR do? In this PR the main feature is adding `MeiliDocumentContainer<T>`, which wraps around documents returned by meilisearch and adds typed, useful information to them. I have also added a dependency on [json_serializable](https://pub.dev/packages/json_serializable) to generate serialization information for models (`toJson` + `fromJson`), but I will be rolling all models gradually. Here is the complete list of changes: 1. chore: updated `CONTRIBUTING.md` to include instructions on generating `json_serializable` files. 2. feat: added `getExperimentalFeatures` and `updateExperimentalFeatures` to `MeiliSearchClient` 3. feat: added to `SearchQuery` and `IndexSearchQuery` 3.1. `List? vector` 3.2. `bool? showRankingScore ` 3.3. `bool? showRankingScoreDetails` 4. feat: added `MeiliDocumentContainer<T>` 5. feat: added `Map<String, dynamic> src` to Searcheable<T>, which exposes the raw json object returned from the server. REASON: just in case we don't keep up with new meilisearch releases, the user has a way to access new features. 7. [BREAKING] fix: `Searcheable<T>` had a wrong `matchesPosition` property, which I have moved into `MeiliDocumentContainer<T>` 8. [BREAKING] fix: Marked all the fields in `Searcheable<T>` constructor as `required` REASON: just in case we forget to add them in `PaginatedSearchResult<T>` or `SearchResult<T>` ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? - [x] Updates to `.code-samples.meilisearch.yml` files Thank you so much for contributing to Meilisearch! Co-authored-by: Ahmed Fwela <[email protected]> Co-authored-by: Ahmed Fwela <[email protected]>
- Loading branch information
Showing
24 changed files
with
794 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import 'package:meilisearch/src/annotations.dart'; | ||
|
||
import 'match_position.dart'; | ||
import 'ranking_rules/base.dart'; | ||
import 'searchable.dart'; | ||
|
||
/// A class that wraps around documents returned from meilisearch to provide useful information. | ||
final class MeiliDocumentContainer<T extends Object> { | ||
const MeiliDocumentContainer._({ | ||
required this.rankingScoreDetails, | ||
required this.src, | ||
required this.parsed, | ||
required this.formatted, | ||
required this.vectors, | ||
required this.semanticScore, | ||
required this.rankingScore, | ||
required this.matchesPosition, | ||
}); | ||
|
||
final Map<String, dynamic> src; | ||
final T parsed; | ||
final Map<String, dynamic>? formatted; | ||
@RequiredMeiliServerVersion('1.3.0') | ||
final List<dynamic /* double | List<double> */ >? vectors; | ||
@RequiredMeiliServerVersion('1.3.0') | ||
final double? semanticScore; | ||
@RequiredMeiliServerVersion('1.3.0') | ||
final double? rankingScore; | ||
@RequiredMeiliServerVersion('1.3.0') | ||
final MeiliRankingScoreDetails? rankingScoreDetails; | ||
|
||
/// Contains the location of each occurrence of queried terms across all fields | ||
final Map<String, List<MatchPosition>>? matchesPosition; | ||
|
||
dynamic operator [](String key) => src[key]; | ||
dynamic getFormatted(String key) => formatted?[key]; | ||
|
||
dynamic getFormattedOrSrc(String key) => getFormatted(key) ?? this[key]; | ||
|
||
static MeiliDocumentContainer<Map<String, dynamic>> fromJson( | ||
Map<String, dynamic> src, | ||
) { | ||
final rankingScoreDetails = | ||
src['_rankingScoreDetails'] as Map<String, dynamic>?; | ||
return MeiliDocumentContainer<Map<String, dynamic>>._( | ||
src: src, | ||
parsed: src, | ||
formatted: src['_formatted'] as Map<String, dynamic>?, | ||
vectors: src['_vectors'] as List?, | ||
semanticScore: src['_semanticScore'] as double?, | ||
rankingScore: src['_rankingScore'] as double?, | ||
matchesPosition: _readMatchesPosition(src), | ||
rankingScoreDetails: rankingScoreDetails == null | ||
? null | ||
: MeiliRankingScoreDetails.fromJson(rankingScoreDetails), | ||
); | ||
} | ||
|
||
MeiliDocumentContainer<TOther> map<TOther extends Object>( | ||
MeilisearchDocumentMapper<T, TOther> mapper, | ||
) { | ||
return MeiliDocumentContainer._( | ||
src: src, | ||
parsed: mapper(parsed), | ||
formatted: formatted, | ||
vectors: vectors, | ||
semanticScore: semanticScore, | ||
rankingScore: rankingScore, | ||
rankingScoreDetails: rankingScoreDetails, | ||
matchesPosition: matchesPosition); | ||
} | ||
|
||
@override | ||
String toString() => src.toString(); | ||
} | ||
|
||
class MeiliRankingScoreDetails { | ||
const MeiliRankingScoreDetails._({ | ||
required this.src, | ||
required this.words, | ||
required this.typo, | ||
required this.proximity, | ||
required this.attribute, | ||
required this.exactness, | ||
required this.customRules, | ||
}); | ||
final Map<String, dynamic> src; | ||
final MeiliRankingScoreDetailsWordsRule? words; | ||
final MeiliRankingScoreDetailsTypoRule? typo; | ||
final MeiliRankingScoreDetailsProximityRule? proximity; | ||
final MeiliRankingScoreDetailsAttributeRule? attribute; | ||
final MeiliRankingScoreDetailsExactnessRule? exactness; | ||
final Map<String, MeiliRankingScoreDetailsCustomRule> customRules; | ||
|
||
factory MeiliRankingScoreDetails.fromJson(Map<String, dynamic> src) { | ||
final reservedKeys = { | ||
'attribute', | ||
'words', | ||
'exactness', | ||
'proximity', | ||
'typo', | ||
}; | ||
|
||
T? ruleGuarded<T>( | ||
String key, | ||
T Function(Map<String, dynamic> src) mapper, | ||
) { | ||
final v = src[key]; | ||
if (v == null) { | ||
return null; | ||
} | ||
return mapper(v as Map<String, dynamic>); | ||
} | ||
|
||
return MeiliRankingScoreDetails._( | ||
src: src, | ||
attribute: ruleGuarded( | ||
'attribute', | ||
MeiliRankingScoreDetailsAttributeRule.fromJson, | ||
), | ||
words: ruleGuarded( | ||
'words', | ||
MeiliRankingScoreDetailsWordsRule.fromJson, | ||
), | ||
exactness: ruleGuarded( | ||
'exactness', | ||
MeiliRankingScoreDetailsExactnessRule.fromJson, | ||
), | ||
proximity: ruleGuarded( | ||
'proximity', | ||
MeiliRankingScoreDetailsProximityRule.fromJson, | ||
), | ||
typo: ruleGuarded( | ||
'typo', | ||
MeiliRankingScoreDetailsTypoRule.fromJson, | ||
), | ||
customRules: { | ||
for (var custom in src.entries | ||
.where((element) => !reservedKeys.contains(element.key))) | ||
custom.key: MeiliRankingScoreDetailsCustomRule.fromJson( | ||
custom.value as Map<String, dynamic>, | ||
) | ||
}, | ||
); | ||
} | ||
} | ||
|
||
Map<String, List<MatchPosition>>? _readMatchesPosition( | ||
Map<String, Object?> map, | ||
) { | ||
final src = map['_matchesPosition']; | ||
|
||
if (src == null) return null; | ||
|
||
return (src as Map<String, Object?>).map( | ||
(key, value) => MapEntry( | ||
key, | ||
(value as List<Object?>) | ||
.map((e) => MatchPosition.fromMap(e as Map<String, Object?>)) | ||
.toList(), | ||
), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import 'package:json_annotation/json_annotation.dart'; | ||
import 'package:meta/meta.dart'; | ||
import '../http_request.dart'; | ||
import '../annotations.dart'; | ||
|
||
part 'experimental_features.g.dart'; | ||
|
||
@visibleForTesting | ||
@JsonSerializable( | ||
createFactory: true, | ||
createToJson: false, | ||
) | ||
class ExperimentalFeatures { | ||
@JsonKey(name: 'vectorStore') | ||
final bool vectorStore; | ||
@JsonKey(name: 'scoreDetails') | ||
final bool scoreDetails; | ||
|
||
const ExperimentalFeatures({ | ||
required this.vectorStore, | ||
required this.scoreDetails, | ||
}); | ||
|
||
factory ExperimentalFeatures.fromJson(Map<String, dynamic> src) { | ||
return _$ExperimentalFeaturesFromJson(src); | ||
} | ||
} | ||
|
||
@JsonSerializable( | ||
includeIfNull: false, | ||
createToJson: true, | ||
createFactory: false, | ||
) | ||
class UpdateExperimentalFeatures { | ||
@JsonKey(name: 'vectorStore') | ||
final bool? vectorStore; | ||
@JsonKey(name: 'scoreDetails') | ||
final bool? scoreDetails; | ||
|
||
const UpdateExperimentalFeatures({ | ||
this.vectorStore, | ||
this.scoreDetails, | ||
}); | ||
|
||
Map<String, dynamic> toJson() => _$UpdateExperimentalFeaturesToJson(this); | ||
} | ||
|
||
extension ExperimentalFeaturesExt on HttpRequest { | ||
/// Get the status of all experimental features that can be toggled at runtime | ||
@RequiredMeiliServerVersion('1.3.0') | ||
@visibleForTesting | ||
Future<ExperimentalFeatures> getExperimentalFeatures() async { | ||
final response = await getMethod<Map<String, Object?>>( | ||
'/experimental-features', | ||
); | ||
return ExperimentalFeatures.fromJson(response.data!); | ||
} | ||
|
||
/// Set the status of experimental features that can be toggled at runtime | ||
@RequiredMeiliServerVersion('1.3.0') | ||
@visibleForTesting | ||
Future<ExperimentalFeatures> updateExperimentalFeatures( | ||
UpdateExperimentalFeatures input, | ||
) async { | ||
final inputJson = input.toJson(); | ||
if (inputJson.isEmpty) { | ||
throw ArgumentError.value( | ||
input, | ||
'input', | ||
'input must contain at least one entry', | ||
); | ||
} | ||
final response = await patchMethod<Map<String, Object?>>( | ||
'/experimental-features', | ||
data: input.toJson(), | ||
); | ||
return ExperimentalFeatures.fromJson(response.data!); | ||
} | ||
} |
Oops, something went wrong.