Skip to content

Commit c9fcdfc

Browse files
Merge #296
296: added proper types for facetStats, facetDistributions and matchesPosition r=brunoocasali a=ahmednfwela # Pull Request ## Related issue Fixes #294 ## What does this PR do? - Introduces new classes 1. FacetStat 2. MatchPosition - [Breaking] change `Searchable<T>` to have proper types: ```diff - final Object? facetDistribution; + final Map<String, Map<String, int>>? facetDistribution; - final Object? matchesPosition; + final Map<String, List<MatchPosition>>? matchesPosition; + final Map<String, FacetStat>? facetStats; ``` - Internal refactoring to not repeat any deserialization logic between `Searchable<T>` and its subclasses `SearchResult<T>` and `PaginatedSearchResult<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? Thank you so much for contributing to Meilisearch! cc `@brunoocasali` Co-authored-by: Ahmed Fwela <[email protected]> Co-authored-by: Ahmed Fwela <[email protected]>
2 parents 3505e83 + e497148 commit c9fcdfc

8 files changed

+181
-53
lines changed

lib/meilisearch.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ library meilisearch;
33
export 'src/client.dart';
44
export 'src/index.dart';
55
export 'src/settings/_exports.dart';
6-
export 'src/search_result.dart';
6+
export 'src/facet_stat.dart';
7+
export 'src/match_position.dart';
78
export 'src/searchable.dart';
89
export 'src/multi_search_result.dart';
910
export 'src/multi_search_query.dart';
1011
export 'src/search_query.dart';
1112
export 'src/filter_builder/_exports.dart';
1213
export 'src/query_parameters/_exports.dart';
13-
export 'src/paginated_search_result.dart';
1414
export 'src/matching_strategy_enum.dart';
1515
export 'src/key.dart';
1616
export 'src/exception.dart';

lib/src/facet_stat.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
///When using the facets parameter, the distributed facets that contain some numeric values are displayed in a facetStats object that contains, per facet, the numeric min and max values of the hits returned by the search query.
2+
///If none of the hits returned by the search query have a numeric value for a facet, this facet is not part of the facetStats object.
3+
class FacetStat {
4+
///The minimum value for the numerical facet being distributed.
5+
final num min;
6+
7+
///The maximum value for the numerical facet being distributed.
8+
final num max;
9+
10+
const FacetStat({
11+
required this.min,
12+
required this.max,
13+
});
14+
15+
factory FacetStat.fromMap(Map<String, Object?> src) {
16+
return FacetStat(
17+
min: src['min'] as num,
18+
max: src['max'] as num,
19+
);
20+
}
21+
}

lib/src/match_position.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class MatchPosition {
2+
final int start;
3+
final int length;
4+
5+
const MatchPosition({
6+
required this.start,
7+
required this.length,
8+
});
9+
10+
factory MatchPosition.fromMap(Map<String, dynamic> map) {
11+
return MatchPosition(
12+
start: map['start'] as int,
13+
length: map['length'] as int,
14+
);
15+
}
16+
}

lib/src/paginated_search_result.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import 'package:meilisearch/src/searchable.dart';
1+
part of 'searchable.dart';
22

