-
Notifications
You must be signed in to change notification settings - Fork 32
(feat): Add Composition API support with CompositionSearcher #435
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
afrencalg
wants to merge
1
commit into
master
Choose a base branch
from
composition-api
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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
237 changes: 237 additions & 0 deletions
237
...lgolia/instantsearch/examples/android/showcase/compose/composition/CompositionShowcase.kt
This file contains hidden or 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,237 @@ | ||
| package com.algolia.instantsearch.examples.android.showcase.compose.composition | ||
|
|
||
| import android.os.Bundle | ||
| import androidx.activity.compose.setContent | ||
| import androidx.appcompat.app.AppCompatActivity | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.lazy.LazyColumn | ||
| import androidx.compose.foundation.lazy.items | ||
| import androidx.compose.foundation.lazy.rememberLazyListState | ||
| import androidx.compose.material.Button | ||
| import androidx.compose.material.Divider | ||
| import androidx.compose.material.MaterialTheme | ||
| import androidx.compose.material.OutlinedTextField | ||
| import androidx.compose.material.Scaffold | ||
| import androidx.compose.material.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.unit.dp | ||
| import com.algolia.client.model.search.Hit | ||
| import com.algolia.instantsearch.compose.hits.HitsState | ||
| import com.algolia.instantsearch.compose.item.StatsTextState | ||
| import com.algolia.instantsearch.compose.searchbox.SearchBoxState | ||
| import com.algolia.instantsearch.core.connection.ConnectionHandler | ||
| import com.algolia.instantsearch.core.hits.connectHitsView | ||
| import com.algolia.instantsearch.examples.android.showcase.compose.ui.ShowcaseTheme | ||
| import com.algolia.instantsearch.examples.android.showcase.compose.ui.component.SearchTopBar | ||
| import com.algolia.instantsearch.examples.android.showcase.compose.ui.component.TitleTopBar | ||
| import com.algolia.instantsearch.searchbox.SearchBoxConnector | ||
| import com.algolia.instantsearch.searchbox.connectView | ||
| import com.algolia.instantsearch.searcher.composition.CompositionSearcher | ||
| import com.algolia.instantsearch.stats.DefaultStatsPresenter | ||
| import com.algolia.instantsearch.stats.StatsConnector | ||
| import com.algolia.instantsearch.stats.connectView | ||
| import kotlinx.serialization.json.JsonPrimitive | ||
| import kotlinx.serialization.json.contentOrNull | ||
|
|
||
| /** | ||
| * Showcase for [CompositionSearcher], the searcher targeting the Algolia | ||
| * Composition API (`/1/compositions/{compositionID}/run` endpoint). | ||
| * | ||
| * A composition is configured per application in the Algolia dashboard, so | ||
| * there is no public demo composition to hardcode here. The screen starts with | ||
| * a small form asking for your application credentials and composition ID, | ||
| * then drives the regular InstantSearch components (search box, stats, hits) | ||
| * through a [CompositionSearcher]. | ||
| */ | ||
| class CompositionShowcase : AppCompatActivity() { | ||
|
|
||
| private var session by mutableStateOf<CompositionSession?>(null) | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
| setContent { | ||
| ShowcaseTheme { | ||
| when (val current = session) { | ||
| null -> CredentialsForm { applicationID, apiKey, compositionID -> | ||
| session = CompositionSession(applicationID, apiKey, compositionID) | ||
| } | ||
| else -> CompositionSearchScreen(current) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun onDestroy() { | ||
| super.onDestroy() | ||
| session?.close() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Holds the searcher and its connections, created once the credentials are | ||
| * submitted. | ||
| */ | ||
| private class CompositionSession( | ||
| applicationID: String, | ||
| apiKey: String, | ||
| compositionID: String, | ||
| ) { | ||
| val searchBoxState = SearchBoxState() | ||
| val statsText = StatsTextState() | ||
| val hitsState = HitsState<CompositionHit>() | ||
| var error by mutableStateOf<String?>(null) | ||
|
|
||
| private val searcher = CompositionSearcher( | ||
| applicationID = applicationID, | ||
| apiKey = apiKey, | ||
| compositionID = compositionID, | ||
| ) | ||
| private val searchBox = SearchBoxConnector(searcher) | ||
| private val stats = StatsConnector(searcher) | ||
| private val connections = ConnectionHandler(searchBox, stats) | ||
|
|
||
| init { | ||
| connections += searchBox.connectView(searchBoxState) | ||
| connections += stats.connectView(statsText, DefaultStatsPresenter()) | ||
| connections += searcher.connectHitsView(hitsState) { response -> | ||
| response.hits.map { it.toCompositionHit() } | ||
| } | ||
| searcher.error.subscribe { error = it?.message } | ||
| searcher.searchAsync() | ||
| } | ||
|
|
||
| fun close() { | ||
| searcher.error.unsubscribeAll() | ||
| searcher.cancel() | ||
| connections.clear() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Composition records don't have a fixed schema, so the row displays the most | ||
| * common "title" attributes and falls back to the objectID. | ||
| */ | ||
| private data class CompositionHit( | ||
| val objectID: String, | ||
| val title: String, | ||
| ) | ||
|
|
||
| private fun Hit.toCompositionHit(): CompositionHit { | ||
| val title = listOf("name", "title", "label") | ||
| .firstNotNullOfOrNull { attribute -> | ||
| (additionalProperties?.get(attribute) as? JsonPrimitive)?.contentOrNull | ||
| } | ||
| return CompositionHit(objectID = objectID, title = title ?: objectID) | ||
| } | ||
|
|
||
| @Composable | ||
| private fun CredentialsForm(onSubmit: (String, String, String) -> Unit) { | ||
| var applicationID by remember { mutableStateOf("") } | ||
| var apiKey by remember { mutableStateOf("") } | ||
| var compositionID by remember { mutableStateOf("") } | ||
|
|
||
| Scaffold( | ||
| topBar = { TitleTopBar(title = "Composition (experimental)") }, | ||
| ) { padding -> | ||
| Column( | ||
| modifier = Modifier | ||
| .fillMaxSize() | ||
| .padding(padding) | ||
| .padding(16.dp), | ||
| verticalArrangement = Arrangement.spacedBy(8.dp), | ||
| ) { | ||
| Text( | ||
| text = "Compositions are configured per application in the Algolia dashboard. " + | ||
| "Enter the credentials of an application with at least one composition.", | ||
| style = MaterialTheme.typography.body2, | ||
| ) | ||
| OutlinedTextField( | ||
| value = applicationID, | ||
| onValueChange = { applicationID = it }, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| label = { Text("Application ID") }, | ||
| singleLine = true, | ||
| ) | ||
| OutlinedTextField( | ||
| value = apiKey, | ||
| onValueChange = { apiKey = it }, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| label = { Text("Search-only API Key") }, | ||
| singleLine = true, | ||
| ) | ||
| OutlinedTextField( | ||
| value = compositionID, | ||
| onValueChange = { compositionID = it }, | ||
| modifier = Modifier.fillMaxWidth(), | ||
| label = { Text("Composition ID") }, | ||
| singleLine = true, | ||
| ) | ||
| Button( | ||
| enabled = applicationID.isNotBlank() && apiKey.isNotBlank() && compositionID.isNotBlank(), | ||
| onClick = { onSubmit(applicationID.trim(), apiKey.trim(), compositionID.trim()) }, | ||
| ) { | ||
| Text("Start searching") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun CompositionSearchScreen(session: CompositionSession) { | ||
| val listState = rememberLazyListState() | ||
| Scaffold( | ||
| topBar = { | ||
| SearchTopBar( | ||
| placeHolderText = "Search in your composition", | ||
| searchBoxState = session.searchBoxState, | ||
| lazyListState = listState, | ||
| ) | ||
| }, | ||
| ) { padding -> | ||
| Column( | ||
| Modifier | ||
| .fillMaxSize() | ||
| .padding(padding), | ||
| ) { | ||
| Text( | ||
| text = session.statsText.stats, | ||
| style = MaterialTheme.typography.caption, | ||
| modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), | ||
| ) | ||
| session.error?.let { | ||
| Text( | ||
| text = it, | ||
| color = MaterialTheme.colors.error, | ||
| style = MaterialTheme.typography.caption, | ||
| modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), | ||
| ) | ||
| } | ||
| LazyColumn(state = listState) { | ||
| items(session.hitsState.hits) { hit -> | ||
| Column( | ||
| Modifier | ||
| .fillMaxWidth() | ||
| .padding(horizontal = 16.dp, vertical = 8.dp), | ||
| ) { | ||
| Text(text = hit.title, style = MaterialTheme.typography.body1) | ||
| Text( | ||
| text = hit.objectID, | ||
| style = MaterialTheme.typography.caption, | ||
| color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), | ||
| ) | ||
| } | ||
| Divider() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
114 changes: 114 additions & 0 deletions
114
...onMain/kotlin/com/algolia/instantsearch/searcher/composition/CompositionFacetsSearcher.kt
This file contains hidden or 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,114 @@ | ||
| package com.algolia.instantsearch.searcher.composition | ||
|
|
||
| import com.algolia.client.api.CompositionClient | ||
| import com.algolia.client.model.search.SearchParamsObject | ||
| import com.algolia.client.transport.RequestOptions | ||
| import com.algolia.instantsearch.searcher.SearcherForFacets | ||
| import com.algolia.instantsearch.searcher.SearcherScope | ||
| import com.algolia.instantsearch.searcher.composition.internal.DefaultCompositionFacetsSearchService | ||
| import com.algolia.instantsearch.searcher.composition.internal.DefaultCompositionFacetsSearcher | ||
| import com.algolia.instantsearch.searcher.facets.SearchForFacetQuery | ||
| import com.algolia.instantsearch.searcher.internal.defaultDispatcher | ||
| import kotlinx.coroutines.CoroutineDispatcher | ||
| import kotlinx.coroutines.CoroutineScope | ||
|
|
||
| /** | ||
| * The component handling search requests and managing the search sessions. | ||
| * This implementation searches for facet values using the Algolia Composition API | ||
| * (`/1/compositions/{compositionID}/facets/{facetName}/query` endpoint). | ||
| */ | ||
| public interface CompositionFacetsSearcher : SearcherForFacets<SearchParamsObject> { | ||
|
|
||
| /** | ||
| * Unique Composition ObjectID. | ||
| */ | ||
| public var compositionID: String | ||
|
|
||
| /** | ||
| * Facets query. | ||
| */ | ||
| public var facetQuery: String? | ||
|
|
||
| /** | ||
| * Maximum number of facet values to return. | ||
| */ | ||
| public var maxFacetHits: Int? | ||
| } | ||
|
|
||
| /** | ||
| * Creates an instance of [CompositionFacetsSearcher]. | ||
| * | ||
| * @param client composition client instance | ||
| * @param compositionID unique Composition ObjectID | ||
| * @param attribute facet attribute | ||
| * @param query the query used to narrow down the facet values search | ||
| * @param facetQuery the facet query used to search for facets | ||
| * @param maxFacetHits maximum number of facet values to return | ||
| * @param requestOptions request local configuration | ||
| * @param coroutineScope scope of coroutine operations | ||
| * @param coroutineDispatcher async search dispatcher | ||
| * @param triggerSearchFor request condition | ||
| */ | ||
| public fun CompositionFacetsSearcher( | ||
| client: CompositionClient, | ||
| compositionID: String, | ||
| attribute: String, | ||
| query: SearchParamsObject = SearchParamsObject(), | ||
| facetQuery: String? = null, | ||
| maxFacetHits: Int? = null, | ||
| requestOptions: RequestOptions? = null, | ||
| coroutineScope: CoroutineScope = SearcherScope(), | ||
| coroutineDispatcher: CoroutineDispatcher = defaultDispatcher, | ||
| triggerSearchFor: SearchForFacetQuery = SearchForFacetQuery.All, | ||
| ): CompositionFacetsSearcher = DefaultCompositionFacetsSearcher( | ||
| searchService = DefaultCompositionFacetsSearchService(client), | ||
| compositionID = compositionID, | ||
| query = query, | ||
| attribute = attribute, | ||
| facetQuery = facetQuery, | ||
| maxFacetHits = maxFacetHits, | ||
| requestOptions = requestOptions, | ||
| coroutineScope = coroutineScope, | ||
| coroutineDispatcher = coroutineDispatcher, | ||
| triggerSearchFor = triggerSearchFor, | ||
| ) | ||
|
|
||
| /** | ||
| * Creates an instance of [CompositionFacetsSearcher]. | ||
| * | ||
| * @param applicationID application ID | ||
| * @param apiKey API Key | ||
| * @param compositionID unique Composition ObjectID | ||
| * @param attribute facet attribute | ||
| * @param query the query used to narrow down the facet values search | ||
| * @param facetQuery the facet query used to search for facets | ||
| * @param maxFacetHits maximum number of facet values to return | ||
| * @param requestOptions request local configuration | ||
| * @param coroutineScope scope of coroutine operations | ||
| * @param coroutineDispatcher async search dispatcher | ||
| * @param triggerSearchFor request condition | ||
| */ | ||
| public fun CompositionFacetsSearcher( | ||
| applicationID: String, | ||
| apiKey: String, | ||
| compositionID: String, | ||
| attribute: String, | ||
| query: SearchParamsObject = SearchParamsObject(), | ||
| facetQuery: String? = null, | ||
| maxFacetHits: Int? = null, | ||
| requestOptions: RequestOptions? = null, | ||
| coroutineScope: CoroutineScope = SearcherScope(), | ||
| coroutineDispatcher: CoroutineDispatcher = defaultDispatcher, | ||
| triggerSearchFor: SearchForFacetQuery = SearchForFacetQuery.All, | ||
| ): CompositionFacetsSearcher = CompositionFacetsSearcher( | ||
| client = CompositionClient(applicationID, apiKey), | ||
| compositionID = compositionID, | ||
| attribute = attribute, | ||
| query = query, | ||
| facetQuery = facetQuery, | ||
| maxFacetHits = maxFacetHits, | ||
| requestOptions = requestOptions, | ||
| coroutineScope = coroutineScope, | ||
| coroutineDispatcher = coroutineDispatcher, | ||
| triggerSearchFor = triggerSearchFor, | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so the latency app does not composition configured on it atm?