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 helper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.7.0

- **FEAT**(composition): add Composition API support with `CompositionSearcher` and `CompositionFacetSearcher`, including `FilterState`, `FacetList`, and Insights integration.

## 1.6.0

- **FEAT**(response): add userData (#172).
Expand Down
17 changes: 11 additions & 6 deletions helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ framework**.

| Component | Description |
|--------------------|----------------------------------------------------------------------------------------------------------------|
| [HitsSearcher][0] | Component handling search requests. |
| [FacetSearcher][1] | Component handling search for facet values requests |
| [MultiSearcher][2] | Component handling multi-search experiences aggregating [HitsSearcher][0] and [FacetSearcher][4] |
| [FilterState][3] | Component providing a friendly interface to manage search filters. |
| [FacetList][4] | Component to get and manage facets, lets the user refine their search results by filtering on specific values. |
| [Highlighting][5] | Set of tools to highlight relevant parts of the search results. |
| [HitsSearcher][0] | Component handling search requests. |
| [FacetSearcher][1] | Component handling search for facet values requests |
| [MultiSearcher][2] | Component handling multi-search experiences aggregating [HitsSearcher][0] and [FacetSearcher][4] |
| [CompositionSearcher][6] | Component handling [Composition][8] run requests. |
| [CompositionFacetSearcher][7] | Component handling search for facet values requests on a composition. |
| [FilterState][3] | Component providing a friendly interface to manage search filters. |
| [FacetList][4] | Component to get and manage facets, lets the user refine their search results by filtering on specific values. |
| [Highlighting][5] | Set of tools to highlight relevant parts of the search results. |


[0]: lib/src/searcher/hits_searcher.dart
Expand All @@ -41,3 +43,6 @@ framework**.
[3]: lib/src/filter_state.dart
[4]: lib/src/facet_list.dart
[5]: lib/src/highlighting.dart
[6]: lib/src/searcher/composition_searcher.dart
[7]: lib/src/searcher/composition_facet_searcher.dart
[8]: https://www.algolia.com/doc/guides/building-search-ui/going-further/composition/what-is-composition/js/
35 changes: 35 additions & 0 deletions helper/example/composition_searcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:algolia_helper_flutter/algolia_helper_flutter.dart';

void main() {
// Create a composition searcher.
// The searcher runs a composition and obtains its results.
final searcher = CompositionSearcher(
applicationID: 'MY_APPLICATION_ID',
apiKey: 'MY_API_KEY',
compositionID: 'MY_COMPOSITION_ID',
);

// Run your composition run operations and listen to the results!
searcher.responses.listen((response) {
print("Search query '${response.query}' (${response.nbHits} hits found)");
for (var hit in response.hits) {
print("> ${hit['name']}");
}

// Multifeed compositions expose each feed's results.
final feeds = response.feeds;
if (feeds != null) {
for (var feed in feeds) {
print("Feed '${feed.feedID}': ${feed.nbHits} hits");
}
}
});

searcher.query('a');

searcher.eventTracker?.clickedObjects(
eventName: 'clicked objects',
objectIDs: ['object1', 'object2', 'object3'],
positions: [1, 2, 3],
);
}
7 changes: 7 additions & 0 deletions helper/lib/algolia_helper_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
library algolia_helper_flutter;

export 'src/client_options.dart';
export 'src/composition_searcher_facet_list_extension.dart';
export 'src/disposable.dart';
export 'src/exception.dart';
export 'src/facet_list.dart';
Expand All @@ -15,9 +16,15 @@ export 'src/filters.dart';
export 'src/highlighting.dart';
export 'src/highlighting_core.dart';
export 'src/hits_searcher_facet_list_extension.dart';
export 'src/model/composition_facet_search_response.dart';
export 'src/model/composition_facet_search_state.dart';
export 'src/model/composition_response.dart';
export 'src/model/composition_state.dart';
export 'src/model/facet.dart';
export 'src/model/multi_search_response.dart';
export 'src/model/multi_search_state.dart';
export 'src/searcher/composition_facet_searcher.dart';
export 'src/searcher/composition_searcher.dart';
export 'src/searcher/facet_searcher.dart';
export 'src/searcher/hits_searcher.dart';
export 'src/searcher/multi_searcher.dart';
Expand Down
70 changes: 70 additions & 0 deletions helper/lib/src/composition_searcher_facet_list_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:algolia_insights/algolia_insights.dart';
import 'package:meta/meta.dart';

import 'facet_list.dart';
import 'filter_group.dart';
import 'filter_state.dart';
import 'filter_state_group_accessor.dart';
import 'searcher/composition_searcher.dart';

extension CompositionSearcherFacetListExtension on CompositionSearcher {
static FilterEventTracker? _makeEventTracker(
CompositionSearcher searcher,
String attribute,
) {
if (searcher.eventTracker == null) {
return null;
}

return FilterEventTracker(
searcher.eventTracker!.tracker,
searcher,
attribute,
);
}

@experimental
FacetList buildFacetList({
required FilterState filterState,
required String attribute,
FilterOperator operator = FilterOperator.or,
SelectionMode selectionMode = SelectionMode.multiple,
bool persistent = false,
}) {
// Setup composition state by adding `attribute` to the search state
applyState(
(state) => state.copyWith(
facets: List.from((state.facets ?? [])..add(attribute)),
disjunctiveFacets: operator == FilterOperator.or
? {...?state.disjunctiveFacets, attribute}
: state.disjunctiveFacets,
),
);

// Extract the Stream<List<Facet>> from CompositionSearcher
final facetsStream = responses.map(
(response) =>
response.disjunctiveFacets[attribute] ??
response.facets[attribute] ??
[],
);

// Establish the filter group accessor proxy as selection state
final state = FiltersGroupAccessor(
filterState: filterState,
groupID: FilterGroupID(
attribute,
operator,
),
attribute: attribute,
);

return FacetList(
facetsStream: facetsStream,
state: state,
selectionMode: selectionMode,
persistent: persistent,
eventTracker: _makeEventTracker(this, attribute),
);
}
}
2 changes: 1 addition & 1 deletion helper/lib/src/lib_version.dart
Original file line number Diff line number Diff line change
@@ -1 +1 @@
const libVersion = '1.6.0';
const libVersion = '1.7.0';
40 changes: 40 additions & 0 deletions helper/lib/src/model/composition_facet_search_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'facet.dart';

/// Search for facet values operation response for a composition.
class CompositionFacetSearchResponse {
/// Creates [CompositionFacetSearchResponse] instance.
CompositionFacetSearchResponse(this.raw)
: facetHits = Facet.fromList(
List<Map<String, dynamic>>.from(
(raw['facetHits'] as List<dynamic>? ?? []).map(
(e) => Map<String, dynamic>.from(e as Map),
),
),
);

/// Raw facet search response
final Map<String, dynamic> raw;

/// Search for facet values hits list
final List<Facet> facetHits;

/// Whether the count returned for each facets is exhaustive.
bool get exhaustiveFacetsCount =>
raw['exhaustiveFacetsCount'] as bool? ?? false;

/// Time the server took to process the request, in milliseconds.
int get processingTimeMS => raw['processingTimeMS'] as int? ?? 0;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CompositionFacetSearchResponse &&
runtimeType == other.runtimeType &&
raw == other.raw;

@override
int get hashCode => raw.hashCode;

@override
String toString() => 'CompositionFacetSearchResponse{raw: $raw}';
}
74 changes: 74 additions & 0 deletions helper/lib/src/model/composition_facet_search_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import 'composition_state.dart';

/// Represents a search for facet values operation state on a composition, and
/// an abstraction over composition facet search queries.
///
/// ## Example
///
/// ```dart
/// final facetSearchState = CompositionFacetSearchState(
/// compositionID: 'MY_COMPOSITION_ID',
/// facet: 'brand',
/// facetQuery: 'samsung',
/// state: const CompositionState(compositionID: 'MY_COMPOSITION_ID'),
/// );
/// ```
final class CompositionFacetSearchState {
/// Creates [CompositionFacetSearchState] instance.
const CompositionFacetSearchState({
required this.compositionID,
required this.facet,
required this.state,
this.facetQuery = '',
});

/// Unique Composition ObjectID to search facet values in.
final String compositionID;

/// Facet name to search for.
final String facet;

/// Text to search inside the facet's values.
final String facetQuery;

/// Composition search operation state.
final CompositionState state;

/// Make a copy of the composition facet search state.
CompositionFacetSearchState copyWith({
String? compositionID,
String? facet,
String? facetQuery,
CompositionState? state,
}) =>
CompositionFacetSearchState(
compositionID: compositionID ?? this.compositionID,
facet: facet ?? this.facet,
facetQuery: facetQuery ?? this.facetQuery,
state: state ?? this.state,
);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CompositionFacetSearchState &&
runtimeType == other.runtimeType &&
compositionID == other.compositionID &&
facet == other.facet &&
facetQuery == other.facetQuery &&
state == other.state;

@override
int get hashCode =>
compositionID.hashCode ^
facet.hashCode ^
facetQuery.hashCode ^
state.hashCode;

@override
String toString() => 'CompositionFacetSearchState{'
'compositionID: $compositionID, '
'facet: $facet, '
'facetQuery: $facetQuery, '
'state: $state}';
}
111 changes: 111 additions & 0 deletions helper/lib/src/model/composition_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import 'package:algoliasearch/algoliasearch.dart' as algolia;

import 'facet.dart';
import 'multi_search_response.dart';

/// Composition run operation response.
///
/// Wraps a single composition run result (a `results` item returned by the
/// Composition run endpoint). When the composition is a multifeed one, the
/// individual feed results are exposed through [feeds] (ordered), and this
/// response points to the first feed.
class CompositionResponse {
/// Creates [CompositionResponse] instance.
CompositionResponse(this.raw, {List<CompositionResponse>? feeds})
: hits = _hitsFromList(raw['hits'] as List<dynamic>?),
feeds =
feeds == null || feeds.isEmpty ? null : List.unmodifiable(feeds),
disjunctiveFacets = {},
hierarchicalFacets = {};

/// Raw composition result response
final Map<String, dynamic> raw;

/// Search hits list
final List<Hit> hits;

/// Ordered list of feed results for a multifeed composition.
///
/// `null` when the composition run returned a single (non-feed) result.
final List<CompositionResponse>? feeds;

/// A mapping of each facet name to the corresponding facet counts for
/// disjunctive facets.
Map<String, List<Facet>> disjunctiveFacets;

/// A mapping of each facet name to the corresponding facet counts for
/// hierarchical facets.
Map<String, List<Facet>> hierarchicalFacets;

/// The ID of the feed, when this response is part of a multifeed composition.
String? get feedID => raw['feedID'] as String?;

/// Index name used for the composition's main source query.
String? get index => raw['index'] as String?;

/// An url-encoded string of all query parameters.
String? get params => raw['params'] as String?;

/// Identifies the query uniquely.
String? get queryID => raw['queryID'] as String?;

/// An echo of the query text.
String get query => raw['query'] as String? ?? '';

/// The maximum number of hits returned per page.
int get hitsPerPage => raw['hitsPerPage'] as int? ?? 0;

/// The number of hits matched by the query.
int get nbHits => raw['nbHits'] as int? ?? 0;

/// The number of returned pages.
int get nbPages => raw['nbPages'] as int? ?? 0;

/// Index of the current page (zero-based).
int get page => raw['page'] as int? ?? 0;

/// A mapping of each facet name to the corresponding facet counts.
Map<String, List<Facet>> get facets =>
Facet.fromMap(raw['facets'] as Map<String, dynamic>? ?? {});

/// Statistics for numerical facets.
Map<String, Map<String, num>> get facetsStats =>
(raw['facets_stats'] as Map<String, dynamic>?)?.map(
(key, value) =>
MapEntry(key, Map<String, num>.from(value as Map? ?? {})),
) ??
{};

/// Time the server took to process the request, in milliseconds.
int get processingTimeMS => raw['processingTimeMS'] as int? ?? 0;

/// An object with custom data.
Object userData() => raw['userData'] as Object? ?? {};

/// Defines how you want to render results in the search interface.
algolia.RenderingContent? get renderingContent =>
raw['renderingContent'] != null
? algolia.RenderingContent.fromJson(
raw['renderingContent'] as Map<String, dynamic>)
: null;

static List<Hit> _hitsFromList(List<dynamic>? data) {
if (data == null) return const [];
return data
.map((hit) => Hit(Map<String, dynamic>.from(hit as Map)))
.toList();
}

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CompositionResponse &&
runtimeType == other.runtimeType &&
raw == other.raw;

@override
int get hashCode => raw.hashCode;

@override
String toString() => 'CompositionResponse{raw: $raw}';
}
Loading
Loading