Skip to content

Commit

Permalink
Merge #350
Browse files Browse the repository at this point in the history
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
3 people authored Sep 14, 2023
2 parents 758a4b8 + 6ebd988 commit 656bd85
Show file tree
Hide file tree
Showing 24 changed files with 794 additions and 86 deletions.
2 changes: 2 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# the documentation on build
# You can read more on https://github.com/meilisearch/documentation/tree/master/.vuepress/code-samples
---
search_parameter_guide_show_ranking_score_1: |-
await client.index('movies').search('dragon', SearchQuery(showRankingScore: true));
facet_search_2: |-
await client.index('books').updateFaceting(Faceting(sortFacetValuesBy: {'genres': 'count'}));
search_parameter_guide_attributes_to_search_on_1: |-
Expand Down
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ First of all, thank you for contributing to Meilisearch! The goal of this docume

Install the official [Dart SDK](https://dart.dev/get-dart) or the [Flutter SDK](https://flutter.dev/docs/get-started/install) (which includes Dart SDK) using guides on the official website.

Both of them include `pub`. But if you want to run the linter you need to install the Flutter SDK.
Both of them include `pub`.

### Setup <!-- omit in TOC -->

Expand All @@ -54,13 +54,13 @@ docker-compose run --rm package bash -c "dart pub get && dart run test --concurr
To install dependencies:

```bash
pub get
dart pub get
```

Or if you are using Flutter SDK:
This package relies on [build_runner](https://pub.dev/packages/build_runner) to generate serialization information for some models, to re-generate files after making any changes, run:

```bash
flutter pub get
dart run build_runner build
```

### Tests and Linter <!-- omit in TOC -->
Expand All @@ -71,9 +71,9 @@ Each PR should pass the tests and the linter to be accepted.
# Tests
curl -L https://install.meilisearch.com | sh # download Meilisearch
./meilisearch --master-key=masterKey --no-analytics # run Meilisearch
pub run test --concurrency=4
dart test
# Linter
flutter analyze
dart analyze
```

## Git Guidelines
Expand Down
10 changes: 10 additions & 0 deletions lib/src/query_parameters/index_search_query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class IndexSearchQuery extends SearchQuery {
super.highlightPostTag,
super.matchingStrategy,
super.attributesToSearchOn,
super.showRankingScore,
super.vector,
super.showRankingScoreDetails,
});

@override
Expand Down Expand Up @@ -62,6 +65,9 @@ class IndexSearchQuery extends SearchQuery {
String? highlightPostTag,
MatchingStrategy? matchingStrategy,
List<String>? attributesToSearchOn,
bool? showRankingScore,
List<dynamic /* double | List<double> */ >? vector,
bool? showRankingScoreDetails,
}) =>
IndexSearchQuery(
query: query ?? this.query,
Expand All @@ -85,5 +91,9 @@ class IndexSearchQuery extends SearchQuery {
highlightPostTag: highlightPostTag ?? this.highlightPostTag,
matchingStrategy: matchingStrategy ?? this.matchingStrategy,
attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn,
showRankingScore: showRankingScore ?? this.showRankingScore,
vector: vector ?? this.vector,
showRankingScoreDetails:
showRankingScoreDetails ?? this.showRankingScoreDetails,
);
}
20 changes: 20 additions & 0 deletions lib/src/query_parameters/search_query.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:meilisearch/meilisearch.dart';
import 'package:meilisearch/src/annotations.dart';
import 'queryable.dart';

class SearchQuery extends Queryable {
Expand All @@ -20,6 +21,12 @@ class SearchQuery extends Queryable {
final String? highlightPostTag;
final MatchingStrategy? matchingStrategy;
final List<String>? attributesToSearchOn;
@RequiredMeiliServerVersion('1.3.0')
final bool? showRankingScore;
@RequiredMeiliServerVersion('1.3.0')
final bool? showRankingScoreDetails;
@RequiredMeiliServerVersion('1.3.0')
final List<dynamic /* double | List<double> */ >? vector;

const SearchQuery({
this.offset,
Expand All @@ -40,6 +47,9 @@ class SearchQuery extends Queryable {
this.highlightPostTag,
this.matchingStrategy,
this.attributesToSearchOn,
this.showRankingScore,
this.showRankingScoreDetails,
this.vector,
});

@override
Expand All @@ -62,6 +72,9 @@ class SearchQuery extends Queryable {
'highlightPostTag': highlightPostTag,
'matchingStrategy': matchingStrategy?.name,
'attributesToSearchOn': attributesToSearchOn,
'showRankingScore': showRankingScore,
'showRankingScoreDetails': showRankingScoreDetails,
'vector': vector,
};
}

Expand All @@ -84,6 +97,9 @@ class SearchQuery extends Queryable {
String? highlightPostTag,
MatchingStrategy? matchingStrategy,
List<String>? attributesToSearchOn,
bool? showRankingScore,
List<dynamic>? vector,
bool? showRankingScoreDetails,
}) =>
SearchQuery(
offset: offset ?? this.offset,
Expand All @@ -105,5 +121,9 @@ class SearchQuery extends Queryable {
highlightPostTag: highlightPostTag ?? this.highlightPostTag,
matchingStrategy: matchingStrategy ?? this.matchingStrategy,
attributesToSearchOn: attributesToSearchOn ?? this.attributesToSearchOn,
showRankingScore: showRankingScore ?? this.showRankingScore,
vector: vector ?? this.vector,
showRankingScoreDetails:
showRankingScoreDetails ?? this.showRankingScoreDetails,
);
}
2 changes: 2 additions & 0 deletions lib/src/results/_exports.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export 'matching_strategy_enum.dart';
export 'index_stats.dart';
export 'all_stats.dart';
export 'facet_stat.dart';
export 'document_container.dart';
export 'ranking_rules/_exports.dart';
163 changes: 163 additions & 0 deletions lib/src/results/document_container.dart
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(),
),
);
}
79 changes: 79 additions & 0 deletions lib/src/results/experimental_features.dart
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!);
}
}
Loading

0 comments on commit 656bd85

Please sign in to comment.