Build a Query Suggestions UI with InstantSearch Android
On this page
When your user interacts with a search box, you can help them discover what they could search for by providing Query suggestions.
Query suggestions are a specific kind of multi-index interface:
- The main search interface will use a regular index.
- As users type a phrase, suggestions from your Query Suggestions index are displayed.
Usage
To display the suggestions:
- Create a Query Suggestions index from your main index.
- Implement a Multi-Index search experience using both indices.
- When clicking on a suggestion, set the query to the chosen suggestion.
Before you begin
To use InstantSearch Android, you need an Algolia account. You can [create a new account][algolia_sign_up], or use the following credentials:
- Application ID:
latency
- Search API key:
afc3dd66dd1293e2e2736a5a51b05c0a
- Results index name:
STAGING_native_ecom_demo_products
- Suggestions index name:
STAGING_native_ecom_demo_products_query_suggestions
These credentials give you access to pre-existing datasets of products and Query Suggestions appropriate for this guide.
Project structure
Algolia’s query suggestions uses:
QuerySuggestionGuide
: main activity presenting the search experience,SuggestionFragment
: fragment presenting the Query Suggestions,ProductFragment
: fragment presenting the search results,QuerySuggestionViewModel
: view model holding connectors and search business logic.
The initial screen shows the search bar and results for an empty query:
When users tap the search box, a list of query suggestions are shown (the most popular for an empty query):
On each keystroke, the list of suggestions is updated:
When users select a suggestion from the list, it replaces the query in the search box, and the suggestions fragment disappears. The products fragment presents search results for the selected query suggestion:
Algolia doesn’t provide a ready-to-use views, but you can create them with the tools in the InstantSearch Android library.
Business logic
Create QuerySuggestionViewModel
view to setup the search components and create the necessary connections between them, establishing the business logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class QuerySuggestionViewModel : ViewModel() {
private val client = ClientSearch(
applicationID = ApplicationID("latency"),
apiKey = APIKey("927c3fe76d4b52c5a2912973f35a3077"),
logLevel = LogLevel.ALL
)
val multiSearcher = MultiSearcher(client)
val productSearcher = multiSearcher.addHitsSearcher(indexName = IndexName("STAGING_native_ecom_demo_products"))
val suggestionSearcher = multiSearcher.addHitsSearcher(indexName = IndexName("STAGING_native_ecom_demo_products_query_suggestions"))
val searchBox = SearchBoxConnector(multiSearcher)
val suggestions = MutableLiveData<Suggestion>()
override fun onCleared() {
multiSearcher.cancel()
searchBox.disconnect()
client.close()
}
}
Products view
- Create
Product
data class corresponding the index hits
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Serializable
data class Product(
val name: String,
@SerialName("image_urls") val images: List<String>,
val price: Price,
val description: String,
override val objectID: ObjectID,
override val _highlightResult: JsonObject?
) : Indexable, Highlightable {
val highlightedName: HighlightedString?
get() = getHighlight(Attribute("name"))
}
@Serializable
data class Price(
val currency: String,
val value: String,
)
- Create
ProductAdapter
to display search results
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ProductAdapter : ListAdapter<Product, ProductViewHolder>(ProductDiffUtil), HitsView<Product> {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ProductViewHolder(parent.inflate(R.layout.list_item_large))
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) =
holder.bind(getItem(position))
override fun setHits(hits: List<Product>) = submitList(hits)
class ProductViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Product) {
view.findViewById<TextView>(R.id.itemTitle).text =
item.highlightedName?.toSpannedString() ?: item.name
view.findViewById<TextView>(R.id.itemSubtitle).text = item.price.value
Glide
.with(view.context)
.load(item.images.first())
.into(view.findViewById(R.id.itemImage))
}
}
private object ProductDiffUtil : DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product) = oldItem.objectID == newItem.objectID
override fun areContentsTheSame(oldItem: Product, newItem: Product) = oldItem == newItem
}
}
- Create
ProductFragment
to display product search results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ProductFragment : Fragment(R.layout.fragment_items) {
private val viewModel: QuerySuggestionViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Configure products view
val productAdapter = ProductAdapter()
view.findViewById<RecyclerView>(R.id.items).configure(productAdapter) // Configure the RecyclerView with the adapter
connection += viewModel.productSearcher.connectHitsView(productAdapter) {
it.hits.deserialize(Product.serializer())
}
// Run initial search
viewModel.productSearcher.searchAsync()
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
Suggestions view
- Create
Suggestion
data class corresponding the suggestions index hits
1
2
3
4
5
6
7
8
9
10
@Serializable
data class Suggestion(
val query: String,
override val objectID: ObjectID,
override val _highlightResult: JsonObject?
) : Indexable, Highlightable {
val highlightedQuery: HighlightedString?
get() = getHighlight(Attribute("query"))
}
- Create
SuggestionAdapter
to display suggestions list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class SuggestionAdapter(private val onSuggestionClick: ((Suggestion) -> Unit)) :
ListAdapter<Suggestion, SuggestionViewHolder>(SuggestionAdapter),
HitsView<Suggestion> {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
SuggestionViewHolder(parent.inflate(R.layout.list_item_suggestion))
override fun onBindViewHolder(holder: SuggestionViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, onSuggestionClick)
}
override fun setHits(hits: List<Suggestion>) = submitList(hits)
class SuggestionViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: Suggestion, onClick: ((Suggestion) -> Unit)) {
view.setOnClickListener { onClick(item) }
view.findViewById<TextView>(R.id.itemName).text = item.highlightedQuery?.toSpannedString() ?: item.query
}
}
companion object : DiffUtil.ItemCallback<Suggestion>() {
override fun areItemsTheSame(oldItem: Suggestion, newItem: Suggestion) =
oldItem.objectID == newItem.objectID
override fun areContentsTheSame(oldItem: Suggestion, newItem: Suggestion): Boolean =
oldItem == newItem
}
}
- Create
SuggestionFragment
to display suggestions’ recycler view adapter, and to notifyQuerySuggestionViewModel
when a suggestion selection is made:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class SuggestionFragment : Fragment(R.layout.fragment_items) {
private val viewModel: QuerySuggestionViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Configure suggestions view
val suggestionAdapter = SuggestionAdapter {
// On suggestion click, update the
viewModel.suggestions.value = it
}
view.findViewById<RecyclerView>(R.id.items).configure(suggestionAdapter) // Configure the RecyclerView with the adapter
connection += viewModel.suggestionSearcher.connectHitsView(suggestionAdapter) {
it.hits.deserialize(Suggestion.serializer())
}
// Run initial search
viewModel.suggestionSearcher.searchAsync()
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
Setup layout
Finally, create QuerySuggestionActivity
activity, and display ProductFragment
and SuggestionFragment
on SearchBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class QuerySuggestionActivity : AppCompatActivity() {
private val viewModel by viewModels<QuerySuggestionViewModel>()
private val connection = ConnectionHandler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_query_suggestion)
// Setup search box
val searchView = findViewById<SearchView>(R.id.searchView)
val searchBoxView = SearchBoxViewAppCompat(searchView)
connection += viewModel.searchBox.connectView(searchBoxView)
// Switch fragments on search box focus
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) showSuggestions() else showProducts()
}
// Observe suggestions
viewModel.suggestions.observe(this) { searchBoxView.setText(it.query, true) }
// Initially show products view
showProducts()
}
private fun showSuggestions() {
supportFragmentManager.commit {
replace<SuggestionFragment>(R.id.container)
setReorderingAllowed(true)
addToBackStack("suggestions") // name can be null
}
}
private fun showProducts() {
supportFragmentManager.commit {
replace<ProductFragment>(R.id.container)
setReorderingAllowed(true)
addToBackStack("products") // name can be null
}
}
override fun onDestroy() {
super.onDestroy()
connection.clear()
}
}
Going further
Your query suggestions search experience is now ready to use. You can find a complete project in the Android examples repository.