33
class PaginatedSearchResult<T> extends Searcheable<T> {
44
const PaginatedSearchResult({
55
List<T> hits = const [],
6-
Object? facetDistribution,
7-
Object? matchesPosition,
6+
Map<String, Map<String, int>>? facetDistribution,
7+
Map<String, List<MatchPosition>>? matchesPosition,
88
int? processingTimeMs,
99
String? query,
10+
Map<String, FacetStat>? facetStats,
1011
required String indexUid,
1112
this.hitsPerPage,
1213
this.page,
@@ -19,6 +20,7 @@ class PaginatedSearchResult<T> extends Searcheable<T> {
1920
processingTimeMs: processingTimeMs,
2021
query: query,
2122
indexUid: indexUid,
23+
facetStats: facetStats,
2224
);
2325

2426
/// Number of documents skipped
@@ -42,12 +44,13 @@ class PaginatedSearchResult<T> extends Searcheable<T> {
4244
hitsPerPage: map['hitsPerPage'] as int?,
4345
totalHits: map['totalHits'] as int?,
4446
totalPages: map['totalPages'] as int?,
45-
hits: (map['hits'] as List?)?.cast<Map<String, Object?>>() ?? [],
46-
query: map['query'] as String?,
47-
processingTimeMs: map['processingTimeMs'] as int?,
48-
facetDistribution: map['facetDistribution'],
49-
matchesPosition: map['_matchesPosition'],
50-
indexUid: indexUid ?? map['indexUid'] as String,
47+
hits: _readHits(map),
48+
query: _readQuery(map),
49+
processingTimeMs: _readProcessingTimeMs(map),
50+
facetDistribution: _readFacetDistribution(map),
51+
matchesPosition: _readMatchesPosition(map),
52+
facetStats: _readFacetStats(map),
53+
indexUid: indexUid ?? _readIndexUid(map),
5154
);
5255
}
5356

lib/src/search_result.dart

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import 'package:meilisearch/src/searchable.dart';
1+
part of 'searchable.dart';
22

33
class SearchResult<T> extends Searcheable<T> {
44
const SearchResult({
55
List<T> hits = const [],
6-
Object? facetDistribution,
7-
Object? matchesPosition,
6+
Map<String, Map<String, int>>? facetDistribution,
7+
Map<String, List<MatchPosition>>? matchesPosition,
88
int? processingTimeMs,
99
String? query,
10+
Map<String, FacetStat>? facetStats,
1011
required String indexUid,
1112
this.offset,
1213
this.limit,
@@ -18,6 +19,7 @@ class SearchResult<T> extends Searcheable<T> {
1819
processingTimeMs: processingTimeMs,
1920
query: query,
2021
indexUid: indexUid,
22+
facetStats: facetStats,
2123
);
2224

2325
/// Number of documents skipped
@@ -37,12 +39,13 @@ class SearchResult<T> extends Searcheable<T> {
3739
limit: map['limit'] as int?,
3840
offset: map['offset'] as int?,
3941
estimatedTotalHits: map['estimatedTotalHits'] as int?,
40-
hits: (map['hits'] as List?)?.cast<Map<String, Object?>>() ?? [],
41-
query: map['query'] as String?,
42-
processingTimeMs: map['processingTimeMs'] as int?,
43-
facetDistribution: map['facetDistribution'],
44-
matchesPosition: map['_matchesPosition'],
45-
indexUid: indexUid ?? map['indexUid'] as String,
42+
hits: _readHits(map),
43+
query: _readQuery(map),
44+
processingTimeMs: _readProcessingTimeMs(map),
45+
facetDistribution: _readFacetDistribution(map),
46+
matchesPosition: _readMatchesPosition(map),
47+
indexUid: indexUid ?? _readIndexUid(map),
48+
facetStats: _readFacetStats(map),
4649
);
4750
}
4851

lib/src/searchable.dart

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:meilisearch/meilisearch.dart';
22

3-
typedef MeilisearchDocumentMapper<TSrc, TOther> = TOther Function(TSrc src);
3+
part 'search_result.dart';
4+
part 'paginated_search_result.dart';
5+
part 'searchable_helpers.dart';
46

57
abstract class Searcheable<T> {
68
final String indexUid;
@@ -12,10 +14,13 @@ abstract class Searcheable<T> {
1214
final List<T> hits;
1315

1416
/// Distribution of the given facets
15-
final Object? facetDistribution;
17+
final Map<String, Map<String, int>>? facetDistribution;
18+
19+
/// Distribution of the given facets
20+
final Map<String, FacetStat>? facetStats;
1621

1722
/// Contains the location of each occurrence of queried terms across all fields
18-
final Object? matchesPosition;
23+
final Map<String, List<MatchPosition>>? matchesPosition;
1924

2025
/// Processing time of the query
2126
final int? processingTimeMs;
@@ -27,6 +32,7 @@ abstract class Searcheable<T> {
2732
this.facetDistribution,
2833
this.matchesPosition,
2934
this.processingTimeMs,
35+
this.facetStats,
3036
});
3137

3238
static Searcheable<Map<String, dynamic>> createSearchResult(
@@ -54,30 +60,3 @@ abstract class Searcheable<T> {
5460
return src as SearchResult<T>;
5561
}
5662
}
57-
58-
extension SearchableExt<T> on Future<Searcheable<T>> {
59-
Future<PaginatedSearchResult<T>> asPaginatedResult() =>
60-
then((value) => value.asPaginatedResult());
61-
62-
Future<SearchResult<T>> asSearchResult() =>
63-
then((value) => value.asSearchResult());
64-
65-
Future<Searcheable<TOther>> map<TOther>(
66-
MeilisearchDocumentMapper<T, TOther> mapper,
67-
) =>
68-
then((value) => value.map(mapper));
69-
}
70-
71-
extension SearchResultExt<T> on Future<SearchResult<T>> {
72-
Future<SearchResult<TOther>> map<TOther>(
73-
MeilisearchDocumentMapper<T, TOther> mapper,
74-
) =>
75-
then((value) => value.map(mapper));
76-
}
77-
78-
extension PaginatedSearchResultExt<T> on Future<PaginatedSearchResult<T>> {
79-
Future<PaginatedSearchResult<TOther>> map<TOther>(
80-
MeilisearchDocumentMapper<T, TOther> mapper,
81-
) =>
82-
then((value) => value.map(mapper));
83-
}

lib/src/searchable_helpers.dart

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
part of 'searchable.dart';
2+
3+
String _readIndexUid(Map<String, Object?> map) => map['indexUid'] as String;
4+
String? _readQuery(Map<String, Object?> map) => map['query'] as String?;
5+
6+
int? _readProcessingTimeMs(Map<String, Object?> map) =>
7+
map['processingTimeMs'] as int?;
8+
9+
List<Map<String, dynamic>> _readHits(Map<String, Object?> map) =>
10+
(map['hits'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
11+
12+
Map<String, FacetStat>? _readFacetStats(
13+
Map<String, Object?> map,
14+
) {
15+
final facetStatsRaw = map['facetStats'] as Map<String, Object?>?;
16+
17+
return facetStatsRaw?.map(
18+
(key, value) => MapEntry(
19+
key,
20+
FacetStat.fromMap(value as Map<String, Object?>),
21+
),
22+
);
23+
}
24+
25+
Map<String, Map<String, int>>? _readFacetDistribution(
26+
Map<String, Object?> map,
27+
) {
28+
final src = map['facetDistribution'];
29+
30+
if (src == null) return null;
31+
32+
return (src as Map<String, Object?>).map(
33+
(key, value) => MapEntry(
34+
key,
35+
(value as Map<String, Object?>).cast<String, int>(),
36+
),
37+
);
38+
}
39+
40+
Map<String, List<MatchPosition>>? _readMatchesPosition(
41+
Map<String, Object?> map,
42+
) {
43+
final src = map['_matchesPosition'];
44+
45+
if (src == null) return null;
46+
47+
return (src as Map<String, Object?>).map(
48+
(key, value) => MapEntry(
49+
key,
50+
(value as List<Object?>)
51+
.map((e) => MatchPosition.fromMap(e as Map<String, Object?>))
52+
.toList(),
53+
),
54+
);
55+
}
56+
57+
typedef MeilisearchDocumentMapper<TSrc, TOther> = TOther Function(TSrc src);
58+
59+
extension SearchableExt<T> on Future<Searcheable<T>> {
60+
Future<PaginatedSearchResult<T>> asPaginatedResult() =>
61+
then((value) => value.asPaginatedResult());
62+
63+
Future<SearchResult<T>> asSearchResult() =>
64+
then((value) => value.asSearchResult());
65+
66+
Future<Searcheable<TOther>> map<TOther>(
67+
MeilisearchDocumentMapper<T, TOther> mapper,
68+
) =>
69+
then((value) => value.map(mapper));
70+
}
71+
72+
extension SearchResultExt<T> on Future<SearchResult<T>> {
73+
Future<SearchResult<TOther>> map<TOther>(
74+
MeilisearchDocumentMapper<T, TOther> mapper,
75+
) =>
76+
then((value) => value.map(mapper));
77+
}
78+
79+
extension PaginatedSearchResultExt<T> on Future<PaginatedSearchResult<T>> {
80+
Future<PaginatedSearchResult<TOther>> map<TOther>(
81+
MeilisearchDocumentMapper<T, TOther> mapper,
82+
) =>
83+
then((value) => value.map(mapper));
84+
}

test/search_test.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,36 @@ void main() {
221221

222222
test('facetDistributions parameter', () async {
223223
var index = await createBooksIndex();
224-
var response = await index
224+
await index
225225
.updateSettings(IndexSettings(
226-
filterableAttributes: ['tag'],
226+
filterableAttributes: [ktag],
227227
))
228228
.waitFor(client: client);
229-
expect(response.status, 'succeeded');
229+
230230
var result = await index.search('prince', facets: ['*']);
231+
231232
expect(result.hits, hasLength(2));
233+
expect(result.facetDistribution?[ktag]?.length, 2);
234+
});
235+
236+
test('facetStats parameter', () async {
237+
final index = client.index(randomUid());
238+
final docs = List.generate(
239+
10,
240+
(index) => <String, Object?>{
241+
'id': 100 - index,
242+
'year': (index * 2) + 2010,
243+
},
244+
);
245+
await index.addDocuments(docs).waitFor(client: client);
246+
await index
247+
.updateFilterableAttributes(['year']).waitFor(client: client);
248+
249+
var result = await index.search('', facets: ['*']);
250+
251+
expect(result.hits, hasLength(10));
252+
expect(result.facetStats?['year']?.min, 2010);
253+
expect(result.facetStats?['year']?.max, 2028);
232254
});
233255

234256
test('Sort parameter', () async {

0 commit comments

Comments
 (0)