### Send and manage data
* [Prepare your data](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Send and update data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
* [Manage indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices)
### Build search and AI experiences
* [Build search UIs](/doc/guides/building-search-ui/getting-started/js)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
* [Recommend](/doc/guides/algolia-recommend/overview)
* [MCP Server](/doc/guides/model-context-protocol)
### Measure and optimize
* [Click and conversion events](/doc/guides/sending-events/getting-started)
* [Search analytics](/doc/guides/search-analytics/overview)
* [A/B testing](/doc/guides/ab-testing/what-is-ab-testing)
More resources
Take self-paced courses and earn certifications.
Find answers to specific issues and browse FAQs.
Connect with other Algolia users on Discord.
Stay up to date with the latest Algolia product releases and changes.
Looking for an older version of the docs? Browse the [archived site](https://www.algolia.com/old-docs).
# Algolia glossary
Source: https://www.algolia.com/doc/glossary
Terms used at Algolia and their definitions.
## API Client
A program that provides an abstraction over the raw Algolia [API](/doc/glossary#application-programming-interface-api),
hiding implementation details to make development for [Subscribers](#subscriber) easier in the language of their choice.
[Learn more](/doc/libraries/sdk)
## API key
A string with alphanumeric characters used to control access to Algolia's [API](/doc/glossary#application-programming-interface-api),
determining what is allowed to do, such as searching an [Index](#index), or adding new [Records](#record).
[Learn more](/doc/guides/security/api-keys)
## Affinity
A [Facet-Value pair](#facet-value-pair) that reflects [End-User](#user)'s preferences.
## Algolia AI
Certain Algolia Services with an ability to train or adapt via use,
such as [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started),
[Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking),
[AI Synonyms](/doc/guides/algolia-ai/dynamic-synonym-suggestions),
[Query Categorization](/doc/guides/algolia-ai/query-categorization),
[Advanced Personalization](/doc/guides/personalization/advanced-personalization/what-is-advanced-personalization) and [Recommend](/doc/guides/algolia-recommend/overview) features:
Frequently Bought Together, Alternative Items, Looking Similar, Content Based Alternative Items, and otherwise identified by Algolia.
## Application
A siloed set of [indices](#index). Data cannot be shared between applications.
## Application ID
A string with alphanumeric characters as a unique identifier for Algolia [applications](#application).
## Application Programming Interface (API)
A software intermediary that allows pieces of software to interact with each other.
The Algolia API describes how the [Subscribers'](#subscriber) systems can interact with Algolia Services.
[Learn more](/doc/rest-api)
## Click-through rate (CTR)
The percentage of tracked searches with at least one click [Event](#event).
A tracked search is a search with the [`clickAnalytics`](/doc/api-reference/api-parameters/clickAnalytics) parameter set to true.
[Learn more](/doc/guides/search-analytics/concepts/metrics#click-through-rate)
## Conversion rate
The percentage of tracked searches with at least one conversion [Event](#event).
A tracked search is a search with the [`clickAnalytics`](/doc/api-reference/api-parameters/clickAnalytics) parameter set to true.
[Learn more](/doc/guides/search-analytics/concepts/metrics#conversion-rate)
## Dashboard
The [Subscriber's](#subscriber) administrator interface to Algolia Services.
The Dashboard is hosted by Algolia.
## Event
An action performed by a [User](#user) on [Subscriber's](#subscriber) app or website.
[Learn more](/doc/guides/sending-events)
## Facet
A specific characteristic for describing [index](#index) items, such as color or brand.
[Learn more](/doc/guides/managing-results/refine-results/faceting)
## Facet-Value pair
A pair made of a [Facet](#facet) name and a value.
For example, if "priority" and "quality" are two Facets, "priority:high" and "quality:high" are two Facet-Value pairs.
## Filter
A [Facet-Value pair](#facet-value-pair)
used to select results with a specific facet value for a given [facet](#facet) name.
[Learn more](/doc/guides/managing-results/refine-results/filtering)
## Generative Experiences
Certain Algolia Services that leverage generative artificial intelligence, such as Ask AI, Agent Studio, AI Assist, and Generative Experience Guides.
## Index
A container for a set of [Records](#record) that can be searched together.
[Learn more](/doc/guides/sending-and-managing-data/send-and-update-your-data)
## Insights API
Algolia's API designed to ingest [Events](#event) from [Subscriber's](#subscriber) apps and websites.
[Learn more](/doc/rest-api/insights)
## InstantSearch
A collection of open source software libraries that let [Subscribers](#subscriber)
build Algolia search interfaces for their frontend application.
The InstantSearch libraries are built on top of [API Clients](#api-client).
[Learn more](/doc/guides/building-search-ui/what-is-instantsearch/js)
## Library
A collection of reusable software, designed to help developers solve common problems.
A library is often targeted to be used with a specific software platform or operating system.
## Primary Serving Region
Region selected by [Subscriber](#subscriber) and used by Algolia as the primary region for Search query processing.
## Profile
Designates a set of information about a single [End-User](#user),
including their [Affinities](#affinity) based on enriched [Events](#event) and audience segment to which the [End-User](#user) belongs.
## Record
[Subscriber](#subscriber) items stored in an Algolia [Index](#index) for retrieval.
[Learn more](/doc/guides/sending-and-managing-data/prepare-your-data)
## Search Request
A group of one or more [search queries](#search-query) sent in a single HTTP request.
## Search query
A sequence of characters typed into a search box, or specific filters or facets applied to a search.
## Subscriber
A customer of Algolia Services, such as Search or Recommend.
## User
Customers or end-users of Algolia [Subscribers](#subscriber) who interact with Algolia Services, such as Search and Recommend.
## User token
A unique string of alphanumeric characters that uniquely identifies a [User](#user) throughout [applications](#application).
A user token must contain only alphanumeric, `=`, `-`, `/`, or `_` characters and be between 1 and 128 characters in length.
[Learn more](/doc/guides/sending-events/concepts/usertoken)
# Get started with Algolia
Source: https://www.algolia.com/doc/guides/get-started
Learn what Algolia is, how it works, and which path to follow to build your first search or AI experience.
## What is Algolia?
Algolia is a hosted AI search and retrieval platform.
Built API-first, it combines keyword, vector, and hybrid retrieval to power search, recommendations, retrieval-augmented generation, and AI agents.
Algolia stores your data as JSON records that you query through the API, client libraries, and UI libraries.
## How Algolia works
You send records from your data sources to an Algolia index.
Index settings determine how Algolia matches, ranks, and filters those records.
Your website, app, or AI agent queries the index and presents the response.
Click and conversion events provide behavioral data for analytics and other features.
Use the Algolia dashboard to tune relevance, set up recommendations, create AI agents with Agent Studio, merchandise results, review analytics, and run A/B tests.
## Choose a starting point
Build a working React search experience with sample data.
Use AI assistants and agents to build and maintain your Algolia integration.
Learn how indices, records, attributes, and events work in Algolia.
Find the API client, UI library, or platform integration for your stack.
## Build your implementation
A basic search implementation has three parts:
1. [Index your data](/doc/guides/sending-and-managing-data/prepare-your-data) as records.
2. [Build a search UI](/doc/guides/building-search-ui/getting-started/js) that queries your index.
3. [Tune relevance](/doc/guides/managing-results/relevance-overview) to improve matching, ranking, and filtering.
Once your data is in an index, you can work on the search UI and relevance in parallel.
## Extend your implementation
Add other features when you need them:
* [Send click and conversion events](/doc/guides/sending-events/getting-started) to collect analytics data and power features such as Personalization, Dynamic Re-Ranking, and most Recommend models.
* [Add recommendations](/doc/guides/algolia-recommend/how-to/set-up) to suggest relevant items.
* [Build a conversational experience with Agent Studio](/doc/guides/algolia-ai/agent-studio) to ground AI responses in your index data.
## Measure and optimize
Once your implementation is working, measure performance and test changes.
* [Search analytics](/doc/guides/search-analytics/overview) to see how users search and where they don't find results.
* [Recommend analytics](/doc/guides/algolia-recommend/analytics/recommend-analytics) to see how users interact with recommendations.
* [Agent Studio analytics](/doc/guides/algolia-ai/agent-studio/how-to/analytics) to see how users interact with your agents.
* [A/B testing](/doc/guides/ab-testing/what-is-ab-testing) to compare index configurations before you roll them out.
## Prepare to go live
Before you launch to production, secure your account and work through the checklist.
* [Security best practices](/doc/guides/security/security-best-practices) to restrict API keys and protect sensitive data.
* [Implementation checklist](/doc/guides/going-to-production/implementation-checklist) to catch gaps before launch.
# Build with AI agents
Source: https://www.algolia.com/doc/guides/get-started/build-with-ai
Use AI assistants and agents to build, integrate, and maintain Algolia in your applications.
AI assistants can write integration code, manage your indices, and power search inside your own agents.
This page shows how to connect AI assistants to Algolia and which tools to use.
**Agent setup**
These are official, Algolia-validated instructions to set up an Algolia development environment.
This section is the complete setup: run only the commands below.
The MCP, CLI, and skills sections further down this page are reference for people.
Don't run them.
Setting up the MCP server is optional and isn't needed here.
**1. Install the Algolia CLI and skills**
For Claude Code:
```sh theme={"system"}
claude plugin marketplace add algolia/skills
claude plugin install algolia-cli@algolia-skills
claude plugin install algolia-quickstart@algolia-skills
```
Then tell the user to run `/reload-plugins` inside Claude Code to activate the plugins.
Don't use `npx skills` for Claude Code.
The plugin commands handle everything.
For any other agent:
```sh theme={"system"}
npx -y skills add algolia/skills --skill algolia-cli algolia-quickstart --yes --global
```
**2. Confirm and hand off**
Tell the user their Algolia agent environment is ready, then suggest the next step:
```text theme={"system"}
Ask me to "create an Algolia account and application" to get started.
```
## Install the Algolia Productivity MCP
[Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open standard for connecting AI assistants to external systems.
Productivity MCP lets you query your Algolia data from your AI assistant.
Enable it in your [Algolia dashboard](https://dashboard.algolia.com), then connect it to your AI assistant.
```sh icon=square-terminal theme={"system"}
claude mcp add --transport http algolia https://mcp.algolia.com/mcp
```
Then run `/mcp` in a Claude Code session and sign in when prompted.
[**Add Algolia MCP to Cursor**](cursor://anysphere.cursor-deeplink/mcp/install?name=algolia\&config=eyJ1cmwiOiJodHRwczovL21jcC5hbGdvbGlhLmNvbS9tY3AifQ%3D%3D) (requires Cursor 1.0+).
Or add this to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for a global setup:
```json .cursor/mcp.json icon=braces theme={"system"}
{
"mcpServers": {
"algolia": {
"url": "https://mcp.algolia.com/mcp"
}
}
}
```
Add this to `~/.vscode/mcp.json` (macOS, Linux) or `%USERPROFILE%\.vscode\mcp.json` (Windows):
```json ~/.vscode/mcp.json icon=braces theme={"system"}
{
"servers": {
"algolia": {
"type": "http",
"url": "https://mcp.algolia.com/mcp"
}
}
}
```
```sh icon=square-terminal theme={"system"}
gemini mcp add algolia https://mcp.algolia.com/mcp -s user -t http
```
In [Connectors settings](https://chatgpt.com/#settings/Connectors), create a connector with:
* **Server URL:** `https://mcp.algolia.com/mcp`
* **Authentication:** OAuth
Sign in when prompted.
Leave OAuth client ID and secret empty.
In [Connectors settings](https://claude.ai/new#settings/customize-connectors), add a custom connector with:
* **Remote MCP server URL:** `https://mcp.algolia.com/mcp`
Sign in when prompted.
Leave OAuth client ID and secret empty.
For more details, see [Productivity MCP](/doc/guides/model-context-protocol/productivity-mcp).
Your favorite AI tool isn't listed?
[Reach out to Algolia support](https://support.algolia.com/hc/en-us/requests/new) and request it.
***
### Productivity MCP example prompts
Once connected, you can ask your assistant questions like:
* *"What are the top searches with no results this week?"*
* *"List all indices in this application and show their record counts."*
* *"Which filters are most used on the `products` index?"*
* *"Find products matching 'noise cancelling headphones' and rank them by sales."*
* *"Show the click-through rate for `black friday sale` over the last 30 days."*
### Use Algolia Public MCP for unauthenticated access
For agent-facing search and recommendations on a curated set of indices, use [Algolia Public MCP](/doc/guides/model-context-protocol/public-mcp) instead.
It serves search and recommendations to your own agents without requiring sign-in.
## Install the Algolia DevEx MCP
DevEx MCP gives your AI coding agent version-accurate API client snippets, generated from Algolia OpenAPI specifications.
Use it when you add Algolia to an app, so the agent uses client methods and parameters that match your installed version.
DevEx MCP doesn't require authentication and doesn't access data in your Algolia applications.
To query data in your Algolia applications, use Productivity MCP.
To search public documentation, use DocSearch MCP.
DevEx MCP covers C#, Dart, Go, Java, JavaScript, Kotlin, PHP, Python, Ruby, Scala, and Swift.
```sh icon=square-terminal theme={"system"}
claude mcp add --transport http algolia-devex https://mcp.algolia.com/1/snippets/mcp
```
[**Add DevEx MCP to Cursor**](cursor://anysphere.cursor-deeplink/mcp/install?name=algolia-devex\&config=eyJ1cmwiOiJodHRwczovL21jcC5hbGdvbGlhLmNvbS8xL3NuaXBwZXRzL21jcCJ9) (requires Cursor 1.0+).
Or add this to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for a global setup:
```json .cursor/mcp.json icon=braces theme={"system"}
{
"mcpServers": {
"algolia-devex": {
"url": "https://mcp.algolia.com/1/snippets/mcp"
}
}
}
```
Add this to `~/.vscode/mcp.json` (macOS, Linux) or `%USERPROFILE%\.vscode\mcp.json` (Windows):
```json ~/.vscode/mcp.json icon=braces theme={"system"}
{
"servers": {
"algolia-devex": {
"type": "http",
"url": "https://mcp.algolia.com/1/snippets/mcp"
}
}
}
```
```sh icon=square-terminal theme={"system"}
gemini mcp add algolia-devex https://mcp.algolia.com/1/snippets/mcp -s user -t http
```
In [Connectors settings](https://chatgpt.com/#settings/Connectors), create a connector with:
* **Server URL:** `https://mcp.algolia.com/1/snippets/mcp`
* **Authentication:** none
In [Connectors settings](https://claude.ai/new#settings/customize-connectors), add a custom connector with:
* **Remote MCP server URL:** `https://mcp.algolia.com/1/snippets/mcp`
* **Authentication:** none
### Example prompts for API client snippets
Once connected, you can ask your assistant questions like:
* *"Show how to save records with the JavaScript API client."*
* *"Get a Python snippet for searching an index."*
* *"Create an API key with the C# API client."*
* *"Return a Go snippet for setting index settings."*
For a full walkthrough with credentials, settings, records, and search, see [Add Algolia to your app with DevEx MCP](/doc/guides/model-context-protocol/devex-mcp).
## Install the Algolia CLI
Command-line tools are part of most developer workflows.
With coding agents running in the terminal, they're even more useful.
The [Algolia CLI](/doc/tools/cli/get-started) lets agents script index setup, update settings, and run admin tasks—without extra integration code.
If you have [Node.js](https://nodejs.org) installed,
you can run the CLI with `npx`, which doesn't require installation:
```sh icon=square-terminal theme={"system"}
npx @algolia/cli --help
```
Or install it globally with npm:
```sh icon=square-terminal theme={"system"}
npm install -g @algolia/cli
```
```sh icon=square-terminal theme={"system"}
brew install algolia/algolia-cli/algolia
```
Download a `.deb` or `.rpm` from the [Releases page](https://github.com/algolia/cli/releases) and install it:
```sh icon=square-terminal theme={"system"}
sudo dpkg -i algolia_*.deb # Debian, Ubuntu
sudo rpm -i algolia_*.rpm # Fedora, RHEL
```
```powershell icon=square-terminal theme={"system"}
choco install algolia
```
After installing, run `algolia auth login` to sign in, or `algolia auth signup` to create an account.
This lets your agent run commands without exporting keys each session.
For other install options and authentication, see [Get started with the Algolia CLI](/doc/tools/cli/get-started).
### What agents can do with the CLI
Pair the CLI with your coding assistant to automate Algolia tasks as it writes integration code:
* **Bootstrap a new index**: create the index, upload sample records, and apply searchable attributes and ranking in one script.
* **Sync data from a file**: import records from JSON or CSV with `algolia objects import`.
* **Tune relevance**: update `searchableAttributes`, `customRanking`, and other settings with `algolia settings set`.
* **Manage rules and synonyms**: create, browse, and export rules and synonyms across environments.
* **Promote between environments**: export an index configuration from staging and import it into production.
* **Run searches and inspect results**: query an index from the terminal to validate changes before shipping.
For more examples, see [Usage examples](/doc/tools/cli/examples).
## Install Algolia skills
[Algolia skills](https://github.com/algolia/skills) are agent skills for Claude Code, Cursor, OpenAI Codex, and OpenCode.
Add them to your assistant to teach it how to work with Algolia.
While MCP servers and the Algolia CLI give your assistant the building blocks, skills package the patterns.
They tell your assistant which tool to reach for, what conventions to follow (such as which InstantSearch widgets to use for a given UI), and how to chain steps for common tasks—so you don't have to re-explain them in every prompt.
The repository includes these skills:
* `algolia-quickstart`: provision an Algolia account and application from the [Algolia CLI](/doc/tools/cli/get-started).
* `algolia-mcp`: search, analytics, and recommendations through the Algolia MCP server.
* `algolia-cli`: provision accounts, and manage applications, indices, settings, rules, and synonyms through the [Algolia CLI](/doc/tools/cli/get-started).
* `algolia-migration`: upgrade existing API client code to a newer major version.
* `algolia-crawler`: crawl and index web pages into Algolia for RAG.
* `algobot-cli`: build AI agents, chat experiences, and RAG on Algolia.
* `instantsearch`: build search interfaces with autocomplete, results, and facets.
Install the skills as plugins:
```sh icon=square-terminal theme={"system"}
claude plugin marketplace add algolia/skills
claude plugin install algolia-quickstart@algolia-skills
claude plugin install algolia-cli@algolia-skills
claude plugin install algolia-migration@algolia-skills
```
Then run `/reload-plugins` inside Claude Code to activate them.
Run the following command and select the skills you want to add:
```sh icon=square-terminal theme={"system"}
npx skills add algolia/skills
```
### Upgrade an API client
The `algolia-migration` skill upgrades existing API client code to a newer major version.
It covers backend clients and search-only frontend builds.
For InstantSearch UI, use the `instantsearch` skill instead.
After you add the skill, open your project and ask the agent to upgrade the Algolia API client.
The skill reads the language and version from your package manifest, then updates breaking call sites such as `initIndex` and chained `.wait()`.
Upgrade the Algolia API client in this project to the latest semantic version available in the language package manager, or GitHub release otherwise
If the skill doesn't start on its own, run `/algolia-migration` in the agent.
The skill supports these upgrades:
| Language | From | To |
| - | - | - |
| JavaScript | Version 4 | Version 5 |
| Python | Version 3 | Version 4 |
| Go | Version 3 | Version 4 |
| PHP | Version 3 | Version 4 |
| Java | Version 3 | Version 4 |
| C# | Version 6 | Version 7 |
| Ruby | Version 2 | Version 3 |
| Kotlin | Version 2 | Version 3 |
| Scala | Version 1 | Version 2 |
| Swift | Version 8 | Version 9 |
You can also name the language and versions:
* *"Migrate the JavaScript API client from version 4 to version 5."*
* *"Replace `initIndex` with client methods in this Python app."*
* *"Upgrade the C# API client from version 6 to version 7."*
## Search developer documentation with DocSearch MCP
Connect DocSearch MCP so that your AI coding agent can search public developer documentation indexed by DocSearch.
DocSearch MCP doesn't require authentication or provide access to data in your Algolia applications.
To query data in your Algolia applications, use Productivity MCP.
Connect DocSearch MCP to your agent.
## Build no-code data pipelines
Not all Algolia workflows require coding.
[Connectors](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors) let merchandisers and operators build data pipelines from sources (JSON, CSV, BigQuery, Supabase, and more) to an Algolia index, with [no-code transformations](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-without-code) that change attributes, filter records, or compute new values from existing ones.
For complex logic, use [AI-assisted transformations](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/ai-assisted-transformations) to describe what you want in plain English and let AI generate the configuration.
Examples of prompts you can give the AI assistant:
* *"If price is greater than 100, add the label 'expensive'."*
* *"Add a discount percentage attribute computed from `full_price` and `sale_price`."*
* *"Remove records where `status` is `draft` or `archived`."*
* *"Combine popularity and recency into a single ranking signal."*
# Understand the basics
Source: https://www.algolia.com/doc/guides/get-started/concepts
Core concepts for building with Algolia: indices, records, attributes, and events.
This page introduces the core concepts used throughout Algolia.
Understanding these terms helps you structure your data, configure relevance, and build search experiences.
## Index
An index is a collection of records optimized for fast retrieval.
It's the searchable representation of your data.
An index contains:
* **Records.** The data you want to make searchable and return as results
* **Settings.** Configuration that controls matching, ranking, and filtering
* **Rules.** Conditional logic for overriding default behavior in specific situations
* **Synonyms.** Equivalences between terms, so a search for "laptop" also matches "notebook"
The same index can power multiple experiences,
such as search results, recommendations, or AI-assisted retrieval,
without duplicating data.
A typical implementation uses several indices, for example,
one per content type or ranking strategy.
For more information, see [Send and manage your data](/doc/guides/sending-and-managing-data/prepare-your-data).
## Record
A record is an object stored in an index and returned as a result.
Records are typically JSON objects with fields called **attributes**.
```json JSON icon=braces theme={"system"}
{
"objectID": "prod-42",
"title": "Trail Running Shoes",
"brand": "Salomon",
"category": "Footwear",
"price": 129.99,
"in_stock": true
}
```
Records are schemaless.
Algolia doesn't require a predefined structure,
but consistent shapes make indexing and UI development easier.
Every record must include an `objectID`,
which uniquely identifies it within an index.
## Attribute
An attribute is a named field in a record,
such as `title`, `category`, or `price`.
Attributes determine:
* **What users can search,** through searchable attributes in your index settings
* **How results are ranked,** for example by popularity or rating
* **What users can filter on,** when configured as facets
* **What you display** in your UI
Choosing the right attributes and configuring them correctly has one of the biggest effects on relevance.
## Query
A query is the text a user enters to search.
Your app sends it to Algolia, which matches it against the searchable attributes in your index and returns ranked results.
## Facet
A facet is an attribute used to let users filter and refine results, such as category, brand, price range, or rating.
Facets are configured in your index settings and exposed in your UI as interactive filters.
When a user selects a facet value, Algolia returns only the records that match.
To learn more, see [Facets](/doc/guides/managing-results/refine-results/faceting).
## Relevance and ranking
Relevance determines which records match a query and in what order they appear.
Algolia combines:
* **Textual relevance.** Matching based on searchable attributes, synonyms, and typo tolerance
* **Ranking criteria.** A sequence of criteria in your ranking formula, such as Typo, Words, Proximity, Attribute, and Exact.
You can also add **custom ranking**, such as popularity or sales, to promote more relevant results.
For more information, see [Relevance overview](/doc/guides/managing-results/relevance-overview).
## Event
An event captures a user interaction with your search experience, such as a click on a result or a completed conversion.
You send events from your UI to Algolia.
They power:
* Analytics
* Personalization
* Recommendations
* AI-based ranking improvements
Events help you improve search quality based on user behavior.
The earlier you start sending events, the more interaction data is available for analytics, personalization, and recommendations.
For details, see [Click and conversion events](/doc/guides/sending-events/getting-started).
## API key
Every request to Algolia must be authenticated with an API key.
The main types are:
* **Search API key.** Read-only key for search requests. Safe to use in frontend code.
* **Write API key.** Key with permissions for indexing operations. Keep it private and use it only in your backend.
* **Admin API key.** Key with full access to your Algolia application.
Keep it private and use it only to create more restricted API keys.
For more information, see [API keys](/doc/guides/security/api-keys).
## Ready to build?
Build your first search experience in the [Quickstart](/doc/guides/get-started/quickstart).
For more terms and their definitions, see the [Glossary](/doc/glossary).
# Build your first search experience
Source: https://www.algolia.com/doc/guides/get-started/quickstart
Set up a React app, index sample data in Algolia, and build a search interface with React InstantSearch.
This quickstart builds a React app that searches a sample product catalog,
filters by product type, and paginates results.
Explore the product search app you'll build in this quickstart.
Download the complete quickstart app from GitHub.
## Before you begin
Make sure you have:
* **An Algolia account**. [Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **[Node.js](https://nodejs.org/en/download)** 20.19 or later.
* Are you building with AI agents?
To prepare your agent to work with Algolia,
see [Build with AI](/doc/guides/get-started/build-with-ai).
## Set up your app and index sample data
Set up your project and index a sample product dataset.
In Algolia, the way you structure and configure your index affects both the search experience and how results are ranked.
Create a new Vite app with React and TypeScript.
```sh icon=square-terminal theme={"system"}
npm create vite@latest algolia-quickstart-products -- --template react-ts --no-interactive
cd algolia-quickstart-products
```
The Vite template includes a global style sheet that centers content and overrides heading colors.
These styles conflict with the search UI, so remove its import from `src/main.tsx`:
```tsx src/main.tsx icon="react" theme={"system"}
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
,
);
```
Install the packages for indexing and search:
* `algoliasearch` sends data to Algolia.
* `react-instantsearch` builds the search UI.
* `instantsearch.css` provides base styles.
* `tsx` runs the indexing script.
* `ora` shows progress in the terminal while the script runs.
```sh icon=square-terminal theme={"system"}
npm install algoliasearch instantsearch.css react-instantsearch ora tsx
```
In the Algolia dashboard, open the [API Keys](https://dashboard.algolia.com/account/api-keys) page and copy these values:
* **Application ID**. Identifies your Algolia application.
* **Write API key**. Used by the indexing script to update records and index settings.
* **Search API key**. Used by the React app to query the index from the browser.
Create a `.env.local` file and add the values to it:
```dotenv .env.local icon=lock-keyhole theme={"system"}
VITE_ALGOLIA_APPLICATION_ID=
ALGOLIA_WRITE_API_KEY=
VITE_ALGOLIA_SEARCH_API_KEY=
```
Keep your Write API key secret.
Don't commit `.env.local` to version control or expose the write key in your app.
Create a `scripts` directory and a `scripts/indexing.ts` file.
```sh icon=square-terminal theme={"system"}
mkdir scripts && touch scripts/indexing.ts
```
This script loads your credentials, then runs two independent steps:
* `indexProducts` fetches a sample product dataset and uploads those products as records with `saveObjects`.
* `configureIndex` uses `setSettings` to add `product_type` as a facet so your UI can filter results,
and to snippet `description` so your UI can show a shortened excerpt of it.
Keeping the two separate lets you reapply the index settings without reimporting the records,
which is what you need when your records come from a connector or another pipeline.
Run this script once to create and configure the index before connecting it to your UI.
Since indexing operations are asynchronous,
`waitForTasks` and `waitForTask` make sure the records and settings are applied before the script exits.
```ts scripts/indexing.ts icon=code expandable theme={"system"}
import { algoliasearch } from "algoliasearch";
import ora from "ora";
import { loadEnv } from "vite";
const env = loadEnv(process.env.MODE ?? "dev", process.cwd(), "");
const appId = env.VITE_ALGOLIA_APPLICATION_ID;
const writeApiKey = env.ALGOLIA_WRITE_API_KEY;
const indexName = "quickstart-products";
if (!appId) {
throw new Error("Missing VITE_ALGOLIA_APPLICATION_ID environment variable.");
}
if (!writeApiKey) {
throw new Error("Missing ALGOLIA_WRITE_API_KEY environment variable.");
}
const client = algoliasearch(appId, writeApiKey);
const spinner = ora();
async function indexProducts() {
spinner.text = "Fetching the products dataset...";
const response = await fetch(
"https://dashboard.algolia.com/api/1/sample_datasets?type=apparel",
);
if (!response.ok) {
throw new Error(
`Error fetching products dataset: ${response.status} ${response.statusText}`,
);
}
const products = await response.json();
spinner.text = `Indexing ${products.length.toLocaleString()} products into ${indexName}...`;
await client.saveObjects({
indexName,
objects: products,
waitForTasks: true,
});
}
async function configureIndex() {
spinner.text = `Configuring ${indexName}...`;
const { taskID } = await client.setSettings({
indexName,
indexSettings: {
attributesForFaceting: ["product_type"],
attributesToSnippet: ["description:30"],
},
});
await client.waitForTask({ indexName, taskID });
}
try {
spinner.start("Beginning index setup...");
await indexProducts();
await configureIndex();
spinner.succeed("Successfully indexed and configured products.");
} catch (error) {
spinner.fail("Indexing failed.");
console.error(error);
process.exitCode = 1;
}
```
Add the indexing script to `package.json`:
```sh icon=square-terminal theme={"system"}
npm pkg set scripts.index:products="tsx scripts/indexing.ts"
```
Run the script to create the `quickstart-products` index, add the sample records, and apply the index settings.
```sh icon=square-terminal theme={"system"}
npm run index:products
```
Look for the message `Successfully indexed and configured products.` in your terminal.
At this point, your data is ready for search and you can explore the `quickstart-products` index
in the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
In the next section, you'll connect a React UI to this index.
## Build the search UI
Connect a React UI to your `quickstart-products` index.
React InstantSearch automatically keeps the search box, filters, and pagination in sync with the current results.
Replace `src/App.tsx` with the following code to connect your app to Algolia and render a basic search interface.
```tsx src/App.tsx icon=code expandable theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import type { Hit } from "instantsearch.js";
import {
Configure,
Highlight,
Hits,
InstantSearch,
Pagination,
PoweredBy,
RefinementList,
SearchBox,
Snippet,
} from "react-instantsearch";
import "instantsearch.css/themes/reset-min.css";
import "./App.css";
const appId = import.meta.env.VITE_ALGOLIA_APPLICATION_ID;
const apiKey = import.meta.env.VITE_ALGOLIA_SEARCH_API_KEY;
if (!appId) {
console.error("Missing environment variable: VITE_ALGOLIA_APPLICATION_ID");
}
if (!apiKey) {
console.error("Missing environment variable: VITE_ALGOLIA_SEARCH_API_KEY");
}
const searchClient = algoliasearch(appId, apiKey);
type ProductRecord = {
title: string;
description: string;
product_type: string;
price: number;
showcase_image: string;
};
type ProductHit = Hit;
function ProductCard({ hit }: { hit: ProductHit }) {
return (
{hit.product_type}
${hit.price}
);
}
export default function App() {
return (
Product type
);
}
```
This app connects to your `quickstart-products` index and renders a search interface.
* [`SearchBox`](/doc/api-reference/widgets/search-box/react) lets users enter search terms.
* [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) adds filtering by product type.
* [`Hits`](/doc/api-reference/widgets/hits/react) displays matching records from your index.
* [`Pagination`](/doc/api-reference/widgets/pagination/react) lets users move through results.
* [`Highlight`](/doc/api-reference/widgets/highlight/react) and [`Snippet`](/doc/api-reference/widgets/snippet/react) emphasize matching parts of `title` and `description`.
Replace `src/App.css` with the following code.
These styles lay out the search interface and make the results and filters easier to scan.
They change the presentation, not the search behavior.
```css src/App.css icon=paintbrush expandable theme={"system"}
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--color-text: #111827;
--color-background: #ffffff;
--color-border: #e5e7eb;
--color-surface: #ffffff;
--color-surface-muted: #f3f4f6;
--color-muted: #6b7280;
--color-accent: #5468ff;
--color-accent-strong: #5468ff;
--color-accent-ring: rgb(84 104 255 / 0.15);
}
body {
margin: 0;
font-family: system-ui, sans-serif;
color: var(--color-text);
background-color: var(--color-background);
}
#root {
inline-size: min(100%, 70rem);
margin-inline: auto;
padding-block: 2rem;
padding-inline: 1.25rem;
color: inherit;
}
.search-header {
display: grid;
gap: 0.5rem;
margin-block-end: 1.5rem;
}
.search-body {
display: grid;
grid-template-columns: 12.5rem minmax(0, 1fr);
gap: 2rem;
}
.ais-SearchBox-input,
.ais-Pagination-link {
border: 1px solid var(--color-border);
border-radius: 0.5rem;
background-color: var(--color-surface);
}
.ais-SearchBox-input {
inline-size: 100%;
padding-block: 0.75rem;
padding-inline: 1rem;
font: inherit;
font-size: 1rem;
color: inherit;
outline: none;
}
.ais-SearchBox-input:focus {
border-color: var(--color-accent);
box-shadow: 0 0 0 0.1875rem var(--color-accent-ring);
}
.ais-SearchBox-submit,
.ais-SearchBox-reset {
display: none;
}
.ais-RefinementList-list,
.ais-Hits-list {
margin: 0;
padding: 0;
list-style: none;
}
.ais-Hits-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
gap: 1rem;
}
.ais-Hits-item {
display: flex;
}
.ais-Pagination-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
padding-block: 0.75rem;
}
.ais-RefinementList-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.ais-PoweredBy,
.ais-RefinementList-count,
.product-card-type {
font-size: 0.75rem;
color: var(--color-muted);
}
.ais-PoweredBy {
display: flex;
align-items: center;
min-block-size: 1.5rem;
line-height: 1;
justify-self: end;
}
.ais-PoweredBy-link {
display: flex;
align-items: center;
}
.ais-PoweredBy-text {
line-height: 1;
}
.ais-RefinementList-list {
display: grid;
gap: 0.5rem;
}
.ais-RefinementList-count {
margin-inline-start: auto;
}
.ais-RefinementList-checkbox {
inline-size: 0.875rem;
block-size: 0.875rem;
accent-color: var(--color-accent);
}
.ais-RefinementList-item--selected .ais-RefinementList-label {
font-weight: 500;
}
.ais-Pagination-link {
display: block;
padding-block: 0.5rem;
padding-inline: 0.75rem;
font-size: 0.875rem;
color: inherit;
text-decoration: none;
}
.ais-Pagination-item--selected .ais-Pagination-link {
color: #fff;
background: var(--color-accent-strong);
border-color: var(--color-accent-strong);
}
.ais-Pagination-item--disabled .ais-Pagination-link {
opacity: 0.4;
pointer-events: none;
}
.product-card {
display: flex;
flex: 1;
flex-direction: column;
inline-size: 100%;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: 0.75rem;
background-color: var(--color-surface);
}
.product-card-image {
aspect-ratio: 1;
overflow: hidden;
background: var(--color-surface-muted);
}
.product-card-image img {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
.product-card-body {
display: flex;
flex: 1;
flex-direction: column;
gap: 0.25rem;
padding: 0.75rem;
}
.product-card-type {
margin: 0;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.product-card-title,
.product-card-price,
.filter-panel-section-title {
margin: 0;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
}
.product-card-description {
flex: 1;
margin: 0;
overflow: hidden;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-muted);
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
}
.filter-panel-section {
padding-block-end: 0.75rem;
}
.filter-panel-section-title {
margin-block-end: 0.5rem;
}
@media (max-width: 800px) {
#root {
padding-block: 1.5rem;
padding-inline: 1rem;
}
.search-body {
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
```
Start the development server.
```sh icon=square-terminal theme={"system"}
npm run dev
```
Open [`http://localhost:5173`](http://localhost:5173) and start searching.
You see a product grid with images, a product type filter,
and a search box.
As you type, the product grid updates.
Matching text is highlighted in the results.
You now have a working end-to-end search:
* Records come from the `quickstart-products` index you created earlier.
* Queries from `SearchBox` update the results and pagination together.
* The `product_type` filter works because that attribute is configured for faceting.
* `Highlight` and `Snippet` show which parts of each record match the query.
## Improve relevance and add features
Your app already returns matching products.
You can improve relevance by adjusting which matches appear first.
Update the existing `indexSettings` object in `scripts/indexing.ts`,
then rerun `npm run index:products`.
```ts scripts/indexing.ts icon=code expandable theme={"system"}
const { taskID } = await client.setSettings({
indexName,
indexSettings: {
attributesForFaceting: ["product_type"],
attributesToSnippet: ["description:30"],
searchableAttributes: [ // [!code ++:6]
"unordered(title)",
"unordered(product_type)",
"unordered(description)",
],
customRanking: ["desc(units_sold)", "desc(price)"],
},
});
```
This makes search results more useful by [prioritizing matches](/doc/api-reference/api-parameters/searchableAttributes) in product titles and types before descriptions,
then [ranking similar matches](/doc/api-reference/api-parameters/customRanking) by best-selling and higher-priced products.
Add features that help users understand and refine results.
For example, you can add [search stats](/doc/api-reference/widgets/stats/react),
[active filters](/doc/api-reference/widgets/current-refinements/react),
or more ways to filter results, such as [filter lists](/doc/api-reference/widgets/refinement-list/react),
[menus](/doc/api-reference/widgets/menu/react),
and [numerical filters](/doc/api-reference/widgets/numeric-menu/react).
If you add more filtering features,
update `attributesForFaceting` in `scripts/indexing.ts` to match.
Collect events from your UI to measure engagement and
support [Algolia features that use behavioral data](/doc/guides/sending-events/concepts/event-types).
Update the `InstantSearch` component in `src/App.tsx`:
```tsx src/App.tsx icon=code theme={"system"}
```
This turns on the default React InstantSearch events,
including `view` events for hits and filter click events for `RefinementList`.
To send click events for hits, see [Send events with React InstantSearch](/doc/guides/building-search-ui/events/react).
## What's next
* [Get started with React InstantSearch](/doc/guides/building-search-ui/getting-started/react) to continue building your search interface.
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data) to replace the sample catalog with your own product data.
* [Manage results](/doc/guides/managing-results/relevance-overview) to learn how to adjust relevance and ranking to your needs.
# Custom ranking
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking
Learn how to use custom ranking attributes to order records by popularity, rating, release date, or other metrics.
Search has two crucial steps:
returning accurate results and ranking them.
Custom ranking lets you influence the order of search results.
After Algolia applies its default relevance criteria, custom ranking breaks ties using attributes you define, such as popularity, rating, or release date.
This helps surface your most important content.
## How custom ranking fits into Algolia's ranking criteria
Algolia ranks results with a tie-breaking algorithm.
It first applies the default ranking criteria, such as Typo, Geo, Words, Filters, Proximity, Attribute, and Exact.
Custom ranking is the final default criterion and is only used when are still tied after the earlier criteria.
For a full explanation of the ranking order, see [The eight ranking criteria](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria).
## Use custom ranking attributes
Algolia's default ranking criteria determine how well records match a query.
When multiple records are equally relevant, custom ranking breaks ties using attributes you define, instead of falling back to arbitrary ordering such as `objectID`.
This lets you use metrics like popularity, rating, or sales to order records that are otherwise equally relevant.
Typical custom ranking attributes include number of sales, views, likes, ratings, and release date.
**Any boolean or numeric attribute works with custom ranking.**
You can also use custom ranking attributes to boost or penalize specific records.
For example, you might add a boolean attribute such as `promoted` or `discontinued` and use it to rank promoted records higher or discontinued records lower.
Custom ranking attributes help align relevance with the metrics that matter for your search experience.
The following dataset illustrates a common use case with custom ranking on the `popularity` attribute in descending order:
```json JSON icon=braces theme={"system"}
[
{
"name": "Graphic t-shirt",
"popularity": 80
},
{
"name": "Limited edition t-shirt",
"popularity": 20
},
{
"name": "Basic t-shirt",
"popularity": 200
}
]
```
If you use `popularity` as a custom ranking attribute in descending order, a search for `t-shirt` ranks the most popular matching records first:
1. Basic t-shirt
2. Graphic t-shirt
3. Limited edition t-shirt
You can decide whether you want the order to be descending (greater values first) or ascending (smaller values first).
### Custom ranking precision
If a custom ranking attribute is too precise, it can prevent other attributes from influencing the ranking.
When records aren't tied, later attributes never come into play.
Consider a movie index that uses rating and views (in that order) as custom ranking attributes.
With precise ratings such as 4.321321, records are unlikely to share the same value.
As a result, views rarely affects the ranking.
This can produce less useful results.
For example, a movie with slightly lower rating but significantly more views may rank lower than expected.
To address this, reduce the precision of the first attribute.
For example, create a `truncated_rating` rounded to one decimal place (4.321321 → 4.3).
With more shared values, records can tie on `truncated_rating`, allowing views to act as a secondary ranking factor.
That's why [reducing precision is critical to effective tie-breaking](/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision).
## See also
* [Create custom ranking attributes](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking)
* [Precision of custom ranking metrics](/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision)
* [Boost or penalize records](/doc/guides/managing-results/must-do/custom-ranking/how-to/boost-or-penalize-some-records)
* [12 ways to improve your search index (blog)](https://www.algolia.com/blog/engineering/12-ways-to-improve-your-search-index/)
* [Building a query suggestion UX like Google's (blog)](https://www.algolia.com/blog/algolia/query-suggestion-ux-like-googles/)
# Bayesian averages in custom ranking
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/bayesian-average
Learn how to use the Bayesian average for a more reliable and accurate rank star rating average.
Product reviews and ratings play an important role in consumer decision making.
Online shoppers look for products with the highest ratings.
They often read reviews that give details behind the ratings.
In the search and discovery context, businesses consider product reviews to be as relevant as product descriptions.
Both are relevant for matching users' queries.
Algolia's [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) feature enables you to use business signals such as the number of sales and profit margins.
You can also use ratings to influence the ranking.
For example, when a user types a broad query such as "headphones", a ratings-based custom ranking ensures that the highest-rated earbuds show up first.
Though this tutorial focuses on star ratings,
this solution can work with any product scoring system.
For example, you could compute the Bayesian average for the number of up and down votes,
or scores based on the number of sells or views.
## Concepts
The challenges with star ratings and how the Bayesian average has become the preferred method for ranking by star ratings.
### The difficulties with calculating a reliable rating
Creating a meaningful ranking strategy based on ratings can be challenging.
Is only one rating enough?
Or do you need a certain quantity of ratings for the ratings to be reliable?
Is a product with ratings ranging from 1 to 5 (in a 5-star rating system) better than a product with a smaller number of positive ratings?
Obviously, a product that receives only 5 stars is a good choice, but is it better than a popular item with hundreds of 4 stars?
A good example is the query "lion" on a movie streaming site.
Should the movie "The Lion King" rank above a higher-rated but lesser known film like "The Lion in Winter"?
Suppose that "The Lion King" has an average of 4.5 stars but "The Lion in Winter" has a higher average of 4.8.
If the 4.5 average rating comes from 10,000 ratings and the 4.8 average rating from 100 ratings, which movie should show up first?
The challenge with any rating system—whether for handbags, electronics, or movies—is that the quantity of ratings is as important as the rating itself.
Intuitively, the more ratings received, the more confidence you can have in the rating.
But again, how large should the sample size be before you can trust that the rating is meaningful?
#### Different ratings comparison
Consider two ways to rank star ratings:
* Use an arithmetic average that adds together all ratings and divides by the total quantity of ratings. If there are 100 1-star ratings and 10 5-star ratings, the calculation is ((100x1) + (10x5))/ (100+10) = 1.36.
* Use a [Bayesian average](https://en.wikipedia.org/wiki/Bayesian_average) that adjusts a product's average rating by how much it varies from the catalog average. This favors products with a higher quantity of ratings.
As already suggested, ignoring the quantity of ratings doesn't help distinguish between items with 10 ratings and 1000 ratings.
You need to at least calculate an average that includes the quantity of ratings.
The following image shows three items ranked by different averages.
The left side uses the arithmetic average for ranking.
The right side uses the Bayesian average.
Both sides display the arithmetic average in parenthesis just right of the stars.
They also display the average used for ranking as `avg_star_rating` and `bayes_avg` respectively, under each item.
The left side's ranking puts Item A at the top.
That's misleading and unsatisfying.
The ranking on the right, based on the Bayesian average, reflects a better balance of rating and quantity of ratings.
This example shows how the Bayesian average lowered item A's average to 4.3 because it measured A's 10 ratings against B and C's much larger numbers of ratings.
The Bayesian average left Items B and C unchanged, as described later.
That's because it affects items with low rating counts much more than items with more ratings.
In sum, by relativizing ratings in this way,
the Bayesian average creates a more reliable comparison between products.
It ensures that products with lower numbers of ratings have less weight in the ranking.
What follows is a description of the Bayesian average and how to code it.
### Bayesian averages
The Bayesian average adjusts the average rating of products whose rating counts fall below a threshold.
Suppose the threshold amount is calculated to be 100.
That means average ratings with less than 100 ratings get adjusted,
while average ratings with more than 100 ratings change only slightly.
This threshold amount of 100 is called a confidence number,
because it gives you confidence that averages with 100 or more ratings are more reliable than averages with fewer than 100 ratings.
This confidence number derives from the catalog's distribution of rating counts and the average rating of all products.
By factoring in ratings counts and averages from the whole catalog,
the Bayesian average has the following effect on an item's individual average rating:
* For an item with fewer than average ratings, the Bayesian average lowers its artificially high rating. It weighs the rating down slightly, toward the lower catalog average.
* For an item with a lot of ratings (that is, more than the threshold), the Bayesian average doesn't change its rating average by a significant amount.
#### How to calculate the Bayesian average
The Bayesian average uses two constants to offset the arithmetic average of an individual product:
* The arithmetic average rating of all products (`m`)
* A confidence number (`C`).
The calculation for `m` is an [arithmetic average](https://en.wikipedia.org/wiki/Arithmetic_mean) for all products:
the sum of all ratings divided by the number of ratings.
Calculating `C` requires a bit more math.
This tutorial calculates `C` based on the distribution of the rating counts for each product, where `C` is equal to the 25% percentile (= the lower [quartile](https://en.wikipedia.org/wiki/Quartile)).
For example, suppose a store has 100 products.
To compute `C`, you take all the products and sort them by the quantity of ratings each has.
Some have 10 ratings and others have 100 or 1000 ratings.
Once sorted, you find the product at the 25% position on the sorted list and note the number of ratings it has.
This is the lower quartile for `C`.
For simplicity, this guide sets `C = 100`.
Thus, if you calculate the overall average rating (`m`) of the store's catalog to be 3.5,
the Bayesian average uses both of these values ( `m = 3.5` and `C = 100`) to adjust the arithmetic average.
It does this using the following formula:
$$
\mathrm{bayesAvg} = \frac{\mathrm{productRatingsAvg} \times \mathrm{productRatingsCount} + C \times m}{\mathrm{productRatingsCount} + C}
$$
Here's the same formula with the example numbers plugged in:
$$
\mathrm{bayesAvg} = \frac{\mathrm{productRatingsAvg} \times \mathrm{productRatingsCount} + 100 \times 3.5}{\mathrm{productRatingsCount} + 100}
$$
## Compute the Bayesian average
The following shows how to code the Bayesian constants (`m` and `C`) and the Bayesian average itself.
It also discusses when to calculate these values.
### Necessary attributes in your index
You need a dataset of products where each product has at least these three attributes:
* The product's arithmetic average rating (`avg_stars_rating`)
* The product's Bayesian average (`bayes_average`), this can be empty or `0` to start
* The quantity of ratings (`ratings_count`)
Here's a sample dataset.
```json JSON icon=braces theme={"system"}
[
{
"name": "Item A",
"avg_stars_rating": 5,
"bayes_average": 0,
"ratings_count": 10
},
{
"name": "Item B",
"avg_stars_rating": 4.8,
"bayes_average": 0,
"ratings_count": 100
},
{
"name": "Item C",
"avg_stars_rating": 4.6,
"bayes_average": 0,
"ratings_count": 1000
}
]
```
This JSON already includes a `bayes_average` attribute.
The purpose of the following code is to calculate the value for `bayes_average`.
Additionally, the sample dataset doesn't show other attributes, such as the description of the product, price, item specifications, etc.
### Bayesian constants
The variables `m` and `C` represent the two Bayesian constants.
In this code, they're assigned the values from the preceding section (`m = 3.5` and `C = 100`):
```js JavaScript icon=code theme={"system"}
// Set the value of the catalog average rating
const m = 3.5; // average rating of all products
// Set the value of the confidence number
const C = 100; // confidence number based on quartile
```
You can calculate the `C` constant in different ways.
As suggested in the preceding section, you can use a lower quartile % that corresponds to the 25% percentile.
You can calculate this value using the following SQL function:
```sql SQL theme={"system"}
/* The value C is equal to: */
PERCENTILE_CONT(0.25) WITHIN GROUP ( ORDER BY ratings_count asc) OVER ()
```
The syntax of this SQL function can differ from one SQL database to another, so you might need to adapt it.
You may also want to calculate separate constants for each product category.
This is helpful for use cases with multiple categories of items, like Amazon and other marketplaces.
### Calculate the Bayesian average for each product
The variables `avg_stars_rating` and `ratings_count` match the attributes in the that represent, respectively, the rating average and quantity of ratings for each product:
```js JavaScript icon=code theme={"system"}
// Name of the attribute having the arithmetic average rating per record
const product_ratings_average = "avg_stars_rating";
// Name of the attribute having the quantity of ratings per record
const product_ratings_count = "ratings_count";
```
Using all four variables, this function generates the Bayesian average for a single product:
```js JavaScript icon=code theme={"system"}
const bayesianAverage = (
product_ratings_count,
product_ratings_average,
m,
C,
) => {
return (
(product_ratings_count * product_ratings_average + m * C) /
(product_ratings_count + C)
);
};
```
Perform the calculations for every product. You can do this by:
* Retrieving every product in the index, using the [`browse`](/doc/libraries/sdk/v1/methods/browse) method.
* Running them through the preceding Bayesian average function to calculate their Bayesian average
* Updating the index with their new values
```js JavaScript icon=code theme={"system"}
// retrieve every product in the index
var browser = index.browseAll();
// go through the records and calculate the Bayesian average
browser.on("result", function onResult(content) {
hits = hits.concat(content.hits);
const decorated_hits = _.map(hits, (hit) => ({
...hit,
bayes_average: bayesianAverage(
hit[product_ratings_count],
hit[product_ratings_average],
m,
C,
),
}));
// update every product with its newly calculated Bayesian average
index.saveObjects(decorated_hits, function (err, content) {
if (err) throw err;
console.log("Updated batch of 1000 records");
});
});
```
### Full example
```js JavaScript icon=code theme={"system"}
var algoliasearch = require("algoliasearch");
var hits = [];
var _ = require("lodash");
// Set your unique credentials and index name here
var client = algoliasearch("", "");
var index = client.initIndex("");
var browser = index.browseAll();
// Name of the attribute having the arithmetic average rating per record
const product_ratings_average = "avg_stars_rating";
// Name of the attribute having the quantity of ratings per record
const product_ratings_count = "ratings_count";
// Set the value of the prior to be used
const m = 3;
// Set the value of the confidence to be used
const C = 25;
// END of configuration
const bayesianAverage = (
product_ratings_count,
product_ratings_average,
m,
C,
) => {
return (
(product_ratings_count * product_ratings_average + m * C) /
(product_ratings_count + C)
);
};
// Use the browse method to retrieve all hits and update them with their Bayesian average
browser.on("result", function onResult(content) {
hits = hits.concat(content.hits);
const decorated_hits = _.map(hits, (hit) => ({
...hit,
bayesian_average: bayesianAverage(
hit[product_ratings_count],
hit[product_ratings_average],
m,
C,
),
}));
index.saveObjects(decorated_hits, function (err, content) {
if (err) throw err;
console.log("Updated batch of 1000 records");
});
});
browser.on("end", function onEnd() {
console.log("We got %d hits", hits.length);
});
browser.on("error", function onError(err) {
throw err;
});
```
### Frequency and methods of updating the Bayesian average
As users rate products, their Bayesian average changes—but not necessarily with every new rating.
Here's some guidance on how often to update the Bayesian average.
For [calculating the Bayesian constants (`m` and `C`)](#bayesian-constants),
you can create a batch job that runs once per week or once per month.
These constants don't need to change that often.
It's important to store these constants so that you can calculate all products based on the same constants.
Thus, whenever you change these constants, recalculate every product's Bayesian average.
For [calculating the Bayesian average for one or more products](#calculate-the-bayesian-average-for-each-product),
you can recalculate the Bayesian average periodically during the day, every night, or once per week.
It depends on the number of ratings you get each day.
For example, Uber might want to update their drivers' averages several times a day,
whereas Amazon might be fine with an overnight process, or once per week.
Update the Bayesian average at a frequency that matches how often people rate your products.
Because the Bayesian calculation is an average,
the values don't change with every new rating.
Thus, recalculating after every new rating isn't necessary.
You can recalculate Bayesian averages whenever you update your product index.
Note that because the Bayesian average affects one attribute per record,
you can do a [partial update](/doc/libraries/sdk/v1/methods/partial-update-objects) to change only the Bayesian average attribute.
## Use the Bayesian average as a custom ranking
As you've seen, you add the Bayesian average attribute to every product.
It's used as a [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) attribute to rank by ratings.
If you're using the Bayesian average as a custom ranking, consider:
* You can use it as a single custom ranking or combine it with other custom ranking attributes.
* Sometimes it's better to group together items with similar Bayesian averages.
* You can add more factors to the Bayesian calculation to improve its accuracy.
### As single custom ranking attribute
Up to this point, the assumption is that the Bayesian average operates alone in ranking your results.
In that case, you can use the new `bayes_average` attribute you've added in each of your records as a custom ranking value,
as seen in the following image:
### Mixed with other custom ranking signals
More often, you want to add a second ranking criteria as a tie-breaker,
to handle cases where two or more results have the same Bayesian average.
In these cases, you can use typical business metrics, such as most sales, views, margins, or anything else.
For example, if you have ten records with a Bayesian average of 4, you need a second custom ranking to break the tie between these 4's.
Here's an example where a product's `profit_margin` acts as a secondary tie-breaker for the primary Bayesian average ranking:
### Round and group scores
You may want to round your scores to one of the 5 ratings (1, 2, 3, 4, or 5), or more granularly (0.5, 1, 1.5, …, 5),
by using [a rounding function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round).
This avoids impacting the ranking with insignificant differences between products.
For example, you may decide that products with the Bayesian averages of 3.15 and 3.16 should have the same ranking.
You can also group ratings.
For example, you can put products with a Bayesian average between 3.8 and 4.2 into one group with a rating of 4,
products with averages between 4.3 and 4.7 into another group with rating 4.5, and so forth.
Not every group needs to have the same range.
For example, one group can contain ratings between 1 and 2,
whereas another group can contain ratings between 3 and 3.5, etc.
This is helpful when your product ratings are unevenly distributed,
for example, more 3's than 1's.
Grouping is another name for [bucketing](/doc/guides/going-to-production/case-study#creating-buckets-to-combine-sorts-with-custom-ranking), which not only removes insignificant differences between products, but tailors the grouping to your precise business needs or dataset.
### Add multiple indicators to the Bayesian average calculation
While using Bayesian averages is an excellent way to rank results,
it might be too simplistic depending on the size or nature of your business.
Some businesses change the way they compute an "average star rating."
For example, they may incorporate other signals into their average,
such as weighing reviews from validated reviewers differently,
or excluding reviews that have less than five likes,
or removing high and low ratings that deviate too much from the average (assuming your ratings follow a bell curve, which most do.)
In sum, whether it be with rounding, grouping,
or using multiple indicators,
you're improving upon the already more reliable Bayesian average,
thereby creating an even more reliable indicator of favorite products.
## See also
* [How to calculate the Bayesian average](https://fulmicoton.com/posts/bayesian_rating/)
* [Background on the Bayesian average](https://medium.com/district-data-labs/computing-a-bayesian-estimate-of-star-rating-means-651496a890ab)
* [How Amazon ranking works](https://www.feedbackwhiz.com/blog/how-does-amazon-calculate-product-ratings/)
# Boost or penalize records
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/boost-or-penalize-some-records
How to boost or penalize a subset of records in the search results.
To boost or penalize records,
you need to add a boost attribute that tells Algolia how to rank them.
Algolia doesn't create this attribute for you:
it only uses the value you [send](/doc/guides/sending-and-managing-data/send-and-update-your-data).
Boosted attributes can be boolean or numeric:
* Boolean values create simple boosts.
* Numeric values allow finer control over boosting.
The following examples use the `featured` attribute for boosting.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```json JSON icon=braces theme={"system"}
[
{
"name": "Apricot",
"featured": false
},
{
"name": "Apple",
"featured": true
},
{
"name": "Almonds",
"featured": false
}
]
```
```json JSON icon=braces theme={"system"}
[
{
"name": "Apricot",
"featured": 4
},
{
"name": "Apple",
"featured": 0
},
{
"name": "Almonds",
"featured": 1
}
]
```
Avoid using quoted numbers, such as `"32"`, which are treated as strings.
For finer control, use numeric values.
In the example dataset:
* With the boolean `featured` value, a query for `a` returns "Apple" first.
* With the numeric `featured` value, a query for `a` returns "Apricot" first because it has the highest `featured` value.
Defining the attribute isn't enough on its own:
you must tell Algolia to use it in the ranking formula (in custom ranking).
## Add the boost attribute to custom ranking
[Custom ranking](/doc/guides/managing-results/must-do/custom-ranking) is the last criterion in Algolia's ranking formula.
Add boost attributes here to affect the order of records with the same textual relevance.
It doesn't override Algolia's default ranking criteria.
To boost records, add the attribute you're boosting to the custom ranking from the dashboard or the API.
If your boosted attribute is a boolean,
ensure you set the order of the custom ranking attribute to descending.
### With the dashboard
On the [ranking and sorting tab](https://dashboard.algolia.com/explorer/configuration/ranking-and-sorting/),
add a new custom ranking attribute.
### From the API
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { CustomRanking = new List { "desc(boosted)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(boosted)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(boosted)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(boosted)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(boosted)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(customRanking = listOf("desc(boosted)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(boosted)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(boosted)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(boosted)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(boosted)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(boosted)"])
)
```
## Use sorting to group items before ranking them
You can sort records by an attribute before applying the ranking formula.
For example, you might want results to appear in price-based groups (such as high-, medium-, and low-priced items).
To do this,
assign numeric values (for example, 1, 2, and 3) to a defined grouping attribute.
Algolia sorts group 1 first, then group 2, and so on.
Use a separate attribute for grouping.
Don't reuse your boost attribute.
This method uses [sorting](/doc/guides/managing-results/refine-results/sorting),
which applies *before* custom ranking and determines the overall group order.
# Create custom ranking attributes
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking
How to create custom ranking attributes according to your business needs, based on your own metrics, like sales or popularity.
Algolia offers good relevance out-of-the-box.
Yet, what makes search great is fine-tuning it according to business relevance.
Consider you're developing an app based on Twitter feeds.
When searching, you want to rank results by relevance and the custom attributes `likes` and `retweets`.
You can do this by using Algolia's [custom ranking feature](/doc/guides/managing-results/must-do/custom-ranking)
to specify what attributes to rank on.
You can configure these attributes with the [`customRanking`](/doc/api-reference/api-parameters/customRanking) parameter or through Algolia's dashboard.
## Example dataset
In this example, you're developing an app based on Twitter feeds and want to rank tweets by the number of likes and retweets.
Here's what the dataset would look like:
```json JSON icon=braces theme={"system"}
[
{
"tweet": "🎈 We're introducing Create InstantSearch App today: a CLI to bootstrap InstantSearch apps from the terminal. Read more in this blog post by @FrancoisChlfr!",
"retweets": 13,
"likes": 27
},
{
"tweet": "Designers from all horizons, don't forget to register for next #ParisDesignMeetup, taking place on Sept 25th!",
"retweets": 9,
"likes": 13
},
{
"tweet": "Are you ready for GSA to be a thing of the past? Register now for our webinar, where we'll help you with migration options, tips, and tricks to make your move as painless as possible. https://go.algolia.com/gsa-migration",
"retweets": 4,
"likes": 6
}
]
```
## Configure custom ranking in the dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Configuration** tab, go to **Ranking and Sorting**.
5. Click **Add custom ranking attribute** and select the attributes `retweets` and then `likes`.
6. Save your changes.
## Configure custom ranking with the API
To rank on retweets and likes,
set [`customRanking`](/doc/api-reference/api-parameters/customRanking).
criteria to be in ascending or descending order.
Custom ranking criteria are applied in order (in this example, first retweets and then likes).
To run the code examples, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
CustomRanking = new List { "desc(retweets)", "desc(likes)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(retweets)",
"desc(likes)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(retweets)", "desc(likes)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(retweets)", "desc(likes)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(retweets)', 'desc(likes)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(customRanking = listOf("desc(retweets)", "desc(likes)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(retweets)',
'desc(likes)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(retweets)",
"desc(likes)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(retweets)", "desc(likes)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(retweets)", "desc(likes)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(retweets)", "desc(likes)"])
)
```
## Custom ranking on different attributes
**Custom ranking applies at the index level**.
This means records that don't have an attribute that's in the [`customRanking`](/doc/api-reference/api-parameters/customRanking) list are pushed to the bottom.
For example, say you want to add Facebook posts to your dataset, and they have attributes `likes` and `comments`.
Because `retweets` are a custom ranking attribute, and Facebook posts don't have a `retweets` attribute, they're unlikely to win the tie-break against a tweet.
You could solve this by creating a computed attribute.
For example, you could compute a single popularity attribute instead of having `likes` and `retweets` on one side and `likes` and `comments` on the other.
## Computed attributes
You might want to create a custom ranking attribute based on the calculated value of other attributes.
In other words, a computed attribute.
For example:
* [A Bayesian average of product ratings](/doc/guides/managing-results/must-do/custom-ranking/how-to/bayesian-average)
* A simple, combined score of other attributes (such as `popularity` being the sum of `likes` and `retweets`).
* A boolean value set to true or false based on whether another attribute is `null`.
To implement a computed attribute, you must create it as an attribute in your records. To populate the record with values for the computed attribute:
1. Retrieve every record in the index, using the [`browse`](/doc/libraries/sdk/v1/methods/browse) method.
2. For each record, calculate the computed attribute value.
3. Update each record's computed attribute in the index with the calculated value, using the [`partialUpdateObjects`](/doc/libraries/sdk/v1/methods/partial-update-objects) method.
The regularity of these updates depends on your use case: it might be once a month,
every week, daily, or even more frequently.
## Metric types
The custom ranking field accepts any numerical or boolean value that represents the relative relevance of your records.
The attribute type can be a raw value like the number of sales, views, or likes.
The field can also be a computed value such as a popularity score that you calculated before adding the record to Algolia.
What you set as your [`customRanking`](/doc/api-reference/api-parameters/customRanking) depends on your use case and what data you have available.
Some retail metrics commonly used in [`customRanking`](/doc/api-reference/api-parameters/customRanking) include sales rank, stock levels, free shipping (boolean), on sale (boolean), and rating.
Publish date (as a [timestamp](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#dates)), page views, and likes are often used in media applications.
Check that the numeric attributes used in [`customRanking`](/doc/api-reference/api-parameters/customRanking)
aren't formatted as strings.
This would cause the records to be ranked alphabetically.
### Boolean values
When using a boolean value attribute within the custom ranking,
the ascending and descending setting uses the alphabetical value of the true or false text.
* Descending: true before false
* Ascending: false before true
### String and null or absent values
If a custom ranking attribute is missing from a record or has a null value,
that record is always ordered last, regardless of its ascending or descending setting.
Records with null or missing values are considered equal.
Strings are sorted after numbers and before null or absent values.
Strings are also compared by lexicographical order.
Algolia compares only the first 50 bytes of each string (50 characters for ASCII text).
If records have the same first 50 bytes for a custom ranking attribute,
Algolia compares the next custom ranking attribute.
# Precision of custom ranking metrics
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision
Improve your custom ranking by reducing granularity, so that similar records are ranked equally.
When you rank records with business metrics, you want to make sure the difference between those values is significant.
Sometimes, the difference is too small to be taken into account.
Yet, when the engine performs tie-breaking between two records,
it doesn't make a difference whether you have 1,000 or 1,003 more views on a blog article.
If one has more than the other, it goes first.
Therefore, **you want to make sure that the values you use for custom ranking are properly weighted to avoid false positives**.
To fix that issue, what you can do is **reduce precision**.
An example is when you have blog articles,
ranked per different attributes like unique page views and number of comments.
You may have two posts with a close number of unique page views (1,000 and 1,003).
Therefore, you don't want the engine to break the tie on this attribute.
Instead, you want to move on to the next criteria to get better tie-breaking and more relevant results.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
## Modifying the data: an example
### Before
In this example, you're adding search to a blog.
Besides Algolia's default ranking formula,
you've set two custom ranking attributes: `pageviews` first, followed by `comments`.
Here's what the dataset would look like:
```json JSON icon=braces theme={"system"}
[
{
"title": "Algolia + SeaUrchin.IO",
"author": "Nicolas Dessaigne",
"pageviews": 1023,
"comments": 23
},
{
"title": "Search Analytics: Gain Insights from User Search Data",
"author": "Nicolas Baissas",
"pageviews": 508,
"comments": 42
},
{
"title": "Algolia Vault – Bringing Physical & Digital Data Security to Search",
"author": "Liam Boogar",
"pageviews": 1022,
"comments": 54
}
]
```
And here's the setting:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
CustomRanking = new List { "desc(pageviews)", "desc(comments)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(pageviews)",
"desc(comments)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(pageviews)", "desc(comments)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(pageviews)", "desc(comments)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(pageviews)', 'desc(comments)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(customRanking = listOf("desc(pageviews)", "desc(comments)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(pageviews)',
'desc(comments)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(pageviews)",
"desc(comments)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(pageviews)", "desc(comments)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(pageviews)", "desc(comments)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(pageviews)", "desc(comments)"])
)
```
Because [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) applies its criteria sequentially,
`pageviews` has more weight than `comments`.
When you search for "Algolia", the "Algolia + SeaUrchin IO" article comes before "Algolia Vault" only because it has one more unique page view.
However, this additional view isn't meaningful.
It doesn't make the article more relevant.
Nonetheless, the engine ignores the next custom ranking attribute because of it.
For these two records, it would be better if the article with the most comments appeared first:
namely, that "Algolia Vault" article should come first because it has significantly more comments.
But changing the order of the attributes in the [`customRanking`](/doc/api-reference/api-parameters/customRanking) parameter isn't a good solution:
* It doesn't necessarily make sense business-wise (in this example, unique page views matter more than the number of comments),
* You may face the same issue with the number of comments, so this doesn't solve anything.
That's why we recommend **reducing precision to effectively use tie-breaking.**
### After
You can solve the issue by adding a `rounded_pageviews` attribute where you reduce the precision of `pageviews`,
and use it for custom ranking instead.
For example, round page views to the nearest hundred.
```json JSON icon=braces theme={"system"}
[
{
"title": "Algolia + SeaUrchin.IO",
"author": "Nicolas Dessaigne",
"pageviews": 1023,
"rounded_pageviews": 1000,
"comments": 23
},
{
"title": "Search Analytics: Gain Insights from User Search Data",
"author": "Nicolas Baissas",
"pageviews": 508,
"rounded_pageviews": 500,
"comments": 42
},
{
"title": "Algolia Vault – Bringing Physical & Digital Data Security to Search",
"author": "Liam Boogar",
"pageviews": 1022,
"rounded_pageviews": 1000,
"comments": 54
}
]
```
Then, you would use the `rounded_pageviews` attribute for custom ranking instead of `pageviews`.
Now, searching for "Algolia" would return the "Algolia Vault" article first, based on the `comments` attribute.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
CustomRanking = new List { "desc(rounded_pageviews)", "desc(comments)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(rounded_pageviews)",
"desc(comments)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(rounded_pageviews)", "desc(comments)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(rounded_pageviews)", "desc(comments)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(rounded_pageviews)', 'desc(comments)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(customRanking = listOf("desc(rounded_pageviews)", "desc(comments)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(rounded_pageviews)',
'desc(comments)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(rounded_pageviews)",
"desc(comments)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(rounded_pageviews)", "desc(comments)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(rounded_pageviews)", "desc(comments)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(rounded_pageviews)", "desc(comments)"])
)
```
Note that this doesn't remove `pageviews` from the dataset.
You may want to keep it for display purposes.
To know more on how to rank per custom attributes, see the [guide](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking).
# Custom ranking of results per category
Source: https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/custom-ranking-per-category
Adjust custom rankings per category.
When you set [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) attributes in Algolia,
they affect the whole (independent of categories).
But what if you want to use different custom rankings per category?
This guide shows how you can use duplicate in the Algolia index and use a to apply different custom rankings per category.
## Implementation steps
To use different custom rankings per category, you need to perform these tasks:
* **Data**: use duplicate records for each product, with extra attributes for "ranked category," and "rank within that category"
* **Index**: select the "rank within category" attribute for custom ranking and select the "ranked category" attribute for faceting. To handle duplicate search results, use a distinct identifier for each product
* **Frontend**: dynamically filter search results with or without category filters
## Prepare your data
**Duplicate records** let you use filters to rank products differently in different categories.
Each record has attributes for the category in which the product ranks and the rank within that category.
For example, assume you have an online clothing store.
One of your products is Sports shoes, and you want to rank these shoes higher as users search in narrower categories.
For each record, you add new attributes:
* `category`: a list with all category names for this product—ranked and not ranked. This is used for [faceting](/doc/guides/managing-results/refine-results/faceting) the search results. See [Creating a category data attribute](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages) for more information, and how to implement hierarchical categories.
* `ranked_category`: the name of the category for which the product ranks. To show a product also for searches without category filters, **one record per product must have `ranked_category: none`**.
* `category_rank`: the rank of the product in the category. Each duplicate of the product's record has one corresponding pair of `ranked_category` and `category_rank` attributes, which enables the per-category custom ranking.
* `parent_objectID`: a unique identifier for events. When [tracking click and conversion events](/doc/guides/sending-events) with the Insights API, you use this identifier to link all events from the duplicate records to the same product.
Conceptually, users search for sports shoes in three different ways:
* Search "Sports shoes" without any category filters
* Search "Sports shoes" in the category Shoes
* Search "Sports shoes" in the category Shoes > Sports
If you want to use different custom rankings per category, you need at least three records per product.
* **Main record for searches without categories and all other categories**.
This record matches when users search without category filters or in other categories,
where this product doesn't rank.
The textual and custom relevance settings for the index decide the position in the search results.
When tracking click and conversion events for this record, use the `objectID` attribute as the unique identifier.
```json JSON icon=braces theme={"system"}
{
"objectID": "492533",
"sku": "4597310",
"name": "Sports shoes",
"category": ["Shoes", "Shoes > Sports"],
"ranked_category": "none",
"sales_count": 852
}
```
* **Record for searches in the *Shoes* category**.
When users search in the *Shoes* category,
this product shows at the custom rank 20.
To mark this record as a duplicate of the first record,
the attribute `parent_objectID` has the same value as the `objectID` of the first record.
For tracking events, use the `parent_objectID` attribute as the unique identifier.
```json JSON icon=braces theme={"system"}
{
"objectID": "473828",
"parent_objectID": "492533",
"sku": "4597310",
"name": "Sports shoes",
"category": ["Shoes", "Shoes > Sports"],
"ranked_category": "Shoes",
"category_rank": 20,
"sales_count": 852
}
```
* **Record for searches in the *Shoes > Sports* category**.
When users search in the *Shoes* > *Sports* category,
this product shows at the top of the custom ranking.
To track events for this record as duplicates of the first record,
use the attribute `parent_objectID`.
It has the same value as the `objectID` attribute of the first record.
```json JSON icon=braces theme={"system"}
{
"objectID": "511621",
"parent_objectID": "492533",
"sku": "4597310",
"name": "Sports shoes",
"category": ["Shoes", "Shoes > Sports"],
"ranked_category": "Shoes > Sports",
"category_rank": 1,
"sales_count": 852
}
```
## Configure the index
To configure the Algolia index for category-based custom ranking, follow these steps:
1. Add the `category_rank` attribute to the top of the custom ranking criteria and sort the results by ascending values so that low values rank high.
In the [Algolia dashboard](https://dashboard.algolia.com/explorer/configuration/),
choose your index and select **Ranking and Sorting**.
2. Add `ranked_category` to the attributes for faceting.
Since you want to use this attribute just for filtering the search results,
use the [`filterOnly`](/doc/api-reference/api-parameters/attributesForFaceting#modifiers) modifier to discard the facet values and counts. This reduces the size of the index and speeds up the search. In the Dashboard, go to **Facets** and add the `ranked_category` attribute.
3. When searching with category filters, the search might return multiple records for the same product.
That's why you need to deduplicate the results.
In the dashboard, go to **Deduplication and Grouping** and set **Distinct** to **true**.
Select the `sku` attribute as **Attribute for Distinct**.
Using `distinct` is computationally intensive and can slow down the search.
You can configure [sorting attributes](/doc/guides/managing-results/refine-results/sorting) independently from the custom ranking attributes per category.
Both settings don't influence each other.
For more information, see:
* [Create custom ranking attributes](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking)
* [Declare attributes for faceting with the API](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting)
* [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct)
* [`distinct`](/doc/api-reference/api-parameters/distinct)
## Show custom ranking per category in your frontend
To take advantage of the per-category custom ranking in the frontend,
you need to filter the search results on the `ranked_category` attribute.
Depending on whether users search with or without categories, you need to filter the results dynamically:
* When users search **without categories**:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Query = "User search query",
FacetingAfterDistinct = true,
Filters = "ranked_category:none",
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "User search query",
facetingAfterDistinct: true,
filters: "ranked_category:none",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("User search query").SetFacetingAfterDistinct(true).SetFilters("ranked_category:none"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setQuery("User search query").setFacetingAfterDistinct(true).setFilters("ranked_category:none"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'User search query', facetingAfterDistinct: true, filters: 'ranked_category:none' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
query = "User search query",
facetingAfterDistinct = true,
filters = "ranked_category:none",
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'User search query',
'facetingAfterDistinct' => true,
'filters' => 'ranked_category:none',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "User search query",
"facetingAfterDistinct": True,
"filters": "ranked_category:none",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
query: "User search query",
faceting_after_distinct: true,
filters: "ranked_category:none"
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("User search query"),
facetingAfterDistinct = Some(true),
filters = Some("ranked_category:none")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "User search query",
filters: "ranked_category:none",
facetingAfterDistinct: true
))
)
```
For searches without categories, the results show records with the `ranked_category: none` attribute.
* When users search **with categories**, there are two possibilities:
the product ranks for the current category, or it doesn't (but it might rank for a different category):
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Query = "User search query",
FacetingAfterDistinct = true,
Filters =
"category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "User search query",
facetingAfterDistinct: true,
filters:
"category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetQuery("User search query").
SetFacetingAfterDistinct(true).
SetFilters("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)"),
)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject()
.setQuery("User search query")
.setFacetingAfterDistinct(true)
.setFilters("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR" + " ranked_category:none)"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: {
query: 'User search query',
facetingAfterDistinct: true,
filters: 'category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)',
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
query = "User search query",
facetingAfterDistinct = true,
filters =
"category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'User search query',
'facetingAfterDistinct' => true,
'filters' => 'category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "User search query",
"facetingAfterDistinct": True,
"filters": "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
query: "User search query",
faceting_after_distinct: true,
filters: "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)"
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("User search query"),
facetingAfterDistinct = Some(true),
filters =
Some("category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "User search query",
filters: "category:{{currentCategory}} AND (ranked_category:{{currentCategory}} OR ranked_category:none)",
facetingAfterDistinct: true
))
)
```
If a product ranks for the current category, the search would return both records.
For example, if users search in the category Shoes, these records match the filter:
* `category: Shoes AND ranked_category: Shoes`
* `category: Shoes AND ranked_category: none`
Since you use `distinct` for this index, results only show the record with `ranked_category: Shoes`.
If users search in a different category, for example,
the category *Accessories*, the results show the record matching `category: Accessories and ranked_category: none`.
These rules decide the ranking between records with and without ranked categories:
1. Both records are the same except for the `ranked_category` and `category_rank` attributes.
Since they have the same textual relevance,
Algolia ranks them by their custom ranking attributes.
2. The first custom ranking attribute is `category_rank`.
As the record with `ranked_category:none` doesn't have a `category_rank` attribute,
it ranks lower than a record with a value for this attribute.
3. With `distinct(true)`, the search returns the first record from a list of identical records.
Because you set `attributeForDistinct` to `sku`, all products with the same `sku` attribute are identical.
By default, faceting is applied before the deduplication.
To ensure correct facet counts,
deduplicate and then set [`facetingAfterDistinct`](/doc/api-reference/api-parameters/facetingAfterDistinct) to `true`.
You have to apply this setting at query time.
# Searchable attributes
Source: https://www.algolia.com/doc/guides/managing-results/must-do/searchable-attributes
How to decide which searchable attributes you want to use
When setting up and configuring your index, decide which [attributes you want to use for searching](/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records).
When you create an index, **all attributes in all your records are searchable by default**.
This means you can perform searches straight away without configuring anything.
However, to enhance search relevance and exclude unnecessary results,
specify which attributes should be searchable.
* An example of a useful searchable attribute is the name of a product.
* An example of an attribute that isn't useful for searching is a URL.
Since users seldom search for URLs, exclude them from the searchable attributes list.
However, you can still use image URLs to display images in the results.
For example, on a recipes website it makes sense to make `title`, `ingredients`, and `comments` searchable, and exclude `images`.
You can still use non-searchable attributes for display or filtering.
## Attributes to include
The following attributes are commonly included (searchable):
* Descriptive attributes: name, description, brand, list of actors in a film, list of features in a computer.
* Filters: color, brand, size.
* Keywords and tags.
* Valuable data like telephone numbers, SKU, ISBN, and other identifying attributes.
## Attributes to exclude
These are often excluded (not searchable):
* For display only: image and other URLs, price, brand logos, SKU.
* Short descriptions are meant to be displayed, not searched since they usually summarize what's in a longer description.
* For information only: not yet available, just released, on promotion.
* For ranking only: most sales, most viewed, highest rated.
## Set searchable attributes
Specify searchable attributes from the dashboard or API.
You can only apply search restrictions to the first searchable attributes and searchable facets in your index, combined.
Algolia ignores attributes beyond this limit:
queries can't match them and [`restrictSearchableAttributes`](/doc/api-reference/api-parameters/restrictSearchableAttributes) doesn't affect them.
### Set searchable attributes in the Algolia dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Configuration** tab, select **Searchable attributes**.
5. Click **Add a Searchable Attribute** and type or select an attribute from the list.
6. Change the [order of attributes](/doc/guides/managing-results/must-do/searchable-attributes/how-to/configuring-searchable-attributes-the-right-way#attribute-order) by dragging them up or down in the **Searchable attributes** list.
To make two attributes equally important, when you add a new attribute,
type the attributes directly as a comma-separated list—for example, `title, comments`.
7. Save your changes.
### Set searchable attributes with the API
To make certain attributes searchable,
include them in the [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) setting when configuring your index with the [`setSettings`](/doc/libraries/sdk/v1/methods/set-settings) method.
To configure [the order of searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes/how-to/configuring-searchable-attributes-the-right-way#attribute-order):
* The higher in the list you place an attribute, the higher matches within that attribute will rank in search results.
* If you want two attributes to have the same priority, include them on the same level, separated by a comma.
In this example, matches in the title and comments attributes rank the same but also rank before matches in the ingredients attribute.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "title,comments", "ingredients" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"title,comments",
"ingredients",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"title,comments", "ingredients"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setSearchableAttributes(Arrays.asList("title,comments", "ingredients"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { searchableAttributes: ['title,comments', 'ingredients'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(searchableAttributes = listOf("title,comments", "ingredients")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'title,comments',
'ingredients',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"title,comments",
"ingredients",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(searchable_attributes: ["title,comments", "ingredients"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("title,comments", "ingredients"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(searchableAttributes: ["title,comments", "ingredients"])
)
```
## See also
* [Configuring searchable attributes the right way](/doc/guides/managing-results/must-do/searchable-attributes/how-to/configuring-searchable-attributes-the-right-way)
* [Records, attributes, and searchable attributes](/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records)
* [User-restricted access to data](/doc/guides/security/api-keys/how-to/user-restricted-access-to-data)
* [Fuzzy search and fuzzy matching (blog)](https://www.algolia.com/blog/engineering/discover-what-fuzzy-search-is-with-fuzzy-matching/)
* [How search queries are processed (blog)](https://www.algolia.com/blog/product/what-is-a-search-query-and-how-is-it-processed-by-a-search-engine/)
# Configure searchable attributes the right way
Source: https://www.algolia.com/doc/guides/managing-results/must-do/searchable-attributes/how-to/configuring-searchable-attributes-the-right-way
Learn how to select, prioritize, and set up searchable attributes for your Algolia index.
Setting searchable attributes is **one of the most important aspects of configuring your search** and one of the first steps you need to take.
It goes much further than telling Algolia which attributes to search for, and it also defines which ones to look at first and what strategy to adopt.
A small change in setting searchable attributes could lead to entirely different results.
For that reason, it's crucial to understand how it works.
Besides deciding [which attributes Algolia should search](/doc/guides/managing-results/must-do/searchable-attributes), there are two aspects you can control with searchable attributes:
* The [relative importance](#attribute-order) of each attribute
* Whether the [position of the match](#word-position) in the attribute matters
## Attribute order
While setting searchable attributes, you can order them, making some more relevant than others.
Knowing how to order your searchable attributes can be challenging when you have several.
One method is to **compare them in pairs**, each attribute with the previous one, and move them around accordingly.
This is similar to [insertion sorting](https://wikipedia.org/wiki/Insertion_sort).
For example, suppose you have the following attributes in the of a "movies" :
* `director`
* `cast`
* `title`
* `genres`
* `plot_summary`
1. Compare the first two: `director` and `cast`.
If you think `cast` should come before `director` (for example, you want to see movies with Clint Eastwood before movies directed by Clint Eastwood), move `cast` to the first place.
2. Compare `director` and `title` and decide that the movie's name is more important than the director's.
That means you'd move `title` to the second place and compare it with the previous one (`cast`).
3. Prioritize a match on an actor's name or a movie title.
If all records in your index represent movies,
it may make sense to prioritize `title`,
so you'd move it first.
Then, because you don't have any more attributes to compare `title` to, you'd move on to `genres`.
The benefit is that **you're making a thorough, granular comparison of each attribute with the others** and considering your use case for every pair.
This method is much more reliable than doing a global, intuitive sort by what seems more important.
### Searchable attributes with different priorities
**The order of searchable attributes directly affects search relevance.**
Attributes higher in the list are more relevant than attributes further down.
Therefore, set attributes higher in the searchable attributes list when their content is more relevant to your users.
Consider a movie database website where users can search movies by title, actors, and director. You may be tempted to prioritize actors before directors Yet, consider the following dataset:
```json JSON icon=braces theme={"system"}
[
{
"title": "Mystic River",
"director": "Clint Eastwood",
"actors": ["Sean Penn", "Tim Robbins", "Kevin Bacon", "Laurence Fishburne"]
},
{
"title": "In the Line of Fire",
"director": "Wolfgang Petersen",
"actors": ["Clint Eastwood", "John Malkovich", "Renee Russo"]
},
{
"title": "Invictus",
"director": "Clint Eastwood",
"actors": ["Morgan Freeman", "Matt Damon"]
},
{
"title": "Million Dollar Baby",
"director": "Clint Eastwood",
"actors": ["Clint Eastwood", "Hilary Swank", "Morgan Freeman"]
}
]
```
For a movie search, setting the `title` as the first searchable attribute makes sense.
But what if someone searched for "Clint Eastwood"?
Would they primarily be looking for movies with Clint Eastwood, by Clint Eastwood, or both?
In other words, how should you rank the `director` and `actors` attributes?
By putting `director` first, movies that Clint Eastwood directed would come before those where he appeared as an actor.
This may or may not be desirable.
For example, if a user types "jane", do you want to first display movies with director Jane Campion or actress Jane Fonda?
There's no one-size-fits-all approach, and the strategy depends on your use case.
**Defining a priority order in your searchable attributes isn't trivial**.
It can vary depending on your data, how it's structured, what your users search for, and what they expect as results.
### Searchable attributes with the same priority
Sometimes, setting an attribute before or after another doesn't make sense because you want them to be considered the same.
Here's another dataset, this time with records representing movies, actors, and directors:
```json JSON icon=braces theme={"system"}
[
{
"movie_title": "John Wick",
"cast": ["Keanu Reeves", "Michael Nyqvist", "Alfie Allen"]
},
{
"actor_name": "John Cleese",
"filmography": [
"Monty Python and the Holy Grail",
"Life of Brian",
"Harry Potter and the Philosopher's Stone"
]
},
{
"director_name": "John Carpenter",
"filmography": ["Halloween", "The Thing", "Escape from New York"]
}
]
```
If someone typed `John`, it wouldn't necessarily make more sense to show movies with *John* in the title than actors or directors named John.
Instead, you may want to rely on other criteria, like popularity, to decide what comes first.
In this case, it makes sense to set `movie_title`, `actor_name`, and `director_name` at the same level.
This way, if someone types `John`, the engine considers all three records equal in the [attribute ranking criterion](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria#attribute), and the engine would go to the next [ranking criterion](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) to try and break the tie.
Attributes with the same priority are always [unordered](#word-position).
## Word position
Searchable attributes can be ordered or unordered:
* **ordered** means that the engine considers matches at the beginning of an attribute more important than matches in the middle or the end. When an attribute is ordered, the earlier a match occurs, the higher the engine ranks it.
* **unordered** means that the position of the match within the attribute doesn't affect ranking.
Usually, set the attribute to be unordered since the position of the matching word in the attribute often doesn't matter.
Consider the following dataset:
```json JSON icon=braces theme={"system"}
[
{
"title": "Avengers: Infinity War",
"cast": [
"Robert Downey Jr.",
"Chris Hemsworth",
"Mark Ruffalo",
"Chris Evans",
"Scarlett Johansson"
]
},
{
"title": "World War Z",
"cast": [
"Brad Pitt",
"Mireille Enos",
"Daniella Kertesz",
"James Badge Dale",
"Ludi Boeken"
]
},
{
"title": "War of the Worlds",
"cast": [
"Tom Cruise",
"Dakota Fanning",
"Justin Chatwin",
"Miranda Otto",
"Tim Robbins"
]
},
{
"title": "Lost in Translation",
"cast": ["Scarlett Johansson", "Bill Murray", "Akiko Takeshita"]
},
{
"title": "North",
"cast": [
"Elijah Wood",
"Jason Alexander",
"Julia Louis-Dreyfus",
"Marc Shaiman",
"Scarlett Johansson"
]
}
]
```
If users type "war," it wouldn't make sense to return "World War Z" before "Avengers: Infinity War" because the string happens to appear earlier in the title.
The ranking should rely on other criteria like popularity or reviews, so it makes sense to set `title` as `unordered`.
Things are different with the `cast` attribute.
If users type "Scarlett Johansson," chances are they would be more interested in movies where Scarlett Johansson has a leading role than movies where she played small parts.
Assuming that the order of actors and actresses in the `cast` attribute reflects role size, keeping `cast` ordered makes more sense.
### Set unordered or ordered with the API
You can set attributes as unordered or ordered with [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) when indexing.
If unordered isn't set for an attribute, it will default to ordered when set by the API.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "unordered(title)", "cast" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"unordered(title)",
"cast",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"unordered(title)", "cast"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setSearchableAttributes(Arrays.asList("unordered(title)", "cast"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { searchableAttributes: ['unordered(title)', 'cast'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(searchableAttributes = listOf("unordered(title)", "cast")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'unordered(title)',
'cast',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"unordered(title)",
"cast",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(searchable_attributes: ["unordered(title)", "cast"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("unordered(title)", "cast"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(searchableAttributes: ["unordered(title)", "cast"])
)
```
### Set unordered or ordered from the dashboard
1. Select the **Search** product icon on your dashboard.
2. On the [**Index** page](https://dashboard.algolia.com/explorer/browse), select your index.
3. Click the **Configuration** tab.
4. Go to the **Searchable Attributes** section.
For each searchable attribute, consider whether it should be [ordered or unordered](#word-position).
5. Save your changes.
When using the dashboard, the UI defaults to setting your attribute as unordered.
## Successful strategies
Setting searchable attributes is more an art than a science and highly depends on your use case.
There's no magic formula that works for everyone.
However, the following tips will get you started.
### Keep searchable attributes to a minimum
As a rule of thumb, **the more searchable attributes, the noisier the search**.
It may be tempting to add searchable attributes to maximize matches.
Yet, this reduces search relevance by creating *noise* (littering good results with not-so-good ones).
Instead, be conservative with your searchable attributes and **focus on what your users tend to search for**.
Some attributes, like image URLs or review scores, shouldn't be searchable because they don't have textual value.
Some other attributes, like the plot summary of a movie, might look interesting but could be unnecessary.
How often do you find a movie by searching for the synopsis?
Probably far less often than with the title or lead actors.
The same advice goes for the length of the attribute values.
Suppose you have a "movies" index where the records have an `actors` attribute.
If this attribute has the entire cast, it's less efficient than if it only had the leading roles.
Most users would probably look at leading roles when searching for an actor.
### Choose the appropriate attribute order
Knowing how to order your searchable attributes can be challenging when you have several.
One method is to **compare them in pairs**, each attribute with the previous one, and move them around accordingly.
The process is like [insertion sorting](https://wikipedia.org/wiki/Insertion_sort).
For example, suppose you have the following attributes in the records of a "movies" index:
* `director`
* `cast`
* `title`
* `genres`
* `plot_summary`
You would:
1. Compare the first two: `director` and `cast`. If you think `cast` should come before `director` (for example, you want to see movies with Clint Eastwood before movies directed by Clint Eastwood), move `cast` to the first place.
2. Compare `director` and `title` and decide that the movie's name is more important than the director's. That means you'd move `title` to the second place and compare it with the previous one (`cast`).
3. Prioritize a match on an actor's name or a movie title. If all records in your index represent movies, it may make sense to prioritize `title`, so you'd move it first. Then, because you don't have any more attributes to compare `title` to, you'd move on to `genres`.
The benefit is that **you're making a thorough, granular comparison of each attribute with the others** and considering your use case for every pair.
This method is much more reliable than doing a global, intuitive sort by what seems more important.
### Where to put filters?
Sometimes it's better to prioritize filters over all other attributes.
This might not seem intuitive at first, as you may think users tend to search by attributes like `name` or `title`.
Suppose you have a "movies" index where the records have a `genre` attribute with values like "crime" and "action".
Films fall into these categories, but there are also films whose titles and descriptions use these exact words,
for example, "Crime Doesn't Pay", "Last Action Hero".
In that case, you can decide that when a user searches for words that are more like genres,
it's best to search attributes before the title.
This is guaranteed to return all crime movies whenever someone types in `crime`, regardless of the title.
Making this decision when setting up searchable attributes can affect your results.
### Change your searchable attributes strategy
Be careful before changing your searchable attributes strategy because this affects your entire index.
Don't make decisions based on a single .
Instead, experiment with several queries close to what your users search for (use [Algolia analytics](/doc/guides/search-analytics/overview) to explore their search habits).
## See also
* [Fuzzy search and fuzzy matching (blog)](https://www.algolia.com/blog/engineering/discover-what-fuzzy-search-is-with-fuzzy-matching/)
# Faceting
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/faceting
Algolia uses facets for categorizing and grouping your data. Users can use facets to refine their search.
Facets let you create **categories on a select group of attributes so that users can refine their search**.
For example, on an of books, helpful facets might be author and genre.
Algolia also calculates results for each facet.
It lets you display facets and facet counts so that users can filter results.
## What's a facet?
Think of facets as a set of filterable categories that allow users to refine results by multiple categories simultaneously.
Algolia derives these facets from attributes.
* For a book index, facets might be `author` or `genre`.
* For a luxury shop, facets might be `brand` or `designer`.
Algolia's faceting features let you:
* List all possible values for the selected attributes and return contextual values and counts with each search result.
* Compute a facet count or the number of matching each attribute's value
* Enable search within an attribute's values ([search for facet values](#search-for-facet-values)).
## Difference between filters and facets
Filters and facets often get mixed up because there's much overlap, but [understanding the difference is essential](https://www.algolia.com/blog/ux/filters-vs-facets-in-site-search/).
Both help to restrict a search to a subset of results.
* **Filters provide basic options to help narrow down search results.** Sometimes users don't see the filter restrictions, for example, when automatically filtering the records they're allowed to see based on their user ID.
* **Facets help users refine their search using several categories simultaneously.** Faceting is still filtering but displays facets to allow users to choose from a set of useful values, features, and counts.
## How to configure faceting
You can configure faceting from the dashboard or the API, but the overall process is the same:
1. [Declare attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting)
2. Retrieve facets from the API (see the following section).
## Retrieve facets
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To retrieve facets and their respective counts as part of the JSON response,
specify a list of facets in the [`facets`](/doc/api-reference/api-parameters/facets) parameter at query time.
For example, retrieve your books' facets by specifying the following list:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Facets = new List { "author", "genre" },
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
facets: [
"author",
"genre",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFacets(
[]string{"author", "genre"}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFacets(Arrays.asList("author", "genre")),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { facets: ['author', 'genre'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(facets = listOf("author", "genre")),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['facets' => [
'author',
'genre',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"facets": [
"author",
"genre",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(facets: ["author", "genre"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
facets = Some(Seq("author", "genre"))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(facets: [
"author",
"genre",
]))
)
```
To extract all facet information, use the wildcard character (`*`).
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Facets = new List { "*" } })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
facets: [
"*",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFacets(
[]string{"*"}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFacets(Arrays.asList("*")),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({ indexName: 'indexName', searchParams: { facets: ['*'] } });
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(facets = listOf("*")),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['facets' => [
'*',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"facets": [
"*",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index("INDEX_NAME", Algolia::Search::SearchParamsObject.new(facets: ["*"]))
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
facets = Some(Seq("*"))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(facets: ["*"]))
)
```
## Faceting types and features
### Conjunctive and disjunctive faceting
Filtering based on facet values is helpful for users since it allows them to find specific,
targeted results in a way that isn't possible with one-size-fits-all filters.
You can combine facet filters with `AND` (conjunctive) and `OR` (disjunctive) operators.
When using these operators, the engine handles the facet counts differently to keep a consistent user experience.
This doesn't happen at the API level but in the [InstantSearch libraries](/doc/guides/building-search-ui/what-is-instantsearch/js).
### Hierarchical facets
You can build a hierarchy in your facet values to enable multi-level navigation and filtering.
This pattern makes deeply nested categories more discoverable,
as your users can browse up and down in the levels to refine their search.
For example, consider a book available in **Books > Science Fiction > Time Travel**:
```json JSON icon=braces theme={"system"}
{
"categories": {
"lvl0": "Books",
"lvl1": "Books > Science Fiction",
"lvl2": "Books > Science Fiction > Time Travel"
}
}
```
Or a book available in both **Books > Science Fiction > Time Travel** and **Books > Literature and Fiction > Modernism**:
```json JSON icon=braces theme={"system"}
{
"categories": {
"lvl0": "Books",
"lvl1": ["Books > Science Fiction", "Books > Literature and Fiction"],
"lvl2": [
"Books > Science Fiction > Time Travel",
"Books > Literature and Fiction > Modernism "
]
}
}
```
The [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js) widget from the [InstantSearch libraries](/doc/guides/building-search-ui/what-is-instantsearch/js) helps you to build hierarchical faceting.
Disjunctive facets with hierarchical facets aren't possible.
Hierarchical facets with many nested attributes have many different facet names.
This adds a lot of metadata that needs to be processed.
This can lead to poor search performance, even if you don't reference these facets directly.
### Contextual facet values and counts
Enabling faceting on attributes computes facet counts for each facet value, and the engine updates and returns the list of values and counts with each search result.
It's helpful to offer users a relevant and contextual set of filters and information.
### How the engine approximates facet counts
When dealing with massive indices, the engine might need to approximate facet counts to guarantee optimal performance.
To see if the facet counts are exact, check the `exhaustiveFacetsCount` property in the JSON response.
### Increase the default limit
By default, the engine lets you retrieve 100 values per facet.
If you need to increase this limit, use the [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet) parameter.
The maximum is 1,000 facet values at a time.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { MaxValuesPerFacet = 1000 }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
maxValuesPerFacet: 1000,
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetMaxValuesPerFacet(1000)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setMaxValuesPerFacet(1000));
```
```js JavaScript theme={"system"}
const response = await client.setSettings({ indexName: 'theIndexName', indexSettings: { maxValuesPerFacet: 1000 } });
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(maxValuesPerFacet = 1000),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['maxValuesPerFacet' => 1000,
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"maxValuesPerFacet": 1000,
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(max_values_per_facet: 1000))
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
maxValuesPerFacet = Some(1000)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(maxValuesPerFacet: 1000)
)
```
### Faceting on object ID
You can't facet on the `objectID` attribute, and the engine ignores it if you declare it in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting).
Faceting on a unique identifier makes little sense, though, because every facet count would be 1.
The engine treats the `objectID` attribute as a filter-only facet.
You can filter on that attribute (even without declaring it in `attributesForFaceting` but not get any facet counts.
### Attributes are case-sensitive
The attribute names you declare in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting) are case-sensitive: **they must exactly match the attribute names in your records**.
The [`facetFilters`](/doc/api-reference/api-parameters/facetFilters) and [`filters`](/doc/api-reference/api-parameters/filters) parameters are only case-insensitive for values.
For example, if your records include an attribute named `color`, with values such as "blue", "red", and "green", you must declare `color` (not `Color`) within your [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting).
However, you can facet and filter using `color:BLUE`, `color:Blue`, or `color:blue`.
As an exception, **[`facetFilters`](/doc/api-reference/api-parameters/facetFilters) and [`filters`](/doc/api-reference/api-parameters/filters) are case-sensitive when filtering on `objectID`**.
For example, `objectID:abc` only returns the record with that specific `objectID`, not one with `objectID:ABC`.
## Search for facet values
Sometimes you may have thousands of different values for a given facet attribute,
and it would be impossible to display them all in your user interface (see the [facet display](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js) guide for more information).
Instead, use [`search_for_facet_values`](/doc/libraries/sdk/v1/methods/search-for-facet-values) to let users search within a specific faceted attribute (for example, `brands`, `authors`, or `categories`) without needing to create a separate index.
It means you can still display the first, most common values for each facet but also let users search for more.
By default, returned facet values are sorted by count.
Facet queries only match [prefixes](/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching), [typos](/doc/guides/managing-results/optimize-search-results/typo-tolerance), and [exact](/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings).
### Search for facet values
In this example, Algolia returns all the authors matching the query "stephen".
```cs C# theme={"system"}
var response = await client.SearchForFacetValuesAsync(
"INDEX_NAME",
"author",
new SearchForFacetValuesRequest { FacetQuery = "stephen" }
);
```
```dart Dart theme={"system"}
final response = await client.searchForFacetValues(
indexName: "INDEX_NAME",
facetName: "author",
searchForFacetValuesRequest: SearchForFacetValuesRequest(
facetQuery: "stephen",
),
);
```
```go Go theme={"system"}
response, err := client.SearchForFacetValues(client.NewApiSearchForFacetValuesRequest(
"INDEX_NAME", "author").WithSearchForFacetValuesRequest(
search.NewEmptySearchForFacetValuesRequest().SetFacetQuery("stephen")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchForFacetValuesResponse response = client.searchForFacetValues(
"INDEX_NAME",
"author",
new SearchForFacetValuesRequest().setFacetQuery("stephen")
);
```
```js JavaScript theme={"system"}
const response = await client.searchForFacetValues({
indexName: 'indexName',
facetName: 'author',
searchForFacetValuesRequest: { facetQuery: 'stephen' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchForFacetValues(
indexName = "INDEX_NAME",
facetName = "author",
searchForFacetValuesRequest = SearchForFacetValuesRequest(facetQuery = "stephen"),
)
```
```php PHP theme={"system"}
$response = $client->searchForFacetValues(
'INDEX_NAME',
'author',
['facetQuery' => 'stephen',
],
);
```
```python Python theme={"system"}
response = client.search_for_facet_values(
index_name="INDEX_NAME",
facet_name="author",
search_for_facet_values_request={
"facetQuery": "stephen",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_for_facet_values(
"INDEX_NAME",
"author",
Algolia::Search::SearchForFacetValuesRequest.new(facet_query: "stephen")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchForFacetValues(
indexName = "INDEX_NAME",
facetName = "author",
searchForFacetValuesRequest = Some(
SearchForFacetValuesRequest(
facetQuery = Some("stephen")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.searchForFacetValues(
indexName: "INDEX_NAME",
facetName: "author",
searchForFacetValuesRequest: SearchForFacetValuesRequest(facetQuery: "stephen")
)
```
### Increase the default limit
By default, you retrieve ten facet values at a time.
You can raise this limit up to 100 by updating the [`maxFacetHits`](/doc/api-reference/api-parameters/maxFacetHits) parameter.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { MaxFacetHits = 100 }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
maxFacetHits: 100,
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetMaxFacetHits(100)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setMaxFacetHits(100));
```
```js JavaScript theme={"system"}
const response = await client.setSettings({ indexName: 'theIndexName', indexSettings: { maxFacetHits: 100 } });
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(maxFacetHits = 100),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['maxFacetHits' => 100,
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"maxFacetHits": 100,
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(max_facet_hits: 100))
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
maxFacetHits = Some(100)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(maxFacetHits: 100)
)
```
# How to declare attributes for faceting with the API
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting
How to declare attributes for faceting with the API.
Algolia lets you create categories based on specific attributes so users can filter search results by those categories.
For example, if you have an of books, you could categorize them by author and genre.
This allows users to filter search results by their favorite author or discover new genres.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To enable this categorization,
declare the attributes `genre` and `author` as [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting):
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesForFaceting = new List { "genre", "author" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"genre",
"author",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"genre", "author"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("genre", "author"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['genre', 'author'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("genre", "author")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'genre',
'author',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"genre",
"author",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["genre", "author"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("genre", "author"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["genre", "author"])
)
```
Sometimes, you may have many facet values.
For instance, if you have a book index, you may also have a lot of different authors.
[The engine can't return more than values per facet](/doc/api-reference/api-parameters/maxValuesPerFacet),
so if you have more than that, you may want to let your users search for them.
Do this by using the `searchable` modifier.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesForFaceting = new List { "genre", "searchable(author)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"genre",
"searchable(author)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"genre", "searchable(author)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("genre", "searchable(author)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['genre', 'searchable(author)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("genre", "searchable(author)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'genre',
'searchable(author)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"genre",
"searchable(author)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["genre", "searchable(author)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("genre", "searchable(author)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["genre", "searchable(author)"])
)
```
If you only need the filtering feature, use `filterOnly` to reduce the index size and improve search speed.
For example, you could automatically filter what genre the users can search based on the section of your website they're on (without displaying genre as a clickable filter).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesForFaceting = new List { "filterOnly(genre)", "author" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"filterOnly(genre)",
"author",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"filterOnly(genre)", "author"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("filterOnly(genre)", "author"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['filterOnly(genre)', 'author'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("filterOnly(genre)", "author")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'filterOnly(genre)',
'author',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"filterOnly(genre)",
"author",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["filterOnly(genre)", "author"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("filterOnly(genre)", "author"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["filterOnly(genre)", "author"])
)
```
**Don't include colons (`:`) in attribute names that you want to use for faceting because the [`filters`](/doc/api-reference/api-parameters/filters) syntax relies on that character as a delimiter.**
## See also
* [How to declare attributes for faceting in the dashboard](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
* [Auto-selected facets](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/auto-selected-facets)
* [Guided search](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/guided-search)
* [Visual facets](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/visual-facets)
# How to declare attributes for faceting in the dashboard
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard
How to declare attributes for faceting in the Algolia dashboard.
Algolia lets you create categories based on specific attributes so users can filter search results by those categories
For example, if you have an index of books, you could categorize them by author and genre.
This allows users to filter search results by their favorite author or discover new genres.
To enable this categorization, declare your attributes for faceting.
To do this:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Configuration** tab, click **Facets** (in the Filtering and faceting section).
5. In **Attributes for faceting**, click **Add an Attribute**
6. Set the attribute as:
* **searchable** if you want to use the attribute for both searching and filtering. For example, if a product has a `color` attribute, users can search for products of a specific color and also filter search results by color.
* **filter-only** if you want to use the attribute only for filtering.
* **not searchable** if you want the attribute to be used only as a facet.
For more information, see [Difference between facets and filters](/doc/guides/managing-results/refine-results/faceting#difference-between-filters-and-facets)
7. Click **Review and Save Settings**.
## See also
* [How to declare attributes for faceting with the API](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting)
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Auto-selected facets](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/auto-selected-facets)
* [Guided search](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/guided-search)
* [Visual facets](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/visual-facets)
# Filtering
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering
How does filtering work with Algolia? Learn how Algolia enables filtered searches.
Filtering allows your users to drill down and create a smaller, more manageable set of results based on meaningful categories.
It gives them direct control over the they query.
Categories can be brand, color, or genre of film.
They can be dress or shoe sizes or price ranges like "expensive" or "cheap".
The most common use for filtering is to provide a UI that enables users to refine their search using one or more clickable items.
## How to filter your data
1. [Define filterable attributes (at indexing time)](#define-filterable-attributes)
2. [Filter by attributes (at query time)](#filter-by-attributes)
Most [records have one or more attributes](/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records), such as color, brand, genre, or category, that you can choose as a filter.
A filter attribute can be a list of single words, more complex phrases, numbers, boolean values, dates, or arrays.
Arrays of objects need more care, because filters don't preserve which values belong to the same element: see [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects).
Users may use any filter combination to group records.
Filtering creates more targeted and therefore relevant subsets of results, and increases the chance that only the best results appear at the top.
### Define filterable attributes
First, define filter attributes as attributes for faceting.
You can do this on the [dashboard](https://www.algolia.com/ecommerce-merchandising-playbook/configuring-faceting/) or with an [API client](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
If you only want to use an attribute for filtering, use the [`filterOnly`](/doc/api-reference/api-parameters/attributesForFaceting#param-filter-only) modifier.
This reduces the size and improves search speed.
For example, if you plan to filter on an attribute, such as `color`, but don't need to calculate or display the number of items with `color:green`, `color:blue`, or similar, use `filterOnly`.
If you use [`filters`](/doc/api-reference/api-parameters/filters) or [`numericFilters`](/doc/api-reference/api-parameters/numericFilters) to filter on **numeric** attributes, you don't need to set these attributes as `attributesForFaceting`.
### Filter by attributes
* **[Filter by string](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-string)**
* **[Filter by numeric value](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-numeric-value)**
* **[Filter by date](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-date)**
* **[Filter by array](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-array)**
* **[Filter by tags](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-tags)**
* **[Filter by boolean](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-boolean)**
* **[Filter by null or missing attributes](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-null-or-missing-attributes)**
* **[Filter by `objectID`](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-objectid)**
## How to apply filtering to the search UI
After defining [filterable attributes](#define-filterable-attributes) and ensuring users can filter every record [with the appropriate attributes](#filter-by-attributes), ensure your frontend represents all these possibilities by using InstantSearch's [refinements](/doc/api-reference/widgets/refinement-list/js).
You'll typically want to display all filtering categories.
Thus, if you have created two filters, `brand` and `price_range`, you'll always want to display a list of brands and a price range slider for every query in your UI.
For example, a search for "phones" returns brands like Apple and Samsung, while a search for "speakers" returns Panasonic and Bose.
While the values may change, the `brand` filter is always present.
To learn more, see:
* [Facet drop down](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-dropdown/js)
* [Guided search](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/guided-search)
* [UI design: filter and sort panel](/doc/guides/building-search-ui/resources/ui-kit/js#filter-and-sort-panel)
## Other uses for filtering
Although the most common use for filtering is to enable users to refine their search and limit the records they search within,
there are other ways to use filters. For example:
* [Security-filtering](/doc/guides/security/api-keys/how-to/user-restricted-access-to-data): using filters as a security measure, where each user has access to only their private data.
* [Geo-filtering](/doc/guides/managing-results/refine-results/geolocation#geographical-filtering-and-sorting): filtering on user's geolocation (if your index has geographical data).
* [Filter scoring](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring): using filters to help promote some records over others.
* [Query-based filtering](/doc/guides/managing-results/rules/detecting-intent): setting rules that parse your users' queries and use specific query terms as filters instead of search terms.
For example, if a user types in "red", "red" can be used as a filter instead of a search term, thereby returning all red records.
# Filter by attributes
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes
How to use Algolia to filter content
Filters can work with strings, numbers, boolean values, dates, tags, or lists.
[Add filters to your UI](/doc/guides/managing-results/refine-results/filtering#how-to-apply-filtering-to-the-search-ui) with InstantSearch widgets or your own custom code.
## Filter by string
[Example dataset](#example-dataset)
Algolia lets you filter search results by string attributes.
For example, users of an online bookstore might want to search for books from different publishers.
With [filters](/doc/guides/managing-results/refine-results/filtering), they can refine results by:
* Showing books published by Penguin
* Showing books published by Bloomsbury or Scribe
* Showing all books except those published by Penguin
### Apply a string filter
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
Set `author` as an [attribute for faceting](/doc/guides/managing-results/refine-results/filtering#define-filterable-attributes), then apply your string filters with the [`filters`](/doc/api-reference/api-parameters/filters) parameter in your [`search`](/doc/libraries/sdk/v1/methods/search) code:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
FacetFilters = new FacetFilters(
new List
{
new FacetFilters("publisher:Penguin"),
new FacetFilters(
new List
{
new FacetFilters("author:Stephen King"),
new FacetFilters("genre:Horror"),
}
),
}
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
facetFilters: [
"publisher:Penguin",
[
"author:Stephen King",
"genre:Horror",
],
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFacetFilters(search.ArrayOfFacetFiltersAsFacetFilters(
[]search.FacetFilters{*search.StringAsFacetFilters("publisher:Penguin"), *search.ArrayOfFacetFiltersAsFacetFilters(
[]search.FacetFilters{*search.StringAsFacetFilters("author:Stephen King"), *search.StringAsFacetFilters("genre:Horror")})})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFacetFilters(
FacetFilters.of(
Arrays.asList(
FacetFilters.of("publisher:Penguin"),
FacetFilters.of(Arrays.asList(FacetFilters.of("author:Stephen King"), FacetFilters.of("genre:Horror")))
)
)
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { facetFilters: ['publisher:Penguin', ['author:Stephen King', 'genre:Horror']] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
facetFilters =
FacetFilters.of(
listOf(
FacetFilters.of("publisher:Penguin"),
FacetFilters.of(
listOf(FacetFilters.of("author:Stephen King"), FacetFilters.of("genre:Horror"))
),
)
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['facetFilters' => [
'publisher:Penguin',
[
'author:Stephen King',
'genre:Horror',
],
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"facetFilters": [
"publisher:Penguin",
[
"author:Stephen King",
"genre:Horror",
],
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
facet_filters: ["publisher:Penguin", ["author:Stephen King", "genre:Horror"]]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
facetFilters = Some(
FacetFilters(
Seq(
FacetFilters("publisher:Penguin"),
FacetFilters(Seq(FacetFilters("author:Stephen King"), FacetFilters("genre:Horror")))
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(facetFilters: SearchFacetFilters
.arrayOfSearchFacetFilters([
SearchFacetFilters.string("publisher:Penguin"),
SearchFacetFilters.arrayOfSearchFacetFilters([
SearchFacetFilters.string("author:Stephen King"),
SearchFacetFilters.string("genre:Horror"),
]),
])))
)
```
## Filter by numeric value
Algolia lets you numerically filter results with a comparison or a range.
This only works with numeric values.
For example, you could allow users to only show books that cost less than a certain price or those in a specific price range.
**The `price` attribute is a numeric attribute, not a string attribute**.
### Apply a numeric filter
[Example dataset](#example-dataset)
Using the example dataset, you've decided to let users define a price limit. For example, they may only want books that cost less than \$10:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Filters = "price < 10" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "price < 10",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("price < 10"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex("INDEX_NAME", new SearchParamsObject().setFilters("price < 10"), Hit.class);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({ indexName: 'indexName', searchParams: { filters: 'price < 10' } });
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "price < 10"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'price < 10',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "price < 10",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "price < 10")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("price < 10")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(filters: "price < 10"))
)
```
Algolia filters use an SQL-like syntax, which lets you use comparison operator attributes: `<`, `<=`, `=`, `!=`, `>=`, and `>`.
To allow users to retrieve books between a range, say $10 and $20, set a range filter:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Query = "books", Filters = "price:10 TO 20" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "books",
filters: "price:10 TO 20",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("books").SetFilters("price:10 TO 20"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setQuery("books").setFilters("price:10 TO 20"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'books', filters: 'price:10 TO 20' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(query = "books", filters = "price:10 TO 20"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'books',
'filters' => 'price:10 TO 20',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "books",
"filters": "price:10 TO 20",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(query: "books", filters: "price:10 TO 20")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("books"),
filters = Some("price:10 TO 20")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "books",
filters: "price:10 TO 20"
))
)
```
## Filter by date
[Example dataset](#example-dataset)
As well as [sorting](/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date), you can use dates to **filter search results within a specific period**.
For example, you can allow users to filter on recently published books or only see books published within a certain period.
### Format your dates suitably
Algolia can filter on date attributes,
but they must be formatted as [Unix timestamps](https://wikipedia.org/wiki/Unix_time).
For more information, see [Formatting dates](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#dates)
Instead of changing the date attribute, you could add an additional timestamp attribute.
In the example dataset,
the `publication_date` attribute has the formatted date for display, while the `date_timestamp` attribute can be used for filtering.
### Apply a date filter
With dates represented as Unix timestamp, use the [`filters`](/doc/api-reference/api-parameters/filters) parameter to only retrieve some results. For example:
* [Recently published books](#recently-published-books)
* [Books for a particular year](#books-for-a-particular-year)
* [Closest dates](#closest-dates)
#### Recently published books
If you want users to filter for recent books, decide what recent means to you.
For example, a recent book might be a book published less than a year ago.
This means you must set a filter that excludes with `date_timestamp` greater than now minus one year.
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class SearchRecentlyPublishedBooks
{
async Task Main(string[] args)
{
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
var date = DateTime.UtcNow.AddYears(-1);
var sTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var unixTime = (long)(date - sTime).TotalSeconds;
var searchParams = new SearchParams(
new SearchParamsObject
{
Query = "SEARCH_QUERY",
Filters = $"date_timestamp > {unixTime}",
}
);
await client.SearchSingleIndexAsync("INDEX_NAME", searchParams);
}
}
```
```dart Dart theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void searchRecentlyPublishedBooks() async {
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
final dateTimestamp =
DateTime.now().subtract(Duration(days: 365)).millisecondsSinceEpoch;
final searchParams = SearchParamsObject(
query: "SEARCH_QUERY", filters: "date_timestamp > $dateTimestamp");
await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams,
);
}
```
```go Go theme={"system"}
package main
import (
"fmt"
"time"
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func searchRecentlyPublishedBooks() {
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
dateTimestamp := time.Now().AddDate(-1, 0, 0).Unix()
searchParams := search.SearchParamsObjectAsSearchParams(
search.NewSearchParamsObject().
SetQuery("SEARCH_QUERY").
SetFilters(fmt.Sprintf("date_timestamp > %d", dateTimestamp)),
)
_, err = client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(searchParams))
if err != nil {
panic(err)
}
}
```
```java Java theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
public class searchRecentlyPublishedBooks {
public static void main(String[] args) throws Exception {
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
long dateTimestamp = ZonedDateTime.now(ZoneOffset.UTC).plusYears(-1).toEpochSecond();
SearchParams searchParams = new SearchParamsObject().setQuery("SEARCH_QUERY").setFilters("date_timestamp > " + dateTimestamp);
client.searchSingleIndex("INDEX_NAME", searchParams, Hit.class);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"}
import { algoliasearch } from 'algoliasearch';
import type { SearchParams } from 'algoliasearch';
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
const d = new Date();
const searchParams: SearchParams = {
query: 'SEARCH_QUERY',
filters: `date_timestamp > ${Math.floor(d.setDate(d.getDate() - 365) / 1000)}`,
};
await client.searchSingleIndex({ indexName: 'indexName', searchParams: searchParams });
```
```kotlin Kotlin theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
import java.time.Instant
import java.time.temporal.ChronoUnit
suspend fun searchRecentlyPublishedBooks() {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val dateTimestamp = Instant.now().minus(365, ChronoUnit.DAYS).epochSecond
val searchParams =
SearchParamsObject(query = "SEARCH_QUERY", filters = "date_timestamp > $dateTimestamp")
client.searchSingleIndex(indexName = "INDEX_NAME", searchParams = searchParams)
}
```
```php PHP theme={"system"}
setQuery('SEARCH_QUERY')
->setFilters('date_timestamp > '.$dateTimestamp)
;
$client->searchSingleIndex(
'INDEX_NAME',
$searchParams,
);
```
```python Python theme={"system"}
import datetime
import time
from algoliasearch.search.client import SearchClientSync
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
date = datetime.datetime.now() - datetime.timedelta(days=365)
date_timestamp = int(time.mktime(date.timetuple()))
search_params = {
"query": "SEARCH_QUERY",
"filters": f"date_timestamp > {date_timestamp}",
}
_client.search_single_index(
index_name="INDEX_NAME",
search_params=search_params,
)
```
```ruby Ruby theme={"system"}
import(time)
require "algolia"
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
date_timestamp = Time.now.to_i - 60 * 60 * 24 * 365
search_params = Algolia::Search::SearchParamsObject.new(
query: "SEARCH_QUERY",
filters: "date_timestamp > #{date_timestamp}"
)
client.search_single_index("INDEX_NAME", search_params)
```
```scala Scala theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import java.time.{Instant, LocalDateTime, ZoneOffset}
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.SearchParamsObject
def searchRecentlyPublishedBooks(): Future[Unit] = {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val dateTimestamp = LocalDateTime.now().minusYears(1).toInstant(ZoneOffset.UTC).getEpochSecond
val searchParams = SearchParamsObject(
query = Some("SEARCH_QUERY"),
filters = Some(s"date_timestamp > $dateTimestamp")
)
client
.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(searchParams)
)
.map { response =>
println(response)
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func searchRecentlyPublishedBooks() async throws {
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
let dateTimestamp = Int(Date().timeIntervalSince1970) - 365 * 24 * 60 * 60
let searchParams = SearchSearchParams.searchSearchParamsObject(
SearchSearchParamsObject(query: "SEARCH_QUERY", filters: "date_timestamp > \(dateTimestamp)")
)
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams
)
print(response)
}
```
Algolia filters use an SQL-like syntax, which lets you use comparison operators: `<`, `<=`, `=`, `!=`, `>=`, and `>`.
#### Books for a particular year
If you want to only retrieve books from 2018, set a range filter and provide it with Unix timestamps for the first and last day of the year. If you want to exclude a particular year or search for all books published outside a specific time you can combine time periods with the `NOT` boolean operator:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject { Filters = "NOT date_timestamp:1514764800 TO 1546300799" }
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "NOT date_timestamp:1514764800 TO 1546300799",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("NOT date_timestamp:1514764800 TO 1546300799"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("NOT date_timestamp:1514764800 TO 1546300799"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: 'NOT date_timestamp:1514764800 TO 1546300799' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "NOT date_timestamp:1514764800 TO 1546300799"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'NOT date_timestamp:1514764800 TO 1546300799',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "NOT date_timestamp:1514764800 TO 1546300799",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "NOT date_timestamp:1514764800 TO 1546300799")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("NOT date_timestamp:1514764800 TO 1546300799")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(
SearchSearchParamsObject(filters: "NOT date_timestamp:1514764800 TO 1546300799")
)
)
```
#### Closest dates
If your contains books that will be published in the future, it can be helpful to [sort results from sooner to later](/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date), so that users will see the next published books first.
Sort records by ascending date so that books due to be published later sit lower in the search results:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
Ranking = new List
{
"asc(date_timestamp)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
},
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
ranking: [
"asc(date_timestamp)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetRanking(
[]string{"asc(date_timestamp)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setRanking(
Arrays.asList("asc(date_timestamp)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom")
)
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
ranking: ['asc(date_timestamp)', 'typo', 'geo', 'words', 'filters', 'proximity', 'attribute', 'exact', 'custom'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
ranking =
listOf(
"asc(date_timestamp)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
)
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['ranking' => [
'asc(date_timestamp)',
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"ranking": [
"asc(date_timestamp)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
ranking: ["asc(date_timestamp)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
ranking = Some(
Seq("asc(date_timestamp)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(ranking: [
"asc(date_timestamp)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
])
)
```
Filter out every record with past dates:
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class SearchFuturePublishedBooks
{
async Task Main(string[] args)
{
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
var dateTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var searchParams = new SearchParams(
new SearchParamsObject
{
Query = "SEARCH_QUERY",
Filters = $"date_timestamp > {dateTimestamp}",
}
);
await client.SearchSingleIndexAsync("INDEX_NAME", searchParams);
}
}
```
```dart Dart theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void searchFuturePublishedBooks() async {
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
final dateTimestamp = DateTime.now().millisecondsSinceEpoch;
final searchParams = SearchParamsObject(
query: "SEARCH_QUERY", filters: "date_timestamp > $dateTimestamp");
await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams,
);
}
```
```go Go theme={"system"}
package main
import (
"fmt"
"time"
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func searchFuturePublishedBooks() {
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
dateTimestamp := time.Now().UnixMilli()
searchParams := search.SearchParamsObjectAsSearchParams(
search.NewSearchParamsObject().
SetQuery("SEARCH_QUERY").
SetFilters(fmt.Sprintf("date_timestamp > %d", dateTimestamp)),
)
_, err = client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(searchParams))
if err != nil {
panic(err)
}
}
```
```java Java theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
public class searchFuturePublishedBooks {
public static void main(String[] args) throws Exception {
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
long dateTimestamp = System.currentTimeMillis();
SearchParams searchParams = new SearchParamsObject().setQuery("SEARCH_QUERY").setFilters("date_timestamp > " + dateTimestamp);
client.searchSingleIndex("INDEX_NAME", searchParams, Hit.class);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"}
import { algoliasearch } from 'algoliasearch';
import type { SearchParams } from 'algoliasearch';
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
const dateTimestamp = Date.now();
const searchParams: SearchParams = {
query: 'SEARCH_QUERY',
filters: `date_timestamp > ${dateTimestamp}`,
};
await client.searchSingleIndex({ indexName: 'indexName', searchParams: searchParams });
```
```kotlin Kotlin theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
import java.time.Instant
suspend fun searchFuturePublishedBooks() {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val dateTimestamp = Instant.now().epochSecond
val searchParams =
SearchParamsObject(query = "SEARCH_QUERY", filters = "date_timestamp > $dateTimestamp")
client.searchSingleIndex(indexName = "INDEX_NAME", searchParams = searchParams)
}
```
```php PHP theme={"system"}
setQuery('SEARCH_QUERY')
->setFilters('date_timestamp > '.$dateTimestamp)
;
$client->searchSingleIndex(
'INDEX_NAME',
$searchParams,
);
```
```python Python theme={"system"}
import time
from algoliasearch.search.client import SearchClientSync
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
date_timestamp = time.time() # Get current timestamp
search_params = {
"query": "SEARCH_QUERY",
"filters": f"date_timestamp > {date_timestamp}",
}
_client.search_single_index(
index_name="INDEX_NAME",
search_params=search_params,
)
```
```ruby Ruby theme={"system"}
import(time)
require "algolia"
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
# Get current timestamp
date_timestamp = Time.now.to_i
search_params = Algolia::Search::SearchParamsObject.new(
query: "SEARCH_QUERY",
filters: "date_timestamp > #{date_timestamp}"
)
client.search_single_index("INDEX_NAME", search_params)
```
```scala Scala theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import java.time.Instant
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.SearchParamsObject
def searchFuturePublishedBooks(): Future[Unit] = {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val dateTimestamp = Instant.now.getEpochSecond
val searchParams = SearchParamsObject(
query = Some("SEARCH_QUERY"),
filters = Some(s"date_timestamp > $dateTimestamp")
)
client
.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(searchParams)
)
.map { response =>
println(response)
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func searchFuturePublishedBooks() async throws {
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
let dateTimestamp = Int(Date().timeIntervalSince1970)
let searchParams = SearchSearchParams.searchSearchParamsObject(
SearchSearchParamsObject(query: "SEARCH_QUERY", filters: "date_timestamp > \(dateTimestamp)")
)
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams
)
print(response)
}
```
## Filter by array
[Example dataset](#example-dataset)
Some attributes, such as the genre of a book, are lists of values.
One book can belong to multiple genres.
Hence, the genre attribute should be an array.
If you have a chain of bookstores with an accompanying ecommerce website, you may want to let users filter by book genre and store.
To do this, the example dataset has two array attributes: `store` and `categories`.
Using this dataset, users could, for example, retrieve all books about politics in stock in The Corner Bookshop.
### Apply an array filter
Once you've set `categories` and `store` as [attributes for faceting](/doc/guides/managing-results/refine-results/filtering#define-filterable-attributes), apply string filters with the [`filters`](/doc/api-reference/api-parameters/filters) parameter in your [`search`](/doc/libraries/sdk/v1/methods/search) code:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Query = "ben",
Filters = "categories:politics AND store:Gibert Joseph Saint-Michel",
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "ben",
filters: "categories:politics AND store:Gibert Joseph Saint-Michel",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("ben").SetFilters("categories:politics AND store:Gibert Joseph Saint-Michel"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setQuery("ben").setFilters("categories:politics AND store:Gibert Joseph Saint-Michel"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'ben', filters: 'categories:politics AND store:Gibert Joseph Saint-Michel' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
query = "ben",
filters = "categories:politics AND store:Gibert Joseph Saint-Michel",
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'ben',
'filters' => 'categories:politics AND store:Gibert Joseph Saint-Michel',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "ben",
"filters": "categories:politics AND store:Gibert Joseph Saint-Michel",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
query: "ben",
filters: "categories:politics AND store:Gibert Joseph Saint-Michel"
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("ben"),
filters = Some("categories:politics AND store:Gibert Joseph Saint-Michel")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "ben",
filters: "categories:politics AND store:Gibert Joseph Saint-Michel"
))
)
```
This works because categories and store are separate arrays, so each condition matches independently.
If both values were properties of objects inside a single array, a condition on `categories` and a condition on `store` could each be satisfied by a different object in that array, not necessarily the same one.
See [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects).
## Filter by boolean
[Example dataset](#example-dataset)
Algolia lets you filter results by boolean attributes.
For example, you can use the `is_available` boolean attribute in the example dataset to exclude all records where `is_available` is `false`.
To do this, first [declare `is_available` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
### Apply a boolean filter
If you want users to see only the available books, do the following:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Filters = "is_available:true" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "is_available:true",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("is_available:true"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("is_available:true"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: 'is_available:true' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "is_available:true"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'is_available:true',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "is_available:true",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "is_available:true")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("is_available:true")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(filters: "is_available:true"))
)
```
Algolia considers boolean values as integers: `false` as `0` and `true` as `1`.
This means you can apply [numeric filters](#filter-by-numeric-value) for boolean attributes.
With the example dataset, `is_available=0` is the same as `is_available:false`.
If creating numeric or boolean filters, you don't need to declare the attribute as `attributesForFaceting`.
## Filter by tags
[Example dataset](#example-dataset)
Sometimes, you may want to filter search results based on specific metadata, like a type or a category.
Using the example dataset, the accompanying online bookstore has fiction and non-fiction sections.
Depending on which section users are searching from, **you can provide a context-sensitive search experience**.
For example, a user goes to the website's non-fiction section, then starts searching for the biography of President Harry S. Truman.
If your search relevance is primarily based on popularity, by typing "harry", they might retrieve Harry Potter books first, which wouldn't make sense to them.
Instead, what you can do is **filter results based on what they most likely want to find**.
Algolia lets you add tags to your records with the reserved `_tags` attribute.
Using the example dataset, if users type "harry" with the search restricted to "non-fiction", it would retrieve "Where the Buck Stops" because it has "non-fiction" in the `_tags` attribute and contains the word "harry" in the record.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Query = "harry", Filters = "_tags:non-fiction" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "harry",
filters: "_tags:non-fiction",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("harry").SetFilters("_tags:non-fiction"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setQuery("harry").setFilters("_tags:non-fiction"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'harry', filters: '_tags:non-fiction' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(query = "harry", filters = "_tags:non-fiction"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'harry',
'filters' => '_tags:non-fiction',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "harry",
"filters": "_tags:non-fiction",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(query: "harry", filters: "_tags:non-fiction")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("harry"),
filters = Some("_tags:non-fiction")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "harry",
filters: "_tags:non-fiction"
))
)
```
If you don't specify an attribute name, the filter is assumed to apply to the `_tags` attribute.
For example, `fiction` translates into `_tags:fiction`.
You can also use [`tagFilters`](/doc/api-reference/api-parameters/tagFilters) to do the same thing.
### The difference between \_tags and custom attributes
It's important to consider the following when deciding whether to use the Algolia `_tags` attribute or a custom attribute for faceting:
* `_tags` is a [reserved attribute](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#reserved-attribute-names) which means it automatically works as a string filter without you having to set it as an attribute for faceting or use the `filterOnly` modifier.
* `_tags` isn't searchable by default.
* You can't use `_tags` for non-filtering purposes, such as displaying in search results or computing counts of records that match the filters.
* `_tags` can only contain strings.
In short, if you have several types of string filters or any non-string filters, creating a custom attribute is usually the way to go.
## Filter by null or missing attributes
Since Algolia **doesn't support filtering on `null` values or missing attributes**, what happens when your index contains an attribute that isn't present in all records?
For example, consider an online bookstore where people can buy and rate books from 0 to 5. Any record without the `rating` attribute is assumed to be unrated.
If you want users to search for both rated and unrated books in the same filter, you must modify your data.
To do this, you can take one of two approaches:
* [Use the `_tags` attribute](#create-a-tag)
* [Use a boolean attribute](#create-a-boolean-attribute).
### Create a tag
[Example dataset](#example-dataset)
At indexing time, you can add a tag value to indicate if a record is or isn't rated.
In the example dataset, some records have a `rating` attribute and value, others have a null `rating`, and some don't have a `rating` attribute.
A `null` or nonexistent attribute is different from zero, which represents a book with the lowest rating.
```jsonc JSON icon=braces theme={"system"}
[
{
// ...
"rating": 5,
"_tags": ["is_rated"]
},
{
// ...
"rating": null,
"_tags": ["is_not_rated"]
},
{
// ...
"_tags": ["is_not_rated"]
}
]
```
To search for records without the attribute or have a null value, you can now use [tags filtering](#filter-by-tags).
### Create a boolean attribute
At indexing time, you can add a boolean value to indicate if a record is or isn't rated:
```jsonc JSON icon=braces theme={"system"}
[
{
// ...
"rating": 5,
"is_rated": true
},
{
// ...
"rating": null,
"is_rated": false
},
{
// ...
"is_rated": false
}
]
```
To search for records without the attribute or have a null value, you can now use [boolean filtering](#filter-by-boolean).
## Filter by objectID
By default, Algolia sets a record's `objectID` attribute as a [filter-only facet](/doc/api-reference/api-parameters/attributesForFaceting#modifiers).
This means **you can use the [`filters`](/doc/api-reference/api-parameters/filters) parameter to filter on `objectID`**.
This is helpful when you want to include or exclude specific records in the current search.
For example, to exclude the record with `objectID` "1234", use the filter `NOT objectID:1234` in the [`filters`](/doc/api-reference/api-parameters/filters) parameter of your search.
## Example dataset
All the examples on this page use a bookstore index: `books`. The index has records that look like this:
```json JSON icon=braces theme={"system"}
[
{
"title": "Harry Potter and the Philosopher's Stone",
"author": "J. K. Rowling",
"publisher": "Bloomsbury",
"price": 7.94,
"publication_date": "1996-06-17",
"date_timestamp": 866505600,
"is_available": true,
"rating": 5,
"_tags": [
"fiction",
"is_rated"
],
"popularity": 1000,
"store": [
"The Corner Bookshop",
"Gibert Joseph Barbès",
"Gibert Joseph Paris 13 - Grande Bibliothèque"
],
"categories": [
"fantasy",
"science fiction",
"children's literature"
]
},
{
"title": "A Perfect Crime",
"author": "Peter Abrahams",
"publisher": "Penguin",
"price": 7.94,
"publication_date": "1998-09-16",
"date_timestamp": 905904000,
"is_available": true,
"rating": null,
"_tags": [
"fiction",
"is_not_rated"
],
"popularity": 800,
"store": [
"The Corner Bookshop",
"Gibert Joseph Barbès"
],
"categories": [
"thriller",
"crime fiction"
]
},
{
"title": "The World as It Is",
"author": "Ben Rhodes",
"publisher": "Penguin",
"price": 9.54,
"publication_date": "2018-06-05",
"date_timestamp": 1528156800,
"is_available": false,
"_tags": [
"non-fiction",
"is_not_rated"
],
"popularity": 900,
"store": [
"The Corner Bookshop"
],
"categories": [
"history",
"politics"
]
},
{
"title": "Yellowface",
"author": "R.F. Kuang",
"publisher": "The Borough Press",
"price": 9.19,
"publication_date": "2023-05-16",
"date_timestamp": 1684108800,
"is_available": true,
"_tags": [
"fiction",
"is_not_rated"
],
"popularity": 900,
"store": [
"The Corner Bookshop"
],
"categories": [
"dark humor",
"satire"
]
},
{
"title": "A Fighting Chance",
"author": "Elizabeth Warren",
"publisher": "Scribe",
"price": 12.97,
"publication_date": "2014-04-22",
"date_timestamp": 1398124800,
"is_available": false,
"rating": 5,
"_tags": [
"non-fiction",
"is_rated"
],
"popularity": 800,
"store": [
"The Corner Bookshop"
],
"categories": [
"history",
"politics"
]
},
{
"title": "Where the Buck Stops",
"author": "Harry S Truman",
"publisher": "Warner Books",
"price": 15.014,
"publication_date": "1990-10-01",
"date_timestamp": 654739200,
"is_available": true,
"_tags": [
"non-fiction",
"is_not_rated"
],
"popularity": 800,
"store": [
"The Corner Bookshop"
],
"categories": [
"history",
"politics",
"biography"
]
},
]
```
# Filters and arrays of objects
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects
Why a filter on an array of objects can match values from different elements, and how to structure records to avoid it.
When store an array of objects, Algolia indexes the values for each attribute path independently.
It doesn't preserve which values came from the same array element.
A filter combining two conditions can then match a record where a different element satisfied each one.
Consider a product sold at two warehouses at different prices:
```json JSON icon=braces theme={"system"}
{
"objectID": "thermostat-1",
"name": "Thermostat",
"prices": [
{ "warehouse": "north", "amount": 93100 },
{ "warehouse": "south", "amount": 100000 }
]
}
```
With `prices.warehouse` declared in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting), the filter `prices.warehouse:"north" AND prices.amount=100000` shouldn't match, because the north warehouse charges 93100.
It matches anyway.
The first element satisfies one condition, the second satisfies the other, and the record satisfies both.
## Why the cross-match happens
Indexing collects every value at an attribute path into a single set.
For the preceding record, `prices.warehouse` contains `["north", "south"]` and `prices.amount` contains `[93100, 100000]`, but indexing doesn't preserve that `north` pairs with `93100`.
Filters on array attributes match if **any** element matches.
Combining filters doesn't require them to match the same element, and there's no filter syntax to enforce that.
The same applies to parallel arrays.
Given `prices: [50, 200]` and `stockCount: [1, 5]`, the filter `prices < 100 AND stockCount > 2` matches even though the cheap item has low stock.
Filtering ignores array position.
Geo queries are the exception.
`_geoloc` stores coordinates as latitude/longitude pairs, so [`insideBoundingBox`](/doc/api-reference/api-parameters/insideBoundingBox) and related filters evaluate each pair together.
Other filters still cross-match independently.
The filter returns more records than expected without raising an error.
The extra records satisfy all conditions, but not for the same element.
## Choose a record structure
Three record shapes avoid the cross-match, at different costs.
Choose by what you need to filter, sort, or facet:
| Goal | Record shape |
| - | - |
| Sort, facet, or rank on each element independently | [One record per element](#one-record-per-element) |
| Filter on a property you know at query time, with a bounded number per record | [One attribute per key](#one-attribute-per-key) |
| Filter one fixed combination of values together | [A composite attribute](#composite-attribute) |
### One record per element
Index one record per array element, then group the results at query time:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "thermostat-1-north",
"productID": "thermostat-1",
"name": "Thermostat",
"warehouse": "north",
"amount": 93100
},
{
"objectID": "thermostat-1-south",
"productID": "thermostat-1",
"name": "Thermostat",
"warehouse": "south",
"amount": 100000
}
]
```
Set [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct) to `productID` and [`distinct`](/doc/api-reference/api-parameters/distinct) to `true`.
Each search returns one row per product, and every filter applies to a single warehouse.
Each attribute now holds one value per record, so the usual declarations behave as written: add it to [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting) to facet on it, or to a replica's ranking to sort by it.
Use this shape when per-element sorting, faceting, or ranking matters, and when a filter combines two conditions without naming the element: `price < 100 AND stockCount > 2` stays two conditions here, where keyed attributes need one clause per key.
The cost is record count.
Shared product data repeats in every row, facet counts reflect either elements or products depending on [`facetingAfterDistinct`](/doc/api-reference/api-parameters/facetingAfterDistinct), and Analytics and Recommend operate on the split records rather than the product.
See [Deduplicate results with distinct](/doc/guides/managing-results/refine-results/grouping) for the full pattern.
### One attribute per key
When you know at query time which property you'll filter on, make that property the attribute name.
Replace the array with an object keyed by it:
```json JSON icon=braces theme={"system"}
{
"objectID": "thermostat-1",
"name": "Thermostat",
"prices": {
"north": 93100,
"south": 100000
}
}
```
Each key is now its own path holding a single value, not a set: `prices.north` is `93100` and nothing else.
A filter like `prices.north=100000` has one value to test, so it can't cross-match.
Numeric attributes are filterable without appearing in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting), unless [`numericAttributesForFiltering`](/doc/api-reference/api-parameters/numericAttributesForFiltering) restricts them to a list.
Use this shape when the number of keys per record stays bounded.
#### Faceting keyed attributes
For facet counts or facet statistics, declare the parent object.
Declaring `prices` covers every nested attribute below it: the setting stays one entry, and every nested attribute becomes a facet of its own.
Request the child paths, such as `prices.north`, because requesting `prices` itself doesn't return any facet.
Facet statistics follow the same paths: one `facets_stats` entry per numeric attribute below `prices`, and none that covers them together.
To get a single range across every key, compute it when you build the record and store it beside the prices:
```json JSON icon=braces theme={"system"}
{
"objectID": "thermostat-1",
"name": "Thermostat",
"prices": {
"north": 93100,
"south": 100000
},
"priceMin": 93100,
"priceMax": 100000
}
```
Declare `priceMin` and `priceMax` in `attributesForFaceting` instead of `prices`, then read the range as `priceMin`'s `min` and `priceMax`'s `max`.
The remaining statistics on those two attributes describe the per-record extremes, not the prices.
Each one holds a single value per record, so faceting the pair costs about what one key costs, however many keys accumulate.
Recompute both whenever a price changes: a stale aggregate narrows the range, and the index doesn't return an error.
Collecting every price into one array gives a range too, but only `min` and `max` are reliable.
Facet counts count records rather than array elements, so a price that repeats at one path in one record counts once.
`sum` then falls short of the true total, and `avg` can land either side of it.
Keyed attributes support filtering but not browsing.
You can't ask "Which warehouses exist, and how many products does each one stock?" because each key becomes a separate attribute.
The facet values are the prices, not the keys.
To browse by warehouse, store the warehouse names alongside the prices:
```json JSON icon=braces theme={"system"}
{
"objectID": "thermostat-1",
"name": "Thermostat",
"warehouses": ["north", "south"],
"prices": {
"north": 93100,
"south": 100000
}
}
```
Declaring `warehouses` gives one facet with a count for each warehouse, and `searchable(warehouses)` lets users search the list.
* Keep it if users browse or search by warehouse.
* Drop it if you only filter.
The array duplicates every warehouse name and can make records larger than the array of objects it replaced.
If you keep both attributes, build them from the same source at indexing time.
Otherwise, a key in `prices` but missing from `warehouses` remains filterable while disappearing from the facet.
The index doesn't return an error.
To detect drift, search with `facets: ["*"]` and compare the `warehouses` facet values with the `prices.*` child names.
Increase [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet) to include every warehouse.
Otherwise, the default of 100 truncates the `warehouses` values but not the `prices.*` names, so the comparison reports false positives.
#### Limitations
* **Cost.** Each property increases record size. Your plan [limits both the size of individual records and the average record size across the index](/doc/guides/scaling/algolia-service-limits#application-record-and-index-limits). Declaring properties for faceting increases index size, once per nested property a record carries. `filterOnly` costs less, and numeric filters don't need any declaration at all.
* **Sorting.** Sorting by one property requires [one replica per property](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas), and each index supports only a limited number of replicas. Sort on a shared base attribute instead, such as the precomputed `priceMin` above.
* **Expressiveness.** Filters that don't specify a key, such as "cheap and well stocked anywhere", must enumerate every key. The rewritten filter grows with the product of the per-key term counts. With more than about seven keys, it exceeds the documented limit of [1,000 filters per query](/doc/guides/scaling/algolia-service-limits#filters-facets-and-rules-limits). Use one record per element instead. See [Boolean rewrites for keyed attributes](#boolean-rewrites-for-keyed-attributes) for the construction and examples of rewrites that look correct but aren't.
### Composite attribute
When the same combination is always filtered together, join the values at indexing time:
```json JSON icon=braces theme={"system"}
{
"objectID": "shirt-1",
"variants": ["red|XL", "blue|S"]
}
```
Declare `variants` in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting), then filter with `variants:"red|XL"`.
The record keeps its shape, and one condition replaces two.
Undeclared, the filter doesn't match anything and doesn't raise an error (see [Missing and negated attributes](#missing-and-negated-attributes)).
This is the smallest change, but it only covers the combination you encoded.
Filtering on color alone takes a separate attribute, and composite values match as a whole value rather than supporting numeric ranges.
## When nesting is safe
Pairing breaks when an attribute path has more than one value.
The number of elements doesn't matter: a one-element array whose fields are arrays breaks pairing, and so does an object whose fields are arrays.
Nesting is safe in two cases:
* **A single value at every path.** Given `price: { "net": 10, "gross": 12 }`, the filter `price.net > 5 AND price.gross < 20` behaves as written. Array-valued fields under `price` would break pairing.
* **Independent values.** Use nested objects when cross-matching is the intended behavior. For example, filtering a `categories` array together with a `store` array should match records that have both, in any combination.
## Reference
Attribute naming and filter edge cases referenced throughout this guide.
### Attribute names with delimiters
Attribute names can contain dashes and underscores, so `north-west` doesn't need rewriting.
Dots are different: a dot becomes an extra path level, so a literal `"north.west"` key and a nested `{"north": {"west": ... }}` share one filter path and one facet entry.
No filter syntax can tell the two apart, so replace or remove dots in property names before indexing.
The delimiters `:`, `=`, `<`, `>`, `!`, `(`, and spaces only break the unquoted spelling.
Quote the key to keep it: `filters: '"prices.north:west" = 100'`.
Unquoted, `facetFilters` splits at the first colon and can match the wrong record.
`numericFilters` doesn't have a quoted form and rejects the key outright.
### Test if keys are present
Records don't all have the same keys: a product stocked in the north has `prices.north` but might not have `prices.south`.
To test whether a key exists, use a comparison that covers the full range of valid values.
For example, `prices.north >= 0` works only if prices can't be negative.
Don't use `!=` for this: `prices.north != 0` also matches records that don't have `prices.north`.
### Value types
A boolean `true` matches numerically as `1`, and a numeric string such as `"7"` doesn't match anything.
### Missing and negated attributes
Filtering on a string attribute that's not in `attributesForFaceting` doesn't match anything.
It doesn't raise an error.
This applies to both `filters` and [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), even if no record has the attribute.
Negation does the opposite: `NOT color:red` and `facetFilters: ["color:-red"]` match every record, even those with `color: red`.
### Boolean rewrites for keyed attributes
A query such as "cheap and well stocked anywhere" doesn't name a key.
To preserve pairing, the filter must consider every key.
This requires a Boolean rewrite.
The rewrite is only possible when both conditions use the same kind of term.
An `OR` clause can't mix numeric comparisons and facet values, so queries such as "open somewhere and cheap in the same warehouse" can't be rewritten.
The natural form is invalid because the filter grammar doesn't allow `AND` inside `OR`:
```text Invalid icon=circle-x theme={"system"}
(prices.north < 100 AND stock.north > 2) OR
(prices.south < 100 AND stock.south > 2)
```
Rewrite it as a conjunction of disjunctions, with one term per key in each clause:
```text Filter icon=circle-check theme={"system"}
(prices.north < 100 OR prices.south < 100) AND
(prices.north < 100 OR stock.south > 2) AND
(stock.north > 2 OR prices.south < 100) AND
(stock.north > 2 OR stock.south > 2)
```
This form is equivalent to the original paired query while using only syntax the filter grammar accepts.
The number of clauses equals the product of the per-key term counts.
Two keys produce four clauses.
Ten keys produce 1,024 clauses and more than 10,000 individual filters, well past the limit of [1,000 filters per query](/doc/guides/scaling/algolia-service-limits#filters-facets-and-rules-limits).
The filter also grows exponentially in size, reaching about 188 KB at ten keys.
**In practice, treat about seven keys as the limit.**
Beyond that, use one record per element.
Two shorter rewrites look correct but aren't:
* **Cross-match.** `(prices.north < 100 OR prices.south < 100) AND (stock.north > 2 OR stock.south > 2)` reintroduces cross-matching. It matches products that are cheap in one warehouse and well stocked in another.
* **Operator precedence.** Omitting the parentheses silently changes the meaning because `OR` binds tighter than `AND`. The filter parses as `prices.north < 100 AND (stock.north > 2 OR prices.south < 100) AND stock.south > 2`, requiring one warehouse to be cheap and another to be well stocked.
## See also
* [Filters and boolean operators](/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators)
* [Create nested attributes](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/creating-and-using-nested-attributes)
* [Handle data relationships](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships)
* [Structure ecommerce product records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records)
# Filters and boolean operators
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators
How to combine filters and `facetFilters` using boolean operators.
Algolia supports the following operators, which must be in capital letters:
* **`OR`**: must match any of the combined conditions
* **`AND`**: must match all the combined conditions
* **`NOT`**: negates a filter
## Use of parentheses
Use parentheses `(` and `)` to combine several OR conditions.
These rules apply:
* **Don't group ANDs within ORs.**
For example, `A OR (B AND C) OR (D AND E)` isn't supported.
* **Don't use parentheses with top-level AND combinations.**
For example, `(A AND (B OR C))` isn't supported.
(But `A AND (B OR C)` is supported)
* **Don't use parentheses to negate a group of expressions.**
For example, `NOT (filter1 OR filter2)` isn't supported.
### Examples of valid combinations
The following examples are valid combinations of filter expressions:
* `A AND (B OR C)`
* `(A OR B) AND C`
* `A AND (B OR C) AND (D OR E)`
* `A AND B AND (C OR D) AND (E OR F)`
* `(A OR B) AND (C OR D) AND E`
## Example scenarios for filter combinations
The `filters` parameter gives precise control over search results with a SQL-like syntax.
Use the following examples to learn how to apply different filters and combine them with boolean logic.
### Basic filters
| Filter type | Example | Filter |
| - | - | - |
| Numeric comparison | Show products that cost \$13 or more | `price > 12.99` |
| Numeric range | Show products with a price between $5.99 and $100 | `price:5.99 TO 100` |
| Facets | Show records categorized as "Book" | `category:Book` |
| Tag filters | Show records marked with the "published" tag | `_tags:published` |
| Boolean filters | Show records where the `isEnabled` attribute is `true` | `isEnabled:true` |
| Filter by date | Show records published between two dates [Unix timestamps](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#dates) | `date: 1441745506 TO 1441755506` |
| Negation | Exclude discontinued products | `NOT is_discontinued:true` |
### Combine filters
* Find books not written by a specific author: `(category:Book OR category:Ebook) AND NOT author:"JK Rowling"`
* Filter products by price range and category: `price:10 TO 50 AND (category:Electronics OR category:Accessories)`
* Complex filter with several conditions: `inStock:true AND (category:Phones OR category:Tablets) AND (brand:Apple OR brand:Samsung) AND price>500`
## Don't mix filter types in OR conditions
You can't compare different filter types (string, numeric, tags) with an `OR`.
For example, `num=3 OR tag1 OR facet:value` isn't allowed.
## Apps and user actions change filter expressions
If a user or app selects a filter, Algolia omits the filters that don't apply from the expression.
For example, if the original filter expression is `((category:Electronics OR category:Home) AND NOT brand:LG) AND NOT price:100 TO 500` and the user selects the Electronics category, the expression simplifies to `(category:Electronics AND NOT brand:LG) AND NOT price:100 TO 500`.
## Filter syntax validator
# Filter scoring
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring
How Algolia calculates filter scores, and how to control the weight of individual filter values in your ranking.
As well as refining results, you can use **filter scoring** to rank according to how well they match a set of filters.
Scoring applies numerical settings to filter values, making some filter values more important than others.
The [Filters ranking criterion](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria#filters) ranks records by their filter scores.
When records tie on all earlier criteria, those with higher filter scores rank first.
If filter scores also tie, Algolia uses the remaining criteria to order the records.
Scoring works with both [`filters`](/doc/api-reference/api-parameters/filters) and [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters).
## Stock portfolio example
Consider stock portfolios where you score companies based on their monetary significance.
You could use filter scoring to rank Google higher than Facebook.
For example:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Filters =
"(company:Google OR company:Amazon OR company:Facebook)",
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters:
"(company:Google OR company:Amazon OR company:Facebook)",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("(company:Google OR company:Amazon OR company:Facebook)"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("(company:Google OR company:Amazon OR" + " company:Facebook)"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: '(company:Google OR company:Amazon OR company:Facebook)' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
filters =
"(company:Google OR company:Amazon OR company:Facebook)"
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => '(company:Google OR company:Amazon OR company:Facebook)',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "(company:Google OR company:Amazon OR company:Facebook)",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
filters: "(company:Google OR company:Amazon OR company:Facebook)"
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("(company:Google OR company:Amazon OR company:Facebook)")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(
SearchSearchParamsObject(
filters: "(company:Google OR company:Amazon OR company:Facebook)"
)
)
)
```
The result is that records with Google stocks sit at the top of the list, higher than Amazon and Facebook.
## Score calculation
Filter scores are integer values from 0 to .
For performance reasons,
don't use filter scoring on searches that may return more than 100,000 results.
### Score aggregation
By default, Algolia adds the scores of `AND` filters and uses the highest matching score within each `OR` group.
It doesn't add multiple matching scores within an `OR` group.
The [stock portfolio](#stock-portfolio-example) filter is a single `OR` group.
A portfolio holding Google, Amazon, and Facebook scores 3: the highest matching score, not the sum.
When required filters use only `AND`, every returned record matches every filter and receives the same filter score.
This restriction doesn't apply to optional filters, because records don't need to match every optional filter to appear in the results.
### Accumulate scores with `sumOrFiltersScores`
You accumulate scores by setting the [`sumOrFiltersScores`](/doc/api-reference/api-parameters/sumOrFiltersScores) parameter to `true`.
The stock portfolio filter then scores 6 for a record holding all three companies (3+2+1).
If `sumOrFiltersScores` is `false`, the default, Algolia takes the highest score instead.
A filtered on Google and Amazon with `sumOrFiltersScores` = `false` returns a score of 3 (google(3) > amazon(2) > facebook(1)).
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Filters =
"(company:Google OR company:Amazon OR company:Facebook)",
SumOrFiltersScores = false,
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters:
"(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores: false,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetFilters("(company:Google OR company:Amazon OR company:Facebook)").
SetSumOrFiltersScores(false),
)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject()
.setFilters("(company:Google OR company:Amazon OR" + " company:Facebook)")
.setSumOrFiltersScores(false),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: {
filters: '(company:Google OR company:Amazon OR company:Facebook)',
sumOrFiltersScores: false,
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
filters =
"(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores = false,
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => '(company:Google OR company:Amazon OR company:Facebook)',
'sumOrFiltersScores' => false,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "(company:Google OR company:Amazon OR company:Facebook)",
"sumOrFiltersScores": False,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
filters: "(company:Google OR company:Amazon OR company:Facebook)",
sum_or_filters_scores: false
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("(company:Google OR company:Amazon OR company:Facebook)"),
sumOrFiltersScores = Some(false)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
filters: "(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores: false
))
)
```
The same query with `sumOrFiltersScores` = `true` returns a score of 6 (google (3) + amazon(2) + facebook(1)).
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Filters =
"(company:Google OR company:Amazon OR company:Facebook)",
SumOrFiltersScores = true,
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters:
"(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores: true,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetFilters("(company:Google OR company:Amazon OR company:Facebook)").
SetSumOrFiltersScores(true),
)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject()
.setFilters("(company:Google OR company:Amazon OR" + " company:Facebook)")
.setSumOrFiltersScores(true),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: {
filters: '(company:Google OR company:Amazon OR company:Facebook)',
sumOrFiltersScores: true,
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
filters =
"(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores = true,
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => '(company:Google OR company:Amazon OR company:Facebook)',
'sumOrFiltersScores' => true,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "(company:Google OR company:Amazon OR company:Facebook)",
"sumOrFiltersScores": True,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
filters: "(company:Google OR company:Amazon OR company:Facebook)",
sum_or_filters_scores: true
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("(company:Google OR company:Amazon OR company:Facebook)"),
sumOrFiltersScores = Some(true)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
filters: "(company:Google OR company:Amazon OR company:Facebook)",
sumOrFiltersScores: true
))
)
```
`sumOrFiltersScores` only changes the scoring of `OR` groups.
## Optional filter scores
[Optional filters](/doc/guides/managing-results/rules/merchandising-and-promoting/in-depth/optional-filters) don't remove records from your results, so their scores only affect ranking.
When you don't specify a score for an optional filter, it defaults to 1.
In `optionalFilters` syntax,
that means Algolia adds the scores of `AND `filters `(optionalFilters: ["a", "b"])`,
and each `OR `group `(optionalFilters: [["a", "b"]])` contributes only its highest matching score.
### Simple optional filters
Here's an example of how Algolia calculates an [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters) score:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
OptionalFilters = new OptionalFilters(
new List
{
new OptionalFilters("brand:Apple"),
new OptionalFilters("type:tablet"),
}
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
optionalFilters: [
"brand:Apple",
"type:tablet",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetOptionalFilters(search.ArrayOfOptionalFiltersAsOptionalFilters(
[]search.OptionalFilters{*search.StringAsOptionalFilters("brand:Apple"), *search.StringAsOptionalFilters("type:tablet")})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setOptionalFilters(
OptionalFilters.of(Arrays.asList(OptionalFilters.of("brand:Apple"), OptionalFilters.of("type:tablet")))
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { optionalFilters: ['brand:Apple', 'type:tablet'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
optionalFilters =
OptionalFilters.of(
listOf(
OptionalFilters.of("brand:Apple"),
OptionalFilters.of("type:tablet"),
)
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['optionalFilters' => [
'brand:Apple',
'type:tablet',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"optionalFilters": [
"brand:Apple",
"type:tablet",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(optional_filters: ["brand:Apple", "type:tablet"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
optionalFilters =
Some(OptionalFilters(Seq(OptionalFilters("brand:Apple"), OptionalFilters("type:tablet"))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(optionalFilters: SearchOptionalFilters
.arrayOfSearchOptionalFilters([
SearchOptionalFilters.string("brand:Apple"),
SearchOptionalFilters.string("type:tablet"),
])))
)
```
Algolia uses the highest matching score for each optional filter.
In the preceding example:
* The `brand:Apple` filter contributes 2 when it matches a record and 0 otherwise.
* The `type:tablet` filter contributes 1 when it matches and 0 otherwise.
In the following example, the iPad Air matches both optional filters and scores 3:
2 from `brand:Apple` and 1 from `type:tablet`.
The iPhone matches only `brand:Apple` and scores 2.
The iPad Air ranks above the iPhone.
```json JSON icon=braces theme={"system"}
[
{
"name": "iPad Air",
"brand": "Apple",
"type": "tablet"
},
{
"name": "iPhone 17",
"brand": "Apple",
"type": "phone"
}
]
```
With these optional filters, Algolia ranks results as follows:
1. Apple tablets (score: 3)
2. Other Apple products (score: 2)
3. Tablets by other brands (score: 1)
4. Other products by other brands (score: 0)
### Complex optional filters
Algolia calculates the highest score for each element of your optional filters,
so you can combine filters to perform more complex score calculations.
For example, you want to promote red and blue products but avoid over-promoting products that are both red and blue.
To do so, use the following optional filters:
```json JSON icon=braces theme={"system"}
[["color:red", "color:blue"], ["type:-jeans"]]
```
And the following record:
```json JSON icon=braces theme={"system"}
{
"name": "Long-sleeved shirt",
"type": "shirt",
"color": ["red", "blue"]
}
```
1. Algolia calculates the highest score for the first optional filter. In this case, since it's an array (`["color:red", "color:blue"]`), Algolia checks all nested optional filters and uses the highest matching score.
2. Since the record contains red and blue colors, it scores 2 (match on red) and 1 (match on blue). Algolia uses the highest matching score instead of a sum, meaning **this record gets a score of 2 on this filter (not 3)**.
3. Algolia matches the second optional filter (`type:-jeans`). The record also matches (it's not "jeans"), so it scores 1. Adding this to the previous optional filter's score, the total score for this record is 3.
### Sum optional filter scores
In the preceding example, the red-and-blue shirt scores 3 by default: 2 from the color group and 1 from `type:-jeans`.
With `sumOrFiltersScores` set to `true`, the same shirt scores 4: 2 for red, 1 for blue, and 1 for `type:-jeans`.
The following table compares scores for each combination:
| Product type | Matching color filters | Default score | Score with `sumOrFiltersScores: true` |
| - | - | - | - |
| Other than jeans | Red and blue | 3 | 4 |
| Other than jeans | Red only | 3 | 3 |
| Other than jeans | Blue only | 2 | 2 |
| Other than jeans | Neither | 1 | 1 |
| Jeans | Red and blue | 2 | 3 |
| Jeans | Red only | 2 | 2 |
| Jeans | Blue only | 1 | 1 |
| Jeans | Neither | 0 | 0 |
These scores decide the order only for the Filters ranking criterion.
Earlier criteria take precedence.
## Numeric filter scores
You can't apply scores with numeric filters (such as `>=`, `!=`, `>`).
You can apply scores only to values using the `attribute:value` syntax.
## See also
* [Using filter scoring to control the order of records (blog)](https://www.algolia.com/blog/engineering/feature-spotlight-optional-filters/)
* [Optional filters and filter scoring (blog)](https://www.algolia.com/blog/engineering/discover-what-fuzzy-search-is-with-fuzzy-matching/)
# Filters and facet filters
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters
Learn about the relationship between filters and facet filters.
For most uses, the `filters` parameter is the best choice for filtering.
However, `numericFilters` and `facetFilters` also offer comparable capabilities and can be used to suit different filtering needs.
## Differences between filtering parameters
Whatever filtering parameter you use, the aim is to filter based on their attributes.
To exclude results, use the NOT [operator](/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators).
However, there are some differences:
### `filters`
| | Notes |
| - | - |
| Parameter | [`filters`](/doc/api-reference/api-parameters/filters) |
| Syntax | String |
| Best for | Filtering with a mix of string and numeric filters |
| Key points | A familiar SQL syntax. You can use boolean logical operators: AND, OR, NOT |
#### Examples
Find all books written by Stephen King:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Filters = "author:\"Stephen King\"" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "author:\"Stephen King\"",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("author:\"Stephen King\""))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("author:\"Stephen King\""),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: 'author:"Stephen King"' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "author:\"Stephen King\""),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'author:"Stephen King"',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": 'author:"Stephen King"',
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "author:\"Stephen King\"")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("author:\"Stephen King\"")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(filters: "author:\"Stephen King\""))
)
```
Exclude all non-fiction books (based on their [tag](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-tags)):
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Filters = "NOT _tags:non-fiction" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "NOT _tags:non-fiction",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("NOT _tags:non-fiction"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("NOT _tags:non-fiction"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: 'NOT _tags:non-fiction' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "NOT _tags:non-fiction"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'NOT _tags:non-fiction',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "NOT _tags:non-fiction",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "NOT _tags:non-fiction")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("NOT _tags:non-fiction")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(filters: "NOT _tags:non-fiction"))
)
```
### `numericFilters`
| | Notes |
| - | - |
| Parameter | [`numericFilters`](/doc/api-reference/api-parameters/numericFilters) |
| Syntax | String |
| Best for | Filtering dates and numeric values |
| Key points | Like `filters` but limited to numeric data |
#### Example
Find all books that cost more than \$20:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { NumericFilters = new NumericFilters("price>20") })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
numericFilters: "price>20",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetNumericFilters(search.StringAsNumericFilters("price>20")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setNumericFilters(NumericFilters.of("price>20")),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { numericFilters: 'price>20' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(numericFilters = NumericFilters.of("price>20")),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['numericFilters' => 'price>20',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"numericFilters": "price>20",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(numeric_filters: "price>20")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
numericFilters = Some(NumericFilters("price>20"))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(numericFilters: SearchNumericFilters
.string("price>20")))
)
```
### `facetFilters`
| | Notes |
| - | - |
| Parameter | [`facetFilters`](/doc/api-reference/api-parameters/facetFilters) |
| Syntax | String or string array |
| Best for | Filtering with a mix of strings and complex boolean logic |
| Key points | Can't be used for numeric filtering |
#### Examples
Find books written by Stephen King or belonging to the horror genre, and also published by Penguin.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
FacetFilters = new FacetFilters(
new List
{
new FacetFilters("publisher:Penguin"),
new FacetFilters(
new List
{
new FacetFilters("author:Stephen King"),
new FacetFilters("genre:Horror"),
}
),
}
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
facetFilters: [
"publisher:Penguin",
[
"author:Stephen King",
"genre:Horror",
],
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFacetFilters(search.ArrayOfFacetFiltersAsFacetFilters(
[]search.FacetFilters{*search.StringAsFacetFilters("publisher:Penguin"), *search.ArrayOfFacetFiltersAsFacetFilters(
[]search.FacetFilters{*search.StringAsFacetFilters("author:Stephen King"), *search.StringAsFacetFilters("genre:Horror")})})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFacetFilters(
FacetFilters.of(
Arrays.asList(
FacetFilters.of("publisher:Penguin"),
FacetFilters.of(Arrays.asList(FacetFilters.of("author:Stephen King"), FacetFilters.of("genre:Horror")))
)
)
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { facetFilters: ['publisher:Penguin', ['author:Stephen King', 'genre:Horror']] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
facetFilters =
FacetFilters.of(
listOf(
FacetFilters.of("publisher:Penguin"),
FacetFilters.of(
listOf(FacetFilters.of("author:Stephen King"), FacetFilters.of("genre:Horror"))
),
)
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['facetFilters' => [
'publisher:Penguin',
[
'author:Stephen King',
'genre:Horror',
],
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"facetFilters": [
"publisher:Penguin",
[
"author:Stephen King",
"genre:Horror",
],
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
facet_filters: ["publisher:Penguin", ["author:Stephen King", "genre:Horror"]]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
facetFilters = Some(
FacetFilters(
Seq(
FacetFilters("publisher:Penguin"),
FacetFilters(Seq(FacetFilters("author:Stephen King"), FacetFilters("genre:Horror")))
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(facetFilters: SearchFacetFilters
.arrayOfSearchFacetFilters([
SearchFacetFilters.string("publisher:Penguin"),
SearchFacetFilters.arrayOfSearchFacetFilters([
SearchFacetFilters.string("author:Stephen King"),
SearchFacetFilters.string("genre:Horror"),
]),
])))
)
```
Exclude eBooks from the results:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject { FacetFilters = new FacetFilters("category:-Ebook") }
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
facetFilters: "category:-Ebook",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFacetFilters(search.StringAsFacetFilters("category:-Ebook")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFacetFilters(FacetFilters.of("category:-Ebook")),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { facetFilters: 'category:-Ebook' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(facetFilters = FacetFilters.of("category:-Ebook")),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['facetFilters' => 'category:-Ebook',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"facetFilters": "category:-Ebook",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(facet_filters: "category:-Ebook")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
facetFilters = Some(FacetFilters("category:-Ebook"))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(facetFilters: SearchFacetFilters
.string("category:-Ebook")))
)
```
## Combining filters and facet filters
You can combine filter and facet filters in the same search query. For example:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Filters = "(author:\"Stephen King\" OR genre:\"Horror\")",
FacetFilters = new FacetFilters(
new List { new FacetFilters("publisher:Penguin") }
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "(author:\"Stephen King\" OR genre:\"Horror\")",
facetFilters: [
"publisher:Penguin",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().
SetFilters("(author:\"Stephen King\" OR genre:\"Horror\")").
SetFacetFilters(search.ArrayOfFacetFiltersAsFacetFilters(
[]search.FacetFilters{*search.StringAsFacetFilters("publisher:Penguin")})),
)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject()
.setFilters("(author:\"Stephen King\" OR genre:\"Horror\")")
.setFacetFilters(FacetFilters.of(Arrays.asList(FacetFilters.of("publisher:Penguin")))),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: '(author:"Stephen King" OR genre:"Horror")', facetFilters: ['publisher:Penguin'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
filters = "(author:\"Stephen King\" OR genre:\"Horror\")",
facetFilters = FacetFilters.of(listOf(FacetFilters.of("publisher:Penguin"))),
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => '(author:"Stephen King" OR genre:"Horror")',
'facetFilters' => [
'publisher:Penguin',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": '(author:"Stephen King" OR genre:"Horror")',
"facetFilters": [
"publisher:Penguin",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
filters: "(author:\"Stephen King\" OR genre:\"Horror\")",
facet_filters: ["publisher:Penguin"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("(author:\"Stephen King\" OR genre:\"Horror\")"),
facetFilters = Some(FacetFilters(Seq(FacetFilters("publisher:Penguin"))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
filters: "(author:\"Stephen King\" OR genre:\"Horror\")",
facetFilters: SearchFacetFilters
.arrayOfSearchFacetFilters([SearchFacetFilters.string("publisher:Penguin")])
))
)
```
In this case, the results of this are the same as the preceding [example for `facetFilters`](#examples) but more generally may prove to be more flexible.
For example, you can use `filters` programmatically to create a reduced set of records based on a user's profile, and then apply `facetFilters` to reflect the facets that users have chosen.
## Limits and performance
Since each filter adds complexity to queries, there's a limit on the total number of combined filters you can use of .
Faceting on unique or uncommon attributes [may degrade search performance and relevance](/doc/api-reference/api-parameters/facets#usage).
# Geo location
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation
Geo searching is a way to search around a specific location or area and rank results based on their distance.
Geo-searching is a way to filter and sort results by distance or around specific geographical locations.
You can limit your results to a street, a city, or parts of the world.
You can sort your results according to how near or far they're from a defined location.
Browse the airport search example built with React InstantSearch.
## Geo-search
Geo-searching lets you:
* Filter and sort around a set of latitude and longitude coordinates
* Filter and sort around user location based on their IP address (IPv4 only)
* Filter by one or more box-shaped geographical areas (bounded boxes)
* Filter by one or more freely drawn geographical areas (polygons)
## Enabling geo-search by adding geolocation data to records
To enable geo-searching, you must have geolocation data in your as a `_geoloc` attribute that contains latitude and longitude (`lat` and `lng` properties).
The coordinates must be numbers, not strings.
Here's an example of a record with one geolocation:
```json JSON icon=braces theme={"system"}
{
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
}
}
```
Here's an example of a record with multiple locations:
```json JSON icon=braces theme={"system"}
{
"_geoloc": [
{ "lat": 47.27943, "lng": 5.10645 },
{ "lat": 47.293228, "lng": 5.00457 },
{ "lat": 47.316669, "lng": 5.01667 }
]
}
```
Algolia searches geolocations with a default of 10 m of precision (4 decimal places).
You can index coordinates with more precision, and the original values are returned with each hit.
However, search uses the floored value.
For details, see [Geographical ranking precision](/doc/guides/managing-results/refine-results/geolocation/in-depth/geo-ranking-precision).
Once your records contain geographical information, you can do two things:
* [Geographical filtering](#geographical-filtering-and-sorting)
* [Geographical ranking](#geographical-ranking)
## Geographical filtering and sorting
Geo-searching filters results based on a set of geographic coordinates.
You can define one or more geographic areas: the engine will only return records within that area.
You can refine this behavior by changing boundaries, distance, or precision.
### Filter around a central point
You can define a central geographical point and show only records within a certain radius of that point.
This central point is defined in one of two ways:
* Sent with the query
* Derived from user IP address (supports only IPv4)
Determine the size of this radius depending on the density of hits around the central point.
The radius should be small if many hits are close to the center—the fewer hits near the center, the larger the radius.
Radius-based filtering creates a circle within which all results fall.
It also **creates a ranking based on the distance from the central point**.
Records closest to the center rank higher than records further away.
For more information, see:
* [Filter results around a location](/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-around-a-location)
* [Understand the engine's geographical ranking](/doc/guides/managing-results/refine-results/geolocation/how-to/geo-ranking-info)
* [`aroundLatLng`](/doc/api-reference/api-parameters/aroundLatLng)
* [`aroundLatLngViaIP`](/doc/api-reference/api-parameters/aroundLatLngViaIP)
#### Increase the search radius
The default behavior is to stop finding records after approximately 1,000 matching records.
Therefore, your search radius could be quite small in dense areas.
Consider the number of cafés in Paris: 1,000 matching records wouldn't be enough to list them all.
You can override this behavior by forcing a larger radius,
either by using [`aroundRadius`](/doc/api-reference/api-parameters/aroundRadius)
or [`minimumAroundRadius`](/doc/api-reference/api-parameters/minimumAroundRadius).
This override only applies to radius-based searches, not area-based ones like rectangles and polygons.
Even with a larger radius, the results still match any applied query or filter.
The number of results can be low if the query or filters are restrictive, even if you set a huge radius.
### Sorting by distance
If you want to sort but not filter results based on radial distance,
set the [`aroundRadius`](/doc/api-reference/api-parameters/aroundRadius) parameter to `all`.
This means your records are **ordered but not restricted** based on distance from a provided point.
### Filtering inside rectangular or polygonal areas
You can restrict results to a given area by setting geometric boundaries in either the shape of a rectangle or other polygon.
To restrict results within a given area, include the `lat` and `lng` of a bounding shape in the search parameters.
The engine excludes all results outside of the shape.
**Filtering within or around an area doesn't affect the ranking of results.**
Additionally, area-based geo-searching doesn't work in combination with radius-based geo-searching.
When filtering on rectangular or polygonal areas, the engine considers all results within the area as being geographically equal.
To learn more, see:
* [Filter results inside a rectangle](/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-inside-a-rectangle-area)
* [Filter results inside a polygon](/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-inside-a-polygonal-area)
* [How to solve the 180th meridian issue](/doc/guides/managing-results/refine-results/geolocation/how-to/how-to-resolve-180-meridian-issue)
### Don't mix bounding boxes and polygons
You can't use [`insideBoundingBox`](/doc/api-reference/api-parameters/insideBoundingBox) and [`insidePolygon`](/doc/api-reference/api-parameters/insidePolygon) together.
If you provide both, only `insideBoundingBox` actions will run.
For example, you have created an app for a chain of restaurants in the South West US,
and you want to show the locations of your restaurants on a map:
* You use a rectangular bounding box to display the South West region of the US.
Blue pins could indicate cities where you have restaurants.
* As users zoom in, you want polygons to mark the boundaries of these cities.
Red pins could mark the locations of individual restaurants within the polygons.
However, since `insideBoundingBox` takes precedence,
you'll only see blue pins on the map.
**For this reason, using a mix of bounding boxes and polygons isn't recommended.**
## Geographical ranking
Geographical ranking is based on the distance between the geolocation in the search query and the best-matching geolocation in the record.
For example, when searching around a location, the results can be sorted by distance - the closer the record is to the `lat`/`lng` you provided, the higher it ranks in the results.
### Geo criterion in the ranking formula
Algolia's ranking criteria contain a [Geo criterion](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria#geo-if-applicable), which by default is the second-ranking criterion.
This criterion is only meaningful when records contain the `_geoloc` attribute with the corresponding `lat` and `lng` properties.
### Change precision
By default, results are ranked by distance in 10 m increments.
Records within 10 m to the center rank higher than records that are 20 m away.
In some cases, this might be too precise.
Consider searching for the best restaurants within walking distance.
Here, larger increments, such as 200 m might be more appropriate.
Change the [Geo criterion's precision](/doc/guides/managing-results/refine-results/geolocation/in-depth/geo-ranking-precision) to alter the default.
## Display results on a map
Use your results' latitude and longitude values to plot each record on a map.
For more information, see [Display results with Google Maps and InstantSearch](/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/js).
## See also
* [How geo-search improves search and discovery (blog)](https://www.algolia.com/blog/product/how-geo-search-improves-search-and-discovery/)
* [Geo-search on mobile devices (blog)](https://www.algolia.com/blog/engineering/geo-spatial-search-on-mobile/)
# Filter results around a location
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-around-a-location
Use Algolia's geographical search capabilities to filter results around a location.
This guide shows you how to filter results around a location.
This location can be set manually or taken from the a user's current position.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
## Dataset
The guide uses a dataset of the 3,000+ biggest airports in the world.
```json JSON icon="braces" theme={"system"}
[
{
"objectID": "3797",
"name": "John F Kennedy Intl",
"city": "New York",
"country": "United States",
"iata_code": "JFK",
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
},
"links_count": 911
}
]
```
To tell Algolia where each record is located,
the latitude and longitude must be stored in the `_geoloc` attribute.
## Initialize the client
1. [Download the data set](https://github.com/algolia/datasets/blob/master/airports/airports.json)
2. [Set up an API client and send your data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api)
## Configure index settings
Even if you just want to sort by distance to a location, your textual relevance should also be good so that users can refine the search with a query.
To do that, you must configure the .
The searchable attributes are: `name`, `city`, `country`, and `iata_code`.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "name", "country", "city", "iata_code" },
CustomRanking = new List { "desc(links_count)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"country",
"city",
"iata_code",
],
customRanking: [
"desc(links_count)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "country", "city", "iata_code"}).SetCustomRanking(
[]string{"desc(links_count)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setSearchableAttributes(Arrays.asList("name", "country", "city", "iata_code"))
.setCustomRanking(Arrays.asList("desc(links_count)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
searchableAttributes: ['name', 'country', 'city', 'iata_code'],
customRanking: ['desc(links_count)'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
searchableAttributes = listOf("name", "country", "city", "iata_code"),
customRanking = listOf("desc(links_count)"),
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'name',
'country',
'city',
'iata_code',
],
'customRanking' => [
'desc(links_count)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"country",
"city",
"iata_code",
],
"customRanking": [
"desc(links_count)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
searchable_attributes: ["name", "country", "city", "iata_code"],
custom_ranking: ["desc(links_count)"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "country", "city", "iata_code")),
customRanking = Some(Seq("desc(links_count)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: ["name", "country", "city", "iata_code"],
customRanking: ["desc(links_count)"]
)
)
```
### Ranking
When filtering around a location, Algolia can also sort the results by distance from this location.
This sorting by distance happens in the ranking formula's `geo` criterion.
If `geo` isn't active, you can't sort by distance.
## Filtering around a given location
To filter airports around New York City (latitude 40.71 and longitude -74.01),
use the [`aroundLatLng`](/doc/api-reference/api-parameters/aroundLatLng) parameter.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { AroundLatLng = "40.71, -74.01" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
aroundLatLng: "40.71, -74.01",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetAroundLatLng("40.71, -74.01"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setAroundLatLng("40.71, -74.01"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { aroundLatLng: '40.71, -74.01' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(aroundLatLng = "40.71, -74.01"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['aroundLatLng' => '40.71, -74.01',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"aroundLatLng": "40.71, -74.01",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(around_lat_lng: "40.71, -74.01")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
aroundLatLng = Some("40.71, -74.01")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(aroundLatLng: "40.71, -74.01"))
)
```
An empty query (`''`) tells Algolia to retrieve all airports
## Filtering around the user's current location
As you don't know your user's coordinates in advance, you can retrieve their IP address's associated location with the [`aroundLatLngViaIP`](/doc/api-reference/api-parameters/aroundLatLngViaIP) parameter.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { AroundLatLngViaIP = true })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
aroundLatLngViaIP: true,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetAroundLatLngViaIP(true))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex("INDEX_NAME", new SearchParamsObject().setAroundLatLngViaIP(true), Hit.class);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { aroundLatLngViaIP: true },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(aroundLatLngViaIP = true),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['aroundLatLngViaIP' => true,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"aroundLatLngViaIP": True,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(around_lat_lng_via_ip: true)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
aroundLatLngViaIP = Some(true)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(aroundLatLngViaIP: true))
)
```
## Filtering by radius
By default, the engine automatically defines a radius to filter on depending on the population density of the user's location.
To define the radius yourself, use the [`aroundRadius`](/doc/api-reference/api-parameters/aroundRadius) parameter.
The bigger the radius, the less filtering you have.
This example sorts by distance to New York City, with a radius of 1,000 km.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
AroundLatLng = "40.71, -74.01",
AroundRadius = new AroundRadius(1000000),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
aroundLatLng: "40.71, -74.01",
aroundRadius: 1000000,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetAroundLatLng("40.71, -74.01").SetAroundRadius(search.Int32AsAroundRadius(1000000)))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setAroundLatLng("40.71, -74.01").setAroundRadius(AroundRadius.of(1000000)),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { aroundLatLng: '40.71, -74.01', aroundRadius: 1000000 },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
aroundLatLng = "40.71, -74.01",
aroundRadius = AroundRadius.of(1000000),
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['aroundLatLng' => '40.71, -74.01',
'aroundRadius' => 1000000,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"aroundLatLng": "40.71, -74.01",
"aroundRadius": 1000000,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(around_lat_lng: "40.71, -74.01", around_radius: 1000000)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
aroundLatLng = Some("40.71, -74.01"),
aroundRadius = Some(AroundRadius(1000000))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
aroundLatLng: "40.71, -74.01",
aroundRadius: SearchAroundRadius.int(1_000_000)
))
)
```
# Filter results inside a polygon
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-inside-a-polygonal-area
Use Algolia's geographical search capabilities to filter results inside a polygon.
In this tutorial, you'll learn how to filter results around a polygonal location.
This location can either be set manually or taken from the current user position.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
## Dataset
The guide uses a dataset of the 3,000+ biggest airports in the world.
```json JSON icon="braces" theme={"system"}
[
{
"objectID": "3797",
"name": "John F Kennedy Intl",
"city": "New York",
"country": "United States",
"iata_code": "JFK",
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
},
"links_count": 911
}
]
```
To tell Algolia where each record is located,
the latitude and longitude must be stored in the `_geoloc` attribute.
## Initialize the client
1. [Download the data set](https://github.com/algolia/datasets/blob/master/airports/airports.json)
2. [Set up an API client and send your data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api)
## Configure index settings
Even if you just want to sort by distance to a location, your textual relevance should also be good so that users can refine the search with a query.
To do that, you must configure the .
The searchable attributes are: `name`, `city`, `country`, and `iata_code`.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "name", "country", "city", "iata_code" },
CustomRanking = new List { "desc(links_count)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"country",
"city",
"iata_code",
],
customRanking: [
"desc(links_count)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "country", "city", "iata_code"}).SetCustomRanking(
[]string{"desc(links_count)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setSearchableAttributes(Arrays.asList("name", "country", "city", "iata_code"))
.setCustomRanking(Arrays.asList("desc(links_count)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
searchableAttributes: ['name', 'country', 'city', 'iata_code'],
customRanking: ['desc(links_count)'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
searchableAttributes = listOf("name", "country", "city", "iata_code"),
customRanking = listOf("desc(links_count)"),
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'name',
'country',
'city',
'iata_code',
],
'customRanking' => [
'desc(links_count)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"country",
"city",
"iata_code",
],
"customRanking": [
"desc(links_count)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
searchable_attributes: ["name", "country", "city", "iata_code"],
custom_ranking: ["desc(links_count)"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "country", "city", "iata_code")),
customRanking = Some(Seq("desc(links_count)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: ["name", "country", "city", "iata_code"],
customRanking: ["desc(links_count)"]
)
)
```
## Filtering inside a polygonal area
The USA can be considered as a polygon:
To filter inside this rectangle, you need to pass the latitude and longitude of all the points to the `insidePolygon` parameter:
* 42.01,-124.31,
* 48.835509470063045,-124.40453125000005
* 45.01082951668149,-65.95726562500005
* 31.247243545293433,-81.06578125000004
* 25.924152577235226,-97.68234374999997
* 32.300311895879545,-117.54828125
The empty query (`''`) returns all matching airports.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
InsidePolygon = new List>
{
new List
{
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
},
},
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
insidePolygon: [
[
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
],
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetInsidePolygon(
[][]float64{
{
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
},
}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setInsidePolygon(
Arrays.asList(
Arrays.asList(
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125
)
)
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: {
insidePolygon: [
[
42.01, -124.31, 48.835509470063045, -124.40453125000005, 45.01082951668149, -65.95726562500005,
31.247243545293433, -81.06578125000004, 25.924152577235226, -97.68234374999997, 32.300311895879545,
-117.54828125,
],
],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
insidePolygon =
listOf(
listOf(
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
)
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['insidePolygon' => [
[
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
],
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"insidePolygon": [
[
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
],
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
inside_polygon: [
[
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125
]
]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
insidePolygon = Some(
Seq(
Seq(42.01, -124.31, 48.835509470063045, -124.40453125000005, 45.01082951668149, -65.95726562500005,
31.247243545293433, -81.06578125000004, 25.924152577235226, -97.68234374999997, 32.300311895879545,
-117.54828125)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(insidePolygon: [[
42.01,
-124.31,
48.835509470063045,
-124.40453125000005,
45.01082951668149,
-65.95726562500005,
31.247243545293433,
-81.06578125000004,
25.924152577235226,
-97.68234374999997,
32.300311895879545,
-117.54828125,
]]))
)
```
# Filter results inside a rectangle
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/how-to/filter-results-inside-a-rectangle-area
Use Algolia's geographical search capabilities to filter results inside a rectangle.
In this tutorial, you'll see how to can filter results inside a rectangular area.
This location can either be set manually or taken from the current user position.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
## Dataset
The guide uses a dataset of the 3,000+ biggest airports in the world.
```json JSON icon="braces" theme={"system"}
[
{
"objectID": "3797",
"name": "John F Kennedy Intl",
"city": "New York",
"country": "United States",
"iata_code": "JFK",
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
},
"links_count": 911
}
]
```
To tell Algolia where each record is located,
the latitude and longitude must be stored in the `_geoloc` attribute.
## Initialize the client
1. [Download the data set](https://github.com/algolia/datasets/blob/master/airports/airports.json)
2. [Set up an API client and send your data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api)
## Configure index settings
Even if you just want to sort by distance to a location, your textual relevance should also be good so that users can refine the search with a query.
To do that, you must configure the .
The searchable attributes are: `name`, `city`, `country`, and `iata_code`.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "name", "country", "city", "iata_code" },
CustomRanking = new List { "desc(links_count)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"country",
"city",
"iata_code",
],
customRanking: [
"desc(links_count)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "country", "city", "iata_code"}).SetCustomRanking(
[]string{"desc(links_count)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setSearchableAttributes(Arrays.asList("name", "country", "city", "iata_code"))
.setCustomRanking(Arrays.asList("desc(links_count)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
searchableAttributes: ['name', 'country', 'city', 'iata_code'],
customRanking: ['desc(links_count)'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
searchableAttributes = listOf("name", "country", "city", "iata_code"),
customRanking = listOf("desc(links_count)"),
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'name',
'country',
'city',
'iata_code',
],
'customRanking' => [
'desc(links_count)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"country",
"city",
"iata_code",
],
"customRanking": [
"desc(links_count)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
searchable_attributes: ["name", "country", "city", "iata_code"],
custom_ranking: ["desc(links_count)"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "country", "city", "iata_code")),
customRanking = Some(Seq("desc(links_count)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: ["name", "country", "city", "iata_code"],
customRanking: ["desc(links_count)"]
)
)
```
## Filtering inside a rectangle area
The USA can be considered as a polygon:
To filter inside this rectangle,
you need to pass the upper-left and bottom-right latitude and longitude to the `insideBoundingBox` parameter:
* 49.067996905313834, 65.73828125
* 25.905859247243498, 128.8046875
The empty query (`''`) returns all matching airports.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
InsideBoundingBox = new InsideBoundingBox(
new List>
{
new List { 49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875 },
}
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
insideBoundingBox: [
[
49.067996905313834,
65.73828125,
25.905859247243498,
128.8046875,
],
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetInsideBoundingBox(search.ArrayOfArrayOfFloat64AsInsideBoundingBox(
[][]float64{
{49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875},
})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setInsideBoundingBox(
InsideBoundingBox.of(Arrays.asList(Arrays.asList(49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875)))
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { insideBoundingBox: [[49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875]] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
insideBoundingBox =
InsideBoundingBox.of(
listOf(listOf(49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875))
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['insideBoundingBox' => [
[
49.067996905313834,
65.73828125,
25.905859247243498,
128.8046875,
],
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"insideBoundingBox": [
[
49.067996905313834,
65.73828125,
25.905859247243498,
128.8046875,
],
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
inside_bounding_box: [[49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875]]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
insideBoundingBox =
Some(InsideBoundingBox(Seq(Seq(49.067996905313834, 65.73828125, 25.905859247243498, 128.8046875))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(insideBoundingBox: SearchInsideBoundingBox
.arrayOfArrayOfDouble([[
49.067996905313834,
65.73828125,
25.905859247243498,
128.8046875,
]])))
)
```
# Geo ranking information
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/how-to/geo-ranking-info
The data Algolia uses for geographical ranking.
When you use Algolia's Geo feature, you can get information about each record's geographical ranking.
Use this information to detect incorrect or missing geographical data.
It can also show whether you need to adjust [precision](/doc/guides/managing-results/refine-results/geolocation/in-depth/geo-ranking-precision)
or [tweak your geographical settings](/doc/guides/managing-results/refine-results/geolocation).
To get geo-ranking data, you need to set [`getRankingInfo=true`](/doc/api-reference/api-parameters/getRankingInfo) in your query parameter.
Doing this adds two new attributes to `_rankingInfo`: `matchedGeoLocation` and `aroundLatLng`.
## `matchedGeoLocation` attribute
The `matchedGeoLocation` attribute contains geographical information,
including the location that matched when you were performing a .
Use it to identify the matched location and retrieve related details, such as its name.
`matchedGeoLocation` is a geographical location object that matches the query.
It consists of:
* `lat` (float): latitude of matched location
* `lng` (float): longitude of matched location
* `distance` (integer): distance between the matched location and the search location (in meters).
`lat` and `lng` are Algolia's internal values, not the original `_geoloc` on the record.
Returned values may show fewer decimals than the values you indexed.
For background, see [Geographical ranking precision](/doc/guides/managing-results/refine-results/geolocation/in-depth/geo-ranking-precision).
Unlike `geoDistance`, the `distance` value isn't divided by geographical precision.
Here's an example of `_rankingInfo` attribute returned on a record with several geo-location
(the `matchedGeoLocation` attribute will be in `_rankingInfo` only if a geographical query is performed):
```json JSON icon=braces theme={"system"}
{
"_rankingInfo": {
"nbTypos": 0,
"firstMatchedWord": 0,
"proximityDistance": 0,
"userScore": 7,
"geoDistance": 1600,
"geoPrecision": 1,
"nbExactWords": 0,
"words": 0,
"filters": 0,
"matchedGeoLocation": {
"lat": 37.3688,
"lng": -122.036,
"distance": 1600
}
}
}
```
## `aroundLatLng` attribute
`aroundLatLng` (string, optional):
* Only returned when `aroundLatLngViaIP` is set
* Computed geographical location
* Legacy reason (`param` is a string not an object): `${lat}, ${lng}`
# How to solve the 180th meridian issue
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/how-to/how-to-resolve-180-meridian-issue
To solve the 180th meridian issue, create two separate areas east and west of this line
The [180th meridian](https://wikipedia.org/wiki/180th_meridian) is the geographical boundary between the Eastern and Western hemispheres.
It passes through the Pacific Ocean, Russia, Alaska, and Fiji.
## The problem
When searching by location, the 180th meridian can lead to unexpected results.
If a rectangle or polygon includes the 180th meridian, the search won't include results from inside the area.
Instead, the results return everything outside the area you selected.
## The reason: longitudinal value reversal
When you cross the 180th meridian, the longitude switches from positive to negative numbers: from 180 to -180, not 181.
## The solution: create two areas
If your area crosses the meridian, split the affected area into two adjacent shapes, one east of the meridian and one west.
### Rectangles
Consider a rectangle that includes the 180th meridian: `[70, 170, 60, -170]`.
To resolve the issue, split this rectangle into two smaller ones:
* Rectangle 1: `[70, 170, 60, 180]` (east of the meridian)
* Rectangle 2: `[70, -180, 60, -170]` (west of the meridian)
Use [`insideBoundingBox`](/doc/api-reference/api-parameters/insideBoundingBox) to define these shapes with the value `[[70, 170, 60, 180], [70, -180, 60, -170]]`.
### Polygons
Consider a polygon that includes the 180th meridian: `[[70, 170], [80, 160], [90, 140], [80, 120], [70, 110], [60, 130], [70, 140]]`.
To resolve the issue, use [`insidePolygon`](/doc/api-reference/api-parameters/insidePolygon) to split this shape into two smaller ones:
* Polygon 1: `[[70, 170], [80, 160], [90, 140], [80, 120], [70, 110], [60, 130], [70, 140]]` (area east of the meridian)
* Polygon 2: `[[60, 130], [70, 140], [70, 170], [60, 180], [50, 160], [60, 150], [60, 130]]` (area west of the meridian)
# Geographical ranking precision
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/geolocation/in-depth/geo-ranking-precision
How Algolia stores, ranks, and displays geographical coordinates.
## How Algolia handles coordinate precision
Algolia uses three distinct precisions when working with geolocations:
* **Indexed precision**: the `lat`/`lng` values you sent in `_geoloc`. Echoed back unchanged in each hit.
* **Internal precision**: the values Algolia uses for geo-search. Stored at roughly 10 m resolution (4 decimal places).
* **Display precision**: how the engine prints internal values in `_rankingInfo.matchedGeoLocation`.
The original `_geoloc` in your hits is preserved at the precision you indexed.
The ranking and `matchedGeoLocation` fields use floored values.
## Geo-search precision
Algolia searches geolocations with a default of about 10 m of resolution.
Coordinates finer than that are floored to fit the grid.
Search therefore by default treats two records within roughly 10 m of each other as the same point.
You can widen this grid with [`aroundPrecision`](/doc/api-reference/api-parameters/aroundPrecision), but you can't go below the 10 m floor.
The original `_geoloc` you indexed is returned unchanged in each hit.
Only the search and `matchedGeoLocation` use approximate values.
## What `matchedGeoLocation` returns
When you request [`getRankingInfo=true`](/doc/api-reference/api-parameters/getRankingInfo), each hit includes `_rankingInfo.matchedGeoLocation` with the `lat`, `lng`, and `distance` of the point that matched your geo-query.
These coordinates are Algolia's floored internal value, not your original `_geoloc`.
To read the coordinates exactly as you sent them, use the `_geoloc` field on the hit.
## Group results with `aroundPrecision`
Precision (using the search query parameter `aroundPrecision`) is used to group results that have more or less the same distance from a central point.
Algolia's default precision is 10 meters.
However, a 10-meter difference might not be significant: intervals of 200 meters may make more sense.
By creating 200-meter intervals, you rank all that fall within that interval the same.
They're ranked higher than all records between 200 and 400 meters.
If you set `aroundPrecision` to 200, you'll have the following search results:
* Group of results in the 0-200 meters range will be the best ranked (ranked equally)
* Group of results in the 200-400 meters range will be ranked a bit lower
* Group of results in the 400-600 meters range will be ranked even lower
Another example is when users are looking for nearby recommended restaurants.
The goal is to display results that are a good mix of nearby and highly rated restaurants.
If there are two restaurants within 200 meters of a user's location:
* Restaurant A with a 5 star rating and distance of 150 meters (farther)
* Restaurant B with a 3 star rating and distance of 100 meters (closer)
If you're only sorting by geographical distance, restaurant B will be ranked higher since it's closer to the user.
But by setting `aroundPrecision` to 200, restaurant A and B will be ranked equally: the distances of 150 and 100 will be considered equal to 0 (and distances of 250 and 210 will be equal to 200, and so on).
Going further, since the results of A and B are tied under the `Geo` criterion, the search engine will examine the other criteria in the ranking formula to further sort the results.
Restaurant A will therefore be ranked higher in the result due to its higher rating.
# Deduplicate results with distinct
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/grouping
Distinct deduplicates search results that have the same value for an attribute.
The `distinct` feature deduplicates search results by grouping that share the same attribute value,
so your search results show one representative record per group instead of every matching record.
This is useful when:
* **[Handling item variations](#handle-item-variations):** if you sell t-shirts and every t-shirt has 10 color variants, show one color variant per design instead of all 10. This helps users discover more products (instead of variants of the same product). You can still show the available colors in the product details.
* **[Indexing long documents](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents):** if you split a page into one record per section, a query might return multiple chunks from the same page. With `distinct`, only the best-matching chunk appears per page or section.
## Deduplicate results with distinct
To deduplicate search results, configure both:
* [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct): the attribute Algolia uses to group records.
* [`distinct`](/doc/api-reference/api-parameters/distinct): how many records to show from each group.
For example, if you set the `url` attribute as `attributeForDistinct`,
Algolia groups records with the same `url` value.
If you set `distinct` to `true`,
Algolia returns only the best matching record from each group.
You can also set `distinct` to a number to return more than one record from each group.
### Choose an attribute for grouping
Choose an attribute that has the same value for all records you want to group together.
Use an identifier instead of a display value if names, titles, or other aspects can change.
For example:
* For product variants, use a product identifier such as `product_id`, `design`, or `sku_group`.
* For job openings, use `company`.
* For long documents, use `url`, `page_id`, or `section_id`.
To define groups of records based on queries or matching filters, see [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
## Flatten your records
To use `distinct` and `attributeForDistinct` effectively,
flatten your records:
* Create one record for each searchable item.
* Repeat the grouping attribute in each record.
The following example shows the difference between nested and flat record structures.
### Nested records
```json JSON icon=braces theme={"system"}
[
{
"objectID": "design-1",
"name": "Algolia Logo T-Shirt",
"variants": [
{
"color": "red",
"size": "S",
"sku": "algolia-logo-red-s"
},
{
"color": "blue",
"size": "M",
"sku": "algolia-logo-blue-m"
}
]
}
]
```
With this structure, when a query matches one variant, Algolia returns the whole t-shirt design with all its variants.
Each record represents a design, not an individual variant.
If you want users to search for specific variants, or if you want to show the best matching variant for each design, this structure doesn't support that use case.
### Flattened records
Instead of nested records, create one record for each variant and repeat the design information in each record:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "algolia-logo-red-s",
"design": "design-1",
"name": "Algolia Logo T-Shirt",
"color": "red",
"size": "S",
"sku": "algolia-logo-red-s"
},
{
"objectID": "algolia-logo-blue-m",
"design": "design-1",
"name": "Algolia Logo T-Shirt",
"color": "blue",
"size": "M",
"sku": "algolia-logo-blue-m"
}
]
```
Each flattened record represents a single t-shirt variant.
The `design` attribute identifies variants that belong to the same design, which you can use as the attribute for `distinct`.
## Handle item variations
Consider a shop that sells t-shirts and sweatshirts in different designs and colors.
To deduplicate variants by design, create one record for each color variant.
The inventory has:
* Two t-shirt designs: A and B
* Two sweatshirt designs: C and D
Each design comes in different colors.
Each record contains the type, design, color, and thumbnail image:
```json JSON icon=braces theme={"system"}
[
{
"type": "t-shirt",
"design": "B",
"color": "blue",
"thumbnail_url": "tshirt-B-blue.png"
},
{
"type": "sweatshirt",
"design": "C",
"color": "red",
"thumbnail_url": "sweatshirt-C-red.png"
}
]
```
You can add all possible color variations to each record.
This makes all variants available for display in your UI,
for example, as color swatches below the product thumbnail.
```json JSON icon=braces theme={"system"}
[
{
"type": "t-shirt",
"design": "B",
"color": "blue",
"thumbnail_url": "tshirt-B-blue.png",
"color_variants": ["orange", "teal", "yellow", "red", "green"]
},
{
"type": "t-shirt",
"design": "B",
"color": "orange",
"thumbnail_url": "tshirt-B-orange.png",
"color_variants": ["blue", "teal", "yellow", "red", "green"]
}
]
```
Having one record per variation lets you add granular custom ranking attributes,
such as `number_of_sales`.
Use `distinct` with `design` as the `attributeForDistinct` to show fewer variants per design,
so users can browse a wider range of designs.
## Configure distinct in the dashboard
After flattening your records, configure `distinct` to group variants.
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Configuration** tab, select **Relevance essentials > Searchable attributes**.
5. Click **Add a Searchable Attribute** and add `design`, `type`, and `color` attributes.
6. Go to **Search behavior > Deduplication and Grouping**.
7. In the **Distinct** menu, select **true**.
8. In the **Attribute for Distinct** menu, select the `design` attribute.
9. Save your changes.
Now, the results include only one color variant per design.
To control which variant appears in the results,
add an attribute such as `number_of_sales` and use it in your [custom ranking](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking).
## Configure distinct with the API
Before deduplicating results,
restrict which attributes are searchable.
For example, searching the `thumbnail_url` and `color_variants` attributes can match queries that aren't relevant.
Set only `design`, `type`, and `color` as [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "design", "type", "color" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"design",
"type",
"color",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"design", "type", "color"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setSearchableAttributes(Arrays.asList("design", "type", "color"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { searchableAttributes: ['design', 'type', 'color'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(searchableAttributes = listOf("design", "type", "color")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'design',
'type',
'color',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"design",
"type",
"color",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(searchable_attributes: ["design", "type", "color"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("design", "type", "color"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(searchableAttributes: ["design", "type", "color"])
)
```
Next, use the [`setSettings`](/doc/libraries/sdk/methods/search/set-settings) method to set `design` as [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct).
To deduplicate all following search requests,
set [`distinct`](/doc/api-reference/api-parameters/distinct) to `true` with `setSettings`.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { AttributeForDistinct = "design", Distinct = new Distinct(true) }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributeForDistinct: "design",
distinct: true,
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributeForDistinct("design").SetDistinct(search.BoolAsDistinct(true))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributeForDistinct("design").setDistinct(Distinct.of(true))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { attributeForDistinct: 'design', distinct: true },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributeForDistinct = "design", distinct = Distinct.of(true)),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributeForDistinct' => 'design',
'distinct' => true,
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributeForDistinct": "design",
"distinct": True,
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attribute_for_distinct: "design", distinct: true)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributeForDistinct = Some("design"),
distinct = Some(Distinct(true))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributeForDistinct: "design", distinct: SearchDistinct.bool(true))
)
```
After setting `attributeForDistinct`,
you can also set `distinct` as a `search` method parameter:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Distinct = new Distinct(true) })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
distinct: true,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetDistinct(search.BoolAsDistinct(true)))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setDistinct(Distinct.of(true)),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({ indexName: 'indexName', searchParams: { distinct: true } });
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(distinct = Distinct.of(true)),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['distinct' => true,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"distinct": True,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index("INDEX_NAME", Algolia::Search::SearchParamsObject.new(distinct: true))
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
distinct = Some(Distinct(true))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(distinct: SearchDistinct.bool(true)))
)
```
This deduplicates results only for the current search.
## See also
* [Structure ecommerce product records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records)
# Sorting results
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting
Learn how to sort by an attribute using replicas.
Although sorting occurs at each level of the [ranking formula](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria), in this guidance, "sorting" refers to **ordering by a specific attribute** like release date or price.
## The challenge with sorting
Sorting exposes the difference between relevance and ranking.
Instead of ranking results based on [textual relevance](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) and [custom ranking](/doc/guides/managing-results/must-do/custom-ranking), sorting ranks results based on attributes like price.
This attribute may not be related to relevance.
That means that any result, no matter how relevant
it is, might be the first result.
For example, if a user sorts on price from lowest to highest, a record with a low price may rank highest, even though it has less textual and business relevance than other records.
This type of sorting is called exhaustive sorting or hard sorting.
Despite being common, this behavior tends to hurt the search experience for most use cases because users don't necessarily get what they're looking for.
Returning to the preceding example: suppose a user searches "iPhone" and sorts results from lowest to highest price.
The top results would be cheaper iPhone accessories such as cases when users were actually looking for the least expensive iPhones.
To solve for this behavior when it's not explicitly required, Algolia supports two types of sorts.
## Exhaustive and relevant sorting
Algolia provides two types of sorts:
* Exhaustive or hard sorting
* Relevant sorting
Your use case determines which is a better fit.
### Exhaustive sorting
As [previously described](/doc/guides/managing-results/refine-results/sorting#the-challenge-with-sorting), [exhaustive sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/exhaustive-sort) is "strict" because it reorders all results based on the sorting attributes.
It's intended for use cases that require exhaustive results or where relevance isn't crucial, like an inventory app or other use cases similar to databases.
Exhaustive sorting uses [standard replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
### Relevant sorting
[Relevant sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort) reorders only relevant results.
This behavior is a state of the art sorting experience, especially in ecommerce, marketplace, and media content discovery.
Relevant sorting uses [virtual replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
## See also
* [Exhaustive sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/exhaustive-sort)
* [Relevant sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort)
# Create a replica index
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas
Replica indices let you use different ranking strategies, such as sorting.
Algolia uses one ranking strategy per index.
If you want to use different rankings for the same data,
you can use [replica indices](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
## Standard and virtual replicas
Standard replicas are exact copies of your index.
They let you to have a completely different configuration of your index,
while keeping the data in sync with your primary index.
[Virtual replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas) are an optional feature that references a primary index instead of duplicating it.
They're more efficient than standard replicas but you can only change a subset of the index settings.
Virtual replicas are optimized for [relevant sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort).
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
## Naming replica indices
To help you identify your replica indices,
adopt a naming pattern,
such as `{primaryIndex}_{sortingAttribute}_{asc_or_desc}`.
For example, `products_price_desc` is a replica index of your `products` index,
where the results are sorted by the `price` attribute in descending order.
## Create replica indices in the dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Replicas** tab, click **Create Replica Index**.
5. In the **Create a new replica** dialog,
select **Standard Replica (default)** or **Virtual Replica for Relevant Sort**,
enter a name for your replica, and click **Create replica**.
6. You can add multiple replicas at once.
7. Click **Review and Save Settings**.
## Create replica indices with the API
To create replica indices,
use the [`replicas`](/doc/api-reference/api-parameters/replicas) setting in your primary index.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { Replicas = new List { "products_price_desc" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
replicas: [
"products_price_desc",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetReplicas(
[]string{"products_price_desc"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setReplicas(Arrays.asList("products_price_desc"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { replicas: ['products_price_desc'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(replicas = listOf("products_price_desc")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['replicas' => [
'products_price_desc',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"replicas": [
"products_price_desc",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(replicas: ["products_price_desc"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
replicas = Some(Seq("products_price_desc"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(replicas: ["products_price_desc"])
)
```
To create virtual indices, use the `virtual` modifier for the replica index name:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { Replicas = new List { "virtual(products_price_desc)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
replicas: [
"virtual(products_price_desc)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetReplicas(
[]string{"virtual(products_price_desc)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setReplicas(Arrays.asList("virtual(products_price_desc)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { replicas: ['virtual(products_price_desc)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(replicas = listOf("virtual(products_price_desc)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['replicas' => [
'virtual(products_price_desc)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"replicas": [
"virtual(products_price_desc)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(replicas: ["virtual(products_price_desc)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
replicas = Some(Seq("virtual(products_price_desc)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(replicas: ["virtual(products_price_desc)"])
)
```
## Next steps
After creating your replica indices,
you can change their settings—for example,
by adding a [attribute for sorting](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute).
# Rename replica indices
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/rename-a-replica-index
Rename a replica index by detaching it from the primary index first.
**You can't rename replica indices directly.**
* Before renaming a **standard replica index**, first, detach it from its primary index. You can then rename it.
* You can't rename [**virtual replicas**](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas#standard-and-virtual-replicas). You need to delete the virtual replica and create a new one.
To rename a replica index in the Algolia dashboard:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse/) and select the **primary index** whose replica you want to rename. Alternatively, select the **replica index** you want to rename and click the name of its associated primary index under the **Index** heading.
2. With the **primary index** selected, go to the **Replica** tab, select the replica index you want to unlink, and click **Unlink Replica**.
The index is now a regular index.
3. Select the (former) replica index and [rename the index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-move-indices#rename-indices-in-the-algolia-dashboard).
4. To reattach the renamed replica index, [create a new replica index](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas) and enter the name of the renamed replica index.
# Search in a replica index
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/search-in-a-replica-index
How to search a replica index, to create different sorting options for your users.
A [replica index](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas) is a copy of your primary index that sorts results in alternative ways.
For instance, on an ecommerce site, you might want to sort search results by default from cheapest to most expensive.
You also offer a drop-down menu to let users sort from most expensive to cheapest.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
If you're using InstantSearch,
use the [`sortBy`](/doc/api-reference/widgets/sort-by/js) widget to let users choose their sorting strategy.
## Create replica indices
Let users sort results in various ways by [creating a replica](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas) for each sorting strategy.
That sorting strategy might order results by price, date, relevance, or any other criteria you provide.
## Switch index as required
When users change the sorting strategy, ensure you switch to the appropriate index (primary or replica).
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class SearchInReplicaIndex
{
async Task Main(string[] args)
{
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
var query = "query";
// 1. Change the sort dynamically based on the UI events
var sortByPrice = false;
// 2. Get the index name based on sortByPrice
var indexName = sortByPrice ? "products_price_desc" : "products";
// 3. Search on dynamic index name (primary or replica)
await client.SearchSingleIndexAsync(
indexName,
new SearchParams(new SearchParamsObject { Query = "query" })
);
}
}
```
```dart Dart theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void searchInReplicaIndex() async {
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
// 1. Change the sort dynamically based on the UI events
final sortByPrice = false;
// 2. Get the index name based on sortByPrice
// ignore: dead_code
final indexName = sortByPrice ? "products_price_desc" : "products";
// 3. Search on dynamic index name (primary or replica)
await client.searchSingleIndex(
indexName: indexName,
searchParams: SearchParamsObject(
query: "query",
),
);
}
```
```go Go theme={"system"}
package main
import (
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func searchInReplicaIndex() {
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
// 1. Change the sort dynamically based on the UI events
sortByPrice := false
// 2. Get the index name based on sortByPrice
indexName := "products"
if sortByPrice {
indexName = "products_price_desc"
}
// 3. Search on dynamic index name (primary or replica)
_, err = client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
indexName).WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("query"))))
if err != nil {
panic(err)
}
}
```
```java Java theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
public class searchInReplicaIndex {
public static void main(String[] args) throws Exception {
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
String query = "query";
// 1. Change the sort dynamically based on the UI events
boolean sortByPrice = false;
// 2. Get the index name based on sortByPrice
String indexName = sortByPrice ? "products_price_desc" : "products";
// 3. Search on dynamic index name (primary or replica)
client.searchSingleIndex(indexName, new SearchParamsObject().setQuery("query"), Hit.class);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"}
import { algoliasearch } from 'algoliasearch';
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
// 1. Change the sort dynamically based on the UI events
const sortByPrice = false;
// 2. Get the index name based on sortByPrice
const indexName = sortByPrice ? 'products_price_desc' : 'products';
// 3. Search on dynamic index name (primary or replica)
await client.searchSingleIndex({ indexName: indexName, searchParams: { query: 'query' } });
```
```kotlin Kotlin theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
suspend fun searchInReplicaIndex() {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
// 1. Change the sort dynamically based on the UI events
val sortByPrice = false
// 2. Get the index name based on sortByPrice
val indexName = if (sortByPrice) "products_price_desc" else "products"
// 3. Search on dynamic index name (primary or replica)
client.searchSingleIndex(
indexName = indexName,
searchParams = SearchParamsObject(query = "query"),
)
}
```
```php PHP theme={"system"}
searchSingleIndex(
$indexName,
['query' => 'query',
],
);
```
```python Python theme={"system"}
from algoliasearch.search.client import SearchClientSync
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
query = "query"
# 1. Change the sort dynamically based on the UI events
sort_by_price = False
# 2. Get the index name based on sortByPrice
index_name = "products_price_desc" if sort_by_price else "products"
# 3. Search on dynamic index name (primary or replica)
_client.search_single_index(
index_name=index_name,
search_params={
"query": "query",
},
)
```
```ruby Ruby theme={"system"}
require "algolia"
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
query = "query"
# 1. Change the sort dynamically based on the UI events
sort_by_price = false
# 2. Get the index name based on sortByPrice
index_name = sort_by_price ? "products_price_desc" : "products"
# 3. Search on dynamic index name (primary or replica)
client.search_single_index(index_name, Algolia::Search::SearchParamsObject.new(query: "query"))
```
```scala Scala theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}
import scala.concurrent.duration.Duration
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.SearchParamsObject
def searchInReplicaIndex(): Future[Unit] = {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
// 1. Change the sort dynamically based on the UI events
val sortByPrice = false
// 2. Get the index name based on sortByPrice
val indexName = if sortByPrice then "products_price_desc" else "products"
// 3. Search on dynamic index name (primary or replica)
client
.searchSingleIndex(
indexName = indexName,
searchParams = Some(
SearchParamsObject(
query = Some("query")
)
)
)
.map { response =>
println(response)
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func searchInReplicaIndex() async throws {
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
// 1. Change the sort dynamically based on the UI events
let sortByPrice = false
// 2. Get the index name based on sortByPrice
let indexName = sortByPrice ? "products_price_desc" : "products"
// 3. Search on dynamic index name (primary or replica)
let response: SearchResponse = try await client.searchSingleIndex(
indexName: indexName,
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(query: "query"))
)
print(response)
}
```
# Manage multiple indices
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/set-settings-and-forward-to-replicas
Copying index settings to replicas and other indices.
It's common to have several replica indices that differ from their primary by a single setting, such as the sorting strategy.
Products in an ecommerce store are a good example.
You may have a primary index sorted by ascending price, and one or more replicas that sort products by descending price, descending popularity, descending rating, and so on.
Despite these different sorting settings, **you may still want to copy settings from one index to its replicas or other indices**.
For example, if you decide to change the [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) setting, you probably want it to apply to all indices.
The `forwardToReplicas` parameter lets you forward new settings to replicas.
From there, you can change only the settings that need to be different and keep all others synchronized.
## Example of multiple indices
You have a primary index called `products` and a `products_price_desc` replica.
Both indices have the same data and settings,
apart from the [`ranking`](/doc/api-reference/api-parameters/ranking) setting.
To add a new `description` attribute to the [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) setting of the primary index and its replicas, use the `forwardToReplicas` parameter.
## Using the API
The [`setSettings`](/doc/libraries/sdk/methods/search/set-settings),
synonyms, and Rules methods, have a `forwardToReplicas` parameter.
Ry default, `forwardToReplicas` is `false`, so any setting, synonym,
or rule change only applies to the target index.
By setting it to `true`, you can forward these changes to all the replicas belonging to the index you target.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "name", "description" },
},
true
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"description",
],
),
forwardToReplicas: true,
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "description"})).WithForwardToReplicas(true))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setSearchableAttributes(Arrays.asList("name", "description")),
true
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { searchableAttributes: ['name', 'description'] },
forwardToReplicas: true,
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(searchableAttributes = listOf("name", "description")),
forwardToReplicas = true,
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'name',
'description',
],
],
true,
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"description",
],
},
forward_to_replicas=True,
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(searchable_attributes: ["name", "description"]),
true
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "description"))
),
forwardToReplicas = Some(true)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(searchableAttributes: ["name", "description"]),
forwardToReplicas: true
)
```
Settings are only forwarded to existing replicas.
If you want to forward settings and create a replica, you first have to create the replica and forward the settings after.
These must be separate operations.
## Using the dashboard
Whenever you make settings changes,
Algolia's dashboard prompts you to review them before saving and gives you the option to **Copy these settings to other indices and replicas**.
**Whenever you copy settings changes to indices, you replace the previous setting on the index.**
For example, if you change the ranking formula on the primary index to include a sort on an `inventory` attribute,
and then choose to copy the updated [`ranking`](/doc/api-reference/api-parameters/ranking) setting to all replicas,
you overwrite any previously set sorts on other attributes on the replicas.
When altering the ranking formula on indices with replicas,
you are recommended to do it on a per index basis.
While [`customRanking`](/doc/api-reference/api-parameters/customRanking) is part of the ranking formula,
it's a setting as well.
You can alter the custom ranking and forward the change to your replicas without worrying about overwriting the entire ranking formula.
**You usually don't need to worry about overwriting settings,
since you typically want most settings to be the same between replicas.**
Notable exceptions include [`ranking`](/doc/api-reference/api-parameters/ranking) and [`typoTolerance`](/doc/api-reference/api-parameters/typoTolerance).
### Copy synonyms and rules
You can import and export synonyms and rules [from the Algolia dashboard](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/export-import-indices).
# Sort an index by date
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date
How to sort your records by date.
By design, Algolia provides one [ranking strategy](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) per [index](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/prepare-data-in-depth#algolia-index): when you want to provide different rankings for the same data, **you need to use different indices for each ranking**.
These indices are called [replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
To set up sorting by attribute,
you need to understand how [replica indices](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas) work.
To sort by attribute, create a replica index and then change the ranking formula of the replica.
You can do this from Algolia's dashboard or through the API.
For chronological sorting, consider [how Algolia handles dates](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#dates).
For example, you have a blog and want to create a replica to sort search results from the most recent to the oldest post.
Because Algolia doesn't interpret dates as ISO 8601 strings (such as "2008-09-15T15:53:00"),
**you must convert your dates into Unix timestamps** (numeric values such as 1221486780) before sorting them.
## Convert dates into Unix timestamps: an example
### Before
Say you have an index called `articles`:
```json JSON icon=braces theme={"system"}
[
{
"post_title": "Let's start the adventure",
"post_date": "2012-07-01",
"author_name": "Nicolas Dessaigne",
"author_image_url": "https://secure.gravatar.com/avatar/785489bc2ac2e08ae66648a8936c1101?s=40&d=mm&r=g",
"permalink": "https://blog.algolia.com/lets-start-the-adventure/",
"excerpt": "Welcome to The Algolia Blog! It's always difficult to write the first post of a blog! What should I talk about? The company, the founders, the business, the culture? And all that knowing that virtually nobody will read except diggers in a few years (hopefully)!\nLet's concentrate instead on what we'll be blogging about. Company news obviously, but not only. I expect we'll write quite a few posts about technology, algorithms, entrepreneurship, marketing, and whatever else we'll want to share with you 🙂\nAnd most important, feel free to participate in comments or by contacting us directly. We appreciate your feedback!\nWelcome to the Algolia blog!"
},
{
"post_title": "Great discussions at LeWeb'12 London",
"post_date": "2012-07-03",
"author_name": "Nicolas Dessaigne",
"author_image_url": "https://secure.gravatar.com/avatar/785489bc2ac2e08ae66648a8936c1101?s=40&d=mm&r=g",
"permalink": "https://blog.algolia.com/great-discussions-at-leweb12-london/",
"image": "https://blog.algolia.com/wp-content/uploads/2014/03/latency-360x200.png",
"excerpt": "… take long for us to decide it was the way to go, since the perception of speed is so natural that the benefit far outweighs the longer integration code. We'll now work on simplifying it!\nWe'll soon do a post about this demo. In the meantime, stay tuned!\n \n "
}
]
```
### After
You want to create a replica that sorts your data by date.
The problem is that the `post_date` attribute has dates formatted as strings, which Algolia can't process for sorting.
Before creating a replica, you must transform these dates into [Unix timestamps](https://wikipedia.org/wiki/Unix_time).
You don't have to remove or change `post_date`.
Add a `post_date_timestamp` attribute with the proper format instead.
```json JSON icon=braces theme={"system"}
[
{
"post_title": "Let's start the adventure",
"post_date": "2012-07-01",
"post_date_timestamp": 1341100800,
"author_name": "Nicolas Dessaigne",
"author_image_url": "https://secure.gravatar.com/avatar/785489bc2ac2e08ae66648a8936c1101?s=40&d=mm&r=g",
"permalink": "https://blog.algolia.com/lets-start-the-adventure/",
"excerpt": "Welcome to The Algolia Blog! It's always difficult to write the first post of a blog! What should I talk about? The company, the founders, the business, the culture? And all that knowing that virtually nobody will read except diggers in a few years (hopefully)!\nLet's concentrate instead on what we'll be blogging about. Company news obviously, but not only. I expect we'll write quite a few posts about technology, algorithms, entrepreneurship, marketing, and whatever else we'll want to share with you 🙂\nAnd most important, feel free to participate in comments or by contacting us directly. We appreciate your feedback!\nWelcome to the Algolia blog!"
},
{
"post_title": "Great discussions at LeWeb'12 London",
"post_date": "2012-07-03",
"post_date_timestamp": 1341273600,
"author_name": "Nicolas Dessaigne",
"author_image_url": "https://secure.gravatar.com/avatar/785489bc2ac2e08ae66648a8936c1101?s=40&d=mm&r=g",
"permalink": "https://blog.algolia.com/great-discussions-at-leweb12-london/",
"image": "https://blog.algolia.com/wp-content/uploads/2014/03/latency-360x200.png",
"excerpt": "… take long for us to decide it was the way to go, since the perception of speed is so natural that the benefit far outweighs the longer integration code. We'll now work on simplifying it!\nWe'll soon do a post about this demo. In the meantime, stay tuned!\n \n "
}
]
```
## Create a replica
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
Create a replica of your `articles` index.
The recommendation is to name your replica indices with a prefix or suffix describing its sorting strategy (for example, `articles_date_desc`).
[Create a standard or virtual replica](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas) (`articles_date_desc`) from the primary index.
The primary index is likely sorted by relevance but you want to change that for `articles_date_desc`.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { Replicas = new List { "articles_date_desc" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
replicas: [
"articles_date_desc",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetReplicas(
[]string{"articles_date_desc"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setReplicas(Arrays.asList("articles_date_desc"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { replicas: ['articles_date_desc'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(replicas = listOf("articles_date_desc")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['replicas' => [
'articles_date_desc',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"replicas": [
"articles_date_desc",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(replicas: ["articles_date_desc"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
replicas = Some(Seq("articles_date_desc"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(replicas: ["articles_date_desc"])
)
```
Use the `post_date_timestamp` attribute to sort the `articles_date_desc` virtual index by date in descending order:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { Ranking = new List { "desc(post_date_timestamp)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
ranking: [
"desc(post_date_timestamp)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetRanking(
[]string{"desc(post_date_timestamp)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setRanking(Arrays.asList("desc(post_date_timestamp)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { ranking: ['desc(post_date_timestamp)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(ranking = listOf("desc(post_date_timestamp)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['ranking' => [
'desc(post_date_timestamp)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"ranking": [
"desc(post_date_timestamp)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(ranking: ["desc(post_date_timestamp)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
ranking = Some(Seq("desc(post_date_timestamp)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(ranking: ["desc(post_date_timestamp)"])
)
```
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { CustomRanking = new List { "desc(post_date_timestamp)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(post_date_timestamp)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(post_date_timestamp)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(post_date_timestamp)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(post_date_timestamp)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(customRanking = listOf("desc(post_date_timestamp)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(post_date_timestamp)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(post_date_timestamp)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(post_date_timestamp)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(post_date_timestamp)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(post_date_timestamp)"])
)
```
# Sort by attribute
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute
Learn how to sort an index by an attribute.
By default, Algolia puts the most relevant items first in results.
However, you might want to sort results by a specific numeric or boolean attribute, like price.
To sort your search results by a specific attribute:
1. [Create a replica index](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas)
2. Configure an attribute for sorting
3. Update your user interface
## Replica indices
Think of a replica index as a duplicate of your primary index but with a different sorting of results.
Choose from two types of replicas: [standard or virtual](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas#standard-and-virtual-replicas):
* Use a standard replica for exhaustive sorting.
This method sorts results by the chosen attribute.
* Use a virtual replica for relevant sorting.
This method prioritizes relevant results while still incorporating the sorting preference for the chosen attribute.
## Configure an attribute for sorting in the Algolia dashboard
Sort indices by numeric or boolean attributes.
1. [Create a replica index](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas#create-replica-indices-in-the-dashboard).
2. Refresh the dashboard page in your browser.
3. Select the replica index.
4. On the **Configuration** tab, go to **Relevant sort** or **Ranking and Sorting**.
You'll see either option, depending on the type of replica you want to configure (virtual or standard).
5. Click **+Add a sort attribute** or **+Add sort-by attribute**
6. Type in the name of the attribute you want to sort.
Be careful here: Algolia doesn't check to see if what you type matches the name of an attribute in the index.
7. Determine the sort direction (**Ascending** or **Descending**).
8. Review and save your changes.
To rank records alphabetically by a string attribute, use [`customRanking`](/doc/api-reference/api-parameters/customRanking).
For longer strings, calculate a numeric ranking value in advance or add more custom ranking attributes as tie-breakers.
For more information, see [String and null or absent values](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking#string-and-null-or-absent-values).
## Configure an attribute for sorting with the API
1. [Create a replica index](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas#create-replica-indices-with-the-api).
2. Update the [`ranking`](/doc/api-reference/api-parameters/ranking) parameter of the replica index with the [`setSettings`](/doc/libraries/sdk/v1/methods/set-settings) method.
## Update user interface
To let users select between different rankings in your user interface,
you also need to update it—for example, by providing a [`sort-by`](/doc/api-reference/widgets/sort-by/js) widget.
## Example: relevant sort by price
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
This example applies relevant sorting to a virtual replica.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { CustomRanking = new List { "desc(price)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(price)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(price)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setCustomRanking(Arrays.asList("desc(price)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(price)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(customRanking = listOf("desc(price)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(price)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(price)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(custom_ranking: ["desc(price)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(price)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(customRanking: ["desc(price)"])
)
```
For virtual replicas, you only need to include the [`customRanking`](/doc/api-reference/api-parameters/customRanking) attribute used for sorting: `price`.
## Example: exhaustive sort by price
This example applies exhaustive sorting to a standard replica.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
Ranking = new List
{
"desc(price)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
},
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
ranking: [
"desc(price)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetRanking(
[]string{"desc(price)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setRanking(
Arrays.asList("desc(price)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom")
)
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
ranking: ['desc(price)', 'typo', 'geo', 'words', 'filters', 'proximity', 'attribute', 'exact', 'custom'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
ranking =
listOf(
"desc(price)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
)
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['ranking' => [
'desc(price)',
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"ranking": [
"desc(price)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
ranking: ["desc(price)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
ranking =
Some(Seq("desc(price)", "typo", "geo", "words", "filters", "proximity", "attribute", "exact", "custom"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(ranking: [
"desc(price)",
"typo",
"geo",
"words",
"filters",
"proximity",
"attribute",
"exact",
"custom",
])
)
```
For standard replicas, you need to provide **all** ranking attributes.
# UI sorting widgets
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/use-sorting-ui-widget
How to sort in InstantSearch with UI widgets
Using replicas with different sorting strategies provides a way for users to change sorting on the frontend.
For example, if you have an ecommerce website, and by default, want to sort search results from cheapest to most expensive.
You might want to provide a drop-down menu or toggle switch to let users sort from most expensive to cheapest.
Because you have one replica per sorting strategy,
you need to **change what index Algolia must search into when users change the sorting method**.
While you could [handle it yourself](/doc/guides/managing-results/refine-results/sorting/how-to/search-in-a-replica-index), it's easier to use the InstantSearch [`sortBy`](/doc/api-reference/widgets/sort-by/js) and [`relevantSort`](/doc/api-reference/widgets/relevant-sort/js) widgets.
## Set up replicas
Before you implement the search, you need to have replicas for each sorting strategy you want to provide.
For more information, see [Sort by attribute](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute).
## Sorting widget
### Sort with `sortBy`
Dropdowns are a familiar widget, common in ecommerce scenarios and power user workflows.
[InstantSearch](/doc/guides/building-search-ui/what-is-instantsearch/js) provides sorting dropdowns with the [`sortBy`](/doc/api-reference/widgets/sort-by/js) widget.
```js JavaScript theme={"system"}
search.addWidgets([
instantsearch.widgets.sortBy({
container: "#sort-by-container",
items: [
{ value: "products", label: "Most relevant" },
{ value: "products_price_desc", label: "Highest price" },
],
}),
]);
```
```jsx React theme={"system"}
;
```
```vue Vue theme={"system"}
```
### Relevance sort toggle with `relevantSort`
If you're using [Relevant sort](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort) you can use the [`relevantSort`](/doc/api-reference/widgets/relevant-sort/js) widget to let users toggle between relevant and regular sorting.
The [`relevantSort`](/doc/api-reference/widgets/relevant-sort/js) widget comes with recommended patterns when using Relevant sort, such as displaying the number of sorted results out of the total number of results, and letting users see more results.
```js JavaScript theme={"system"}
search.addWidgets([
instantsearch.widgets.relevantSort({
container: "#relevant-sort",
templates: {
button({ isRelevantSorted }) {
return isRelevantSorted ? "See all results" : "See relevant results";
},
},
}),
]);
```
```jsx React theme={"system"}
const TextComponent = ({ isRelevantSorted }) => (
{isRelevantSorted
? 'We removed some search results to show you the most relevant ones'
: 'Currently showing all results'}
);
const ButtonTextComponent = ({ isRelevantSorted }) => (
{isRelevantSorted ? 'See all results' : 'See relevant results'}
);
```
```vue Vue theme={"system"}
We removed some search results to show you the most relevant ones
Currently showing all results
See all results
See relevant results
```
# Exhaustive sorting
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/exhaustive-sort
Learn how to exhaustively sort by an attribute using standard replicas.
Exhaustive sorting refers to strictly reordering results by an attribute.
For example, you can exhaustively sort results from lowest to highest price,
regardless of [textual relevance](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria)
and [custom ranking](/doc/guides/managing-results/must-do/custom-ranking).
It's intended for use cases requiring strict sorting or where relevance isn't essential,
like an inventory app or a database.
An alternative to exhaustive sorting is [relevant sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort).
## Effect on ranking formula
In Algolia, each of your indices has a ranking formula of two or three parts:
* [Sort-by attributes](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute) (optional)
* [Textual ranking criteria](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria)
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking), also known as business ranking criteria
### Sort-by attribute
Only add a [sort-by attribute](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute) to [replica indices](#backend-implementation).
If a sort-by attribute is present, Algolia first orders results by the sort-by attribute's value.
For example, suppose you have an where you've set "price (ascending)" as a sort-by attribute.
For the `iphone`, Algolia first selects all that match `iphone` (and alternatives like prefixes, typos, and synonyms) in the [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes).
Then, it orders the results from lowest to highest price.
* After sorting by the sort-by attribute's value (price), Algolia applies its default [textual ranking criteria](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) to records with the same price.
* If records have the same values for all textual ranking criteria, Algolia orders them by [custom ranking](/doc/guides/managing-results/must-do/custom-ranking).
In summary, **textual ranking criteria come after your selected sort-by attribute.**
With exhaustive sorting,
textual ranking criteria have little effect if the sort-by attribute values are sufficiently granular or unique.
For example, consider exhaustively sorting on a timestamp attribute down to the millisecond.
If none of your records have the same timestamp value,
the timestamp is the only attribute that plays a role in ranking.
If this is the intended behavior, no further changes are needed.
Otherwise, reduce the timestamp granularity or use [relevant sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort) instead.
To learn more, see:
* [Sort by attribute](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute)
* [Sort an index by date](/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date)
## Backend implementation
To ensure a fast search experience, the engine sorts your data at indexing time.
Therefore, each of your indices can only be sorted in one way.
However, to allow for multiple sort orders, Algolia uses replica indices.
### Exhaustive sorting uses standard index replicas
A replica is a copy of one of your indices,
with the same data and synchronized data updates, but can have unique settings.
The index from which you copy a replica's data is the replica's primary index.
**If you want to configure an exhaustive sort-by, use [standard replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas#standard-and-virtual-replicas).**
For each attribute you want to sort by,
create a replica index,
which contains a copy of all the data in your primary index.
## Frontend implementation
Replica indices manage the backend of sorting, but you still need to implement the frontend with
custom logic or [InstantSearch's SortBy UI widget](/doc/guides/managing-results/refine-results/sorting/how-to/use-sorting-ui-widget).
## Sorting and optional filters
[Optional filters](/doc/api-reference/api-parameters/optionalFilters) are applied *after* the sort-by attribute.
Therefore, you can't use rules to boost or bury records with indices with sort-by attributes.
# Relevant sorting
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort
Learn about the relevant sorting feature and how you can use it to provide more relevant results for users.
Use relevant sorting to only show the most relevant results for a user's search instead of displaying all (exhaustive sorting).
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
## Examples
These examples illustrate the difference between [exhaustive](/doc/guides/managing-results/refine-results/sorting/in-depth/exhaustive-sort) and relevant sorting.
Exhaustive sorting returns and sorts all results that match the search term `iphone`:
here, a list of phone cases appear at the top of the results.
For the same search term, relevant sorting has applied a virtual replica's [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) criteria to ensure that iPhone devices appear at the top of the results.
### Exhaustive sorting
### Relevant sorting
## Effect of relevant sorting on the ranking formula
Algolia's [ranking formula](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) isn't overridden when you use relevant sorting. Instead, the relevant sorting algorithm:
1. Sorts results using the ranking formula of the primary .
2. Selects the most relevant results. You can determine how strict this should be by adjusting [`relevancyStrictness`](/doc/api-reference/api-parameters/relevancyStrictness) on a [virtual replica](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas#standard-and-virtual-replicas).
3. Orders the selected results according to the primary index's [`sortBy`](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute) attributes (if present) and the virtual replica's [`customRanking`](/doc/api-reference/api-parameters/customRanking).
4. Settle remaining ties according to the [filter score](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring).
5. Returns the relevant results.
The ranking formula determines which results are most relevant to the ,
but custom ranking determines the order of results.
This means that parameters that affect the ranking formula don't affect result ordering.
If multiple results have the same value for a sorting attribute,
the other sorting attributes are used to decide which one comes first.
For example, if you sort according to price and then popularity,
and three results have the same cost,
the engine will prioritize them according to their popularity score.
For more information, see:
* [Sort by attribute](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute)
* [Sort an index by date](/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date)
## Relevant sorting considerations
Relevant sorting interacts with other features in ways you might not expect.
### Other re-ranking features can affect results
Other re-ranking features, such as rules, apply after relevant sorting and will modify sort order.
For example, if [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization) promotes expensive items for a particular user based on their past actions,
that user might see expensive items at the top of the results, even if they've chosen to *relevant sort* from lowest to highest price.
To ensure predictable behavior, don't use the following features or parameters with relevant sorting:
* [`distinct`](/doc/api-reference/api-parameters/distinct) > 1
* [Dynamic Re-ranking](/doc/guides/algolia-ai/re-ranking)
* [Personalization](/doc/guides/personalization/classic-personalization/personalizing-results)
### Facets count
When relevant sorting is enabled, Algolia still computes each on all results, not just those defined by the relevant sort.
A [UI banner](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners) detailing the behavior of the relevant sort may help manage user expectations.
### Enabling relevant sorting reduces the number of results
Essentially, that's the point: it gives users more relevant results, not all results ([exhaustive sorting](/doc/guides/managing-results/refine-results/sorting/in-depth/exhaustive-sort)).
For example, you search for `chromebook` on an ecommerce site and get Chromebook computers at the top of your results (so far, so good).
However, if you then sort by ascending price (because you want a cheaper computer),
the top (cheapest) listed items might be "Chromebook power adapters".
With a relevant sort, you get fewer results but only see the relevant items (computers).
Using a virtual replica even with `relevancyStrictness` set to 0 can't mimic the exhaustive sorting of a standard replica because it doesn't use the same algorithm. Be aware that a virtual replica:
* Doesn't duplicate the data. It sorts at query time.
* Can only retrieve a maximum of 20k results, so there's no guarantee that it will fetch the "deepest" hits.
When using virtual replicas, use `nbSortedHits` rather than `nbHits` to get the number of results.
### Relevant sort doesn't apply when browsing an index
When you use the [Browse](/doc/libraries/sdk/methods/search/browse) method on a virtual replica index, records are returned in the same order as the primary index.
This means that relevant sort parameters, such as [relevancy strictness](/doc/api-reference/api-parameters/relevancyStrictness), don't have any effect.
## How to configure relevant sorting
### Create a virtual replica index
If you want to configure relevant sorting,
you first need to create a [virtual replica index](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas#standard-and-virtual-replicas) for each attribute you want to sort by.
Virtual replicas provide an alternative "view" of the primary index.
Virtual replicas reuse the primary's ranking formula to return relevant results.
For that reason, you can't change the [`ranking`](/doc/api-reference/api-parameters/ranking)
on virtual replicas but can use [`customRanking`](/doc/api-reference/api-parameters/customRanking)
to define the sort criteria.
For more information, see:
* [Understanding replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas)
* [What is the effect of a replica on the number of records?](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas-impact-on-pricing)
### Connect your UI to your index
Virtual replica indices manage the backend of sorting, but you still need to implement the frontend.
You can do this with either custom logic or [InstantSearch](/doc/guides/building-search-ui/what-is-instantsearch/js).
If you're using InstantSearch, use the [`sortBy` UI widget](/doc/guides/managing-results/refine-results/sorting/how-to/use-sorting-ui-widget).
To ensure users understand how relevant sort works and how they can control their experience:
* Indicate that sorting removed some results
* Offer them the option of displaying more.
You can do this with the `relevantSort` widget:
For more details, see:
* [`sortBy` widget](/doc/api-reference/widgets/sort-by/js)
* [`relevantSort` widget](/doc/api-reference/widgets/relevant-sort/js)
# Understanding replicas
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas
What replicas are and how to handle them.
Algolia has one ranking formula per .
Every index has a unique sorting strategy but you can't change it at query time.
This is because pre-sorting during indexing instead of at query time leads to a considerable performance boost.
**The key use of replica indices is they let you provide different rankings for the same data.**
Algolia lets you automatically replicate the content of one index (the primary) onto other indices (standard and virtual replicas) and synchronize content changes.
## Standard and virtual replicas
Standard or virtual replicas are optimized for different use cases:
* [**Exhaustive sorting**](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute).
Use standard replicas if you want to show everything that matches,
strictly sorted by a chosen attribute.
For example, on a news site sort articles by publication date.
* [**Relevant sorting**](/doc/guides/managing-results/refine-results/sorting/in-depth/relevant-sort).
Use virtual replicas to prioritize relevant results while still incorporating the sorting preference for the selected attribute.
For example, searching for "notebook" should show laptops first, not notebook accessories.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
### Similarities
* You can [search a replica index](/doc/guides/managing-results/refine-results/sorting/how-to/search-in-a-replica-index) as you would any primary index.
* You can't add, update, or delete in replica indices.
* Both replica types synchronize with any updates made to records in the primary index. Every time you add, update, or delete records in your primary index, your replica is automatically modified. You can't alter this automatic data synchronization.
#### Index settings
Replicas start with the same settings as their primary index, but you can change them.
For example, to sort:
* By price
* In ascending order
* In descending order
* By most viewed
Algolia applies one sorting strategy to each index.
You can only sort by attributes with **boolean or numerical values**.
If you want to sort by dates, [convert the dates to Unix timestamps](/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-by-date).
To keep replica settings in sync with your primary index, set the primary index's `forwardToReplicas` parameter to `true` with the [`setSettings`](/doc/libraries/sdk/v1/methods/set-settings) method.
### Differences
| | Standard replicas | Virtual replicas |
| - | - | - |
| **Records** | [Increases](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas-impact-on-pricing) the number of records since it's a *copy* of its primary index | Doesn't increase the number of records since it's just a *view* of its primary index. It does slightly increase the primary index's *size* (but by less than 10 MB per million records) |
| **Usage** | Optimized for exhaustive sorting | Optimized for relevant sorting |
| **Number of replicas per index** | Unlimited | 20 |
| **Supported index settings** | All | [A subset](#supported-index-settings-for-virtual-replicas) |
| **Synonyms** | Supports all synonym types | Supports most synonym types, except [placeholders](/doc/guides/managing-results/optimize-search-results/adding-synonyms#placeholders) |
| **[API response](/doc/rest-api/search/search-single-index) that returns the number of hits** | `nbHits` | `nbSortedHits` |
## Manage replicas
For more information about managing replica indices, see:
* [Create a replica index](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas)
* [Manage multiple indices](/doc/guides/managing-results/refine-results/sorting/how-to/set-settings-and-forward-to-replicas)
* [Delete replica indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices#delete-indices-with-replicas)
## Supported index settings for virtual replicas
Virtual replicas support a *subset* of the index settings.
### Supported settings
* [`advancedSyntax`](/doc/api-reference/api-parameters/advancedSyntax)
* [`advancedSyntaxFeatures`](/doc/api-reference/api-parameters/advancedSyntaxFeatures)
* [`allowTyposOnNumericTokens`](/doc/api-reference/api-parameters/allowTyposOnNumericTokens)
* [`alternativesAsExact`](/doc/api-reference/api-parameters/alternativesAsExact)
* [`aroundPrecision`](/doc/api-reference/api-parameters/aroundPrecision)
* [`attributeCriteriaComputedByMinProximity`](/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity)
* [`attributesToHighlight`](/doc/api-reference/api-parameters/attributesToHighlight)
* [`attributesToRetrieve`](/doc/api-reference/api-parameters/attributesToRetrieve)
* [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet)
* [`distinct`](/doc/api-reference/api-parameters/distinct)
* [`enablePersonalization`](/doc/api-reference/api-parameters/enablePersonalization)
* [`enableRules`](/doc/api-reference/api-parameters/enableRules)
* [`exactOnSingleWordQuery`](/doc/api-reference/api-parameters/exactOnSingleWordQuery)
* [`highlightPostTag`](/doc/api-reference/api-parameters/highlightPostTag)
* [`highlightPreTag`](/doc/api-reference/api-parameters/highlightPreTag)
* [`hitsPerPage`](/doc/api-reference/api-parameters/hitsPerPage)
* [`ignorePlurals`](/doc/api-reference/api-parameters/ignorePlurals)
* [`maxFacetHits`](/doc/api-reference/api-parameters/maxFacetHits)
* [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet)
* [`minProximity`](/doc/api-reference/api-parameters/minProximity)
* [`minWordSizefor1Typo`](/doc/api-reference/api-parameters/minWordSizefor1Typo)
* [`minWordSizefor2Typos`](/doc/api-reference/api-parameters/minWordSizefor2Typos)
* [`paginationLimitedTo`](/doc/api-reference/api-parameters/paginationLimitedTo)
* [`queryLanguages`](/doc/api-reference/api-parameters/queryLanguages)
* [`queryType`](/doc/api-reference/api-parameters/queryType)
* [`removeStopWords`](/doc/api-reference/api-parameters/removeStopWords)
* [`replaceSynonymsInHighlight`](/doc/api-reference/api-parameters/replaceSynonymsInHighlight)
* [`responseFields`](/doc/api-reference/api-parameters/responseFields)
* [`restrictHighlightAndSnippetArrays`](/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays)
* [`snippetEllipsisText`](/doc/api-reference/api-parameters/snippetEllipsisText)
* [`sortFacetValuesBy`](/doc/api-reference/api-parameters/sortFacetValuesBy)
* [`typoTolerance`](/doc/api-reference/api-parameters/typoTolerance)
* [`unretrievableAttributes`](/doc/api-reference/api-parameters/unretrievableAttributes)
### Unsupported settings
You can't change the following parameters for virtual indices.
Instead, change them on the corresponding primary index or create a standard replica.
* [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct)
* [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting)
* [`camelCaseAttributes`](/doc/api-reference/api-parameters/camelCaseAttributes)
* [`customNormalization`](/doc/api-reference/api-parameters/customNormalization)
* [`decompoundedAttributes`](/doc/api-reference/api-parameters/decompoundedAttributes)
* [`disableExactOnAttributes`](/doc/api-reference/api-parameters/disableExactOnAttributes)
* [`disablePrefixOnAttributes`](/doc/api-reference/api-parameters/disablePrefixOnAttributes)
* [`disableTypoToleranceOnAttributes`](/doc/api-reference/api-parameters/disableTypoToleranceOnAttributes)
* [`disableTypoToleranceOnWords`](/doc/api-reference/api-parameters/disableTypoToleranceOnWords)
* [`indexLanguages`](/doc/api-reference/api-parameters/indexLanguages)
* [`keepDiacriticsOnCharacters`](/doc/api-reference/api-parameters/keepDiacriticsOnCharacters)
* [`numericAttributesForFiltering`](/doc/api-reference/api-parameters/numericAttributesForFiltering)
* [`optionalWords`](/doc/api-reference/api-parameters/optionalWords)
* [`ranking`](/doc/api-reference/api-parameters/ranking)
* [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes)
* [`separatorsToIndex`](/doc/api-reference/api-parameters/separatorsToIndex)
* [`userData`](/doc/api-reference/api-parameters/userData)
# What is the effect of a replica on the number of records?
Source: https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas-impact-on-pricing
Find out how replicas impact the number of records. Standard replicas increase the number of records but virtual replicas don't.
## Number of records
Algolia counts the number of records differently depending on the [replica type](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
This means the count differs between standard and virtual replicas.
### Standard replicas
Standard replica indices "replicate" or copy the primary records.
Standard replicas have the same content but can have their own configuration.
For example, say your primary index has 1,000 records.
If you create two standard replicas, you end up with 3,000 records in total.
The total number of records for an index equals the records in its primary index.
Multiply that by the replica count to get the total.
* total records size = number of records in primary index \* (1 + number of standard replicas).
### Virtual replicas
Virtual replicas are a "view" of their primary indices.
They don't increase the number of records.
## Indexing operations
Only operations on the primary index count as indexing operations.
This applies to both standard and virtual replicas.
* You send data to a primary index, or update existing data. Algolia then automatically forwards the changes to your replica indices. These changes don't add anything to your operation count.
* The `forwardToReplicas` setting forwards settings from your primary index to your replicas without adding extra operation counts.
* Adding settings directly to your standard or virtual replicas count as one operation.
* Adding synonyms and Rules to your standard or virtual replicas count as operations.
## What happens when you detach a replica from its primary?
You can turn a standard or virtual replica into a regular index by
[un-linking it from its primary index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices#unlink-a-replica-from-its-primary).
After that, all its indexing operations count toward your monthly operations total.
# Relevance overview
Source: https://www.algolia.com/doc/guides/managing-results/relevance-overview
Learn how to achieve strong relevance and improve it.
[**Relevance**](/doc/guides/managing-results/relevance-overview/in-depth/defining-relevance) is finding results that match a and ranking them so the best-matched results appear at the top of the list.
* To **find results**, Algolia uses [text-based](/doc/guides/managing-results/relevance-overview/in-depth/textual-matching) comparisons like typos and stop words, and non-textual factors such as criteria and geolocation.
* **Ranking**, which determines the order of results, relies on a [tie-breaking algorithm](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) to present the most relevant items first.
Good relevance is vital because it directly affects the quality of user's search results.
In ecommerce, poorly ranked results decrease user satisfaction and .
Some common indications that you need to improve relevance are:
* **Too many irrelevant results**. Relevance suffers when searching large text attributes. Consider searching smaller, targeted attributes instead.
* **Important content is buried**. You haven't implemented custom ranking or failed to set some key attributes as searchable.
## How to improve relevance
If you don't configure anything else, change these two settings:
* Choose a good set of [**searchable attributes**](/doc/guides/managing-results/must-do/searchable-attributes).
* Apply [**custom ranking**](/doc/guides/managing-results/must-do/custom-ranking) to adapt Algolia to your needs.
### Further relevance optimizations
Algolia has some key features to let you further enhance relevance:
* Use [**rules**](/doc/guides/managing-results/rules/rules-overview) to override relevance in some situations. For example, for seasonal product promotions.
* Use [**synonyms**](/doc/guides/managing-results/optimize-search-results/adding-synonyms) if your contain terms that users may be unfamiliar with.
* Offer [**facets**](/doc/guides/managing-results/refine-results/faceting) so users can fine-tune the relevance of their results.
* Change [**natural language settings**](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp) to help anticipate user errors (such as misspellings) and to optimize for a multi-lingual audience.
* Use **Personalization** ([classic](/doc/guides/personalization/classic-personalization/what-is-personalization) or [advanced](/doc/guides/personalization/advanced-personalization/what-is-advanced-personalization)) to show returning users results that align with their established preferences, like works by a specific author or clothing from a particular brand.
With any change, use [search analytics](/doc/guides/search-analytics/overview) for insights into what your users search for, and which results they select.
Consider [A/B testing](/doc/guides/ab-testing/what-is-ab-testing) to create two alternative search experiences, put them both live and see which performs best.
## Enhance the user experience
Algolia also offers features that enhance the user experience:
* [Sorting](/doc/guides/managing-results/refine-results/sorting)
* [Grouping](/doc/guides/managing-results/refine-results/grouping)
* [Geolocation](/doc/guides/managing-results/refine-results/geolocation)
* [Prefix searching](/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/prefix-searching)
* [Multi-lingual support](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp).
## See also
For more information, see:
* [Implementation checklist](/doc/guides/going-to-production/implementation-checklist)
* [Empty or insufficient results](/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results)
* [What is search relevance? (blog)](https://www.algolia.com/blog/product/what-is-search-relevance)
* [Search engine relevance (blog)](https://www.algolia.com/blog/product/defining-what-a-search-index-is-and-the-speed-and-relevance-of-a-search-engine-index/)
# Relevance
Source: https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/defining-relevance
Relevance is about finding and ranking results from your index in a way that matches the users' needs and expectations.
Relevance is about finding and ranking results from your in a way that matches the users' needs and expectations.
Relevance is a balance between finding all the that match a search and ranking them so that the most important results are returned first.
What important means depends on your and your users' goals.
While finding all matches is essential, if the best match is somewhere among hundreds of results, your search isn't relevant.
## Finding (intelligent matching)
Relevance makes sure that a search finds **all records that match a query**.
Relevance is an **intelligent matching** process that takes into account [typo tolerance](/doc/guides/managing-results/optimize-search-results/typo-tolerance), [partial word matching](/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings), the spatial distance between matching words, the number of attributes that match, [synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms), [Rules](/doc/guides/managing-results/rules/rules-overview), [natural language characteristics](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp) (like stop words and plurals), [geolocation](/doc/guides/managing-results/refine-results/geolocation#enabling-geo-search-by-adding-geolocation-data-to-records), and many other aspects of what users expect from a modern search experience.
## Ranking (putting the best at the top)
Finding records is only part of the story.
Ranking ensures that the records most closely related to the search appear first, while less related records are shown further down the list of results.
* **Textual relevance** is about how well a record matches the search terms. Records that are a better match are ranked higher.
* **Custom ranking.** If several records have equal textual relevance, Algolia lets you determine the order of results with custom metrics appropriate for your organization.
Algolia's [ranking criteria](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria) define where a record will sit in the list of results for a query.
If you want to find out why a record is ranked the way it is, you can [do this in the dashboard or with the API](/doc/guides/managing-results/troubleshooting/troubleshooting-relevance#the-engine’s-ranking-decisions).
## Promote your content
Relevant results help promote your content and give users what they're looking for through the use of:
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking)
* [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting)
* [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization)
* [Analytics](/doc/guides/search-analytics/overview).
When a search matches the user's [intent](/doc/guides/managing-results/rules/detecting-intent), your relevance is good.
## Discovery
Users may not always be actively searching for specific items: sometimes, they **discover** them intentionally or unexpectedly.
Discovery is a user-driven process where they browse and define what they need with each new query.
A well-designed frontend UI/UX helps a user discover what they want.
## UI/UX
The UI/UX should allow users to find results, navigate them, discover more content, and refine their search.
A good UI/UX incorporates rapid searching, highlighting key terms in results, faceting, infinite scrolling, and more.
## See also
* [Relevance overview](/doc/guides/managing-results/relevance-overview)
* [Search is more than textual matching](/doc/guides/managing-results/relevance-overview/in-depth/textual-matching)
* [The speed and relevance of a search engine (blog)](https://www.algolia.com/blog/product/defining-what-a-search-index-is-and-the-speed-and-relevance-of-a-search-engine-index/)
* [What is search relevance? (blog)](https://www.algolia.com/blog/product/what-is-search-relevance/)
* [Mobile search and discovery (blog)](https://www.algolia.com/blog/ux/mobile-search-ux-best-practices/)
# The eight ranking criteria
Source: https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria
Discover the eight tie-breaking criteria that Algolia uses to rank your results.
Algolia's **eight ranking criteria** help define what's both [textually and business relevant](/doc/guides/managing-results/relevance-overview/in-depth/defining-relevance#ranking-putting-the-best-at-the-top).
## Typo
Algolia is [typo-tolerant](/doc/guides/managing-results/optimize-search-results/typo-tolerance).
The Typo criterion in the ranking formula ensures the following ranking of search results for queries:
1. Queries without typos (exact matches)
2. Queries with one typo
3. Queries with two typos
### Typo versus Exact versus Words
Algolia's [Exact](#exact), [Words](#words-if-applicable), and Typo ranking criteria are all used to determine how well a record matches a query.
However, they differ in the following ways:
| Criterion | Behavior | Helpful when |
| - | - | - |
| **Typo** | Returns results for queries that match all words in a record, including words with typos. However, matches without typos in the query terms rank higher | Users make typing errors |
| **Exact** | Returns results for queries that match all words in a record. The ranking favors records where the query sequence matches the record's word order | Users type the query as written, such as the name of a well-known product or company |
| **Words** | Returns results for queries that match some words in a record. | Misinterpretation of user input is more common such as in a [voice-to-text search](/doc/guides/solutions/ecommerce/voice-search) |
## Geo (if applicable)
If you're using the [geo-search feature](/doc/guides/managing-results/refine-results/geolocation),
results are ranked by distance, from the closest to the farthest.
The [`aroundPrecision`](/doc/api-reference/api-parameters/aroundPrecision) parameter sets the precision of this ranking.
## Words (if applicable)
This criterion only breaks ties if you're using the [`optionalWords` setting](/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results#create-a-list-of-optional-words).
If you're not using `optionalWords`,
this criterion will have the same value for each result.
By default, Algolia only matches results that contain all the query's words.
With `optionalWords`,
you can declare some words as optional.
The Words criterion ranks them by the number of matching words typed by users (not the number of times the word appears in the record).
For example, if a user typed two words,
the maximum score for this criterion is 2—even if a record contains this word 10 times.
## Filters
If a query uses [filters](/doc/api-reference/api-parameters/filters) or [optional filters](/doc/api-reference/api-parameters/optionalFilters),
this criterion ranks according to the **number of matching filters** so that records that don't match a filter score 0 and records that match two filters score 2.
You can adjust the scoring with:
* [Filter scoring](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring#score-calculation), you can assign different scores to each filter.
* Using [sumOrFiltersScores](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring#accumulate-scores-with-sumorfiltersscores) to accumulate the scores of disjunctive (OR) matches to create a **total score**.
The Filters criterion can be helpful when defining relevance—for example, when [promoting results](/doc/guides/managing-results/rules/merchandising-and-promoting).
## Proximity
For a query with two or more words,
Proximity calculates how near those words are to each other in the matching record.
This criterion prioritizes records that have words closer to each other.
For example, consider two records with an `actor_name` attribute:
`George Clooney` and `George Timothy Clooney`.
If a user searches for "george clooney",
`George Clooney` has a better proximity distance (the two query words are 1 word apart) compared to `George Timothy Clooney` (the two query words are 2 words apart).
## Attribute
This criterion only considers [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes):
attributes at the top of the `searchableAttributes` list rank higher than lower ones.
The [order](/doc/guides/managing-results/must-do/searchable-attributes/how-to/configuring-searchable-attributes-the-right-way#word-position) of matches within the attribute itself is also important.
By default, records with matched words closer to the beginning of a given attribute rank higher.
## Exact
Records with words that match query terms without typos will rank higher.
The more matching words in a record's attribute,
the higher that record's rank.
By default:
* **An exact match** is when a complete word in a query,
without typos, matches a word in an attribute.
Single-word queries are only exact if they match a single-word attribute.
* **An inexact match** has typos or only matches a prefix.
Synonym matching and singular/plural matching are considered exact.
Thus, a word is considered an exact match if its synonym matches a query.
You can [change the settings for the Exact criterion](/doc/guides/managing-results/optimize-search-results/override-search-engine-defaults/in-depth/adjust-exact-settings).
## Custom
This criterion takes into account your [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) attributes.
With multiple custom ranking attributes, the behavior is the same as other criteria:
a criterion is only used if there is a tie between all the previous criteria.
## Attribute and proximity combinations
When Proximity appears before Attribute in the ranking,
the calculation of Attribute ranking is different than if Proximity appears after Attribute.
This is called the **best-matched attribute**.
This ordering is a subtle distinction.
We recommend keeping the default ranking since Proximity usually leads to better identification of the best-matched attribute.
### Best-matched attribute
To determine the best-matched attribute,
Algolia uses two methods:
* **Closest in proximity.** Ranking is based on how close two or more query terms are to each other.
* **Best position.** Considers words near the beginning of an attribute to be better than those towards the end.
Algolia's default ranking formula puts Proximity before Attribute,
which has a subtle but important effect on computing the best-matched attribute:
attributes with matched terms **closest** to each other will rank highest.
If you put Proximity after Attribute or remove the Proximity criterion,
the best-matched attributes are those whose matched terms are in the **best position**.
### Example
For example, consider an with two searchable attributes, `profession` and `full-name`, and the following two records:
```json JSON icon=braces theme={"system"}
[
{
"profession": "Singer and comedian",
"full-name": "Jerry Lewis",
"objectID": "3"
},
{
"profession": "Born a singer",
"full-name": "Jerry Singer",
"objectID": "1"
}
]
```
Consider the search query "Jerry Singer".
The default ranking formula order is **Proximity before Attribute**.
In this case, the record containing the two words "jerry" and "singer" in closest proximity ranks higher (regardless of attribute order).
For the two example records:
* `objectID` 1 has the query words side-by-side in the full-name attribute
* `objectID` 3 has the query words in different attributes.
Because the former has a better Proximity, it's ranked first.
However, if you put **Proximity after Attribute**,
the ranking is based on the **best position** of the matched terms in the searchable attributes (`profession` and `full-name`).
With the query "jerry singer",
the term "singer" appears in `profession` before `full-name`.
# Search is more than textual matching
Source: https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/textual-matching
Understand that a good search experience is more than textual matching.
Search isn't just a letter-by-letter comparison.
Compare searches on these sentences ([records](/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records)):
1. To be or not to be that is the question
2. This was the best of times the worst of times
3. Ask not what your country asks of you but what you can do for your country
4. For a long time I used to go to bed early
5. Shakespeare and Dickens are both famous British authors
6. By George, I should've known that! I'll be a dickens!
7. An accomplished woman almost always knows more than we men, though her knowledge is of a different sort
* A search for "ask" returns one result: sentence 3.
* A search for "time" returns two result: sentences 2 and 4.
* A search for "be" returns four results: sentence 1 and 6 "be", sentence 2 "**be**st", and sentence 4 "**be**d".
## Where textual matching doesn't work
Consider these searches on the preceding sentences:
* "qestion" ("question" mistyped). No sentences are returned.
* "be". What sentence should come first? The ones that match exactly (the word "be")? Or some other criteria?
* "literary quotes"? Textual matching doesn't know anything about these sentences - are these political or literary quotes? The quotes need to be classified, tagged as coming from novels, politicians, authors, countries.
All these queries require something more.
## Going beyond textual matching
Simple textual matching won't solve every search problem: it's only the starting point.
## Add structure
By adding more information to the preceding sentences (records), their relevance can be improved:
1. "To be or not to be, that is the question", shakespeare, british, hamlet, theater
2. "It was the best of times, it was the worst of times", dickens, british, tale of two cities, novel
3. "Ask not what your country asks of you but what you can do for your country", kennedy, american, speech, politics
4. "For a long time I used to go to bed early", proust, french, recherche, novel
5. "Shakespeare and Dickens are both famous British authors", georgette, british, history of british literature, essay
6. "By George, I should've known that! I'll be a dickens!", landly, british, my fair lady, theater
7. "An accomplished woman almost always knows more than we men, though her knowledge is of a different sort.", george elliot, british, middlemarch, novel
## Going further
A good search experience also needs to consider [filtering](/doc/guides/managing-results/refine-results/filtering), [attribute priorities](/doc/guides/managing-results/must-do/searchable-attributes), [custom ranking](/doc/guides/managing-results/must-do/custom-ranking), [handling typos](/doc/guides/managing-results/optimize-search-results/typo-tolerance), [synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms), and more.
Insights from [user actions](/doc/guides/sending-events), [analytics](/doc/guides/search-analytics/overview), [personalization](/doc/guides/personalization/classic-personalization/what-is-personalization), and [A/B testing](/doc/guides/ab-testing/what-is-ab-testing) can also help you fine-tune your search experience to better meet the needs of your users.
# Detect intent
Source: https://www.algolia.com/doc/guides/managing-results/rules/detecting-intent
Use Rules to detect user intent by looking at the keywords they use in their queries.
**Keywords** in a are crucial terms that show user intent.
They vary depending on the data in your .
For example, if a user types "red":
* On a clothing site, they're probably looking for red clothes.
* On a movie site, "red" might not refer to color.
Once you understand what specific keywords mean in queries,
[use rules to adjust the search results](/doc/guides/managing-results/rules/rules-overview#rules-responding-to-user-queries) users get when they use those keywords.
For example, you can:
* Dynamically filter
* Change the entire query
* Category searches
* Dynamically change search parameters.
## Dynamically filter based on the query
Dynamic filtering applies filters based on keywords.
For example, if someone searches for "cheap toaster 800w",
you could create a rule to filter on toasters priced under \$25 with an 800w power rating.
For more information, see:
* [Add filters based on the query](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query)
* [Dynamic filtering with Rules](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-filter-if-the-query-match-a-facet-value)
* [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting)
## Change the entire query
Sometimes, users may search using informal terms not in your .
For example, "iwatch" instead of "apple watch".
To handle this, you can create rules to replace an entire query with an alternative set of words.
For example, to replace queries containing "iwatch" with an "apple watch" query:
```json JSON icon=braces theme={"system"}
{
"conditions": [
{
"anchoring": "is",
"pattern": "iwatch",
"alternatives": false
}
],
"consequence": {
"params": {
"query": "apple watch"
}
}
}
```
To help determine these informal terms, review your [Search analytics](/doc/guides/search-analytics/overview).
## Category searches
You can use rules to recognize [categories](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index#create-a-sitemap-for-categories) in queries and refine the results based on that.
For example, on a document library site, in the query "article ref21," "article" is a category.
* Without a rule, the search probably fails since it searches for the terms "article" and "ref21" but it's unlikely that your records contain the category name "article".
* With a rule that [recognizes and removes](/doc/guides/managing-results/rules/detecting-intent/how-to/detecting-keywords) the "article" category, the search succeeds ("ref21" replaces "article ref21").
## Dynamically change search parameters
After parsing a query, you can use a rule to determine which [parameters best apply to the search](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-search-parameters-for-a-specific-query).
For example, you can turn typo tolerance on or off, change geolocation settings, search for only certain types of attributes, or add filters.
# Add filters based on the query
Source: https://www.algolia.com/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query
Applying a custom filter for a specific query
Sometimes, specific terms can act as cues that you can use to filter search results.
You can use positive, negative, or numerical filters:
* [**Positive filters**](#positive-filters) include a specific subset of matching records in the results. For example, if a user types "diet" on a restaurant website, return every record that has "low-carb" or "low-fat".
* [**Negative filters**](#negative-filters) exclude matching records from results. For example, if a user types "gluten-free" on a restaurant website, you could filter out any gluten-containing meal.
* [**Numerical filters**](#numerical-filters) convert text queries into a numerical range. For example, if a user types "cheap" on a website for kitchen appliances, you could filter out anything costing more than \$50.
## Positive filters
If you want to filter out every non-diet-friendly meal whenever user's search queries contain the term "diet",
you could use the [`_tags`](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-tags) attribute to categorize meals depending on their individual qualities:
```json JSON icon=braces theme={"system"}
[
{
"name": "Chicken Stuffed Baked Avocados",
"restaurant": "The Hive",
"_tags": ["low-carb"]
},
{
"name": "Spinach Quiche",
"restaurant": "Bert's Inn",
"_tags": ["low-carb", "vegetarian"]
},
{
"name": "Pizza Chicken Bake",
"restaurant": "Millbrook Deli",
"_tags": ["cheese"]
},
{
"name": "Strawberry Sorbet",
"restaurant": "The Hive",
"_tags": ["low-fat", "vegetarian", "vegan"]
}
]
```
When users include the term "diet" in their search, you want to automatically return every record that has "low-carb" or "low-fat" in their `_tags` attribute.
Because `_tags` is already optimized for filtering, you don't have to set it as an attribute for faceting.
You can directly create a new Rule that detects the term "diet" in a query and applies a positive filter on tags "low-carb" and "low-fat".
To use the term "diet" only for filtering and not as a search term,
add a consequence in your rule to remove the word from your query.
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To add a rule, use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method.
When creating a rule, you must define a condition and a consequence.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"diet-rule",
new Rule
{
ObjectID = "diet-rule",
Conditions = new List
{
new Condition { Pattern = "diet", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Filters = "'low-carb' OR 'low-fat'",
Query = new ConsequenceQuery(
new ConsequenceQueryObject
{
Edits = new List
{
new Edit { Type = Enum.Parse("Remove"), Delete = "diet" },
},
}
),
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "diet-rule",
rule: Rule(
objectID: "diet-rule",
conditions: [
Condition(
pattern: "diet",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
filters: "'low-carb' OR 'low-fat'",
query: ConsequenceQueryObject(
edits: [
Edit(
type: EditType.fromJson("remove"),
delete: "diet",
),
],
),
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "diet-rule",
search.NewEmptyRule().SetObjectID("diet-rule").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("diet").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetFilters("'low-carb' OR 'low-fat'").SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetEdits(
[]search.Edit{*search.NewEmptyEdit().SetType(search.EditType("remove")).SetDelete("diet")})))))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"diet-rule",
new Rule()
.setObjectID("diet-rule")
.setConditions(Arrays.asList(new Condition().setPattern("diet").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setFilters("'low-carb' OR 'low-fat'")
.setQuery(new ConsequenceQueryObject().setEdits(Arrays.asList(new Edit().setType(EditType.REMOVE).setDelete("diet"))))
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'diet-rule',
rule: {
objectID: 'diet-rule',
conditions: [{ pattern: 'diet', anchoring: 'contains' }],
consequence: {
params: { filters: "'low-carb' OR 'low-fat'", query: { edits: [{ type: 'remove', delete: 'diet' }] } },
},
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "diet-rule",
rule =
Rule(
objectID = "diet-rule",
conditions =
listOf(
Condition(
pattern = "diet",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
filters = "'low-carb' OR 'low-fat'",
query =
ConsequenceQueryObject(
edits =
listOf(
Edit(
type = EditType.entries.first { it.value == "remove" },
delete = "diet",
)
)
),
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'diet-rule',
['objectID' => 'diet-rule',
'conditions' => [
['pattern' => 'diet',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['filters' => "'low-carb' OR 'low-fat'",
'query' => ['edits' => [
['type' => 'remove',
'delete' => 'diet',
],
],
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="diet-rule",
rule={
"objectID": "diet-rule",
"conditions": [
{
"pattern": "diet",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"filters": "'low-carb' OR 'low-fat'",
"query": {
"edits": [
{
"type": "remove",
"delete": "diet",
},
],
},
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"diet-rule",
Algolia::Search::Rule.new(
algolia_object_id: "diet-rule",
conditions: [Algolia::Search::Condition.new(pattern: "diet", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
filters: "'low-carb' OR 'low-fat'",
query: Algolia::Search::ConsequenceQueryObject.new(
edits: [Algolia::Search::Edit.new(type: "remove", delete: "diet")]
)
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "diet-rule",
rule = Rule(
objectID = "diet-rule",
conditions = Some(
Seq(
Condition(
pattern = Some("diet"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
filters = Some("'low-carb' OR 'low-fat'"),
query = Some(
ConsequenceQueryObject(
edits = Some(
Seq(
Edit(
`type` = Some(EditType.withName("remove")),
delete = Some("diet")
)
)
)
)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "diet-rule",
rule: Rule(
objectID: "diet-rule",
conditions: [SearchCondition(pattern: "diet", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "'low-carb' OR 'low-fat'",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(edits: [SearchEdit(
type: SearchEditType.remove,
delete: "diet"
)]))
))
)
)
```
### Using the dashboard
You can add rules from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, and enter `diet`.
Click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `_tags` as the attribute and `low-carb` as the value.
Add `low-fat` as another value and join the values with **Or**.
Click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `diet` in **Word(s)** and click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
On the left sidebar, select **Search**, select your index, and open [**Rules**](https://dashboard.algolia.com/rules).
Select **Create your first rule** or **New rule**, then select **Manual Editor**.
2. In **Condition(s)**, keep **Query contains** and enter `diet`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "filters": "'low-carb' OR 'low-fat'" }`.
4. Click **Add consequence** again and select **Remove Word**.
Enter `diet`.
5. Save your changes.
## Negative filters
To exclude gluten-containing foods from the search results when a user searches for gluten-free meals, do the following:
1. Create an `allergens` attribute (with "gluten" as one of the potential values).
2. Create a rule that filters out records with "gluten" in that attribute.
### Example records
```json JSON icon=braces theme={"system"}
[
{
"name": "Pasta Bolognese",
"restaurant": "Millbrook Deli",
"allergens": ["eggs", "lactose"]
},
{
"name": "Breakfast Waffles",
"restaurant": "The Hive",
"allergens": ["gluten", "lactose"]
}
]
```
### Using the API
Set `allergens` as an [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting) in your index:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { AttributesForFaceting = new List { "allergens" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"allergens",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"allergens"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("allergens"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['allergens'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("allergens")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'allergens',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"allergens",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["allergens"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("allergens"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["allergens"])
)
```
* Use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method to create a rule that detects the term "gluten-free" in a query and applies a negative filter on facet value `allergens:gluten`.
* Add a consequence in your rule to remove the word "gluten-free" from your query. This way, it won't be used as a search term, only for filtering purposes.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"gluten-free-rule",
new Rule
{
ObjectID = "gluten-free-rule",
Conditions = new List
{
new Condition { Pattern = "gluten-free", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Filters = "NOT allergens:gluten",
Query = new ConsequenceQuery(
new ConsequenceQueryObject
{
Edits = new List
{
new Edit { Type = Enum.Parse("Remove"), Delete = "gluten-free" },
},
}
),
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "gluten-free-rule",
rule: Rule(
objectID: "gluten-free-rule",
conditions: [
Condition(
pattern: "gluten-free",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
filters: "NOT allergens:gluten",
query: ConsequenceQueryObject(
edits: [
Edit(
type: EditType.fromJson("remove"),
delete: "gluten-free",
),
],
),
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "gluten-free-rule",
search.NewEmptyRule().SetObjectID("gluten-free-rule").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("gluten-free").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetFilters("NOT allergens:gluten").SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetEdits(
[]search.Edit{*search.NewEmptyEdit().SetType(search.EditType("remove")).SetDelete("gluten-free")})))))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"gluten-free-rule",
new Rule()
.setObjectID("gluten-free-rule")
.setConditions(Arrays.asList(new Condition().setPattern("gluten-free").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setFilters("NOT allergens:gluten")
.setQuery(new ConsequenceQueryObject().setEdits(Arrays.asList(new Edit().setType(EditType.REMOVE).setDelete("gluten-free"))))
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'gluten-free-rule',
rule: {
objectID: 'gluten-free-rule',
conditions: [{ pattern: 'gluten-free', anchoring: 'contains' }],
consequence: {
params: { filters: 'NOT allergens:gluten', query: { edits: [{ type: 'remove', delete: 'gluten-free' }] } },
},
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "gluten-free-rule",
rule =
Rule(
objectID = "gluten-free-rule",
conditions =
listOf(
Condition(
pattern = "gluten-free",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
filters = "NOT allergens:gluten",
query =
ConsequenceQueryObject(
edits =
listOf(
Edit(
type = EditType.entries.first { it.value == "remove" },
delete = "gluten-free",
)
)
),
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'gluten-free-rule',
['objectID' => 'gluten-free-rule',
'conditions' => [
['pattern' => 'gluten-free',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['filters' => 'NOT allergens:gluten',
'query' => ['edits' => [
['type' => 'remove',
'delete' => 'gluten-free',
],
],
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="gluten-free-rule",
rule={
"objectID": "gluten-free-rule",
"conditions": [
{
"pattern": "gluten-free",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"filters": "NOT allergens:gluten",
"query": {
"edits": [
{
"type": "remove",
"delete": "gluten-free",
},
],
},
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"gluten-free-rule",
Algolia::Search::Rule.new(
algolia_object_id: "gluten-free-rule",
conditions: [Algolia::Search::Condition.new(pattern: "gluten-free", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
filters: "NOT allergens:gluten",
query: Algolia::Search::ConsequenceQueryObject.new(
edits: [Algolia::Search::Edit.new(type: "remove", delete: "gluten-free")]
)
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "gluten-free-rule",
rule = Rule(
objectID = "gluten-free-rule",
conditions = Some(
Seq(
Condition(
pattern = Some("gluten-free"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
filters = Some("NOT allergens:gluten"),
query = Some(
ConsequenceQueryObject(
edits = Some(
Seq(
Edit(
`type` = Some(EditType.withName("remove")),
delete = Some("gluten-free")
)
)
)
)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "gluten-free-rule",
rule: Rule(
objectID: "gluten-free-rule",
conditions: [SearchCondition(pattern: "gluten-free", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "NOT allergens:gluten",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(edits: [SearchEdit(
type: SearchEditType.remove,
delete: "gluten-free"
)]))
))
)
)
```
### Using the dashboard
You can add rules from the Algolia dashboard using either the Visual Editor or the Manual Editor.
Before creating the rule, open **Configuration > Filtering and Faceting > Facets** for your index and add the `allergens` attribute.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, and enter `gluten-free`.
Click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `allergens` as the attribute, **is not** as the operator, and `gluten` as the value.
Click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `gluten-free` in **Word(s)** and click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
1. Open **Rules**, select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `gluten-free`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "filters": "NOT allergens:gluten" }`.
4. Click **Add consequence** again and select **Remove Word**.
Enter `gluten-free`.
5. Save your changes.
## Numerical filters
Consider the query "cheap toaster 800w".
You can use Rules to filter the results by "toaster" and "prices under \$10" so that the only textual search is the remaining term,
"800w", which could further be used to limit the results with that wattage.
### Rule
If `query = "cheap toaster"` then `price < 10` and `product_type:toaster`.
This requires two rules because each query term applies a different filter.
### Using the API
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"cheap",
new Rule
{
ObjectID = "cheap",
Conditions = new List
{
new Condition { Pattern = "cheap", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Query = new ConsequenceQuery(
new ConsequenceQueryObject { Remove = new List { "cheap" } }
),
Filters = "price < 10",
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "cheap",
rule: Rule(
objectID: "cheap",
conditions: [
Condition(
pattern: "cheap",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
query: ConsequenceQueryObject(
remove: [
"cheap",
],
),
filters: "price < 10",
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "cheap",
search.NewEmptyRule().SetObjectID("cheap").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("cheap").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetRemove(
[]string{"cheap"}))).SetFilters("price < 10")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"cheap",
new Rule()
.setObjectID("cheap")
.setConditions(Arrays.asList(new Condition().setPattern("cheap").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams().setQuery(new ConsequenceQueryObject().setRemove(Arrays.asList("cheap"))).setFilters("price < 10")
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'cheap',
rule: {
objectID: 'cheap',
conditions: [{ pattern: 'cheap', anchoring: 'contains' }],
consequence: { params: { query: { remove: ['cheap'] }, filters: 'price < 10' } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "cheap",
rule =
Rule(
objectID = "cheap",
conditions =
listOf(
Condition(
pattern = "cheap",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
query = ConsequenceQueryObject(remove = listOf("cheap")),
filters = "price < 10",
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'cheap',
['objectID' => 'cheap',
'conditions' => [
['pattern' => 'cheap',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['query' => ['remove' => [
'cheap',
],
],
'filters' => 'price < 10',
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="cheap",
rule={
"objectID": "cheap",
"conditions": [
{
"pattern": "cheap",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"query": {
"remove": [
"cheap",
],
},
"filters": "price < 10",
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"cheap",
Algolia::Search::Rule.new(
algolia_object_id: "cheap",
conditions: [Algolia::Search::Condition.new(pattern: "cheap", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
query: Algolia::Search::ConsequenceQueryObject.new(remove: ["cheap"]),
filters: "price < 10"
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "cheap",
rule = Rule(
objectID = "cheap",
conditions = Some(
Seq(
Condition(
pattern = Some("cheap"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
query = Some(
ConsequenceQueryObject(
remove = Some(Seq("cheap"))
)
),
filters = Some("price < 10")
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "cheap",
rule: Rule(
objectID: "cheap",
conditions: [SearchCondition(pattern: "cheap", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "price < 10",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(remove: ["cheap"]))
))
)
)
```
### Using the dashboard
You can configure this example from the Algolia dashboard using either the Visual Editor or the Manual Editor.
#### Preparation
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index and open the **Configuration** tab.
4. On the **Filtering and Facets** page,
click **Add an attribute** and select the `product_type` attribute. Repeat to add `price`.
In the Visual Editor, set up separate rules for `toaster` and `cheap`.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, and enter `toaster`.
Click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `product_type` as the attribute and `toaster` as the value.
Click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `toaster` in **Word(s)**, then click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
Repeat these steps to create the second rule.
Use `cheap` for the condition and removed word, and filter on `price < 10`.
In the Manual Editor, set up separate rules for `toaster` and `cheap`.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `toaster`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "filters": "product_type:toaster" }`.
4. Click **Add consequence** again and select **Remove Word**.
Enter `toaster`.
5. Save your changes.
Repeat these steps to create the second rule.
Use `cheap` for the condition and removed word, and enter `{ "filters": "price<10" }` as the query parameter.
## See also
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Filtering](/doc/guides/managing-results/refine-results/filtering)
* [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting)
# Dynamic filtering with Rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-filter-if-the-query-match-a-facet-value
Use rules to apply facet and numeric filters automatically based on search queries.
To help users find what they're looking for straight from the search box,
you can use Algolia Rules to automatically apply a filter to results if the query matches a facet value.
## Before you begin
Add the records from the [example dataset](#example-dataset) to an Algolia index.
Configure `color`, `product_type`, and `brand` as
[attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard).
## How to create a dynamic filtering rule
In this example for a product catalog,
you create a rule that displays products with the "red" value in the `color` facet if the query contains the word "red".
With the [example dataset](#example-dataset),
this rule limits results to records with "red" in the `color` attribute.
It ignores "red" in other attributes such as `brand`.
| Query | Results |
| - | - |
| red | FM, Clothing, LondonLook, Red, 21.99 The Mandal 800w, Toaster, Black & Decker, Red, 149.99 |
### Create the rule in the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, enter `red`, and click **Apply**.
3. In **Consequences**, select **Filter results**.
Select the `color` attribute and the `red` value, then click **Apply**.
To boost red products while keeping other colors in the results, use **Boost categories** instead.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `red` in **Word(s)** and click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
On the left sidebar, select **Search**, select your index, and open [**Rules**](https://dashboard.algolia.com/rules).
Select **Create your first rule** or **New rule**, then select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `red`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{"filters":"color:red"}`.
To promote red products without excluding products of other colors, see [Optional filters](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-promote-with-optional-filters).
4. Click **Add consequence** and select **Remove Word**.
Enter `red`.
5. Save your changes.
### Create the rule with the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
This example uses the [`filters`](/doc/api-reference/api-parameters/filters) parameter.
With this approach, you need one rule per filter value.
If you have 10 color options, you need 10 rules, one for each color.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"red-color",
new Rule
{
ObjectID = "red-color",
Conditions = new List
{
new Condition { Pattern = "red", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Query = new ConsequenceQuery(
new ConsequenceQueryObject { Remove = new List { "red" } }
),
Filters = "color:red",
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "red-color",
rule: Rule(
objectID: "red-color",
conditions: [
Condition(
pattern: "red",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
query: ConsequenceQueryObject(
remove: [
"red",
],
),
filters: "color:red",
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "red-color",
search.NewEmptyRule().SetObjectID("red-color").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("red").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetRemove(
[]string{"red"}))).SetFilters("color:red")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"red-color",
new Rule()
.setObjectID("red-color")
.setConditions(Arrays.asList(new Condition().setPattern("red").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams().setQuery(new ConsequenceQueryObject().setRemove(Arrays.asList("red"))).setFilters("color:red")
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'red-color',
rule: {
objectID: 'red-color',
conditions: [{ pattern: 'red', anchoring: 'contains' }],
consequence: { params: { query: { remove: ['red'] }, filters: 'color:red' } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "red-color",
rule =
Rule(
objectID = "red-color",
conditions =
listOf(
Condition(
pattern = "red",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
query = ConsequenceQueryObject(remove = listOf("red")),
filters = "color:red",
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'red-color',
['objectID' => 'red-color',
'conditions' => [
['pattern' => 'red',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['query' => ['remove' => [
'red',
],
],
'filters' => 'color:red',
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="red-color",
rule={
"objectID": "red-color",
"conditions": [
{
"pattern": "red",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"query": {
"remove": [
"red",
],
},
"filters": "color:red",
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"red-color",
Algolia::Search::Rule.new(
algolia_object_id: "red-color",
conditions: [Algolia::Search::Condition.new(pattern: "red", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
query: Algolia::Search::ConsequenceQueryObject.new(remove: ["red"]),
filters: "color:red"
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "red-color",
rule = Rule(
objectID = "red-color",
conditions = Some(
Seq(
Condition(
pattern = Some("red"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
query = Some(
ConsequenceQueryObject(
remove = Some(Seq("red"))
)
),
filters = Some("color:red")
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "red-color",
rule: Rule(
objectID: "red-color",
conditions: [SearchCondition(pattern: "red", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "color:red",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(remove: ["red"]))
))
)
)
```
## Example: one rule per facet
Use one rule to detect values of the `color` facet and apply the corresponding filter.
For example, the query `red` applies `color:red`, while `black` applies `color:black`.
This avoids creating a separate rule for each color.
| Query | Results |
| - | - |
| red | FM, Clothing, LondonLook, Red, 21.99 |
| red | The Mandal 800w, Toaster, Black & Decker, Red, 149.99 |
| black | Will Carpenter, T-shirt, Red or Dead, Black, 199.99 |
### Create the rule in the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. Create a rule with **Visual Editor**.
2. Click **Set query condition(s)** and select **contains**.
Select **Match any value of a facet** and choose `color`.
Click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Filter / Boost matching attribute**.
Select `color` in **Facet**.
The facet must be present in every query condition.
4. Choose **Filter** to keep only matching records or **Boost** to promote them without excluding other records.
Set **Score** from 1 to 50 to control the filter score.
If your plan doesn't support optional filters in rules, select **Filter facets** to filter the results.
5. Under **Filter type**, select **Conjunctive** to require all matching values or **Disjunctive** to accept any of them.
Click **Apply**.
6. Review the results for different color queries, click **Review and Publish**, and click **Publish**.
1. Create a rule with the Manual Editor.
See [how to create a dynamic filtering rule](#how-to-create-a-dynamic-filtering-rule).
2. In **Condition(s)**, click the box next to **Query contains** and select **Add Facet "color"**.
3. In **Consequence(s)**, click **Add consequence** and select **Filter/Boost Matching Attributes**.
Click **Filter** and select **Add Facet "color"**.
4. Save your changes.
### Create the rule with the API
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"color-facets",
new Rule
{
ObjectID = "color-facets",
Conditions = new List { new Condition { Pattern = "{facet:color}" } },
Consequence = new Consequence
{
Params = new ConsequenceParams
{
AutomaticFacetFilters = new AutomaticFacetFilters(
new List { new AutomaticFacetFilter { Facet = "color" } }
),
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "color-facets",
rule: Rule(
objectID: "color-facets",
conditions: [
Condition(
pattern: "{facet:color}",
),
],
consequence: Consequence(
params: ConsequenceParams(
automaticFacetFilters: [
AutomaticFacetFilter(
facet: "color",
),
],
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "color-facets",
search.NewEmptyRule().SetObjectID("color-facets").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("{facet:color}")}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetAutomaticFacetFilters(search.ArrayOfAutomaticFacetFilterAsAutomaticFacetFilters(
[]search.AutomaticFacetFilter{*search.NewEmptyAutomaticFacetFilter().SetFacet("color")}))))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"color-facets",
new Rule()
.setObjectID("color-facets")
.setConditions(Arrays.asList(new Condition().setPattern("{facet:color}")))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams().setAutomaticFacetFilters(
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(Arrays.asList(new AutomaticFacetFilter().setFacet("color")))
)
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'color-facets',
rule: {
objectID: 'color-facets',
conditions: [{ pattern: '{facet:color}' }],
consequence: { params: { automaticFacetFilters: [{ facet: 'color' }] } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "color-facets",
rule =
Rule(
objectID = "color-facets",
conditions = listOf(Condition(pattern = "{facet:color}")),
consequence =
Consequence(
params =
ConsequenceParams(
automaticFacetFilters =
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(
listOf(AutomaticFacetFilter(facet = "color"))
)
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'color-facets',
['objectID' => 'color-facets',
'conditions' => [
['pattern' => '{facet:color}',
],
],
'consequence' => ['params' => ['automaticFacetFilters' => [
['facet' => 'color',
],
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="color-facets",
rule={
"objectID": "color-facets",
"conditions": [
{
"pattern": "{facet:color}",
},
],
"consequence": {
"params": {
"automaticFacetFilters": [
{
"facet": "color",
},
],
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"color-facets",
Algolia::Search::Rule.new(
algolia_object_id: "color-facets",
conditions: [Algolia::Search::Condition.new(pattern: "{facet:color}")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
automatic_facet_filters: [Algolia::Search::AutomaticFacetFilter.new(facet: "color")]
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "color-facets",
rule = Rule(
objectID = "color-facets",
conditions = Some(
Seq(
Condition(
pattern = Some("{facet:color}")
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
automaticFacetFilters = Some(
AutomaticFacetFilters(
Seq(
AutomaticFacetFilter(
facet = "color"
)
)
)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "color-facets",
rule: Rule(
objectID: "color-facets",
conditions: [SearchCondition(pattern: "{facet:color}")],
consequence: SearchConsequence(
params: SearchConsequenceParams(automaticFacetFilters: SearchAutomaticFacetFilters
.arrayOfSearchAutomaticFacetFilter([SearchAutomaticFacetFilter(facet: "color")]))
)
)
)
```
To show the filters applied by this rule as selected options in your search UI,
see [Show automatically applied filters](/doc/guides/solutions/ecommerce/filtering-and-navigation/tutorials/auto-selected-facets).
## Numerical dynamic filtering
For the query `cheap toaster 800w`, use one rule to filter by `product_type:toaster`
and another to filter by `price < 15`.
Remove `toaster` and `cheap` from the query so the search uses `800w` to find matching products.
With the [example dataset](#example-dataset),
the query returns the Essentials 800w toaster, priced at \$14.99.
| Query | Results |
| - | - |
| cheap toaster 800w | Essentials 800w, Toaster, Daewoo, Black, 14.99 |
### Create the rules in the dashboard
You can add these rules from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. Open **Rules** and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, enter `toaster`, and click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `product_type` as the attribute and `toaster` as the value, then click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `toaster` in **Word(s)** and click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
Repeat these steps to create the second rule.
Use `cheap` for the condition and removed word, and filter on `price < 15`.
1. Open **Rules**, select **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `toaster`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{"filters":"product_type:toaster"}`.
4. Click **Add consequence** and select **Remove Word**.
Enter `toaster`.
5. Save your changes.
Repeat these steps to create the second rule.
Use `cheap` for the condition and removed word, and enter `{"filters":"price < 15"}` as the query parameter.
### Create the rules with the API
```cs C# theme={"system"}
var response = await client.SaveRulesAsync(
"INDEX_NAME",
new List
{
new Rule
{
ObjectID = "toaster",
Conditions = new List
{
new Condition { Pattern = "toaster", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Query = new ConsequenceQuery(
new ConsequenceQueryObject { Remove = new List { "toaster" } }
),
Filters = "product_type:toaster",
},
},
},
new Rule
{
ObjectID = "cheap",
Conditions = new List
{
new Condition { Pattern = "cheap", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Query = new ConsequenceQuery(
new ConsequenceQueryObject { Remove = new List { "cheap" } }
),
Filters = "price < 15",
},
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRules(
indexName: "INDEX_NAME",
rules: [
Rule(
objectID: "toaster",
conditions: [
Condition(
pattern: "toaster",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
query: ConsequenceQueryObject(
remove: [
"toaster",
],
),
filters: "product_type:toaster",
),
),
),
Rule(
objectID: "cheap",
conditions: [
Condition(
pattern: "cheap",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
query: ConsequenceQueryObject(
remove: [
"cheap",
],
),
filters: "price < 15",
),
),
),
],
);
```
```go Go theme={"system"}
response, err := client.SaveRules(client.NewApiSaveRulesRequest(
"INDEX_NAME",
[]search.Rule{*search.NewEmptyRule().SetObjectID("toaster").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("toaster").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetRemove(
[]string{"toaster"}))).SetFilters("product_type:toaster"))), *search.NewEmptyRule().SetObjectID("cheap").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("cheap").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetRemove(
[]string{"cheap"}))).SetFilters("price < 15")))}))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRules(
"INDEX_NAME",
Arrays.asList(
new Rule()
.setObjectID("toaster")
.setConditions(Arrays.asList(new Condition().setPattern("toaster").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setQuery(new ConsequenceQueryObject().setRemove(Arrays.asList("toaster")))
.setFilters("product_type:toaster")
)
),
new Rule()
.setObjectID("cheap")
.setConditions(Arrays.asList(new Condition().setPattern("cheap").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams().setQuery(new ConsequenceQueryObject().setRemove(Arrays.asList("cheap"))).setFilters("price < 15")
)
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRules({
indexName: 'INDEX_NAME',
rules: [
{
objectID: 'toaster',
conditions: [{ pattern: 'toaster', anchoring: 'contains' }],
consequence: { params: { query: { remove: ['toaster'] }, filters: 'product_type:toaster' } },
},
{
objectID: 'cheap',
conditions: [{ pattern: 'cheap', anchoring: 'contains' }],
consequence: { params: { query: { remove: ['cheap'] }, filters: 'price < 15' } },
},
],
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRules(
indexName = "INDEX_NAME",
rules =
listOf(
Rule(
objectID = "toaster",
conditions =
listOf(
Condition(
pattern = "toaster",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
query = ConsequenceQueryObject(remove = listOf("toaster")),
filters = "product_type:toaster",
)
),
),
Rule(
objectID = "cheap",
conditions =
listOf(
Condition(
pattern = "cheap",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
query = ConsequenceQueryObject(remove = listOf("cheap")),
filters = "price < 15",
)
),
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRules(
'INDEX_NAME',
[
['objectID' => 'toaster',
'conditions' => [
['pattern' => 'toaster',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['query' => ['remove' => [
'toaster',
],
],
'filters' => 'product_type:toaster',
],
],
],
['objectID' => 'cheap',
'conditions' => [
['pattern' => 'cheap',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['query' => ['remove' => [
'cheap',
],
],
'filters' => 'price < 15',
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rules(
index_name="INDEX_NAME",
rules=[
{
"objectID": "toaster",
"conditions": [
{
"pattern": "toaster",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"query": {
"remove": [
"toaster",
],
},
"filters": "product_type:toaster",
},
},
},
{
"objectID": "cheap",
"conditions": [
{
"pattern": "cheap",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"query": {
"remove": [
"cheap",
],
},
"filters": "price < 15",
},
},
},
],
)
```
```ruby Ruby theme={"system"}
response = client.save_rules(
"INDEX_NAME",
[
Algolia::Search::Rule.new(
algolia_object_id: "toaster",
conditions: [Algolia::Search::Condition.new(pattern: "toaster", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
query: Algolia::Search::ConsequenceQueryObject.new(remove: ["toaster"]),
filters: "product_type:toaster"
)
)
),
Algolia::Search::Rule.new(
algolia_object_id: "cheap",
conditions: [Algolia::Search::Condition.new(pattern: "cheap", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
query: Algolia::Search::ConsequenceQueryObject.new(remove: ["cheap"]),
filters: "price < 15"
)
)
)
]
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRules(
indexName = "INDEX_NAME",
rules = Seq(
Rule(
objectID = "toaster",
conditions = Some(
Seq(
Condition(
pattern = Some("toaster"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
query = Some(
ConsequenceQueryObject(
remove = Some(Seq("toaster"))
)
),
filters = Some("product_type:toaster")
)
)
)
),
Rule(
objectID = "cheap",
conditions = Some(
Seq(
Condition(
pattern = Some("cheap"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
query = Some(
ConsequenceQueryObject(
remove = Some(Seq("cheap"))
)
),
filters = Some("price < 15")
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRules(
indexName: "INDEX_NAME",
rules: [
Rule(objectID: "toaster", conditions: [SearchCondition(
pattern: "toaster",
anchoring: SearchAnchoring.contains
)], consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "product_type:toaster",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(remove: ["toaster"]))
))),
Rule(
objectID: "cheap",
conditions: [SearchCondition(pattern: "cheap", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(
filters: "price < 15",
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(remove: ["cheap"]))
))
),
]
)
```
## Filter by brand
Users can enter `apple headphones` to search for Apple headphones.
The rule applies a filter for the Apple brand and removes the matched brand value from the query,
leaving `headphones` as the search term.
This extends [one rule per facet](#example-one-rule-per-facet)
by also removing the matched brand value from the query.
With the [example dataset](#example-dataset), the query returns Airpods Max.
It excludes the Sony headphones, even though their `color` attribute contains `Apple`.
| Query | Results |
| - | - |
| apple headphones | Airpods Max, Headphones, Apple, Gray, 548.99 |
### Create the rule in the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. Open **Rules** and select **New rule > Visual Editor**.
2. Click **Set query condition(s)** and select **contains**.
Select **Match any value of a facet** and choose `brand`.
Clear the checkbox for matching plurals, synonyms, and typos, then click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Filter / Boost matching attribute**.
Select `brand` in **Facet**, choose **Filter**, and click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Select `{facet:brand}` in **Word(s)** and click **Apply**.
5. Review the results, click **Review and Publish**, and click **Publish**.
1. Create a rule with the Manual Editor.
2. In **Condition(s)**, click the box next to **Query contains** and select **Add Facet "brand"**.
Clear **Apply to plurals, synonyms and typos** to ensure precise brand matching.
3. In **Consequence(s)**, click **Add consequence** and select **Filter/Boost Matching Attributes**.
Click **Filter** and select **Add Facet "brand"**.
4. Click **Add consequence** and select **Remove word**.
Select **Remove `{facet:brand}`**.
5. Save your changes.
### Create the rule with the API
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"tagged-brand-rule",
new Rule
{
Conditions = new List
{
new Condition
{
Pattern = "brand: {facet:brand}",
Anchoring = Enum.Parse("Contains"),
Alternatives = false,
},
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
AutomaticFacetFilters = new AutomaticFacetFilters(
new List { new AutomaticFacetFilter { Facet = "brand" } }
),
Query = new ConsequenceQuery(
new ConsequenceQueryObject
{
Remove = new List { "brand:", "{facet:brand}" },
}
),
},
},
Description = "filter on brand: {brand}",
ObjectID = "tagged-brand-rule",
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "tagged-brand-rule",
rule: Rule(
conditions: [
Condition(
pattern: "brand: {facet:brand}",
anchoring: Anchoring.fromJson("contains"),
alternatives: false,
),
],
consequence: Consequence(
params: ConsequenceParams(
automaticFacetFilters: [
AutomaticFacetFilter(
facet: "brand",
),
],
query: ConsequenceQueryObject(
remove: [
"brand:",
"{facet:brand}",
],
),
),
),
description: "filter on brand: {brand}",
objectID: "tagged-brand-rule",
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "tagged-brand-rule",
search.NewEmptyRule().SetConditions(
[]search.Condition{
*search.NewEmptyCondition().SetPattern("brand: {facet:brand}").SetAnchoring(search.Anchoring("contains")).SetAlternatives(false),
}).
SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetAutomaticFacetFilters(search.ArrayOfAutomaticFacetFilterAsAutomaticFacetFilters(
[]search.AutomaticFacetFilter{
*search.NewEmptyAutomaticFacetFilter().SetFacet("brand"),
},
)).SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetRemove(
[]string{"brand:", "{facet:brand}"}))))).SetDescription("filter on brand: {brand}").SetObjectID("tagged-brand-rule")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"tagged-brand-rule",
new Rule()
.setConditions(
Arrays.asList(new Condition().setPattern("brand: {facet:brand}").setAnchoring(Anchoring.CONTAINS).setAlternatives(false))
)
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setAutomaticFacetFilters(
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(Arrays.asList(new AutomaticFacetFilter().setFacet("brand")))
)
.setQuery(new ConsequenceQueryObject().setRemove(Arrays.asList("brand:", "{facet:brand}")))
)
)
.setDescription("filter on brand: {brand}")
.setObjectID("tagged-brand-rule")
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'tagged-brand-rule',
rule: {
conditions: [{ pattern: 'brand: {facet:brand}', anchoring: 'contains', alternatives: false }],
consequence: {
params: { automaticFacetFilters: [{ facet: 'brand' }], query: { remove: ['brand:', '{facet:brand}'] } },
},
description: 'filter on brand: {brand}',
objectID: 'tagged-brand-rule',
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "tagged-brand-rule",
rule =
Rule(
conditions =
listOf(
Condition(
pattern = "brand: {facet:brand}",
anchoring = Anchoring.entries.first { it.value == "contains" },
alternatives = false,
)
),
consequence =
Consequence(
params =
ConsequenceParams(
automaticFacetFilters =
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(
listOf(AutomaticFacetFilter(facet = "brand"))
),
query = ConsequenceQueryObject(remove = listOf("brand:", "{facet:brand}")),
)
),
description = "filter on brand: {brand}",
objectID = "tagged-brand-rule",
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'tagged-brand-rule',
['conditions' => [
['pattern' => 'brand: {facet:brand}',
'anchoring' => 'contains',
'alternatives' => false,
],
],
'consequence' => ['params' => ['automaticFacetFilters' => [
['facet' => 'brand',
],
],
'query' => ['remove' => [
'brand:',
'{facet:brand}',
],
],
],
],
'description' => 'filter on brand: {brand}',
'objectID' => 'tagged-brand-rule',
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="tagged-brand-rule",
rule={
"conditions": [
{
"pattern": "brand: {facet:brand}",
"anchoring": "contains",
"alternatives": False,
},
],
"consequence": {
"params": {
"automaticFacetFilters": [
{
"facet": "brand",
},
],
"query": {
"remove": [
"brand:",
"{facet:brand}",
],
},
},
},
"description": "filter on brand: {brand}",
"objectID": "tagged-brand-rule",
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"tagged-brand-rule",
Algolia::Search::Rule.new(
conditions: [
Algolia::Search::Condition.new(pattern: "brand: {facet:brand}", anchoring: "contains", alternatives: false)
],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
automatic_facet_filters: [Algolia::Search::AutomaticFacetFilter.new(facet: "brand")],
query: Algolia::Search::ConsequenceQueryObject.new(remove: ["brand:", "{facet:brand}"])
)
),
description: "filter on brand: {brand}",
algolia_object_id: "tagged-brand-rule"
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "tagged-brand-rule",
rule = Rule(
conditions = Some(
Seq(
Condition(
pattern = Some("brand: {facet:brand}"),
anchoring = Some(Anchoring.withName("contains")),
alternatives = Some(false)
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
automaticFacetFilters = Some(
AutomaticFacetFilters(
Seq(
AutomaticFacetFilter(
facet = "brand"
)
)
)
),
query = Some(
ConsequenceQueryObject(
remove = Some(Seq("brand:", "{facet:brand}"))
)
)
)
)
),
description = Some("filter on brand: {brand}"),
objectID = "tagged-brand-rule"
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "tagged-brand-rule",
rule: Rule(
objectID: "tagged-brand-rule",
conditions: [SearchCondition(
pattern: "brand: {facet:brand}",
anchoring: SearchAnchoring.contains,
alternatives: false
)],
consequence: SearchConsequence(params: SearchConsequenceParams(
query: SearchConsequenceQuery.searchConsequenceQueryObject(SearchConsequenceQueryObject(remove: [
"brand:",
"{facet:brand}",
])),
automaticFacetFilters: SearchAutomaticFacetFilters
.arrayOfSearchAutomaticFacetFilter([SearchAutomaticFacetFilter(facet: "brand")])
)),
description: "filter on brand: {brand}"
)
)
```
## Example dataset
The examples on this page use the following product catalog records:
```json JSON icon=braces theme={"system"}
[
{
"product_name": "FM",
"product_type": "T-shirt",
"brand": "LondonLook",
"color": "Red",
"price": 21.99
},
{
"product_name": "Caveman",
"product_type": "T-shirt",
"brand": "Red or Dead",
"color": "Brown",
"price": 9.99
},
{
"product_name": "iPhone 13 Pro Max",
"product_type": "Phone",
"brand": "Apple",
"color": "Blue",
"price": 1824.61
},
{
"product_name": "Will Carpenter",
"product_type": "T-shirt",
"brand": "Red or Dead",
"color": "Black",
"price": 199.99
},
{
"product_name": "The Mandal 800w",
"product_type": "Toaster",
"brand": "Black & Decker",
"color": "Red",
"price": 149.99
},
{
"product_name": "Essentials 800w",
"product_type": "Toaster",
"brand": "Daewoo",
"color": "Black",
"price": 14.99
},
{
"product_name": "Airpods Max",
"product_type": "Headphones",
"brand": "Apple",
"color": "Gray",
"price": 548.99
},
{
"product_name": "WH-CH520",
"product_type": "Headphones",
"brand": "Sony",
"color": "Apple",
"price": 39.99
}
]
```
## See also
* [Adding filters based on the query](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query)
* [Faceting](/doc/guides/managing-results/refine-results/faceting#conjunctive-and-disjunctive-faceting)
# Enhance search results with query-based rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/detecting-intent/how-to/applying-search-parameters-for-a-specific-query
Use rules to enhance search results based on user queries.
You can use rules to enhance the user experience based on what they search for.
For example, if you're building an app to search for airports,
Algolia's [geolocation feature](/doc/guides/managing-results/refine-results/geolocation) helpfully ranks results by proximity.
However, the nearest airport isn't necessarily the best choice.
In Paris, a search based on proximity might rank Le Bourget, a small airport for private jets, over large airports like Charles de Gaulle or Orly.
As a solution, you could prioritize other factors over [proximity](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria#geo-if-applicable).
For example, you could rank airports by their number of airline connections.
However, if you also set a geolocation parameter like [`aroundLatLngViaIP`](/doc/api-reference/api-parameters/aroundLatLngViaIP),
proximity remains the primary factor in determining the order of search results.
A better solution is to create a rule so that when users search for a specific city or country,
it turns off the `aroundLatLngViaIP` parameter for that query.
## Before you begin
Before creating the rule, ensure that:
* `nb_airline_connections` is the [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) attribute
* `city` and `country` are set as [attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard), so you can detect whenever a query matches them
## Create the rule
### Using the dashboard
This example assumes you're using the [sample dataset](#example-dataset).
You can add these rules from the Algolia dashboard using either the Visual Editor or the Manual Editor.
#### Custom ranking and attributes for faceting
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. On the **Configuration** tab, click **Ranking and Sorting**.
5. Click **Add custom ranking attribute** and select the `nb_airline_connections` attribute from the drop-down menu.
6. Click **Facets**, under Filtering and faceting, then click **Add an Attribute** and select `country` and `city` from the drop-down menu.
7. Save your changes.
After configuring custom ranking and attributes for faceting, create a rule for each facet:
1. Open **Rules** and select **New rule > Visual Editor**.
2. Click **Set query condition(s)** and select **contains**.
Select **Match any value of a facet**, choose `country`, and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Apply search parameters**.
Enter `{ "aroundLatLngViaIP": false }` in **Value** and click **Apply**.
4. Click **Review and Publish**, then **Publish**.
Repeat these steps to create the second rule, but choose `city` instead of `country` as the facet.
In the Manual Editor, create a separate rule for the `country` and `city` facets.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and select `country`.
The input displays `{facet:country}`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "aroundLatLngViaIP": false }`.
4. Save your changes.
Repeat these steps to create the second rule, but select `city` so the condition displays `{facet:city}`.
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
Before creating the rule,
set custom ranking and the [attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
This example assumes you're using the [sample dataset](#example-dataset).
#### Custom ranking and attributes for faceting
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
CustomRanking = new List { "desc(nb_airline_liaisons)" },
AttributesForFaceting = new List { "city, country" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
customRanking: [
"desc(nb_airline_liaisons)",
],
attributesForFaceting: [
"city, country",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetCustomRanking(
[]string{"desc(nb_airline_liaisons)"}).SetAttributesForFaceting(
[]string{"city, country"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setCustomRanking(Arrays.asList("desc(nb_airline_liaisons)"))
.setAttributesForFaceting(Arrays.asList("city, country"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { customRanking: ['desc(nb_airline_liaisons)'], attributesForFaceting: ['city, country'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
customRanking = listOf("desc(nb_airline_liaisons)"),
attributesForFaceting = listOf("city, country"),
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['customRanking' => [
'desc(nb_airline_liaisons)',
],
'attributesForFaceting' => [
'city, country',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"customRanking": [
"desc(nb_airline_liaisons)",
],
"attributesForFaceting": [
"city, country",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
custom_ranking: ["desc(nb_airline_liaisons)"],
attributes_for_faceting: ["city, country"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
customRanking = Some(Seq("desc(nb_airline_liaisons)")),
attributesForFaceting = Some(Seq("city, country"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: ["city, country"],
customRanking: ["desc(nb_airline_liaisons)"]
)
)
```
#### Create the rules
Use the [`batch-rules`](/doc/libraries/sdk/v1/methods/batch-rules) method to create rules that detect matches in facets `city` and `country`, and change the search parameters accordingly.
```cs C# theme={"system"}
var response = await client.SaveRulesAsync(
"INDEX_NAME",
new List
{
new Rule
{
ObjectID = "country",
Conditions = new List
{
new Condition
{
Pattern = "{facet:country}",
Anchoring = Enum.Parse("Contains"),
},
},
Consequence = new Consequence
{
Params = new ConsequenceParams { AroundLatLngViaIP = false },
},
},
new Rule
{
ObjectID = "city",
Conditions = new List
{
new Condition
{
Pattern = "{facet:city}",
Anchoring = Enum.Parse("Contains"),
},
},
Consequence = new Consequence
{
Params = new ConsequenceParams { AroundLatLngViaIP = false },
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRules(
indexName: "INDEX_NAME",
rules: [
Rule(
objectID: "country",
conditions: [
Condition(
pattern: "{facet:country}",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
aroundLatLngViaIP: false,
),
),
),
Rule(
objectID: "city",
conditions: [
Condition(
pattern: "{facet:city}",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
aroundLatLngViaIP: false,
),
),
),
],
);
```
```go Go theme={"system"}
response, err := client.SaveRules(client.NewApiSaveRulesRequest(
"INDEX_NAME",
[]search.Rule{*search.NewEmptyRule().SetObjectID("country").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("{facet:country}").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetAroundLatLngViaIP(false))), *search.NewEmptyRule().SetObjectID("city").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("{facet:city}").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetAroundLatLngViaIP(false)))}))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRules(
"INDEX_NAME",
Arrays.asList(
new Rule()
.setObjectID("country")
.setConditions(Arrays.asList(new Condition().setPattern("{facet:country}").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(new Consequence().setParams(new ConsequenceParams().setAroundLatLngViaIP(false))),
new Rule()
.setObjectID("city")
.setConditions(Arrays.asList(new Condition().setPattern("{facet:city}").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(new Consequence().setParams(new ConsequenceParams().setAroundLatLngViaIP(false)))
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRules({
indexName: 'INDEX_NAME',
rules: [
{
objectID: 'country',
conditions: [{ pattern: '{facet:country}', anchoring: 'contains' }],
consequence: { params: { aroundLatLngViaIP: false } },
},
{
objectID: 'city',
conditions: [{ pattern: '{facet:city}', anchoring: 'contains' }],
consequence: { params: { aroundLatLngViaIP: false } },
},
],
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRules(
indexName = "INDEX_NAME",
rules =
listOf(
Rule(
objectID = "country",
conditions =
listOf(
Condition(
pattern = "{facet:country}",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence = Consequence(params = ConsequenceParams(aroundLatLngViaIP = false)),
),
Rule(
objectID = "city",
conditions =
listOf(
Condition(
pattern = "{facet:city}",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence = Consequence(params = ConsequenceParams(aroundLatLngViaIP = false)),
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRules(
'INDEX_NAME',
[
['objectID' => 'country',
'conditions' => [
['pattern' => '{facet:country}',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['aroundLatLngViaIP' => false,
],
],
],
['objectID' => 'city',
'conditions' => [
['pattern' => '{facet:city}',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['aroundLatLngViaIP' => false,
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rules(
index_name="INDEX_NAME",
rules=[
{
"objectID": "country",
"conditions": [
{
"pattern": "{facet:country}",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"aroundLatLngViaIP": False,
},
},
},
{
"objectID": "city",
"conditions": [
{
"pattern": "{facet:city}",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"aroundLatLngViaIP": False,
},
},
},
],
)
```
```ruby Ruby theme={"system"}
response = client.save_rules(
"INDEX_NAME",
[
Algolia::Search::Rule.new(
algolia_object_id: "country",
conditions: [Algolia::Search::Condition.new(pattern: "{facet:country}", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(around_lat_lng_via_ip: false)
)
),
Algolia::Search::Rule.new(
algolia_object_id: "city",
conditions: [Algolia::Search::Condition.new(pattern: "{facet:city}", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(around_lat_lng_via_ip: false)
)
)
]
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRules(
indexName = "INDEX_NAME",
rules = Seq(
Rule(
objectID = "country",
conditions = Some(
Seq(
Condition(
pattern = Some("{facet:country}"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
aroundLatLngViaIP = Some(false)
)
)
)
),
Rule(
objectID = "city",
conditions = Some(
Seq(
Condition(
pattern = Some("{facet:city}"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
aroundLatLngViaIP = Some(false)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRules(
indexName: "INDEX_NAME",
rules: [
Rule(objectID: "country", conditions: [SearchCondition(
pattern: "{facet:country}",
anchoring: SearchAnchoring.contains
)], consequence: SearchConsequence(params: SearchConsequenceParams(aroundLatLngViaIP: false))),
Rule(
objectID: "city",
conditions: [SearchCondition(pattern: "{facet:city}", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(params: SearchConsequenceParams(aroundLatLngViaIP: false))
),
]
)
```
## Query time
If a user query exactly matches a city or a country **in** your dataset,
the rule sets `aroundLatLngViaIP` to `false` overriding any other parameters in the query.
For example, if a user searches for "Paris" or "China", matching airports are ranked by `nb_airline_connections`.
When not searching for either a city or a country, airports are ranked by distance from the user's location.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { AroundLatLngViaIP = true })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
aroundLatLngViaIP: true,
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetAroundLatLngViaIP(true))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex("INDEX_NAME", new SearchParamsObject().setAroundLatLngViaIP(true), Hit.class);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { aroundLatLngViaIP: true },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(aroundLatLngViaIP = true),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['aroundLatLngViaIP' => true,
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"aroundLatLngViaIP": True,
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(around_lat_lng_via_ip: true)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
aroundLatLngViaIP = Some(true)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(aroundLatLngViaIP: true))
)
```
## Example dataset
```json JSON icon=braces theme={"system"}
[
{
"name": "Orly",
"city": "Paris",
"country": "France",
"_geoloc": { "lat": 48.725278, "lng": 2.359444 },
"nb_airline_connections": 404
},
{
"name": "Charles De Gaulle",
"city": "Paris",
"country": "France",
"_geoloc": { "lat": 49.012779, "lng": 2.55 },
"nb_airline_connections": 1041
},
{
"name": "Le Bourget",
"city": "Paris",
"country": "France",
"_geoloc": { "lat": 48.961487, "lng": 2.436966 },
"nb_airline_connections": 0
},
{
"name": "Ben Gurion",
"city": "Tel-Aviv",
"country": "Israel",
"_geoloc": { "lat": 32.011389, "lng": 34.886667 },
"nb_airline_connections": 271
},
{
"name": "Haifa",
"city": "Haifa",
"country": "Israel",
"_geoloc": { "lat": 32.809444, "lng": 35.043056 },
"nb_airline_connections": 4
},
{
"name": "Pudong",
"city": "Shanghai",
"country": "China",
"_geoloc": { "lat": 31.143378, "lng": 121.805214 },
"nb_airline_connections": 825
},
{
"name": "Hongqiao Intl",
"city": "Shanghai",
"country": "China",
"_geoloc": { "lat": 31.197875, "lng": 121.336319 },
"nb_airline_connections": 411
}
]
```
## See also
* [Geographical search with InstantSearch.js](/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/js)
* [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
# Detect keywords
Source: https://www.algolia.com/doc/guides/managing-results/rules/detecting-intent/how-to/detecting-keywords
Detect keywords and reserved words in the query using Algolia Rules
You can use rules to detect a specific word and modify a parameter because of this word.
## Limit the search to only a subset of attributes
A good example of altering a query is with an online document library that allows keyword searches inside documents.
If a user types in the word "article ref21", they're probably looking for an article whose title or ID contains "ref21".\_
### Rule
*If query = "article ref21" then remove article and search for an article whose title or ID contains 'ref21'*
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"article-rule",
new Rule
{
ObjectID = "article-rule",
Conditions = new List
{
new Condition { Pattern = "article", Anchoring = Enum.Parse("StartsWith") },
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
Query = new ConsequenceQuery(
new ConsequenceQueryObject
{
Edits = new List
{
new Edit { Type = Enum.Parse("Remove"), Delete = "article" },
},
}
),
RestrictSearchableAttributes = new List { "title", "book_id" },
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "article-rule",
rule: Rule(
objectID: "article-rule",
conditions: [
Condition(
pattern: "article",
anchoring: Anchoring.fromJson("startsWith"),
),
],
consequence: Consequence(
params: ConsequenceParams(
query: ConsequenceQueryObject(
edits: [
Edit(
type: EditType.fromJson("remove"),
delete: "article",
),
],
),
restrictSearchableAttributes: [
"title",
"book_id",
],
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "article-rule",
search.NewEmptyRule().SetObjectID("article-rule").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("article").SetAnchoring(search.Anchoring("startsWith"))}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetEdits(
[]search.Edit{
*search.NewEmptyEdit().SetType(search.EditType("remove")).SetDelete("article"),
}),
)).SetRestrictSearchableAttributes(
[]string{"title", "book_id"})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"article-rule",
new Rule()
.setObjectID("article-rule")
.setConditions(Arrays.asList(new Condition().setPattern("article").setAnchoring(Anchoring.STARTS_WITH)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setQuery(new ConsequenceQueryObject().setEdits(Arrays.asList(new Edit().setType(EditType.REMOVE).setDelete("article"))))
.setRestrictSearchableAttributes(Arrays.asList("title", "book_id"))
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'article-rule',
rule: {
objectID: 'article-rule',
conditions: [{ pattern: 'article', anchoring: 'startsWith' }],
consequence: {
params: {
query: { edits: [{ type: 'remove', delete: 'article' }] },
restrictSearchableAttributes: ['title', 'book_id'],
},
},
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "article-rule",
rule =
Rule(
objectID = "article-rule",
conditions =
listOf(
Condition(
pattern = "article",
anchoring = Anchoring.entries.first { it.value == "startsWith" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
query =
ConsequenceQueryObject(
edits =
listOf(
Edit(
type = EditType.entries.first { it.value == "remove" },
delete = "article",
)
)
),
restrictSearchableAttributes = listOf("title", "book_id"),
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'article-rule',
['objectID' => 'article-rule',
'conditions' => [
['pattern' => 'article',
'anchoring' => 'startsWith',
],
],
'consequence' => ['params' => ['query' => ['edits' => [
['type' => 'remove',
'delete' => 'article',
],
],
],
'restrictSearchableAttributes' => [
'title',
'book_id',
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="article-rule",
rule={
"objectID": "article-rule",
"conditions": [
{
"pattern": "article",
"anchoring": "startsWith",
},
],
"consequence": {
"params": {
"query": {
"edits": [
{
"type": "remove",
"delete": "article",
},
],
},
"restrictSearchableAttributes": [
"title",
"book_id",
],
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"article-rule",
Algolia::Search::Rule.new(
algolia_object_id: "article-rule",
conditions: [Algolia::Search::Condition.new(pattern: "article", anchoring: "startsWith")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
query: Algolia::Search::ConsequenceQueryObject.new(
edits: [Algolia::Search::Edit.new(type: "remove", delete: "article")]
),
restrict_searchable_attributes: ["title", "book_id"]
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "article-rule",
rule = Rule(
objectID = "article-rule",
conditions = Some(
Seq(
Condition(
pattern = Some("article"),
anchoring = Some(Anchoring.withName("startsWith"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
query = Some(
ConsequenceQueryObject(
edits = Some(
Seq(
Edit(
`type` = Some(EditType.withName("remove")),
delete = Some("article")
)
)
)
)
),
restrictSearchableAttributes = Some(Seq("title", "book_id"))
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "article-rule",
rule: Rule(
objectID: "article-rule",
conditions: [SearchCondition(pattern: "article", anchoring: SearchAnchoring.startsWith)],
consequence: SearchConsequence(params: SearchConsequenceParams(
restrictSearchableAttributes: ["title", "book_id"],
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(edits: [SearchEdit(
type: SearchEditType.remove,
delete: "article"
)]))
))
)
)
```
### Using the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
The `title` and `id` attributes must already be in your index's `searchableAttributes`.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, enter `article`, and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Apply search parameters**.
Enter `{ "restrictSearchableAttributes": ["title", "id"] }` in **Value** and click **Apply**.
4. Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
Enter `article` in **Word(s)** and click **Apply**.
5. Preview the results for `article ref21`, click **Review and Publish**, and click **Publish**.
1. In the Algolia dashboard, select **Search**, select your , and open [**Rules**](https://dashboard.algolia.com/rules).
Select **Create your first rule** or **New rule**, then select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `article`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "restrictSearchableAttributes": ["title", "id"] }`.
4. Click **Add consequence** again and select **Remove Word**.
Enter `article`.
5. Save your changes.
# Merchandising
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting
Merchandise search results, facets, and category pages with Algolia. Apply rules to influence the ranking of individual records or whole categories.
Merchandising is the promotion of specific products or categories of products.
Depending on your goals, merchandising lets you:
* Offer recommendations to your users
* Highlight your best products
* Follow trends
* Improve
Algolia offers multiple merchandising capabilities, all powered by rules.
In the Algolia dashboard, use the Visual Editor for most merchandising rules.
It supports the same conditions and consequences as the Manual Editor and previews their effect on search results.
Use the Manual Editor when you prefer to enter rule details directly, including JSON values, and don't need a preview.
For more control and dynamic compositions of your results feed,
see [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
For more information, see:
* [Rules overview](/doc/guides/managing-results/rules/rules-overview)
* [Merchandising category pages](https://www.algolia.com/ecommerce-merchandising-playbook/merchandise-category-pages/)
* [Merchandising search results pages](https://www.algolia.com/ecommerce-merchandising-playbook/merchandise-search-results-page/)
* [What can I do with the Visual Editor?](https://support.algolia.com/hc/en-us/articles/4406981935761)
## Merchandising Studio
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
The [Merchandising Studio](https://www.algolia.com/industries-and-solutions/ecommerce/digital-merchandising/) is an optional feature designed for digital merchandisers who prefer a no-code approach.
It allows ecommerce users to focus on search merchandising, category merchandising, and facet merchandising.
## Promote hits
[Promoting a hit](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits) means you want it to appear first in results for a specific .
For example:
* A bookstore may want to recommend a Harry Potter box set whenever the term "Harry Potter" appears in the search.
* You may want to manage new releases for the latest iPhone. With custom ranking, you've placed best-selling items at the top of your search results. Unfortunately, this doesn't work well with new releases, which don't have any sales yet (meaning they're probably listed last). With rules, you can force new releases to the top of the results list without changing your custom ranking.
You can promote up to hits in a single rule.
For more information, see:
* [Promoting hits](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits)
* [Pin blocks](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/pin-blocks)
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking)
## Hide hits
You can [hide hits](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-hide-hits) if required.
Your may contain relevant only to a specific search.
For example, you have a website where people can search for dishes from restaurants with a home delivery service
If someone types "gluten free", you don't want to show items with gluten (even though gluten is in the search query).
Hiding hits lets you remove all gluten items from the results.
## Redirect to URL
You can [add a redirect](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects) to a specific URL.
For example, consider that you find in your search analytics that a lot of your users are searching for "help" but don't get any results.
You can add a redirect for this query to your website's FAQ or support page.
This improves the user experience and reduces the no-results rate.
Other examples include searching for a category. For example the query "TV" redirects users to the corresponding category page. Or, searching for a specific product or brand redirects users to a seasonal promotional page for it.
## Display banners
You can [display a banner](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners) to promote an item, a group of products, a business-wide sale, and so on.
Using the same logic as hits promotion (looking for specific keywords in a query), you can create a rule with the necessary data to display a banner: the text, HTML, CSS, and associated images.
## Dynamically promote filters
You can use a rule to promote products based on [user intent](/doc/guides/managing-results/rules/detecting-intent).
For example, if a user appears interested in an important product category,
you can apply a dynamic to promote relevant items.
You can also generate optional filters based on the user's behavior.
### Use optional filters
Optional filters let you favor some filters over others.
They behave like regular filters, except that with optional filtering, **results that don't match a filter aren't excluded altogether**: they're only ranked lower in the results.
For example, if you know a user is interested in the brand Samsung, you can promote that brand with optional filters: when users type "phone", all Samsung phones will be returned first, followed by all other phones.
For more information, see:
* [Promote records with optional filters](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-promote-with-optional-filters)
* [Optional filters](/doc/guides/managing-results/rules/merchandising-and-promoting/in-depth/optional-filters)
## Combine rules
You can use the merchandising techniques individually or [in combination](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/combining-multiple-consequences): one condition can have more than one consequence.
For example, if a user types "Harry Potter", you can simultaneously:
* Promote movies over books
* Set the most recent Harry Potter box set as the first result
* Add filters for J. K. Rowling, children's novels, or the fantasy genre.
# Promote results with rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits
Use rules to promote (pin) specific search results or display a banner at the top of your search results.
To promote groups of results dynamically, see [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
To keep pinned items together when one of them becomes unavailable, see [Keep pinned items together with pin blocks](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/pin-blocks).
If `typoTolerance` is `min` or `strict`,
promoted results might not appear if they contain more typos than the top results.
## Promote a single items
For example, a book store wants to recommend a Harry Potter box set whenever the words "Harry Potter" form part of a search.
### Rule
If the is `Harry Potter`,
promote the Harry Potter box set.
By default, you can pin up to 300 per rule.
### With the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"} theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"promote-harry-potter-box-set",
new Rule
{
ObjectID = "promote-harry-potter-box-set",
Conditions = new List
{
new Condition { Pattern = "Harry Potter", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Promote = new List
{
new Promote(new PromoteObjectID { ObjectID = "HP-12345", Position = 0 }),
},
},
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "promote-harry-potter-box-set",
rule: Rule(
objectID: "promote-harry-potter-box-set",
conditions: [
Condition(
pattern: "Harry Potter",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
promote: [
PromoteObjectID(
objectID: "HP-12345",
position: 0,
),
],
),
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "promote-harry-potter-box-set",
search.NewEmptyRule().SetObjectID("promote-harry-potter-box-set").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("Harry Potter").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetPromote(
[]search.Promote{*search.PromoteObjectIDAsPromote(
search.NewEmptyPromoteObjectID().SetObjectID("HP-12345").SetPosition(0))}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"promote-harry-potter-box-set",
new Rule()
.setObjectID("promote-harry-potter-box-set")
.setConditions(Arrays.asList(new Condition().setPattern("Harry Potter").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(new Consequence().setPromote(Arrays.asList(new PromoteObjectID().setObjectID("HP-12345").setPosition(0))))
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'promote-harry-potter-box-set',
rule: {
objectID: 'promote-harry-potter-box-set',
conditions: [{ pattern: 'Harry Potter', anchoring: 'contains' }],
consequence: { promote: [{ objectID: 'HP-12345', position: 0 }] },
},
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "promote-harry-potter-box-set",
rule =
Rule(
objectID = "promote-harry-potter-box-set",
conditions =
listOf(
Condition(
pattern = "Harry Potter",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(promote = listOf(PromoteObjectID(objectID = "HP-12345", position = 0))),
),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'promote-harry-potter-box-set',
['objectID' => 'promote-harry-potter-box-set',
'conditions' => [
['pattern' => 'Harry Potter',
'anchoring' => 'contains',
],
],
'consequence' => ['promote' => [
['objectID' => 'HP-12345',
'position' => 0,
],
],
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="promote-harry-potter-box-set",
rule={
"objectID": "promote-harry-potter-box-set",
"conditions": [
{
"pattern": "Harry Potter",
"anchoring": "contains",
},
],
"consequence": {
"promote": [
{
"objectID": "HP-12345",
"position": 0,
},
],
},
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"promote-harry-potter-box-set",
Algolia::Search::Rule.new(
algolia_object_id: "promote-harry-potter-box-set",
conditions: [Algolia::Search::Condition.new(pattern: "Harry Potter", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
promote: [Algolia::Search::PromoteObjectID.new(algolia_object_id: "HP-12345", position: 0)]
)
)
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "promote-harry-potter-box-set",
rule = Rule(
objectID = "promote-harry-potter-box-set",
conditions = Some(
Seq(
Condition(
pattern = Some("Harry Potter"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
promote = Some(
Seq(
PromoteObjectID(
objectID = "HP-12345",
position = 0
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "promote-harry-potter-box-set",
rule: Rule(
objectID: "promote-harry-potter-box-set",
conditions: [SearchCondition(pattern: "Harry Potter", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(promote: [SearchPromote.searchPromoteObjectID(SearchPromoteObjectID(
objectID: "HP-12345",
position: 0
))])
)
)
```
### With the Visual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia .
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Visual Editor**.
7. Under **Conditions**:
1. Click **Set query condition(s)**.
2. In **Your search**, type `Harry Potter` and click **Apply**.
8. Under **Consequences**:
1. Click **Pin items**.
2. In **Pinned items**, select the item you want to promote (for example, `HP-12345`) and click **Apply**.
9. **Review and Publish** your rule.
### With the Manual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Manual Editor**.
7. In the **Condition(s)** section, enter `Harry Potter` in the **Query** field.
8. In the **Consequence(s)** section:
1. Click **Add consequence** and select **Pin an item**.
2. Find and select the item you want to promote. For example, `HP-12345`.
9. **Save** your rule.
## Promote the newest release
For example, you've placed "best-selling items" at the top of your search results by using [custom ranking](/doc/guides/managing-results/must-do/custom-ranking).
But the newest release?
Set up a rule that promotes the newest iPhone for searches containing `iPhone`,
while keeping other phones sorted by most-sold.
### Rule
If query is `iphone`,
promote the newest iPhone release.
### With the API
```cs C# theme={"system"} theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"Promote-iPhone-X",
new Rule
{
ObjectID = "Promote-iPhone-X",
Conditions = new List
{
new Condition { Pattern = "iPhone", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Promote = new List
{
new Promote(new PromoteObjectID { ObjectID = "iPhone-12345", Position = 0 }),
},
},
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "Promote-iPhone-X",
rule: Rule(
objectID: "Promote-iPhone-X",
conditions: [
Condition(
pattern: "iPhone",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
promote: [
PromoteObjectID(
objectID: "iPhone-12345",
position: 0,
),
],
),
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "Promote-iPhone-X",
search.NewEmptyRule().SetObjectID("Promote-iPhone-X").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("iPhone").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetPromote(
[]search.Promote{*search.PromoteObjectIDAsPromote(
search.NewEmptyPromoteObjectID().SetObjectID("iPhone-12345").SetPosition(0))}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"Promote-iPhone-X",
new Rule()
.setObjectID("Promote-iPhone-X")
.setConditions(Arrays.asList(new Condition().setPattern("iPhone").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(new Consequence().setPromote(Arrays.asList(new PromoteObjectID().setObjectID("iPhone-12345").setPosition(0))))
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'Promote-iPhone-X',
rule: {
objectID: 'Promote-iPhone-X',
conditions: [{ pattern: 'iPhone', anchoring: 'contains' }],
consequence: { promote: [{ objectID: 'iPhone-12345', position: 0 }] },
},
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "Promote-iPhone-X",
rule =
Rule(
objectID = "Promote-iPhone-X",
conditions =
listOf(
Condition(
pattern = "iPhone",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
promote = listOf(PromoteObjectID(objectID = "iPhone-12345", position = 0))
),
),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'Promote-iPhone-X',
['objectID' => 'Promote-iPhone-X',
'conditions' => [
['pattern' => 'iPhone',
'anchoring' => 'contains',
],
],
'consequence' => ['promote' => [
['objectID' => 'iPhone-12345',
'position' => 0,
],
],
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="Promote-iPhone-X",
rule={
"objectID": "Promote-iPhone-X",
"conditions": [
{
"pattern": "iPhone",
"anchoring": "contains",
},
],
"consequence": {
"promote": [
{
"objectID": "iPhone-12345",
"position": 0,
},
],
},
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"Promote-iPhone-X",
Algolia::Search::Rule.new(
algolia_object_id: "Promote-iPhone-X",
conditions: [Algolia::Search::Condition.new(pattern: "iPhone", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
promote: [Algolia::Search::PromoteObjectID.new(algolia_object_id: "iPhone-12345", position: 0)]
)
)
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "Promote-iPhone-X",
rule = Rule(
objectID = "Promote-iPhone-X",
conditions = Some(
Seq(
Condition(
pattern = Some("iPhone"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
promote = Some(
Seq(
PromoteObjectID(
objectID = "iPhone-12345",
position = 0
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "Promote-iPhone-X",
rule: Rule(
objectID: "Promote-iPhone-X",
conditions: [SearchCondition(pattern: "iPhone", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(promote: [SearchPromote.searchPromoteObjectID(SearchPromoteObjectID(
objectID: "iPhone-12345",
position: 0
))])
)
)
```
### With the Visual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Visual Editor**.
7. Under **Conditions**:
1. Click **Set query condition(s)**.
2. In **Your search**, type `iPhone` and click **Apply**.
8. Under **Consequences**:
1. Click **Pin item**.
2. In **Pinned items**, select the item you want to promote (for example, `iPhone-12345`) and then click **Apply**.
9. **Review and Publish** your rule.
### With the Manual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Manual Editor**.
7. In the **Condition(s)** section, enter `iPhone` in the **Query** field and change the drop-down from **Is** to **Contains**.
8. In the **Consequence(s)** section:
1. Click **Add consequence** and select **Pin an item**.
2. Find and select the item you want to promote (for example, `iPhone-12345`).
9. **Save** your rule.
## Promote several results
For example, you're running a promotion on the newest Apple products.
Set up a rule that promotes the newest Apple releases at the top of results for searches containing `apple`.
### Rule
If query is `apple`,
promote the newest Apple releases.
### With the API
```cs C# theme={"system"} theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"Promote-Apple-Newest",
new Rule
{
ObjectID = "Promote-Apple-Newest",
Conditions = new List
{
new Condition { Pattern = "apple", Anchoring = Enum.Parse("Is") },
},
Consequence = new Consequence
{
Promote = new List
{
new Promote(
new PromoteObjectIDs
{
ObjectIDs = new List { "iPhone-12345", "watch-123" },
Position = 0,
}
),
},
},
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "Promote-Apple-Newest",
rule: Rule(
objectID: "Promote-Apple-Newest",
conditions: [
Condition(
pattern: "apple",
anchoring: Anchoring.fromJson("is"),
),
],
consequence: Consequence(
promote: [
PromoteObjectIDs(
objectIDs: [
"iPhone-12345",
"watch-123",
],
position: 0,
),
],
),
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "Promote-Apple-Newest",
search.NewEmptyRule().SetObjectID("Promote-Apple-Newest").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("apple").SetAnchoring(search.Anchoring("is"))}).SetConsequence(
search.NewEmptyConsequence().SetPromote(
[]search.Promote{*search.PromoteObjectIDsAsPromote(
search.NewEmptyPromoteObjectIDs().SetObjectIDs(
[]string{"iPhone-12345", "watch-123"}).SetPosition(0))}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"Promote-Apple-Newest",
new Rule()
.setObjectID("Promote-Apple-Newest")
.setConditions(Arrays.asList(new Condition().setPattern("apple").setAnchoring(Anchoring.IS)))
.setConsequence(
new Consequence().setPromote(
Arrays.asList(new PromoteObjectIDs().setObjectIDs(Arrays.asList("iPhone-12345", "watch-123")).setPosition(0))
)
)
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'Promote-Apple-Newest',
rule: {
objectID: 'Promote-Apple-Newest',
conditions: [{ pattern: 'apple', anchoring: 'is' }],
consequence: { promote: [{ objectIDs: ['iPhone-12345', 'watch-123'], position: 0 }] },
},
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "Promote-Apple-Newest",
rule =
Rule(
objectID = "Promote-Apple-Newest",
conditions =
listOf(
Condition(
pattern = "apple",
anchoring = Anchoring.entries.first { it.value == "is" },
)
),
consequence =
Consequence(
promote =
listOf(
PromoteObjectIDs(objectIDs = listOf("iPhone-12345", "watch-123"), position = 0)
)
),
),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'Promote-Apple-Newest',
['objectID' => 'Promote-Apple-Newest',
'conditions' => [
['pattern' => 'apple',
'anchoring' => 'is',
],
],
'consequence' => ['promote' => [
['objectIDs' => [
'iPhone-12345',
'watch-123',
],
'position' => 0,
],
],
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="Promote-Apple-Newest",
rule={
"objectID": "Promote-Apple-Newest",
"conditions": [
{
"pattern": "apple",
"anchoring": "is",
},
],
"consequence": {
"promote": [
{
"objectIDs": [
"iPhone-12345",
"watch-123",
],
"position": 0,
},
],
},
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"Promote-Apple-Newest",
Algolia::Search::Rule.new(
algolia_object_id: "Promote-Apple-Newest",
conditions: [Algolia::Search::Condition.new(pattern: "apple", anchoring: "is")],
consequence: Algolia::Search::Consequence.new(
promote: [Algolia::Search::PromoteObjectIDs.new(object_ids: ["iPhone-12345", "watch-123"], position: 0)]
)
)
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "Promote-Apple-Newest",
rule = Rule(
objectID = "Promote-Apple-Newest",
conditions = Some(
Seq(
Condition(
pattern = Some("apple"),
anchoring = Some(Anchoring.withName("is"))
)
)
),
consequence = Consequence(
promote = Some(
Seq(
PromoteObjectIDs(
objectIDs = Seq("iPhone-12345", "watch-123"),
position = 0
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "Promote-Apple-Newest",
rule: Rule(
objectID: "Promote-Apple-Newest",
conditions: [SearchCondition(pattern: "apple", anchoring: SearchAnchoring.`is`)],
consequence: SearchConsequence(promote: [SearchPromote.searchPromoteObjectIDs(SearchPromoteObjectIDs(
objectIDs: ["iPhone-12345", "watch-123"],
position: 0
))])
)
)
```
### With the Visual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Visual Editor**.
7. Under **Conditions**:
1. Click **Set query condition(s)**.
2. In **Your search**, type `apple` and click **Apply**.
8. Under **Consequences**:
1. Click **Pin items**.
2. Click **Pin multiple items**.
3. One per line, enter the object IDs of the items you want to pin, for example, `iPhone-12345` and `watch-123`, in the input field and then click **Pin items**.
4. Click **Apply**.
9. **Review and Publish** your rule.
You can't promote more than one item with the Manual Editor.
## Promote results matching active filters
For example, you only want to promote items if they match a user's active ,
such as stock availability, color, or category.
This prevents irrelevant items such as out-of-stock products from appearing.
### Rule
Create a rule with a consequence that pins the item only if it matches active filters.
### With the API
To pin an item only if it matches active filters,
set [`filterPromotes`](/doc/rest-api/search/save-rule#body-consequence) to `true` in the rule's consequence.
```cs C# theme={"system"} theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class EnableFilterPromote
{
async Task Main(string[] args)
{
var condition = new Condition { Anchoring = Anchoring.Is, Pattern = "{facet:brand}" };
var consequence = new Consequence { FilterPromotes = true };
var rule = new Rule
{
ObjectID = "rule_with_filterPromotes",
Conditions = [condition],
Consequence = consequence,
};
}
}
```
```dart Dart theme={"system"} theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void enableFilterPromote() async {
final condition = Condition(
pattern: "{facet:brand}",
anchoring: Anchoring.is_,
);
final consequence = Consequence(filterPromotes: true);
// ignore: unused_local_variable
final rule = Rule(
objectID: "rule_with_filterPromotes",
conditions: [condition],
consequence: consequence);
}
```
```go Go theme={"system"} theme={"system"}
package main
import (
"fmt"
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func enableFilterPromote() {
condition := search.NewCondition().
SetPattern("{facet:brand}").
SetAnchoring(search.ANCHORING_IS)
rule := search.NewRule(
"rule_with_filterPromotes",
*search.NewConsequence().SetFilterPromotes(true),
).SetEnabled(true).SetConditions([]search.Condition{*condition})
fmt.Printf("Rule: %#v\n", rule)
}
```
```java Java theme={"system"} theme={"system"}
package com.algolia;
import com.algolia.config.*;
import com.algolia.model.search.*;
import java.util.List;
public class enableFilterPromote {
public static void main(String[] args) throws Exception {
Condition condition = new Condition().setAnchoring(Anchoring.IS).setPattern("{facet:brand}");
Consequence consequence = new Consequence().setFilterPromotes(true);
Rule rule = new Rule().setObjectID("rule_with_filterPromotes").setConditions(List.of(condition)).setConsequence(consequence);
}
}
```
```js JavaScript theme={"system"} theme={"system"}
import type { Condition, Consequence } from 'algoliasearch';
const condition: Condition = {
anchoring: 'is',
pattern: '{facet:brand}',
};
const consequence: Consequence = {
filterPromotes: true,
};
```
```kotlin Kotlin theme={"system"} theme={"system"}
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
suspend fun enableFilterPromote() {
val condition = Condition(pattern = "{facet:brand}", anchoring = Anchoring.Is)
val consequence = Consequence(filterPromotes = true)
val rule =
Rule(
objectID = "rule_with_filterPromotes",
conditions = listOf(condition),
consequence = consequence,
)
}
```
```php PHP theme={"system"} theme={"system"}
setAnchoring((new Anchoring())::IS)
->setPattern('{facet:brand}')
;
$consequence = (new Consequence())
->setFilterPromotes(true)
;
$rule = (new Rule())
->setObjectID('rule_with_filterPromotes')
->setConditions([$condition])
->setConsequence($consequence);
```
```python Python theme={"system"} theme={"system"}
from algoliasearch.search.models.anchoring import Anchoring
from algoliasearch.search.models.condition import Condition
from algoliasearch.search.models.consequence import Consequence
from algoliasearch.search.models.rule import Rule
condition = Condition(
anchoring=Anchoring.IS,
pattern="{facet:brand}",
)
consequence = Consequence(
filter_promotes=True,
)
rule = Rule(
enabled=True,
object_id="rule_with_filterPromotes",
conditions=[condition],
consequence=consequence,
)
```
```ruby Ruby theme={"system"} theme={"system"}
condition = Algolia::Search::Condition.new(
anchoring: Algolia::Search::Anchoring::IS,
pattern: "{facet:brand}"
)
consequence = Algolia::Search::Consequence.new(
filter_promotes: true
)
rule = Algolia::Search::Rule.new(
enabled: true,
object_id: "rule_with_filterPromotes",
conditions: [condition],
consequence: consequence
)
```
```scala Scala theme={"system"} theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.{Anchoring, Condition, Consequence, Rule}
def enableFilterPromote(): Unit = {
val condition = Condition(
pattern = Some("{facet:brand}"),
anchoring = Some(Anchoring.Is)
)
val consequence = Consequence(
filterPromotes = Some(true)
)
val rule = Rule(
objectID = "rule_with_filterPromotes",
conditions = Some(Seq(condition)),
consequence = consequence
)
}
```
```swift Swift theme={"system"} theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func enableFilterPromote() async throws {
let condition = SearchCondition(
pattern: "{facet:brand}",
anchoring: .is
)
let consequence = SearchConsequence(
filterPromotes: true
)
let rule = Rule(objectID: "rule_with_filterPromotes", conditions: [condition], consequence: consequence)
print(rule)
}
```
### With the Visual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Visual Editor**.
7. Under **Conditions**:
1. Click **Set query condition(s)**.
2. In **Your search**, enter the item you want to promote (for example, `shoes`) and click **Apply**.
8. Under **Consequences**:
1. Click **Pin items**.
2. In **Pinned items**, select the item you want to promote.
3. Keep **Must match active filters** turned on and click **Apply**.
9. **Review and Publish** your rule.
### With the Manual Editor
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Manual Editor**.
7. In the **Condition(s)** section, enter the query (for example, `shoes`) in the **Query** field.
8. In the **Consequence(s)** section:
1. Click **Add consequence** and select **Pin an item**.
2. Find and select the item you want to promote (for example, `brogue-12345`).
9. In the **Additional settings** section, keep the checkbox **Pinned items must match active filters to be displayed** selected.
10. **Save** your rule.
# Rules overview
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview
How to use Algolia's Rules feature to create flexible and dynamic search results.
Search needs to be flexible.
Product promotions should be seamless:
search setting adjustments or record updates aren't needed.
If one of your products isn't appearing where you want it to in your search results,
you can reposition it.
Rules let you make precise and (if desired) temporary modifications to your search results.
Relevance and settings provide the general structure for your search results.
**Rules let you optimize and dynamically adjust that structure.**
Rules are no substitute for a well-configured index.
Make sure you [adjust your general relevance](/doc/guides/managing-results/relevance-overview) before adding rules.
## Index rules and composition rules
Algolia uses two different types of rules: index rules and composition rules.
This section describes index rules.
For more information about composition rules, see [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
## What are rules used for?
Rules let you make precise, predetermined changes to your search results.
For example, you can pin or hide items,
boost or bury categories,
or results based on the query.
You can also enable rules for a fixed time,
making rules a great way of implementing sales or promotions.
**Rules are typically used either for merchandising or fine-tuning your search and relevance.**
For more information about typical uses for rules,
see [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting) and [Detecting intent](/doc/guides/managing-results/rules/detecting-intent).
The maximum number of rules per index depends on your [Algolia plan](https://www.algolia.com/pricing).
## Rule structure
Three elements define a rule:
conditions, consequences, and a validity period.
Of these three elements, only a consequence is mandatory.
**When a rule's condition is satisfied, Algolia applies its consequences**.
If a rule doesn't have any conditions (a [conditionless rule](#conditionless-rules)),
Algolia always applies its consequences.
For more information, see:
* [Implement rules](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules)
* [Troubleshoot rules](/doc/guides/managing-results/rules/rules-overview/in-depth/debugging-rules)
## Conditions
A rule can have up to 25 conditions.
A rule with several conditions activates when any condition is satisfied.
In other words, conditions have an `or` relationship to one another.
Conditions may contain a pattern string, anchoring, context, or filters.
To determine if a rule's consequences apply:
1. Algolia compares the user's query to the condition's pattern.
It bases the comparison on the condition's anchoring.
Values for anchoring are:
`is`, `contains`, `starts with`, and `ends with`.
The anchoring determines what part of the query the pattern must match:
all of it, part of it, the beginning, or the end.
If you enable [`alternatives`](/doc/rest-api/search/save-rule#body-condition) in the condition, user queries can match the condition's pattern,
even if the query is a plural, synonym, or typo of the pattern.
Without alternatives, queries must precisely match the condition's pattern and anchoring.
2. It compares the condition's filters to the [filters](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-string) applied to the search.
3. It compares the condition's context to the search's [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts).
If a [single condition includes a pattern string, filters, and a context](#pattern-filters-and-context),
a search must match all three for the condition to be satisfied.
With three conditions, a rule activates if any is met.
If a condition uses `is` anchoring with an empty pattern,
Algolia applies the rule if users haven't yet entered a query.
If a condition doesn't include a pattern, anchoring, context, or filters, it's a conditionless rule, and Algolia applies it to every search.
Rules can respond to different types of conditions:
* [Query patterns](#rules-responding-to-user-queries)
* [Filters](#rules-responding-to-applied-filters)
* [Context-only](#context-only-rules)
* [Conditionless](#conditionless-rules).
### Example conditions
#### Using a pattern with is anchoring
Trigger a rule with this condition whenever the query string is the word `sale` and nothing else.
If the query contains other words, the rule isn't triggered.
```json JSON icon=braces theme={"system"}
{
"pattern": "sale",
"anchoring": "is"
}
```
#### A pattern with contains anchoring
Trigger a rule with this condition whenever the query string contains the word `featured`.
The query can include other words.
```json JSON icon=braces theme={"system"}
{
"pattern": "featured",
"anchoring": "contains"
}
```
#### A context
Trigger a rule when the search includes `mobile` in the [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts).
```json JSON icon=braces theme={"system"}
{
"context": "mobile"
}
```
#### Filters
Trigger a rule when the filter `{ "filters": "category:TV" }` applies to the query.
```json JSON icon=braces theme={"system"}
{
"filters": "category:TV"
}
```
The filters condition rejects an `OR` combination of different attributes.
For example, `"filters": "brand:Guess OR color:orange"` returns an error.
You also can't use numeric filters in conditions.
#### Pattern, filters, and context
Trigger a rule when:
* The query contains the word `featured`
* [`filters`](/doc/api-reference/api-parameters/filters) includes `category:TV`
* [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) includes `mobile`.
```json JSON icon=braces theme={"system"}
{
"pattern": "featured",
"anchoring": "contains",
"filters": "category:TV",
"context": "mobile"
}
```
#### An empty string pattern
Trigger a rule when there is an empty search.
The rule turns off as soon as a user starts typing.
```json JSON icon=braces theme={"system"}
{
"pattern": "",
"anchoring": "is"
}
```
## Consequences
Though conditions aren't required,
rules must have at least one consequence.
Consequences change the search results.
Examples of consequences include:
* **Pin an item**. Insert an item at a specific position.
If the item is already in the results, it moves to that position.
* **Hide an item**. Remove a specific result from the list of results.
* **Add a query parameter**. Add a query parameter to your user's search.
For example, you could decrease the [`aroundRadius`](/doc/api-reference/api-parameters/aroundRadius) if your user's query includes the words "near me".
* **Remove word**. Remove a specific word from your user's (for search, not display purposes).
* **Replace word**. Replace a word from your user's search query with another word (for search, not display purposes).
* **Replace query**. Replace the entire user search query with another query (for search, not display purposes).
* **Return custom data**. Add custom JSON data to the search response.
* **Filter/Boost matching attributes**. Apply [`filters`](/doc/api-reference/api-parameters/filters) or [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters) matching the query to the results.
Use the **Visual Editor** to configure rules with guided controls and preview their effect on search results.
The **Manual Editor** offers a more technical workflow for configuring rule conditions, consequences, and search parameters directly.
Both editors use the [Rules API](/doc/libraries/sdk/methods/search/save-rules).
The Visual Editor supports custom data, search parameters, query edits, matching-attribute filters, and merchandising actions such as pinning, hiding, and displaying banners.
Custom data and search parameters use JSON inputs in both editors.
For more information, see:
* [Rules consequences](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#consequence—adapt-the-results)
* [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting)
## Validity period
If you want to apply a rule temporarily, you can set a validity period.
The validity period determines how long a rule remains active.
Use this to ensure promotions and sales end automatically.
## Rules responding to user queries
Rules can parse user queries and apply consequences if the query matches the rule's condition.
Queries can match with conditions in the following ways:
* `is`: the entire query matches the condition string.
* `contains`: the entire query contains the condition string.
* `startsWith`: the query starts with the condition string.
* `endsWith`: the query ends with the condition string.
To [trigger a rule on an empty query](/doc/guides/managing-results/rules/detecting-intent),
use `is` anchoring and set the condition's pattern to an empty string.
## Rules responding to applied filters
If a rule's condition includes filters, Algolia applies that rule's consequences only if the filters in a search's query parameters exactly match the condition's filters.
For example, Algolia only triggers a rule with the condition `{"filters": "category:TV"}` if the `category` value is `TV`.
Other filters like `"category:Smartphone"` or `"category:TV OR category:Smartphone"` don't trigger the rule.
**The primary goal of this condition is to create rules that trigger on specific category pages or when a user applies specific filters.**
For example, you can define a rule with the condition `{"filters": "category:TV"}` and a consequence to promote specific items.
The rule triggers when a user lands on the TV category page.
For more information, see [Filters can trigger rules](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#filters-can-trigger-rules).
To use an attribute in a rule condition with filters,
you must first [declare it in `attributesForFaceting`](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
## Context-only rules
If a rule's condition only includes a context,
the consequence only applies if the rule's context exactly matches a value in the [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) parameter of a user's search.
### What are contexts?
Contexts provide information about your user's search environment:
for example, what section of a website they're currently visiting or what device they're using.
A context can be any string value that doesn't include whitespace characters.
The search condition it specifies must be identifiable at search time.
You need to implement logic for sending contexts with queries.
You can pass contexts through the [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) search parameter.
### Implementation
To create a rule with a context condition, you must:
1. Create the rule (with the dashboard or the API).
2. Conditionally assign its associated context to your user's searches.
To clarify the process, consider the example of an electronics store.
By looking at [analytics tags](/doc/guides/search-analytics/guides/segments), it's clear that mobile users rarely find what they're searching for on the first page of results.
Testing indicates this is because vague searches for accessories like "chargers" or "cases" return laptop chargers and cases first.
If mobile users search for cases, you can assume that they prefer phone cases over laptop cases.
If they search for chargers, phone chargers should show up first.
To remedy this, add a rule that promotes all items with the "phone" tag but only for users searching from a mobile platform.
To learn more, see [Using rules to customize search results by device](/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform).
## Conditionless rules
Rules without conditions apply to every search.
They're an effective way of temporarily modifying search for a predefined time, for example, for seasonal promotions.
For more information, see [Add default search parameters with rules](/doc/guides/managing-results/rules/rules-overview/how-to/add-default-search-parameters-with-rules).
You can create conditionless rules from the Algolia dashboard or with one of the API clients.
### Using the dashboard
You can create a conditionless rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
If you're using the Merchandising Studio Visual Merchandiser instead, select **Actions > Create new rule**.
2. In **Conditions**, select **Create conditionless rule**, review its scope, and click **Apply**.
A conditionless rule applies to every search. Leaving a query field empty instead creates an empty-query condition.
3. Select the consequences you want to apply.
Optional: select **Add a date range** to limit when the rule applies.
4. Click **Review and Publish**, review the rule, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open [**Rules**](https://dashboard.algolia.com/rules), and select **New rule > Manual Editor**.
2. In **Condition(s)**, remove the default condition.
3. In **Consequence(s)**, add the consequences you want to apply.
To limit when the rule applies, select a time range in **Additional Settings > Timeframe in UTC**.
To forward the rule to replicas or other indices, select **Copy this rule to other indices** and enter their names.
4. Save your changes.
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To create a conditionless rule with an API client,
use the [`saveRule`](/doc/libraries/sdk/methods/search/save-rule) method on a rule object but without a condition.
```cs C# theme={"system"} theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class UseConditionlessRule
{
async Task Main(string[] args)
{
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
var objectID = "a-rule-id";
var rule = new Rule
{
ObjectID = objectID,
Consequence =
new Consequence( /* Set relevant consequence */
),
// Set validity (optional)
Validity = [new TimeRange { From = 1688774400L, Until = 1738972800L }],
};
await client.SaveRuleAsync("INDEX_NAME", objectID, rule);
}
}
```
```dart Dart theme={"system"} theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void useConditionlessRule() async {
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
final objectID = "a-rule-id";
final rule = Rule(
objectID: objectID,
consequence: Consequence(
// Set relevant consequence
),
// Set validity (optional)
validity: [TimeRange(from: 1688774400, until: 1738972800)]);
await client.saveRule(
indexName: "INDEX_NAME",
objectID: objectID,
rule: rule,
);
}
```
```go Go theme={"system"} theme={"system"}
package main
import (
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
"github.com/algolia/algoliasearch-client-go/v4/algolia/utils"
)
func useConditionlessRule() {
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
objectID := "a-rule-id"
rule := search.NewRule(
objectID,
*search.NewConsequence( /* Set relevant consequence */ ),
). // Set validity (optional)
SetValidity(
[]search.TimeRange{{
From: utils.ToPtr(int64(1_688_774_400)),
Until: utils.ToPtr(int64(1_738_972_800)),
}},
)
_, err = client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", objectID, rule))
if err != nil {
panic(err)
}
}
```
```java Java theme={"system"} theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
import java.util.List;
public class useConditionlessRule {
public static void main(String[] args) throws Exception {
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
String objectID = "a-rule-id";
Rule rule = new Rule().setObjectID(objectID).setConsequence(new Consequence(/* Set relevant consequence */));
// Set validity (optional)
rule.setValidity(List.of(new TimeRange().setFrom(1688774400L).setUntil(1738972800L)));
client.saveRule("INDEX_NAME", objectID, rule);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"} theme={"system"}
import { algoliasearch } from 'algoliasearch';
import type { Rule } from 'algoliasearch';
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
const objectID = 'a-rule-id';
const rule: Rule = {
objectID,
consequence: {
// Set relevant consequence
},
};
// Set validity (optional)
rule.validity = [
{
from: 1688774400,
until: 1738972800,
},
];
await client.saveRule({ indexName: 'indexName', objectID: objectID, rule: rule });
```
```kotlin Kotlin theme={"system"} theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
suspend fun useConditionlessRule() {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val objectID = "a-rule-id"
val rule =
Rule(
objectID = objectID,
consequence =
Consequence(
// Set relevant consequence
),
// Set validity (optional)
validity = listOf(TimeRange(from = 1_688_774_400, until = 1_738_972_800)),
)
client.saveRule(indexName = "INDEX_NAME", objectID = objectID, rule = rule)
}
```
```php PHP theme={"system"} theme={"system"}
setObjectID($objectID)
->setConsequence(new Consequence(/* Set relevant consequence */))
;
// Set validity (optional)
$rule->setValidity(
[
(new TimeRange())
->setFrom(1688774400)
->setUntil(1738972800),
]
);
$client->saveRule(
'INDEX_NAME',
$objectID,
$rule,
);
```
```python Python theme={"system"} theme={"system"}
from algoliasearch.search.client import SearchClientSync
from algoliasearch.search.models.consequence import Consequence
from algoliasearch.search.models.rule import Rule
from algoliasearch.search.models.time_range import TimeRange
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
object_id = "a-rule-id"
rule = Rule(
object_id=object_id,
consequence=Consequence(
# Set relevant consequence
),
)
# Set validity (optional)
rule.validity = [
TimeRange(
var_from=1688774400,
until=1738972800,
),
]
_client.save_rule(
index_name="INDEX_NAME",
object_id=object_id,
rule=rule,
)
```
```ruby Ruby theme={"system"} theme={"system"}
require "algolia"
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
object_id = "a-rule-id"
rule = Algolia::Search::Rule.new(
object_id: object_id,
consequence: Algolia::Search::Consequence.new
# Set relevant consequence
)
# Set validity (optional)
rule.validity = [
Algolia::Search::TimeRange.new(
from: 1688774400,
until: 1738972800
)
]
client.save_rule("INDEX_NAME", algolia_object_id, rule)
```
```scala Scala theme={"system"} theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.{Consequence, Rule, TimeRange}
def useConditionlessRule(): Future[Unit] = {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val objectID = "a-rule-id";
val rule = Rule(
objectID = objectID,
consequence = Consequence(
// Set relevant consequence
),
// Set validity (optional)
validity = Some(
Seq(
TimeRange(
from = Some(1_688_774_400),
until = Some(1_738_972_800)
)
)
)
)
client
.saveRule(
indexName = "INDEX_NAME",
objectID = objectID,
rule = rule
)
.map { response =>
println(response)
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"} theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func useConditionlessRule() async throws {
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
let objectID = "a-rule-id"
let rule = Rule(
objectID: objectID,
consequence: SearchConsequence(
// Set relevant consequence
),
// Set validity (optional)
validity: [
SearchTimeRange(
from: 1_688_774_400,
until: 1_738_972_800
),
]
)
try await client.saveRule(indexName: "INDEX_NAME", objectID: objectID, rule: rule)
}
```
## Context-only and conditionless rules deactivate consequences
Since conditionless and context-only rules don't have a query as a condition, these consequences are deactivated.
| Consequence | Why is it deactivated? |
| - | - |
| Pin an item | The same item would always appear in the same position, usually at the top, for all queries and all users. Instead, consider [adding a banner](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners) to promote items you want to pin. |
| Remove word, Replace word, Replace query | Since there's no query, there are no words to remove or replace. |
| Filter/Boost matching attributes | Rules that filter and boost facets require users to type a attribute into the query. Since there's no query or facet in the condition, there's no filter for the rule to boost. |
These limitations only apply to **context-only** and **conditionless** rules.
Other rules can use the full range of consequences.
## Choose a rule editor
Use the Visual Editor for most rules.
It supports the same conditions and consequences as the Manual Editor and lets you preview their effect on search results.
Choose another option when you need a different workflow.
| Option | Use it when |
| - | - |
| **Visual Editor** | You want a guided workflow and an instant preview of how a rule affects search results. |
| **Manual Editor** | You want to enter rule details directly, including JSON values, and don't need an instant preview. |
| **Rules API** | You want to create or update rules in bulk, automate rule management, or build a custom interface. |
## Rule tags
Create and manage rule tags in the [Algolia dashboard](https://dashboard.algolia.com/rules) or Merchandising Studio.
On the **Rules** page, click **Manage tags**.
A rule tag includes:
| Attribute | Required | Description |
| - | - | - |
| Name | Yes | Unique within the index. |
| Description | No | Describes the tag's purpose. |
| Color | No | Color shown for the tag in the dashboard. |
By default, indices may contain up to 100 rule tags.
After you create a rule tag, you can add it to individual rules in the **Visual Editor** and **Manual Editor**.
# Add default search parameters with rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/add-default-search-parameters-with-rules
Learn how to use an API client or the dashboard to add search parameters using always active rules.
You can create rules to add default parameters to every search made to the associated .
This is most useful when applied with a [validity period](/doc/guides/managing-results/rules/rules-overview#validity-period).
If you want to add always active parameters for an indefinite period of time, change your [index's settings](/doc/api-reference/api-parameters).
This page shows you how to create a rule that sets:
* The [`aroundRadius`](/doc/api-reference/api-parameters/aroundRadius) search parameter, for all queries, to 1,000 meters.
* The validity period of this rule to one month.
This is an example of a [conditionless rule](/doc/guides/managing-results/rules/rules-overview#conditionless-rules).
## Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To create a rule with one of the API clients,
use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method using a rule object without a condition.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"a-rule-id",
new Rule
{
ObjectID = "a-rule-id",
Consequence = new Consequence
{
Params = new ConsequenceParams { AroundRadius = new AroundRadius(1000) },
},
Validity = new List
{
new TimeRange { From = 1577836800L, Until = 1577836800L },
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "a-rule-id",
rule: Rule(
objectID: "a-rule-id",
consequence: Consequence(
params: ConsequenceParams(
aroundRadius: 1000,
),
),
validity: [
TimeRange(
from: 1577836800,
until: 1577836800,
),
],
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "a-rule-id",
search.NewEmptyRule().SetObjectID("a-rule-id").SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetAroundRadius(search.Int32AsAroundRadius(1000)))).SetValidity(
[]search.TimeRange{*search.NewEmptyTimeRange().SetFrom(1577836800).SetUntil(1577836800)})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"a-rule-id",
new Rule()
.setObjectID("a-rule-id")
.setConsequence(new Consequence().setParams(new ConsequenceParams().setAroundRadius(AroundRadius.of(1000))))
.setValidity(Arrays.asList(new TimeRange().setFrom(1577836800L).setUntil(1577836800L)))
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'a-rule-id',
rule: {
objectID: 'a-rule-id',
consequence: { params: { aroundRadius: 1000 } },
validity: [{ from: 1577836800, until: 1577836800 }],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "a-rule-id",
rule =
Rule(
objectID = "a-rule-id",
consequence =
Consequence(params = ConsequenceParams(aroundRadius = AroundRadius.of(1000))),
validity = listOf(TimeRange(from = 1577836800L, until = 1577836800L)),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'a-rule-id',
['objectID' => 'a-rule-id',
'consequence' => ['params' => ['aroundRadius' => 1000,
],
],
'validity' => [
['from' => 1577836800,
'until' => 1577836800,
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="a-rule-id",
rule={
"objectID": "a-rule-id",
"consequence": {
"params": {
"aroundRadius": 1000,
},
},
"validity": [
{
"from": 1577836800,
"until": 1577836800,
},
],
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"a-rule-id",
Algolia::Search::Rule.new(
algolia_object_id: "a-rule-id",
consequence: Algolia::Search::Consequence.new(params: Algolia::Search::ConsequenceParams.new(around_radius: 1000)),
validity: [Algolia::Search::TimeRange.new(from: 1577836800, _until: 1577836800)]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "a-rule-id",
rule = Rule(
objectID = "a-rule-id",
consequence = Consequence(
params = Some(
ConsequenceParams(
aroundRadius = Some(AroundRadius(1000))
)
)
),
validity = Some(
Seq(
TimeRange(
from = Some(1577836800L),
until = Some(1577836800L)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "a-rule-id",
rule: Rule(
objectID: "a-rule-id",
consequence: SearchConsequence(params: SearchConsequenceParams(aroundRadius: SearchAroundRadius
.int(1000))),
validity: [SearchTimeRange(from: Int64(1_577_836_800), until: Int64(1_577_836_800))]
)
)
```
## Using the dashboard
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Select **Create conditionless rule** and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Apply search parameters**.
Enter `{ "aroundRadius": 1000 }` in **Value** and click **Apply**.
4. Select **Add a date range**, set the start and end dates one month apart, and click **Apply**.
5. Click **Review and Publish** and review the rule.
To forward the rule to replicas or other indices, select them under **Copy rule to**.
Click **Publish**.
1. In the Algolia dashboard, select your application and index, open [**Rules**](https://dashboard.algolia.com/rules), and select **New rule > Manual Editor**.
2. In **Condition(s)**, remove the default condition.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "aroundRadius": 1000 }`.
4. In **Additional Settings > Timeframe in UTC**, select a time range of one month.
5. To forward the rule to replicas or other indices, select **Copy this rule to other indices** and enter their names.
Save your changes.
In JSON, you must write string values inside quotation marks, and number values and booleans without quotations.
For more information about JSON syntax,
see [Algolia records](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record).
# Customize search results by device
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform
Learn how to customize your search results based on whether your user is on a mobile or desktop device.
Search needs to be flexible.
Depending on the user's context, results can vary.
For example, because some products are more likely to be bought or viewed from phones than desktops, you may want to promote items based on the user's device.
To do this:
1. Identify your user's device.
2. Assign a `mobile` context when users search from mobile devices. A context is a string passed as a search parameter to InstantSearch or one of the Algolia API clients.
3. Create a contextual rule that acts on the `mobile` context.
To help understand user behavior across devices, pair your device-specific experience with analytics.
You can even use the same context string to tag search queries for analytics.
These [`analyticsTags`](/doc/guides/search-analytics/guides/segments) help you group search analytics into different segments.
## Example scenario
Suppose you work for a movie database company.
Research shows that mobile users, unlike desktop users, tend to see a film in theaters after it has appeared in their search results.
To capitalize on this, you partner with a movie theater chain and decide to promote newly released movies to your mobile users.
The in your Algolia include a `release_date` attribute,
in [Unix timestamp](https://www.unixtimestamp.com/) format, this is useful for identifying newly released movies.
```json JSON icon=braces theme={"system"}
{
"title": "Fargo",
"year": 1996,
"director": "Joel Coen",
"release_date": 841795200,
"rating": 93
}
```
By creating a mobile context and contextual rules,
you can promote recent movies to mobile users.
## Identify your user's device
Before assigning a mobile context to searches,
write a function that detects what device the app is running on,
and return "desktop" or "mobile" depending on what it finds:
```js JavaScript icon=code theme={"system"}
const getPlatform = () => {
// const isMobile = ...
// Your logic to determine whether a user is on a mobile or desktop device
return isMobile ? "mobile" : "desktop";
};
```
## Assign context
You can assign context with InstantSearch or one of the Algolia API clients.
### Assign context with InstantSearch
Set the user's device context at the beginning of their search session by adding it to the `searchParameters` setting of your InstantSearch instance [during initialization](/doc/api-reference/widgets/instantsearch/js).
```js JavaScript theme={"system"}
const platformTag = getPlatform();
instantsearch.widgets.configure({
ruleContexts: [platformTag],
});
```
```swift Swift theme={"system"}
let platform = getPlatform()
let searcher = HitsSearcher(appId: "YourAppId", apiKey: "ALGOLIA_API_KEY", indexName: "YourIndexName")
searcher.request.query.ruleContexts = [platform]
```
```jsx React theme={"system"}
import { Configure } from 'react-instantsearch';
const platformTag = getPlatform()
```
```vue Vue theme={"system"}
```
```kotlin Android theme={"system"}
val platform: String = getPlatform()
val query = query {
ruleContexts {
+platform
}
}
val searcher = HitsSearcher(movies, query)
```
### Assign context with an API client
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
Pass the user's device context as a search parameter whenever you search your index.
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class SearchWithRuleContexts
{
private static string GetPlatformTag()
{
return ""; // Implement your logic here
}
async Task Main(string[] args)
{
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
var platformTag = GetPlatformTag();
var searchParams = new SearchParams(
new SearchParamsObject { Query = "SEARCH_QUERY", RuleContexts = [platformTag] }
);
await client.SearchSingleIndexAsync("INDEX_NAME", searchParams);
}
}
```
```dart Dart theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
String getPlatformTag() {
return ""; // Implement your logic here
}
void searchWithRuleContexts() async {
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
final platformTag = getPlatformTag();
final searchParams = SearchParamsObject(
query: "SEARCH_QUERY", ruleContexts: [platformTag]);
await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams,
);
}
```
```go Go theme={"system"}
package main
import (
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func getPlatformTag() (string, error) {
return "", nil // Implement your logic here
}
func searchWithRuleContexts() {
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
platformTag, err := getPlatformTag()
if err != nil {
panic(err)
}
searchParams := search.SearchParamsObjectAsSearchParams(
search.NewSearchParamsObject().
SetQuery("SEARCH_QUERY").
SetRuleContexts([]string{platformTag}),
)
_, err = client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(searchParams))
if err != nil {
panic(err)
}
}
```
```java Java theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
import java.util.List;
public class searchWithRuleContexts {
private static String getPlatformTag() {
return ""; // Implement your logic here
}
public static void main(String[] args) throws Exception {
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
String platformTag = getPlatformTag();
SearchParams searchParams = new SearchParamsObject().setQuery("SEARCH_QUERY").setRuleContexts(List.of(platformTag));
client.searchSingleIndex("INDEX_NAME", searchParams, Hit.class);
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"}
import { algoliasearch } from 'algoliasearch';
import type { SearchParams } from 'algoliasearch';
const getPlatformTag = () => {
return ''; // Implement your logic here
};
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
const platformTag = getPlatformTag();
const searchParams: SearchParams = {
query: 'SEARCH_QUERY',
ruleContexts: [platformTag],
};
await client.searchSingleIndex({ indexName: 'indexName', searchParams: searchParams });
```
```kotlin Kotlin theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
val getPlatformTag: () -> String = {
"" // Implement your logic here
}
suspend fun searchWithRuleContexts() {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val platformTag = getPlatformTag()
val searchParams =
SearchParamsObject(query = "SEARCH_QUERY", ruleContexts = listOf(platformTag))
client.searchSingleIndex(indexName = "INDEX_NAME", searchParams = searchParams)
}
```
```php PHP theme={"system"}
setQuery('SEARCH_QUERY')
->setRuleContexts([$platformTag])
;
$client->searchSingleIndex(
'INDEX_NAME',
$searchParams,
);
```
```python Python theme={"system"}
from algoliasearch.search.client import SearchClientSync
from algoliasearch.search.models.search_params import SearchParams
def _get_platform_tag() -> str:
# Implement your logic here
return ""
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
# get the buyer account information
platform_tag = _get_platform_tag()
search_params = SearchParams(
query="SEARCH_QUERY",
rule_contexts=[platform_tag],
)
_client.search_single_index(
index_name="INDEX_NAME",
search_params=search_params,
)
```
```ruby Ruby theme={"system"}
require "algolia"
def get_platform_tag
# Implement your logic here
""
end
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
# get the buyer account information
platform_tag = get_platform_tag
search_params = Algolia::Search::SearchParamsObject.new(
query: "SEARCH_QUERY",
rule_contexts: [platform_tag]
)
client.search_single_index("INDEX_NAME", search_params)
```
```scala Scala theme={"system"}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.SearchParamsObject
val getPlatformTag: String = {
"" // Implement your logic here
}
def searchWithRuleContexts(): Future[Unit] = {
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
val platformTag = getPlatformTag
val searchParams = SearchParamsObject(
query = Some("SEARCH_QUERY"),
ruleContexts = Some(Seq(platformTag))
)
client
.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(searchParams)
)
.map { response =>
println(response)
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
let getPlatformTag = { "" } // Implement your logic here
func searchWithRuleContexts() async throws {
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
let platformTag = getPlatformTag()
let searchParams = SearchSearchParams.searchSearchParamsObject(
SearchSearchParamsObject(query: "SEARCH_QUERY", ruleContexts: [platformTag])
)
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: searchParams
)
print(response)
}
```
## Create a contextual rule
You can create a mobile contextual rule with an API client or from the Algolia dashboard.
### Using the API
Use the [`saveRule`](/doc/libraries/sdk/methods/search/save-rule) method to create a context-dependent rule.
For example, to promote movies released after January 1, 2020, for mobile users:
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"a-rule-id",
new Rule
{
ObjectID = "a-rule-id",
Conditions = new List { new Condition { Context = "mobile" } },
Consequence = new Consequence
{
Params = new ConsequenceParams { Filters = "release_date >= 1577836800" },
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "a-rule-id",
rule: Rule(
objectID: "a-rule-id",
conditions: [
Condition(
context: "mobile",
),
],
consequence: Consequence(
params: ConsequenceParams(
filters: "release_date >= 1577836800",
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "a-rule-id",
search.NewEmptyRule().SetObjectID("a-rule-id").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetContext("mobile")}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetFilters("release_date >= 1577836800")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"a-rule-id",
new Rule()
.setObjectID("a-rule-id")
.setConditions(Arrays.asList(new Condition().setContext("mobile")))
.setConsequence(new Consequence().setParams(new ConsequenceParams().setFilters("release_date >= 1577836800")))
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'a-rule-id',
rule: {
objectID: 'a-rule-id',
conditions: [{ context: 'mobile' }],
consequence: { params: { filters: 'release_date >= 1577836800' } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "a-rule-id",
rule =
Rule(
objectID = "a-rule-id",
conditions = listOf(Condition(context = "mobile")),
consequence =
Consequence(params = ConsequenceParams(filters = "release_date >= 1577836800")),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'a-rule-id',
['objectID' => 'a-rule-id',
'conditions' => [
['context' => 'mobile',
],
],
'consequence' => ['params' => ['filters' => 'release_date >= 1577836800',
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="a-rule-id",
rule={
"objectID": "a-rule-id",
"conditions": [
{
"context": "mobile",
},
],
"consequence": {
"params": {
"filters": "release_date >= 1577836800",
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"a-rule-id",
Algolia::Search::Rule.new(
algolia_object_id: "a-rule-id",
conditions: [Algolia::Search::Condition.new(context: "mobile")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(filters: "release_date >= 1577836800")
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "a-rule-id",
rule = Rule(
objectID = "a-rule-id",
conditions = Some(
Seq(
Condition(
context = Some("mobile")
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
filters = Some("release_date >= 1577836800")
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "a-rule-id",
rule: Rule(
objectID: "a-rule-id",
conditions: [SearchCondition(context: "mobile")],
consequence: SearchConsequence(params: SearchConsequenceParams(filters: "release_date >= 1577836800"))
)
)
```
### Using the dashboard
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**.
Turn **Query** off, turn **Context** on, enter `mobile`, and click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `release_date`, set the operator to `>=`, enter `1577836800`, and click **Apply**.
4. Click **Review and Publish**, review the rule, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open [**Rules**](https://dashboard.algolia.com/rules), and select **New rule > Manual Editor**.
2. In **Condition(s)**, turn **Query** off, turn **Context** on, and enter `mobile`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
To add a for movies released since January 1, 2020, enter `{ "filters": "release_date >= 1577836800" }`.
4. Save your changes.
# Debug rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/in-depth/debugging-rules
How to debug rules.
If you have many rules, it can be difficult to understand which rules apply to a given .
Algolia provides tools to help you with this.
The [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) parameter adds extra information about rules:
* **Promoted hits** are labeled with a `"promoted": true` field inside the `_rankingInfo` object.
This field is omitted from regular hits.
* The list of **applied Rules** is available inside the `appliedRules` top-level field.
This field is omitted when no rule has been applied (even if `enableRules` is true).
The `params` response field also has search parameters after Rule application,
and as such reflects the side-effects of the rules.
When in doubt, you can always run the same query with `enableRules=false` and compare the results you get with and without rules.
## Improve accuracy of rules
Sometimes, creating rules can lead to lower search accuracy.
The following table lists common causes for a lower-than-expected search accuracy when applying rules.
| Cause of less accurate results | Solution |
| - | - |
| Not adding a "remove word" consequence when turning search phrases into query parameters | [Add a remove word consequence](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query#positive-filters) after any add query parameter consequence |
| Not creating an optional filter as a facet | If you want to use an attribute as an optional filter, [declare it as a facet](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting) |
| Not taking advantage of "add search parameter" consequences | See some [common uses for this consequence](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/rules-query-parameters) |
# Implement rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules
Learn how to add index rules to curate your search results.
**Use the dashboard to familiarize yourself with rules**.
The dashboard provides a responsive interface to create and configure your rules.
And importantly, test your choices using the dashboard's live search interface.
You may never need to go beyond the dashboard if your rules are manageable in the UI or follow a defined pattern.
In this way, you can consider the dashboard as a way to manage static rules—rules that remain in place for some time or whose effect is global and constant.
Open the **Rules** page for a selected and select **New rule**.
Use the Visual Editor for most rules.
Choose the Manual Editor when you prefer to enter rule details directly, including JSON values, and don't need a preview.
Both editors support the same conditions and consequences.
To automate or manage rules in bulk, use the Rules API.
## Coding rules with the API clients
Coding rules using the Rules API becomes necessary when you need more programmatic control.
This move from static to more dynamic rules can arise for several reasons—like when you can't manage rules one-by-one or when you want to start building (and deleting) rules based on the content of your database.
If your rules change with every new update of your index, or if you offer a frontend to manage your rules, use the API.
You must set up your index for the API and then set up the rules at indexing time.
## How index rules works
**Algolia adapts its behavior according to a rule's consequences**.
Rules are if-then configurations or condition/consequence pairs:
the consequence applies to the returned results if the condition is satisfied.
For example, a rule with the condition "user's query contains the word red" and the consequence "filter on the facet color:red".
And that's only one kind of consequence.
As described in the overview and presented in technical detail below—
Different kinds of consequences adapt the search and alter the relevance.
Rules are processed at indexing time to prepare the index so that when someone performs a search,
the data is ready for the if-then matching.
Keep in mind:
* If the `alternatives` setting of a rule is `false`, the condition must match the entire word (as a whole word) for the rule to be triggered.
* All text matching is case insensitive.
* Conditions can match on multiple phrases.
* A rule can have multiple conditions and multiple consequences.
Rules don't noticeably affect search performance.
Most of the work happens during indexing.
## Turning rules on and off
Turn a rule off by setting its `enabled` flag to `false`.
Rules that have been turned off remain in the index.
They're still searchable, but ignored at query time.
This is an alternative to deleting a rule when you wish to turn it off temporarily.
For even more fine-grained control,
you can specify **validity windows** on rules (with the `validity` attribute).
Rules with validity windows only apply during the specified time frames:
they're ignored the rest of the time.
Rules with no time windows always apply.
Like with the `enabled` flag, rules with validity windows remain searchable all the time.
## API overview
### Pre-processing and post-processing
As suggested, rules allow fine-tuning results for queries matching specific patterns.
They do so in two complementary ways:
1. **Query pre-processing.**
Rules may alter the query parameters—not only the query string but also features such filters and facets—before the query is processed.
2. **Results post-processing.**
Rules might cause results (hits) to be ranked differently for specific queries.
They can also add user data to the results.
Rules are complementary to the traditional ranking and textual relevance settings:
while settings act globally on every search to an index, rules act selectively.
### Conditions and consequences
The general syntax is as follows:
```jsonc JSON icon=braces theme={"system"}
{
"conditions": [
{
// What the query must match for the rule to be applied.
},
],
"consequence": {
// How the query will be modified if the rule is applied.
}
}
```
#### Condition: what the query must match
Conditions identify query strings matching a specific pattern.
If the query string matches all the rule's conditions,
it activates the rule for that search.
More precisely, a condition consists of:
* An optional **query pattern** acting on the whole text query string, that is, the `query` search parameter.
* Optional **anchoring** defining how the rule's query pattern compares to user queries.
* Optional **filters**, which must match the filters applied with the query. These can be [`filters`](/doc/api-reference/api-parameters/filters) or [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), but not [`numericFilters`](/doc/api-reference/api-parameters/numericFilters), [`tagFilters`](/doc/api-reference/api-parameters/tagFilters), nor [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters).
* An optional **context** must match a context supplied at query time with the [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) parameter
* Optionally enabled **alternatives**, which indicates whether synonyms and plurals of the query pattern are treated as matches.
#### Consequence: modify the query
A rule's consequence can be one or more actions:
* [Add query parameters](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/rules-query-parameters)
* [Automatic facet filter](#automatic-facet-filter)
* [Automatic optional facet filter](#automatic-optional-facet-filter)
* [Remove or replace a word from the query string](#remove-or-replace-a-word-from-the-query-string)
* Replace the query string entirely
* Promote or hide specific hits
* Return user data
## Conditions: adapt the query
A rule can have any number of conditions:
* If a rule doesn't have any conditions, it applies to every search.
* If a rule has more than one condition, it applies when any condition matches.
For more information about multiple conditions, see [rule structure](/doc/guides/managing-results/rules/rules-overview#conditions)
### Rule query pattern
The query pattern is the most crucial part of the rule's condition.
It consists of a sequence of tokens, treated as a phrase.
All tokens must appear contiguously and in the specified order.
The allowed token types are:
* **Literal**: matches a word.
The matching is case insensitive.
If a rule's `alternatives` setting is `true`,
then matching considers typo tolerance, plurals, and synonyms.
If you set [`ignorePlurals`](/doc/api-reference/api-parameters/ignorePlurals) to `true` or [`synonyms`](/doc/api-reference/api-parameters/synonyms) to `false`, your `alternatives` setting is ignored.
* **Facet value placeholder**: matches any value of a given facet in the same index.
You must declare the facet in `attributesForFaceting`.
Matching is case insensitive.
Contrary to literals, facet values may be phrases.
The pattern is implicitly a phrase.
The order of words matters: `foo bar` and `bar foo` aren't identical patterns.
The pattern also has an **anchoring type**,
depending on whether its boundaries (beginning, end) must coincide with the boundaries of the query string:
* **starts with**: the pattern must match the words at the beginning of the query string, but there may be extra words at the end
* **ends with**: the pattern must match the words at the end of the query string, but there may be extra words at the beginning
* **contains**: the pattern must match any contiguous sequence of words of the query string.
#### Escape characters within a pattern
To include special characters like colons (`:)`, braces (`{`, `}`), or backslashes (`\`) in a query pattern, you need to escape them.
For example, to match the pattern `thrones:episode`, write it as:
```json JSON icon=braces theme={"system"}
"pattern": "thrones\\:episode"
```
The first backslash escapes the colon for Algolia, and the second is required by JSON syntax.
### Filters can trigger rules
[`filters`](/doc/api-reference/api-parameters/filters) or [`facetFilters`](/doc/api-reference/api-parameters/facetFilters) can trigger rules.
The primary goal of this condition is to create rules that are triggered on specific category pages or when a user applies specific filters.
If a rule's condition has only filters and not query pattern nor context,
Algolia applies the rule's consequences.
Algolia applies the rule's consequences if a condition only has filters:
no query patterns or context.
If a condition also has query patterns or context,
those must match for the rule to be triggered.
To use an attribute in a condition with filters,
[declare it in `attributesForFaceting`](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
#### A single rule per filter value
Just like only a single rule can be triggered per word in the query,
only a single rule can be triggered per applied filter. For example:
* Rule A has the filter `brand:Nike` as its condition
* Rule B has the filter `brand:Nike` and the query "shoes" as its condition
* If the query is "shoes" and the filter `brand:Nike` is applied, only rule B is triggered.
* If the query is anything other than "shoes" and the filter `brand:Nike` is applied, only rule A is triggered.
This follows the same [rule matching algorithm](/doc/guides/managing-results/rules/rules-overview/in-depth/rule-matching-algorithm) applied to rules on queries and contexts.
#### Matches must be exact
A rule with filter values in the condition triggers only when the search contains that value just for that filter.
For example, if a rule's condition contains `"filters": "brand:Nike"`,
this rule triggers if **only** the value `Nike` is selected in the filter `brand`.
Algolia doesn't trigger the rule for a query filtered on `brand:Nike OR brand:Supreme`.
If a condition has multiple filters, all applied filters must match the condition with no additional filter values to trigger the rule.
For example, if the rule condition is `"filters": "brand:Nike AND color:red"`,
then Algolia triggers this rule if only the value `Nike` is selected in the `brand` filter and only the value `red` is selected in the `color` filter.
The rule isn't triggered if only `Nike` is selected without `red`,
and it doesn't trigger if `brand:Nike` and `color:red OR color:blue` are selected.
#### Only the filtering attributes in the condition matter
If a rule's condition includes the filter `brand:Nike` and no other filters,
Algolia only considers the `brand` attribute's applied filter values to decide whether to trigger the rule.
For example, if a query filters on `brand:Nike AND color:red`,
that rule doesn't consider the value of the `color` filter because it's not part of the condition.
Algolia triggers the rule since `brand` is filtered on `Nike`.
#### Multiple filtering attributes
If a condition has multiple filters, the following applies:
* Multiple values for the same attribute are grouped with an `OR` condition
* Values for different attributes are grouped with an `AND` condition.
That means Algolia interprets a condition with `brand:Supreme`, `brand:Nike`, `gender:men` as `(brand:Supreme OR brand:Nike) AND (gender:men)`.
In other words, Algolia triggers the rule if a user selects the same set of values (`Supreme`, `Nike` and `men`) in the filters on the frontend.
These conditions only hold if the frontend filters work with the same logic.
For example, if a user filters on `Nike`, `Supreme` for brand, and `red` for color, the filtering logic follows the `AND of ORs` pattern: `(brand:Nike OR brand:Supreme) AND color:red`.
Thus the filters are compatible with filtering conditions possible on rules.
Filters in rules' conditions aren't compatible with `ORs of ANDs`, `ANDs of ANDs` or `ORs of ORs` logic.
For example, consider a real estate search where a user filters for a two-bedroom apartment in Paris with a balcony and fireplace.
In this use case, the desired logic would be `ANDs of ANDs`: `bedrooms:2 AND (amenities:balcony AND amenities:fireplace) AND location:Paris`.
You can't create a rule with a condition to match this filtering.
### Context
To narrow, customize, or make rules query-independent,
you can have a rule respond to the [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) parameter.
The contexts found in the `ruleContexts` of user searches are compared to the contexts you assign to your rules.
Contexts passed as parameters during your search:
doing so activates all rules that share the context you passed.
**Activation means the rule is considered not to have been applied**.
If your rule's condition only contains a context,
the rules consequence is applied to search results.
If your rule also contains a query pattern,
that pattern must match for the consequence to be applied.
For example, you can define a set of rules with the context "homepage" and pass the same context value along with any search that originates from your homepage.
Now you have a set of rules that apply to the homepage search,
giving a more customized experience.
A rule can have one context at most, but a search may have multiple contexts: these are treated as disjunctive (OR).
When one or more contexts are specified at query time, rules matching any of those contexts are activated.
General rules are always activated.
The context's primary goal is to conditionally enable rules for specific search environments.
If present, the context is a string that must be passed at query time in the `ruleContexts` search parameter
with the same value for the rule to be triggered.
Matching is case-sensitive.
The `ruleContexts` parameter lets you enable multiple contexts simultaneously.
A rule context must consist only of alphanumeric characters, hyphens, and underscores.
## Consequence—adapt the results
A rule's consequence can be one or more actions.
### Add query parameters
Any number of valid search parameters are supported.
These parameters are literals (constants).
This consequence lets you add or modify any search settings that can be changed when the query is sent to Algolia.
Some settings that affect search, like searchable attributes,
can only be modified at indexing time, meaning you can't change them at query time.
[Any parameter](/doc/api-reference/api-parameters) with `search` as part of its scope can be changed at query time.
One common example of a parameter that's useful to change at query time is `filters`.
For example, when someone searches "tablet",
you want to show not only tablets (products with 'tablets' in their `categories` attribute)
but also laptops used as tablets (products with 'hybrids' in their `categories` attribute).
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. In **Conditions**, click **Set query condition(s)**, select **contains**, enter `tablet`, and click **Apply**.
3. In **Consequences**, select **Filter results**.
Select `categories` as the attribute and `tablets` as its value.
Add `hybrids` as another value, join the values with **Or**, and click **Apply**.
4. Review the results, click **Review and Publish**, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `tablet`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "filters": "categories:tablets OR categories:hybrids" }`.
4. Save your changes.
You can also [add filters with the API](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query#using-the-api).
### Automatic facet filter
Automatically transform a word or group of words into a facet filter if it matches a value in a given facet.
A user enters "red", which is also a value in the color facet.
You can set up a rule to transform red to behave like a filter.
Another approach would be to use the filters parameter (using the "add query parameters" rule) but,
with this approach, you must have one rule per filter value.
If you have 10 color values in your color facet,
you must create 10 rules, one for each color.
It's more powerful to use automatic faceting: instead of creating one rule for each color, you create one rule per facet.
Whether users types "blue" or "red", this single rule will apply, it also applies to new colors added to the facet.
When the pattern matches multiple values of the same facet, filters can be either **conjunctive** (`AND`, default) or **disjunctive** (`OR`).
By default, the filters are combined with an `AND` operator.
However, if you specify the filter to be disjunctive, an `OR` operator is used instead.
In both cases, relationships to other filters in the query will always be conjunctive (`AND`).
You can also specify a score for the created filter, mainly useful for optional facet filters (see below).
For example, to automatically filter on the `brand` facet, using `OR` in case of multiple occurrences:
```json JSON icon=braces theme={"system"}
{
"conditions": [
{
"pattern": "{facet:brand}"
}
],
"consequence": {
"params": {
"automaticFacetFilters": [
{
"facet": "brand",
"disjunctive": "true"
}
]
}
}
}
```
And if you want to reverse the logic to exclude the captured facet for example, you can use the `negative` field and set it to `true`.
### Automatic optional facet filter
Same as `automaticFacetFilters`, but the filters are optional.
Behaves like [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters).
### Remove or replace a word from the query string
You can remove or replace words using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, enter `foo bar`, and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Remove word**.
In **Word(s)**, select or enter `foo` and click **Apply**.
You can select more than one word or a facet placeholder from the condition.
4. Click **Add consequence**, expand **More consequences**, and select **Replace word**.
Set **Original word** to `bar` and **New word** to `baz`, then click **Apply**.
To replace more words, use **Add another** in the same panel.
5. Review the search results, click **Review and Publish**, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `foo bar`.
3. In **Consequence(s)**, click **Add consequence**, select **Remove Word**, and enter `foo`.
4. Click **Add consequence** again and select **Replace Word**.
Set the original word to `bar` and its replacement to `baz`.
5. Save your changes.
The rule changes the query used for search, not the text users entered in their search box.
Removing or replacing words is achieved by specifying a list of **edits** to be applied to the query string.
An edit can either:
* Remove a word (or facet placeholder)
* Replace a word (or facet placeholder) with another word
For example, the following rule specifies that when matching `foo bar`,
Algolia should remove the word `foo` and the word `bar`
should be replaced with `baz`:
```json JSON icon=braces theme={"system"}
{
"condition": [
{
"pattern": "foo bar"
}
],
"consequence": {
"params": {
"query": {
"edits": [
{
"type": "remove",
"delete": "foo"
},
{
"type": "replace",
"delete": "bar",
"insert": "baz"
}
]
}
}
}
}
```
Words that are removed or replaced from the query string are still highlighted or showing in the snippets in the query's results,
so it's not misleading for users.
### Entirely replace the query string
You can replace a query using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Select **Set query condition(s)** to define the query that triggers the rule.
3. In **Consequences**, expand **More consequences** and select **Replace query**.
Enter the new text in **Replacement query** and click **Apply**.
Leave the field empty to search with an empty query.
4. Review the search results, click **Review and Publish**, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, define the query that triggers the rule.
3. In **Consequence(s)**, click **Add consequence** and select **Replace Query**.
Enter the new text, or leave the field empty to search with an empty query.
4. Save your changes.
To entirely replace the query string,
specify a new string for the `query` parameter, as you would do for any
other search parameters:
```json JSON icon=braces theme={"system"}
{
"condition": [
{
"pattern": "original query that will be replaced"
}
],
"consequence": {
"params": {
"query": "new text to be searched"
}
}
}
```
You can't combine full query replacement with removing or replacing individual words in the same rule.
But you can remove and replace individual words in the same rule.
This can affect how later rules apply.
For more information, see [Matching algorithm](/doc/guides/managing-results/rules/rules-overview/in-depth/rule-matching-algorithm).
### Promote or hide specific hits
One or more objects from the same index, identified by their `objectID`, are promoted to specific positions in the results list or removed from them.
This promotion works seamlessly with Algolia's built-in pagination, mingling promoted hits with regular
hits and removing hidden hits while keeping pages the same size.
For more information, see [Hit promotion's effect on relevance](#hit-promotion’s-effect-on-relevance).
### Return user data
User data is returned in a dedicated `userData` array.
If multiple rules are applied,
each set of user data is appended to this array.
User data can be used to display specific information that doesn't affect pagination.
For more information, see [Add banners to results With `userData`](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
## Hit promotion's effect on relevance
You can only promote objects coming from the same index.
Promoted objects have to be explicitly identified by their `objectID`.
A promoted object will always be considered a hit, even if it doesn't match the query.
If it would have matched the query, it's removed from its original position and inserted at its promoted position,
even if the original position would have been better than the promoted position.
In other words, promoted hits can also be "demoted".
For performance reasons,
promoted positions can be between 0 and .
Inside the same rule, each promoted object must have a different promoted position.
If promoted objects from two distinct rules are triggered for the same query:
* Any duplicates are merged using the best position.
* If the resulting positions conflict between distinct ,
records are shifted down until a free slot is found.
* All regular hits are shifted down until all promoted records get as close to their promoted position as possible (except conflicts between records, as stated before).
Hidden records are removed from the hits.
The following hits are shifted up so that pagination works seamlessly.
## Inject user data
User data lets you inject data inside the results that aren't in the index,
and as such, don't compete with other hits for pagination.
A typical use-case would be to display a banner on top of the result list.
User data can be any JSON object.
The API doesn't interpret user data.
If you want to keep your analytics data for longer,
[upgrade your plan](https://www.algolia.com/pricing) or contact the [Algolia support](https://support.algolia.com/hc/en-us/requests/new) team.
# Rules matching algorithm
Source: https://www.algolia.com/doc/guides/managing-results/rules/rules-overview/in-depth/rule-matching-algorithm
Learn about the precedence order for applying multiple rules.
If your has many rules,
you need to be careful not to create conflicts,
since **rules may contradict or override each other.**
To reduce conflicts, Algolia applies the following **rules matching algorithm**:
1. Sort all matching rules with a [tie-breaking algorithm](#rules-precedence).
2. Compare the sorted rules to check for conflicts.
If there is a conflict, exclude rules with the lowest precedence.
3. Apply the remaining rules to the .
## Rules precedence
Algolia uses a tie-breaking algorithm to determine the precedence of all rules that apply to a query.
The next criterion is only considered if there's a tie in the preceding criteria.
More specific rules typically have higher precedence.
The tie-breaking algorithm that determines rule precedence applies these criteria:
1. **Position**. The match closest to the beginning of the query string wins.
2. **Match length**. The longest match wins.
The match length is determined by the number of words matched from the query string and the number of filters matched from the query parameters.
3. **Anchoring**. Rules with anchoring patterns are ranked in this order: `is` > `starts with` > `ends with` > `contains`.
4. **Context**. Rules with context have a higher priority than other rules.
5. **Filters**. Rules with filters have a higher priority than other rules.
6. **Literals over placeholders**. If a word matches a literal and a value, the literal takes precedence. Matches with alternatives, such as synonyms and plurals, don't take precedence over matches with facet values.
7. **Temporary over permanent**. If the temporary and permanent rules match, the temporary rule takes precedence.
8. **Rule ID**. If there are still conflicts after all other criteria have been applied, the rule with the smallest rule ID (`objectID`) wins, in [lexicographical order](https://www.educative.io/answers/what-is-a-lexicographic-order) wins.
This final criterion guarantees to break any remaining ties.
It often only applies when there are duplicate rules.
Rules apply from the beginning of the query to its end.
The precedence algorithm also applies to rules with multiple conditions.
Each condition is evaluated as if it were an independent rule.
For more information about rules applied to recommendations,
see [Recommend rules](/doc/guides/algolia-recommend/how-to/rules#recommend-rules-precedence-algorithm).
## Conflicting conditions
After ranking rules by precedence,
only conflicting rules with the highest precedence are retained.
Conflicting rules with lower precedence are excluded.
### Overlapping pattern conditions
Rules with the [pattern](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#rule-query-pattern) condition
might conflict with each other due to overlapping patterns.
**Rules with overlapping patterns are excluded.**
Consider the query `Enchanted forest adventure`, and an index with several rules.
Each rule matches a different pattern.
The rules are ranked by precedence and only rules A and D are retained.
The pattern of rules B and C overlaps with rule A, which leads to their exclusion.
```txt Text theme={"system"}
`pattern` [enchanted] [forest] [adventure]
Rule A [------------------] KEPT
Rule B [------------------] EXCLUDED: overlaps with rule A
Rule C [------] EXCLUDED: overlaps with rule A
Rule D [---------] KEPT
```
Multiple rules can still match the same query, provided they match **a distinct set of words.**
Rules with an **empty query** pattern don't overlap with any other pattern.
They will all be applied, as long as there isn't any other conflict.
### Intersecting filter conditions
For rules with intersecting [`filters`](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#filters-can-trigger-rules)
conditions, the rule with the lowest precedence is excluded.
## Conflicting consequences
Two rules might try to apply the same consequences, for example, pinning different to the first position.
Consider the following example:
* **Rule 1:** if a query contains "Shakespeare", then promote the record "The Lost Shakespeare Diaries", with objectID "1", to the first result.
* **Rule 2:** if a query contains the phrase "how much is", then promote the record "Full Price List", with objectID "2", to the first result.
What happens when the query is "how much is Shakespeare"?
The conditions of rules 1 and 2 are different.
Yet the consequences overlap:
both rules try to pin two different records to the first position.
In this case, the consequences apply depending on the `objectID` of the promoted record.
The record with the lowest `objectID` (in lexicographical order) wins.
Subsequent promotions are shifted down by one position.
In the previous example, the record "The Lost Shakespeare Diaries" is promoted to the first result, since it has a smaller `objectID`.
The record "Full Price List" is then pinned to the second position.
## Edge cases
If a **rule removes a word from the query string**,
all subsequent rules that would have been triggered by this word are ignored.
If a **rule replaces the query string entirely**, all subsequent rules are ignored.
### Context-only rules
For rules that have only a context as a condition, the following applies:
* They have a higher priority than general rules and rules without conditions.
* They always trigger in this context and never conflict with other rules.
### Rules without conditions
For rules that don't have any conditions, the following applies:
* They have a higher priority than general rules.
* They always trigger and never conflict with other rules.
## Effect of promotions on relevance
Only records from the same index can be promoted.
Promoted records have to be explicitly identified by their `objectID`.
A promoted record is included in search results, even if it doesn't match the query.
If it matches the query, it's removed from its original position and inserted at its promoted position,
even if the original position was better than the promoted position.
Promoted positions are restricted to a range of 0 to .
Inside the same rule, each promoted record must have a unique promoted position.
If promoted records from two distinct rules are triggered for the same query,
the following applies:
* Duplicates are merged, using the best position.
* If the resulting positions conflict between distinct records, records are shifted down until a free slot is found.
* All regular search results are shifted down to ensure that all promoted records are as close to their promoted position as possible.
Hidden records are removed from the search results.
The following hits are shifted up so that pagination works seamlessly.
## User data
You can use rules to inject custom data into the search results, such as [banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
Since this data isn't part of the index, it doesn't interfere with things like pagination.
User data can be any JSON object.
It's not interpreted by the API.
# Manage indices
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices
Learn how to copy, rename, or delete your indices and understand the effect on analytics. Learn how you can build a sitemap from your index for SEO.
You can rename, copy, and delete indices, and export or import their settings.
You can perform many of these tasks in the Algolia dashboard or with the API.
To copy an index between different Algolia applications, use the API.
Analytics data is associated with each index.
For more information about how renaming, copying, and deleting indices affects their analytics data,
see [Indices and analytics](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/indices-analytics).
## Name your indices
Choose a unique, readable name for each index.
Don't include sensitive or personally identifiable information (PII), such as
usernames, user IDs, or email addresses.
Index names appear in network requests, so **consider them publicly available**.
## Indices and applications
Common uses for several indices include:
* Query Suggestions or sorting. See [Choosing between one or more indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices).
* Several Algolia applications, such as testing, staging, and production. See [Manage your Algolia applications](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-your-apps).
## Create sitemaps from your indices
Search engines like Google and Bing can find content from your Algolia indices.
This includes product pages or category pages.
To help them, [create sitemaps](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index).
## See also
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Send your data to an index](/doc/guides/sending-and-managing-data/send-and-update-your-data)
* [Export and import indices and settings](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/export-import-indices)
* [Copy and move indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-move-indices)
* [Delete indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices)
# Indices and analytics
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/indices-analytics
Learn the impact of renaming, deleting or copying an index on analytics
Algolia **stores analytics data on dedicated servers**, separate from the indices.
Algolia collects analytics data in parallel with search, in a separate process.
This means collecting analytics never slows down search.
Indices and analytics are separate data sets.
Because of this, copying, moving, or deleting an index doesn't affect the analytics data.
**Algolia refers to analytics data by the name of its index.** This link never changes.
## Effect of renaming indices on analytics
**Changing the name of an index doesn't change the name of the index's analytics.**
The analytics belonging to the original index keep their original name.
Algolia starts a new set of analytics under the new name.
The old and new analytics aren't merged.
To preserve an index's analytics history, you need to keep using the old name.
For more information, see [Copy and move (rename) indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-move-indices).
## Effect of deleting indices on analytics
**Deleting an index doesn't delete the index's analytics.**
The analytics data remains available under the deleted index's name.
You can use the index's name to export the analytics data using the [Analytics API](/doc/rest-api/analytics).
For more information, see [Delete indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices)
## Copying an index
**Copying an index doesn't copy its analytics data.**
Be careful if you copy over an existing index.
The analytics data stays linked to the index's name.
For more information, see [Copy and move (rename) indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-move-indices).
# Choosing between one or more indices
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices
Learn when to use one index or several indices, depending on your relevance strategy and the user experience you want to create.
Should you create one large index for all your ,
or create multiple indices?
It depends on your ranking strategy and which user experience you want to create.
The number of indices you can create depends on your [plan](https://www.algolia.com/pricing).
## One index or multiple indices for federated search
Federated search describes searching multiple types of data sources.
One data source may be your product catalog.
Another data source may be a support FAQ, or Query Suggestions.
Depending on how you want to organize and present the search results,
you can create one index for all data sources or separate indices for each data source.
For more information,
see [Multi-index search with InstantSearch.js](/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/js).
### Multiple indices for federated search
Using multiple indices is useful when:
* You need to configure each index independently.
For example, one index for movies, another for actors.
With federated search, you can show results from both indices in the same interface,
allowing users to search for both actors and movies.
* You want to **use different ranking strategies** for the same data.
For example, showing highest-rated and showing most recent movies.
For a step-by-step example, see [Autocomplete with Federated Search](/doc/guides/solutions/ecommerce/search/autocomplete/federated-search).
### Single index for federated search
Using a single index is useful when you want to use a **single ranking strategy**.
Users can search for anything, see movies and actors in the same results list,
ranked by the same relevance configuration.
## Multiple indices for Query Suggestions
[Query Suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) suggest popular search queries to users to help them search faster.
Since Query Suggestions are stored in a separate index, you'd use **multiple indices**:
one for your content, and one for the Query Suggestions.
## Multiple indices for different ranking strategies
Each Algolia index can have one ranking strategy.
If you want to let users switch between different ranking strategies,
for example, to allow sorting by price,
you can use **replica indices**—one index per sorting strategy.
A [replica index](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas) is a copy of the main index with the same records, but a different ranking strategy.
To learn more, see [Create replica indices](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas).
## Multiple indices for different environments
If you use different environments, for example, staging and production environments,
it's best to use separate indices for each environment.
This prevents your [Analytics](/doc/guides/search-analytics/overview)
from getting skewed by test queries, prevents indexing accidents, and keeps each environment more secure.
You can use common prefixes to distinguish the different environments, for example, `staging_products` and `prod_products`.
To reduce the number of indexing operations, you can reduce the number of records in your staging environments,
for example, only using 10% of your records.
To learn more, see [Manage your Algolia applications](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-your-apps).
# Copy and move indices
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/copy-move-indices
Learn how to copy, move, or rename indices including their records, settings, synonyms, and rules.
You can copy (duplicate) or move (rename) indices,
including their associated , settings, synonyms, and rules,
using the Algolia dashboard or one of the API clients.
Copying or moving an [doesn't move the associated analytics data](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/indices-analytics).
Copying or moving an index is resource-intensive and [rate-limited](https://support.algolia.com/hc/en-us/articles/4406975251089#when-do-we-trigger-the-rate-limit).
## Move or rename indices
You can move or rename an index within the same Algolia in the Algolia dashboard or with an API client.
### Rename indices in the Algolia dashboard
You can't rename replica indices or (primary) indices with replicas in the dashboard.
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select **Manage index > Rename**.
5. Enter the current and new index names and click **Rename**.
If an index with the new name already exists, it's overwritten.
### Rename indices with an API client
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To rename or move an index with the API, you can use:
* API clients: [`operationIndex`](/doc/rest-api/search/operation-index)
* Algolia CLI: [`algolia indices move`](/doc/tools/cli/commands/indices/move)
You can't move an index between different Algolia applications but you can create a copy and delete the original index.
For more information, see [Copy indices between apps](#copy-indices-between-different-applications).
```cs C# theme={"system"}
var response = await client.OperationIndexAsync(
"",
new OperationIndexParams
{
Operation = Enum.Parse("Move"),
Destination = "",
}
);
```
```dart Dart theme={"system"}
final response = await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.fromJson("move"),
destination: "",
),
);
```
```go Go theme={"system"}
response, err := client.OperationIndex(client.NewApiOperationIndexRequest(
"",
search.NewEmptyOperationIndexParams().SetOperation(search.OperationType("move")).SetDestination("")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.operationIndex(
"",
new OperationIndexParams().setOperation(OperationType.MOVE).setDestination("")
);
```
```js JavaScript theme={"system"}
const response = await client.operationIndex({
indexName: '',
operationIndexParams: { operation: 'move', destination: '' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.operationIndex(
indexName = "",
operationIndexParams =
OperationIndexParams(
operation = OperationType.entries.first { it.value == "move" },
destination = "",
),
)
```
```php PHP theme={"system"}
$response = $client->operationIndex(
'',
['operation' => 'move',
'destination' => '',
],
);
```
```python Python theme={"system"}
response = client.operation_index(
index_name="",
operation_index_params={
"operation": "move",
"destination": "",
},
)
```
```ruby Ruby theme={"system"}
response = client.operation_index(
"",
Algolia::Search::OperationIndexParams.new(operation: "move", destination: "")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.operationIndex(
indexName = "",
operationIndexParams = OperationIndexParams(
operation = OperationType.withName("move"),
destination = ""
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.move,
destination: ""
)
)
```
If an index with the new name already exists, moving overwrites it.
To avoid overwriting existing indices,
check if an index exists with [`indexExists`](/doc/libraries/sdk/methods/search/index-exists).
If the source index doesn't exist, Algolia ignores the move operation.
Renaming an index doesn't change the associated analytics:
* Analytics associated with the original index keep their name.
* A new set of analytics is started with the new name.
For more information, see [Indices and analytics](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/indices-analytics).
### Indices with replicas
[Renaming replica indices](/doc/guides/managing-results/refine-results/sorting/how-to/rename-a-replica-index) requires several steps.
**You can't move a source index with replicas.**
You can move an index to a destination index with replicas.
First, the source index data replaces the destination index data.
Then, the data is copied to the replicas.
### Indices used as a Recommend data source
If an index is the data source for Recommend models, you can't rename it directly.
If you do, you'll lose the relationship between the recommended records and their source.
Instead:
1. [Copy the index.](#copy-indices)
2. To keep the events you already collected, train a new model on the copy index using the old index as an additional [events source](/doc/guides/algolia-recommend/how-to/set-up#train-the-recommend-models).
3. After training, switch your apps and services to use the new index.
4. [Delete the old index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices) and the old model (from the [Algolia dashboard](https://dashboard.algolia.com/recommend/models)).
## Copy indices
You can copy an index in the Algolia dashboard or with an API client but be aware that:
* Copying an index duplicates the records and configuration, including synonyms and rules of an existing index (except the `enableReRanking` setting).
* Copying an index [**doesn't create a replica**](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas). The new index is an independent copy.
* To [copy an index between two Algolia applications](#copy-indices-between-different-applications), you must use the API.
### Copy indices in the Algolia dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select the Algolia index that you want to copy.
4. Select **Manage index > Duplicate**.
5. Select **Create a new index** or **Overwrite an existing index** and enter the destination index name.
6. If you're overwriting an existing index, enter `DUPLICATE` to confirm.
7. Click **Duplicate**.
### Copy indices with an API client
To copy indices, you can use:
* API clients: [`operationIndex`](/doc/rest-api/search/operation-index)
* Algolia CLI: [`algolia indices copy`](/doc/tools/cli/commands/indices/copy)
Provide a `scope` parameter to choose what you want to copy: records, settings, synonyms, or rules.
#### Copy indices fully
To copy an index **including records, settings, synonyms, and rules**:
```cs C# theme={"system"}
var response = await client.OperationIndexAsync(
"",
new OperationIndexParams
{
Operation = Enum.Parse("Copy"),
Destination = "",
}
);
```
```dart Dart theme={"system"}
final response = await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.fromJson("copy"),
destination: "",
),
);
```
```go Go theme={"system"}
response, err := client.OperationIndex(client.NewApiOperationIndexRequest(
"",
search.NewEmptyOperationIndexParams().SetOperation(search.OperationType("copy")).SetDestination("")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.operationIndex(
"",
new OperationIndexParams().setOperation(OperationType.COPY).setDestination("")
);
```
```js JavaScript theme={"system"}
const response = await client.operationIndex({
indexName: '',
operationIndexParams: { operation: 'copy', destination: '' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.operationIndex(
indexName = "",
operationIndexParams =
OperationIndexParams(
operation = OperationType.entries.first { it.value == "copy" },
destination = "",
),
)
```
```php PHP theme={"system"}
$response = $client->operationIndex(
'',
['operation' => 'copy',
'destination' => '',
],
);
```
```python Python theme={"system"}
response = client.operation_index(
index_name="",
operation_index_params={
"operation": "copy",
"destination": "",
},
)
```
```ruby Ruby theme={"system"}
response = client.operation_index(
"",
Algolia::Search::OperationIndexParams.new(operation: "copy", destination: "")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.operationIndex(
indexName = "",
operationIndexParams = OperationIndexParams(
operation = OperationType.withName("copy"),
destination = ""
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.copy,
destination: ""
)
)
```
* If the source index doesn't exist, an empty index is created.
* If an index with this name already exists, it will be overwritten.
To prevent overwriting existing indices,
check if an index exists with [`indexExists`](/doc/libraries/sdk/methods/search/index-exists).
#### Copy indices partially
To copy **parts of an index**,
use the `scope` parameter of the [`operationIndex`](/doc/rest-api/search/operation-index) method.
For example, to copy only the synonyms and settings between indices, but not records or rules:
```cs C# theme={"system"}
var response = await client.OperationIndexAsync(
"",
new OperationIndexParams
{
Operation = Enum.Parse("Move"),
Destination = "",
Scope = new List
{
Enum.Parse("Rules"),
Enum.Parse("Settings"),
},
}
);
```
```dart Dart theme={"system"}
final response = await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.fromJson("move"),
destination: "",
scope: [
ScopeType.fromJson("rules"),
ScopeType.fromJson("settings"),
],
),
);
```
```go Go theme={"system"}
response, err := client.OperationIndex(client.NewApiOperationIndexRequest(
"",
search.NewEmptyOperationIndexParams().SetOperation(search.OperationType("move")).SetDestination("").SetScope(
[]search.ScopeType{search.ScopeType("rules"), search.ScopeType("settings")})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.operationIndex(
"",
new OperationIndexParams()
.setOperation(OperationType.MOVE)
.setDestination("")
.setScope(Arrays.asList(ScopeType.RULES, ScopeType.SETTINGS))
);
```
```js JavaScript theme={"system"}
const response = await client.operationIndex({
indexName: '',
operationIndexParams: { operation: 'move', destination: '', scope: ['rules', 'settings'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.operationIndex(
indexName = "",
operationIndexParams =
OperationIndexParams(
operation = OperationType.entries.first { it.value == "move" },
destination = "",
scope =
listOf(
ScopeType.entries.first { it.value == "rules" },
ScopeType.entries.first { it.value == "settings" },
),
),
)
```
```php PHP theme={"system"}
$response = $client->operationIndex(
'',
['operation' => 'move',
'destination' => '',
'scope' => [
'rules',
'settings',
],
],
);
```
```python Python theme={"system"}
response = client.operation_index(
index_name="",
operation_index_params={
"operation": "move",
"destination": "",
"scope": [
"rules",
"settings",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.operation_index(
"",
Algolia::Search::OperationIndexParams.new(
operation: "move",
destination: "",
scope: ["rules", "settings"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.operationIndex(
indexName = "",
operationIndexParams = OperationIndexParams(
operation = OperationType.withName("move"),
destination = "",
scope = Some(Seq(ScopeType.withName("rules"), ScopeType.withName("settings")))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.operationIndex(
indexName: "",
operationIndexParams: OperationIndexParams(
operation: OperationType.move,
destination: "",
scope: [ScopeType.rules, ScopeType.settings]
)
)
```
If you use the `scope` parameter, records aren't copied to the new index.
Existing items of the scope are replaced.
Items belonging to other scopes are preserved.
For example, if you use the `settings` scope,
only the settings are copied to the destination index,
keeping the records, synonyms, and rules.
### Indices with replicas
Copying a source index that has replicas copies the index's data, but not its replicas.
The destination index won't have replicas.
**You can't copy to a destination index with replicas.**
### Copy indices between different applications
To copy an index between different Algolia applications,
[use the Algolia CLI](/doc/tools/cli/examples#copy-indices-between-applications).
# Delete indices
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/delete-indices
Delete indices and their configuration, or clear records while keeping settings, rules, and synonyms.
You can either delete an or [clear it](#clear-indices) (only delete its ).
## Before you begin
Deleting an index removes that index and its records, rules, settings, and synonyms.
It doesn't remove the [associated analytics data](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/indices-analytics).
Before you delete or clear an index,
consider [exporting its records and configuration](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/export-import-indices) to a create a backup.
If you delete an index by mistake, contact [Algolia support](https://support.algolia.com/hc/en-us/requests/new).
They may be able to restore it,
but recovery isn't guaranteed.
To run the API examples, [install the latest API client](/doc/libraries/sdk/install).
Use an API key with the permissions required by each method.
Deleting or clearing an index requires the `deleteIndex` permission.
Before running CLI commands, [install and configure the Algolia CLI](/doc/tools/cli/get-started) for the that contains your indices.
## Delete indices in the Algolia dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select **Manage index > Delete**.
5. Type `DELETE` to confirm and click **Delete**.
## Delete indices with the API
Delete an index with either of the following:
* Algolia CLI: [`algolia indices delete`](/doc/tools/cli/commands/indices/delete) (deletes one or more indices and, with `--include-replicas`, their replicas)
* API clients: [`deleteIndex`](/doc/libraries/sdk/methods/search/delete-index)
```cs C# theme={"system"}
var response = await client.DeleteIndexAsync("INDEX_NAME");
```
```dart Dart theme={"system"}
final response = await client.deleteIndex(
indexName: "INDEX_NAME",
);
```
```go Go theme={"system"}
response, err := client.DeleteIndex(client.NewApiDeleteIndexRequest(
"INDEX_NAME"))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
DeletedAtResponse response = client.deleteIndex("INDEX_NAME");
```
```js JavaScript theme={"system"}
const response = await client.deleteIndex({ indexName: 'theIndexName' });
```
```kotlin Kotlin theme={"system"}
var response = client.deleteIndex(indexName = "INDEX_NAME")
```
```php PHP theme={"system"}
$response = $client->deleteIndex(
'INDEX_NAME',
);
```
```python Python theme={"system"}
response = client.delete_index(
index_name="INDEX_NAME",
)
```
```ruby Ruby theme={"system"}
response = client.delete_index("INDEX_NAME")
```
```scala Scala theme={"system"}
val response = Await.result(
client.deleteIndex(
indexName = "INDEX_NAME"
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.deleteIndex(indexName: "INDEX_NAME")
```
### Delete multiple indices
To delete replica indices with an API client,
first [unlink them from their primary indices](#delete-indices-with-replicas).
To delete more than one index:
1. Select the indices to delete. To find available indices, use [`listIndices`](/doc/libraries/sdk/methods/search/list-indices).
2. Use [`multipleBatch`](/doc/libraries/sdk/methods/search/multiple-batch) to delete the selected indices with a single request.
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class DeleteMultipleIndices
{
async Task Main(string[] args)
{
// You need an API key with `deleteIndex`
var client = new SearchClient(new SearchConfig("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY"));
// List all indices
var indices = await client.ListIndicesAsync();
// Primary indices don't have a `primary` key
var primaryIndices = indices.Items.Where(item => item.Primary == null).ToList();
var replicaIndices = indices.Items.Where(item => item.Primary != null).ToList();
// Delete primary indices first
if (primaryIndices.Count > 0)
{
var requests = primaryIndices
.Select(index => new MultipleBatchRequest(Search.Models.Search.Action.Delete, index.Name))
.ToList();
await client.MultipleBatchAsync(new BatchParams { Requests = requests });
Console.WriteLine("Deleted primary indices.");
}
// Now, delete replica indices
if (replicaIndices.Count > 0)
{
var requests = replicaIndices
.Select(index => new MultipleBatchRequest(Search.Models.Search.Action.Delete, index.Name))
.ToList();
await client.MultipleBatchAsync(new BatchParams { Requests = requests });
Console.WriteLine("Deleted replica indices.");
}
}
}
```
```dart Dart theme={"system"}
import 'package:algolia_client_search/algolia_client_search.dart';
void deleteMultipleIndices() async {
// You need an API key with `deleteIndex`
final client =
SearchClient(appId: 'ALGOLIA_APPLICATION_ID', apiKey: 'ALGOLIA_API_KEY');
// List all indices
final indices = await client.listIndices();
// Primary indices don't have a `primary` key
final primaryIndices =
indices.items.where((element) => element.primary == null).toList();
final replicaIndices =
indices.items.where((element) => element.primary != null).toList();
// Delete primary indices first
if (primaryIndices.isNotEmpty) {
final List requests = primaryIndices
.map((element) => MultipleBatchRequest(
action: Action.delete, indexName: element.name))
.toList();
await client.multipleBatch(
batchParams: BatchParams(
requests: requests,
),
);
print("Deleted primary indices.");
}
// Now, delete replica indices
if (replicaIndices.isNotEmpty) {
final List requests = replicaIndices
.map((element) => MultipleBatchRequest(
action: Action.delete, indexName: element.name))
.toList();
await client.multipleBatch(
batchParams: BatchParams(
requests: requests,
),
);
print("Deleted replica indices.");
}
}
```
```go Go theme={"system"}
package main
import (
"fmt"
"github.com/algolia/algoliasearch-client-go/v4/algolia/search"
)
func deleteMultipleIndices() {
// You need an API key with `deleteIndex`
client, err := search.NewClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
if err != nil {
// The client can fail to initialize if you pass an invalid parameter.
panic(err)
}
// List all indices
indices, err := client.ListIndices(client.NewApiListIndicesRequest())
if err != nil {
panic(err)
}
// Primary indices don't have a `primary` key
primaryIndices := make([]search.FetchedIndex, 0, len(indices.Items))
replicaIndices := make([]search.FetchedIndex, 0, len(indices.Items))
for _, index := range indices.Items {
if index.Primary == nil {
primaryIndices = append(primaryIndices, index)
} else {
replicaIndices = append(replicaIndices, index)
}
}
// Delete primary indices first
if len(primaryIndices) > 0 {
requests := make([]search.MultipleBatchRequest, 0, len(primaryIndices))
for _, index := range primaryIndices {
requests = append(requests, search.MultipleBatchRequest{
Action: "delete",
IndexName: index.Name,
})
}
_, err = client.MultipleBatch(client.NewApiMultipleBatchRequest(
search.NewEmptyBatchParams().SetRequests(requests)))
if err != nil {
panic(err)
}
fmt.Println("Deleted primary indices.")
}
// Now, delete replica indices
if len(replicaIndices) > 0 {
requests := make([]search.MultipleBatchRequest, 0, len(replicaIndices))
for _, index := range primaryIndices {
requests = append(requests, search.MultipleBatchRequest{
Action: "delete",
IndexName: index.Name,
})
}
_, err = client.MultipleBatch(client.NewApiMultipleBatchRequest(
search.NewEmptyBatchParams().SetRequests(requests)))
if err != nil {
panic(err)
}
fmt.Println("Deleted replica indices.")
}
}
```
```java Java theme={"system"}
package com.algolia;
import com.algolia.api.SearchClient;
import com.algolia.config.*;
import com.algolia.model.search.*;
import java.util.List;
public class deleteMultipleIndices {
public static void main(String[] args) throws Exception {
// You need an API key with `deleteIndex`
try (SearchClient client = new SearchClient("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")) {
// List all indices
ListIndicesResponse indices = client.listIndices();
// Primary indices don't have a `primary` key
List primaryIndices = indices
.getItems()
.stream()
.filter(item -> item.getPrimary() == null)
.toList();
List replicaIndices = indices
.getItems()
.stream()
.filter(item -> item.getPrimary() != null)
.toList();
// Delete primary indices first
if (!primaryIndices.isEmpty()) {
List requests = primaryIndices
.stream()
.map(index -> new MultipleBatchRequest().setAction(Action.DELETE).setIndexName(index.getName()))
.toList();
client.multipleBatch(new BatchParams().setRequests(requests));
System.out.println("Deleted primary indices.");
}
// Now, delete replica indices
if (!replicaIndices.isEmpty()) {
List requests = replicaIndices
.stream()
.map(index -> new MultipleBatchRequest().setAction(Action.DELETE).setIndexName(index.getName()))
.toList();
client.multipleBatch(new BatchParams().setRequests(requests));
System.out.println("Deleted replica indices.");
}
} catch (Exception e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
```
```js JavaScript theme={"system"}
import { algoliasearch } from 'algoliasearch';
import type { MultipleBatchRequest } from 'algoliasearch';
// You need an API key with `deleteIndex`
const client = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_API_KEY');
// List all indices
const indices = await client.listIndices();
// Primary indices don't have a `primary` key
const primaryIndices = indices.items.filter((item) => item.primary == null);
const replicaIndices = indices.items.filter((item) => item.primary != null);
// Delete primary indices first
if (primaryIndices.length > 0) {
const requests: MultipleBatchRequest[] = primaryIndices.map((index) => ({
action: 'delete',
indexName: index.name,
}));
await client.multipleBatch({ requests: requests });
console.log('Deleted primary indices.');
}
// Now, delete replica indices
if (replicaIndices.length > 0) {
const requests: MultipleBatchRequest[] = replicaIndices.map((index) => ({
action: 'delete',
indexName: index.name,
}));
await client.multipleBatch({ requests: requests });
console.log('Deleted replica indices.');
}
```
```kotlin Kotlin theme={"system"}
import com.algolia.client.api.SearchClient
import com.algolia.client.configuration.*
import com.algolia.client.extensions.*
import com.algolia.client.model.search.*
import com.algolia.client.transport.*
suspend fun deleteMultipleIndices() {
// You need an API key with `deleteIndex`
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
// List all indices
val indices = client.listIndices()
// Primary indices don't have a `primary` key
val primaryIndices = indices.items.filter { it.primary == null }
val replicaIndices = indices.items.filter { it.primary != null }
// Delete primary indices first
if (primaryIndices.isNotEmpty()) {
val requests =
primaryIndices.map { MultipleBatchRequest(action = Action.Delete, indexName = it.name) }
client.multipleBatch(batchParams = BatchParams(requests = requests))
println("Deleted primary indices.")
}
// Now, delete replica indices
if (replicaIndices.isNotEmpty()) {
val requests =
replicaIndices.map { MultipleBatchRequest(action = Action.Delete, indexName = it.name) }
client.multipleBatch(batchParams = BatchParams(requests = requests))
println("Deleted replica indices.")
}
}
```
```php PHP theme={"system"}
listIndices();
$indices = $indicesResponse['items'];
// Primary indices don't have a `primary` key
$primaryIndices = array_filter($indices, function ($index) {
return !isset($index['primary']);
});
$replicaIndices = array_filter($indices, function ($index) {
return isset($index['primary']);
});
// Delete primary indices first
if (!empty($primaryIndices)) {
$requests = array_map(function ($index) {
return (new MultipleBatchRequest())
->setAction((new Action())::DELETE)
->setIndexName($index['name'])
;
}, $primaryIndices);
$client->multipleBatch(
['requests' => $requests,
],
);
echo "Deleted primary indices.\n";
}
// Now, delete replica indices
if (!empty($replicaIndices)) {
$requests = array_map(function ($index) {
return [
'action' => 'delete',
'indexName' => $index['name'],
];
}, $replicaIndices);
$client->multipleBatch(
['requests' => $requests,
],
);
echo "Deleted replica indices.\n";
}
```
```python Python theme={"system"}
from algoliasearch.search.client import SearchClientSync
# You need an API key with `deleteIndex`
_client = SearchClientSync("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
# List all indices
indices = _client.list_indices()
# Primary indices don't have a `primary` key
primary_indices = [index for index in indices.items if index.primary is None]
replica_indices = [index for index in indices.items if index.primary is not None]
# Delete primary indices first
if primary_indices:
requests = [
{"action": "delete", "indexName": index.name} for index in primary_indices
]
_client.multiple_batch(
batch_params={
"requests": requests,
},
)
print("Deleted primary indices.")
# Now, delete replica indices
if replica_indices:
requests = [
{"action": "delete", "indexName": index.name} for index in replica_indices
]
_client.multiple_batch(
batch_params={
"requests": requests,
},
)
print("Deleted replica indices.\n")
```
```ruby Ruby theme={"system"}
require "algolia"
# You need an API key with `deleteIndex`
client = Algolia::SearchClient.create("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY")
# List all indices
indices = client.list_indices
# Primary indices don't have a `primary` key
primary_indices, replica_indices = indices.items.partition { |index| index.primary.nil? }.map(&:to_a)
# Delete primary indices first
if primary_indices.any?
requests = primary_indices.map { |index|
Algolia::Search::BatchRequest.new(action: Algolia::Search::Action::DELETE, indexName: index.name)
}
client.multiple_batch(Algolia::Search::BatchParams.new(requests: requests))
print("Deleted primary indices.")
end
# Now, delete replica indices
if replica_indices.any?
requests = replica_indices.map { |index|
Algolia::Search::BatchRequest.new(action: Algolia::Search::Action::DELETE, indexName: index.name)
}
client.multiple_batch(Algolia::Search::BatchParams.new(requests: requests))
print("Deleted replica indices.\n")
end
```
```scala Scala theme={"system"}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import algoliasearch.api.SearchClient
import algoliasearch.config.*
import algoliasearch.extension.SearchClientExtensions
import algoliasearch.search.{Action, BatchParams, MultipleBatchRequest}
def deleteMultipleIndices(): Future[Unit] = {
// You need an API key with `deleteIndex`
val client = SearchClient(appId = "ALGOLIA_APPLICATION_ID", apiKey = "ALGOLIA_API_KEY")
client
.listIndices(
)
.flatMap { indices =>
val (primaryIndices, replicaIndices) = indices.items.partition(_.primary.isEmpty)
val deletePrimary = if (primaryIndices.nonEmpty) {
val requests = primaryIndices.map(index => MultipleBatchRequest(Action.Delete, None, index.name))
client
.multipleBatch(
batchParams = BatchParams(
requests = requests
)
)
.map { _ =>
println("Deleted primary indices.")
}
} else Future.unit
val deleteReplica = if (replicaIndices.nonEmpty) {
val requests = replicaIndices.map(index => MultipleBatchRequest(Action.Delete, None, index.name))
client
.multipleBatch(
batchParams = BatchParams(
requests = requests
)
)
.map { _ =>
println("Deleted replica indices.")
}
} else Future.unit
for {
_ <- deletePrimary
_ <- deleteReplica
} yield ()
}
.recover { case ex: Exception =>
println(s"An error occurred: ${ex.getMessage}")
}
}
```
```swift Swift theme={"system"}
import Foundation
#if os(Linux) // For linux interop
import FoundationNetworking
#endif
import AlgoliaCore
import AlgoliaSearch
func deleteMultipleIndices() async throws {
do {
// You need an API key with `deleteIndex`
let client = try SearchClient(appID: "ALGOLIA_APPLICATION_ID", apiKey: "ALGOLIA_API_KEY")
// List all indices
let indices = try await client.listIndices()
// Primary indices don't have a `primary` key
let primaryIndices = indices.items.filter { $0.primary == nil }
let replicaIndices = indices.items.filter { $0.primary != nil }
// Delete primary indices first
if primaryIndices.isEmpty == false {
let requests = primaryIndices.map { MultipleBatchRequest(
action: .delete,
indexName: $0.name
) }
try await client.multipleBatch(batchParams: BatchParams(requests: requests))
print("Deleted primary indices.")
}
// Now, delete replica indices
if replicaIndices.isEmpty == false {
let requests = replicaIndices.map { MultipleBatchRequest(
action: .delete,
indexName: $0.name
) }
try await client.multipleBatch(batchParams: BatchParams(requests: requests))
print("Deleted replica indices.")
}
} catch {
print(error)
}
}
```
In a `multipleBatch` request body, the `requests` property must contain a list of operations.
Don't add an extra `requests` wrapper around this list.
An extra wrapper can cause an error such as "Requests attribute must be an array."
## Delete indices with replicas
Deleting a primary index with an API client makes its replicas independent indices.
To delete a replica index with an API client, first unlink it from its primary index.
In the Algolia dashboard, you can delete replicas without manually unlinking them.
The Algolia CLI `algolia indices delete` command automatically unlinks replica indices before deleting them.
### Unlink a replica from its primary
The [`replicas`](/doc/api-reference/api-parameters/replicas) setting contains the list of replicas for a primary index.
To unlink a replica, remove its entry from this list with the [`setSettings`](/doc/libraries/sdk/methods/search/set-settings) method.
Keep the entries for all other replicas.
Setting `replicas` to an empty list (`[]`) unlinks all replicas.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { Replicas = new List { "" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
replicas: [
"",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetReplicas(
[]string{""})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setReplicas(Arrays.asList("")));
```
```js JavaScript theme={"system"}
const response = await client.setSettings({ indexName: 'theIndexName', indexSettings: { replicas: [''] } });
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(replicas = listOf("")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['replicas' => [
'',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"replicas": [
"",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(replicas: [""]))
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
replicas = Some(Seq(""))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(replicas: [""])
)
```
Wait for the settings update on the primary index to complete with [`waitForTask`](/doc/libraries/sdk/methods/search/wait-for-task).
Then delete the unlinked index [in the Algolia dashboard](#delete-indices-in-the-algolia-dashboard) or [with the API](#delete-indices-with-the-api).
## Clear indices
Clearing an index deletes only its records and keeps its settings, synonyms, and rules.
The index doesn't return any results until you add records again.
Clearing a primary index also clears the records from its replicas.
### Clear indices in the Algolia dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select **Manage index > Clear**.
5. Type `CLEAR` to confirm and click **Clear**.
### Clear indices with the API
To replace records without interrupting search,
use [`replaceAllObjects`](/doc/libraries/sdk/methods/search/replace-all-objects) instead of clearing the index first.
To clear an index and delete all records,
use either of the following:
* API clients: [`clearObjects`](/doc/libraries/sdk/methods/search/clear-objects)
* Algolia CLI: [`algolia indices clear`](/doc/tools/cli/commands/indices/clear)
```cs C# theme={"system"}
var response = await client.ClearObjectsAsync("INDEX_NAME");
```
```dart Dart theme={"system"}
final response = await client.clearObjects(
indexName: "INDEX_NAME",
);
```
```go Go theme={"system"}
response, err := client.ClearObjects(client.NewApiClearObjectsRequest(
"INDEX_NAME"))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.clearObjects("INDEX_NAME");
```
```js JavaScript theme={"system"}
const response = await client.clearObjects({ indexName: 'theIndexName' });
```
```kotlin Kotlin theme={"system"}
var response = client.clearObjects(indexName = "INDEX_NAME")
```
```php PHP theme={"system"}
$response = $client->clearObjects(
'INDEX_NAME',
);
```
```python Python theme={"system"}
response = client.clear_objects(
index_name="INDEX_NAME",
)
```
```ruby Ruby theme={"system"}
response = client.clear_objects("INDEX_NAME")
```
```scala Scala theme={"system"}
val response = Await.result(
client.clearObjects(
indexName = "INDEX_NAME"
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.clearObjects(indexName: "INDEX_NAME")
```
# Export and import indices and settings
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/export-import-indices
Export and your records and settings from your Algolia index from the dashboard or the API.
An Algolia index has and configuration data, including settings, synonyms, and rules.
Exporting your records and configuration can be useful when you need to:
* Back up your index.
* Track changes to the index configuration (for example, with a version control system).
* Apply the same configuration to multiple indices. For example,
in staging and production environments,
or if you use different indices for different geographical regions.
## Export and import records
To **export your records** as a JSON file, use one of these options:
* Algolia CLI: [`algolia objects browse`](/doc/tools/cli/commands/objects/browse)
* API clients: [`browse`](/doc/libraries/sdk/v1/methods/browse)
You can't export records from the Algolia dashboard.
For information about importing or **uploading your records**,
see [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data).
## Export and import index configurations
Your index configuration includes:
* Settings, such as:
* [Searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes)
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking)
* [Typo tolerance](/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance)
* [Stop words](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/how-to/customize-stop-words)
* [Synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms)
* [Rules](/doc/guides/managing-results/rules/rules-overview)
### Export index configuration in the Algolia dashboard
To export the configuration of an index from the Algolia dashboard as a JSON file:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select **Manage index > Export Configuration**.
5. Select what you want to export: **Settings**, **Synonyms**, or **Rules**.
6. Click **Export Configuration** to download a JSON file with the index configuration.
### Export index configuration with the Algolia CLI
With the **Algolia CLI**, you can export an index configuration with the
[`algolia indices config export`](/doc/tools/cli/commands/indices/config/export) command.
This creates a JSON file that you can [import](#import-index-configuration-in-the-algolia-dashboard)
in the dashboard, for example, for another index.
To export your settings, rules, or synonyms individually, use these commands:
* [`algolia settings get`](/doc/tools/cli/commands/settings/get)
* [`algolia rules browse`](/doc/tools/cli/commands/rules/browse)
* [`algolia synonyms browse`](/doc/tools/cli/commands/synonyms/browse)
Check the [Algolia CLI examples](/doc/tools/cli/examples#export-your-algolia-data) for more information.
### Export index configuration with the API
With the API clients,
you can export your index settings, rules, and synonyms, using these methods:
* [`getSettings`](/doc/libraries/sdk/v1/methods/get-settings)
* [`exportRules`](/doc/libraries/sdk/v1/methods/export-rules)
* [`exportSynonyms`](/doc/libraries/sdk/v1/methods/export-synonyms)
### Import index configuration in the Algolia dashboard
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Select **Manage index > Import Configuration**.
5. Select the JSON file you want to import.
6. Choose what you want to import: **Settings**, **Synonyms**, or **Rules**.
7. Type `IMPORT` to confirm and click **Import Configuration**.
Importing settings **replaces all existing index settings** with the imported ones.
Synonyms and rules are replaced if a synonym or rule with the same `objectID` already exists.
Otherwise, new synonyms or rules are added.
### Import index configuration with the Algolia CLI
To import the configuration of an index with the Algolia CLI,
use the [`algolia indices config import`](/doc/tools/cli/commands/indices/config/import) command.
The command expects a JSON file with `"settings"`, `"rules"`, or `"synonyms"` keys,
which you can get by exporting an index configuration from the dashboard or with the Algolia CLI.
To import settings, rules, or synonyms individually, use these commands:
* [`algolia settings import`](/doc/tools/cli/commands/settings/import)
* [`algolia rules import`](/doc/tools/cli/commands/rules/import)
* [`algolia synonyms import`](/doc/tools/cli/commands/synonyms/import)
Check the [Algolia CLI examples](/doc/tools/cli/examples#import-your-algolia-data) for more information.
### Import index configuration with the API
With the API clients, you can use these methods for importing settings, rules, or synonyms into your index:
* [`setSettings`](/doc/libraries/sdk/v1/methods/set-settings)
* [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule)
* [`saveSynonym`](/doc/libraries/sdk/v1/methods/save-synonym)
# Generate a sitemap from an Algolia index
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index
Learn how to generate sitemaps from your Algolia indices dynamically.
A [sitemap](https://www.sitemaps.org/index.html) is an XML file that describes all pages of your website that are available for crawling by a search engine.
Sitemaps contain the URLs of your pages along with metadata,
such as when a page was last updated,
how often a page is updated,
or how important a page is relative to other pages.
Sitemaps play a significant role in search engine optimization (SEO),
especially when you're using Algolia in your frontend.
Because facets are flexible, Algolia can power navigation in addition to search results pages,
which lets you implement dynamic category pages based on the data in your .
These are great candidates to add to your sitemap.
## Before you begin
This tutorial assumes that you installed [Node.js](https://nodejs.org/en/) in your environment,
and you understand how to create and run Node.js scripts.
You also need an Algolia account.
If you don't have one already,
you can [create an account](https://dashboard.algolia.com/users/sign_up) for free.
### Dataset
For this tutorial, you'll use an ecommerce dataset where each result is a product.
All have a `categories` attribute with one or more categories.
[Download the dataset](https://github.com/algolia/datasets/blob/master/ecommerce/bestbuy_seo.json) and import it into your Algolia .
For more information, see [Import with the dashboard](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard).
### Install dependencies
Install the [`algolia-sitemap`](https://github.com/algolia/algolia-sitemap) as a dependency.
This helper library lets you dynamically generate sitemaps from your Algolia indices.
```sh Command line icon=square-terminal theme={"system"}
npm install algolia-sitemap
```
## Create a sitemap of all the records in your index
First, create a sitemap with all your products to let search engines know where to find them.
You need your Algolia application ID and your [API key](https://dashboard.algolia.com/account/api-keys/).
The API key must have [`browse` permission](/doc/guides/security/api-keys#access-control-list-acl).
```js JavaScript icon=code theme={"system"}
// sitemap.js
const sitemap = require("algolia-sitemap");
// You need an API key with `browse` permission
const algoliaConfig = {
appId: "ALGOLIA_APPLICATION_ID",
apiKey: "ALGOLIA_API_KEY",
indexName: "YourIndexName",
};
sitemap({
algoliaConfig,
// ...
});
```
To the `sitemap` function, you need to add a `hitsToParams` callback function.
This function turns a record from your index into a sitemap entry.
The return value of the `hitsToParams` function must be an object with attributes
that match an `` entry of a sitemap file:
* `loc` (required): the URL of the page for the record
* `lastmod`: the last modified date (ISO 8601)
* `priority`: the priority of this page compared to other pages on your site (between 0 and 1)
* `changefreq`: describes how often the page is likely to change
* `alternates`: alternate versions of this link
* `alternates.languages`: an array of enabled languages for this link
* `alternates.hitToURL`: a function that transforms a language into a URL
To learn more, see [Sitemaps XML format](https://www.sitemaps.org/protocol.html).
In this tutorial, you'll create a simple sitemap, only specifying the `loc` attribute.
```js JavaScript icon=code theme={"system"}
// sitemap.js
// Turn a record into a sitemap entry
function hitToParams({ url }) {
return { loc: url };
}
sitemap({
algoliaConfig,
hitToParams,
// The URL of the sitemaps directory
sitemapLoc: "https://example.com/sitemaps",
// The directory with all sitemaps (default: `sitemaps`)
outputFolder: "sitemaps",
});
```
In your project directory, create a `sitemaps/` directory.
Now, you can run the `sitemap.js` script:
```sh Command line icon=square-terminal theme={"system"}
node sitemap.js
```
The `/sitemaps` directory now has two types of files:
* The `sitemap-index.xml` file with links to each sitemap file
* The `sitemap.0.xml` file with links to your products
You can validate your sitemaps with [external tools](https://www.xml-sitemaps.com/validate-xml-sitemap.html).
To inspect the sitemap, you can use a tool like [`xmllint`](https://xmllint.com/).
```sh Command line icon=square-terminal theme={"system"}
xmllint --format sitemaps/sitemap.0.xml
```
## Create a sitemap for categories
Add your category pages to the sitemap.
If you're using the sample dataset, use the `categories` attribute for the URL:
```json JSON icon=braces theme={"system"}
{
"categories": ["Mobile Phones", "Phones & Tablets"]
}
```
In this example, the product belongs to two categories.
Usually, category pages have URLs like the following: `https://example.com/{CATEGORY_NAME}`.
You need to modify the `hitToParams` callback function to return an array of all categories that belong to a given record.
Since categories apply to many records, you need to make sure to add them to your sitemaps only once.
With ES6, you can use a [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set).
```js JavaScript icon=code theme={"system"}
// sitemap.js
// Turn categories into sitemap entries
const alreadyAdded = {};
function hitToParams({ categories }) {
const newCategories = categories.filter(
(category) =>
category !== null && category !== undefined && !alreadyAdded[category],
);
if (!newCategories.length) return;
const locs = newCategories.map((category) => {
alreadyAdded[category] = category;
return { loc: `https://example.com/${encodeURI(category)}` };
});
return locs;
}
// ...
```
Check each record to see if it contains categories that you didn't yet add to the sitemap, and add them.
This lets you save all your category pages to your sitemap.
## Create a sitemap for categories and all records
You can modify the code for adding category pages to your sitemap to create a combined sitemap for both records and categories.
```js JavaScript icon=code theme={"system"}
// sitemap.js
// ...
function hitToParams({ categories, url }) {
const newCategories = categories.filter(
(category) =>
category !== null && category !== undefined && !alreadyAdded[category],
);
if (!newCategories.length) return;
const locs = [];
newCategories.forEach((category) => {
alreadyAdded[category] = category;
alreadyAdded[url] = url;
locs.push(
...[{ loc: `https://example.com/${encodeURI(category)}` }, { loc: url }],
);
});
return locs;
}
// ...
```
## Notify search engines of sitemap changes
Finally, you can let search engines know that your sitemap has changed.
Most search engines have a `ping` mechanism to inform them of a new sitemap,
so you can perform this directly from your script.
```js JavaScript icon=code theme={"system"}
const endpoints = [
"http://www.google.com/webmasters/sitemaps/ping?sitemap=http://example.com/sitemap.xml",
"http://www.bing.com/webmaster/ping.aspx?siteMap=http://example.com/sitemap.xml",
];
Promise.all(endpoints.map(fetch)).then(() => {
console.log("Done");
});
```
For more information, see:
* [Submit your sitemap to Google](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)
* [Bing sitemaps](https://www.bing.com/webmasters/help/Sitemaps-3b5cf6ed)
# Manage your Algolia applications
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-your-apps
Set up different Algolia applications for your production, testing, or other environments.
Set up different Algolia applications to keep environments and projects separate.
## Manage environments
If you have several environments, such as development, staging, and production, you can choose:
* [Use one index for each environment in the same Algolia application.](#one-index-for-each-environment-in-the-same-algolia-application)
* [Use one Algolia application for each environment.](#separate-algolia-applications-for-each-environment)
You can use different datasets outside production.
For example, index a small part of your production data to cut down on indexing.
### One index for each environment in the same Algolia application
If you're using a single Algolia ,
you can separate your production from your development and testing environments.
Create separate indices to do this.
To distinguish the different environments,
make sure to name your indices consistently,
for example, by using prefixes or suffixes:
`dev_index`, `staging_index`, `prod_index`.
#### Control access to your indices
Indices on the same Algolia application share the same .
To **control access to the index**, create an Algolia user for each environment with different search and admin API keys.
To create a dedicated user per environment, you need the Team Advanced Permissions feature.
Team Advanced Permissions is available as part of the Enterprise add-on to your [pricing plan](https://dashboard.algolia.com/info/plans/).
To create a new team member with access to all indices of a single environment:
1. Sign in to the [Algolia dashboard](https://dashboard.algolia.com/).
2. On the left sidebar, select Settings **Settings**.
3. In the **Team and Access** section, click [**Team**](https://dashboard.algolia.com/account/teams).
4. Click **Add Team Member** and select the application to which you want to grant access.
5. Select the **Index restrictions** option and add the indices to which you want to grant access.
Use `"*"` as a wildcard, for example, `dev_*` or `staging_*`, to grant access to all indices with a common prefix.
You can further refine the permissions in the **Give them permissions** section.
This gives your team members access to individual indices in your application.
You can repeat the same operation for all your environments.
The only things your environments share are the number of and operations, since they belong to the same Algolia application.
### Separate Algolia applications for each environment
If you need full separation between your development, staging, and production environments, create an Algolia application for each.
**Separate Algolia applications each have their own pricing plan.**
The Free plan allows only one application.
To isolate your environments, use a [paid plan](https://www.algolia.com/pricing/) for each extra application.
Create new Algolia applications on the [**Applications**](https://dashboard.algolia.com/account/applications) page in the Algolia dashboard.
A dedicated cluster of servers is available as part of the Enterprise add-on to your [pricing plan](https://dashboard.algolia.com/info/plans/).
## Manage your Algolia applications
Manage your Algolia applications from the dashboard.
For example, to delete an unwanted Algolia application:
1. Go to the [**Applications**](https://dashboard.algolia.com/account/applications) page in the Algolia dashboard.
2. Click **Application actions** and select **Delete**.
If you don't own this application, select **Leave** instead.
3. Confirm your request.
There's no API for managing applications.
Use the dashboard instead.
## See also
* [Choose between one or more indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices)
# Prepare your records for indexing
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data
How to format and structure your attributes and records to improve search results with Algolia.
## Algolia records
An Algolia record is a collection of attributes where each attribute has a name and a value (a key-value pair).
Here is an example record with four [different kinds of attributes](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record) (string, integer, array, and boolean):
```json JSON icon=braces theme={"system"}
{
"title": "Blackberry and blueberry pie",
"description": "A delicious pie recipe that combines blueberries and blackberries.",
"image": "https://yourdomain.com/blackberry-blueberry-pie.jpg",
"likes": 1128,
"sales": 284,
"categories": ["pie", "dessert", "sweet"],
"gluten_free": false
}
```
Your records should only include information that helps with searching,
showing results, sorting, and [relevance](/doc/guides/managing-results/relevance-overview).
Attributes don't have to follow a specific schema (pattern)—they can differ for each record.
## Unique record identifiers
Each record in an index has a unique `objectID`.
Set it yourself so you can match the record to its source data.
If you don't set one, Algolia generates it for you, and you need to find it in the index before you can [update or delete a record](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/incremental-updates).
## Searchable attributes
All attributes are searchable by default.
To make searching more relevant,
[set only some attributes as searchable](/doc/guides/managing-results/must-do/searchable-attributes).
Textual, descriptive attributes, such as summaries, brands, or colors, can be helpful as searchable attributes.
For instance, in a recipe app, to help users search for "blueberry pie",
you need an attribute containing those words, such as a `title` attribute.
## Structuring records
When adding data to your records, be selective.
For example, if you're working with a product line, you don't need to send every piece of information about your products: only what serves the purposes of search.
Include all the necessary information to find products, rank them, and display them on your website or app.
Building records involves:
* Extracting valuable data
* Reworking the data to remove unnecessary elements
* Adding or computing extra information that improves the chances of finding the most relevant results.
### Simplify your records
When making an Algolia index, make your records as simple as possible.
Each record should have enough information for users to find it.
When users search for something, Algolia returns records as results that match their search.
**Each record should contain all the information users need to find it and display its content**.
Don't worry about relational database principles,
such as not repeating data or creating hierarchical structures with primary and foreign keys.
For more information, see:
* [Reduce record size](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/reducing-object-size)
* [Data relationships](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships)
* [Index long pages](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents)
* [Structure ecommerce product records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records)
### Rework your data
Think about what needs to happen if you want to create a movie UI for users to:
* Search and view titles, synopsis, and distribution details
* Display (but not search) images and country of release
* Filter on genre or a range of dates
* Rank based on review scores
* You don't care about technical information, such as how long the movie is.
Assuming your data comes from a relational database, with the information you need in different tables, you need to query the data from these tables. After fetching it, a record for one movie may look like the following:
```json JSON icon=braces theme={"system"}
[
{
"title": "Spirited Away",
"synopsis": "During her family's move to the suburbs, a sullen 10-year-old girl wanders into a world ruled by gods, witches, and spirits, and where humans are changed into beasts.",
"director": "Hayao Miyazaki",
"cast": [
{
"name": "Rumi Hiiragi",
"birth_date": "August 1, 1987",
"birth_place": "Tokyo, Japan"
},
{
"name": "Miyu Irino",
"birth_date": "February 19, 1988",
"birth_place": "Tokyo, Japan"
},
{
"name": "Mari Natsuki",
"birth_date": "May 2, 1952",
"birth_place": "Tokyo, Japan"
}
],
"release_year": 2001,
"country": "Japan",
"genres": ["Animation", "Adventure", "Family", "Fantasy", "Mystery"],
"runtime": 125,
"aspect_ratio": "1.85:1",
"content_rating": "PG",
"review_scores": {
"imdb": 8.6,
"rotten_tomatoes": {
"critics": 97,
"audience": 96
}
},
"images": [
"https://example.com/spirited-away/image1.jpg",
"https://example.com/spirited-away/image2.jpg"
]
}
]
```
The example contains several kinds of content,
some useful for a search experience,
some that aren't, and others that require reformatting.
For example, you don't need to keep `runtime` or `aspect_ratio`.
While they're helpful in other contexts,
they have little value when searching, filtering, ranking, or displaying search results.
While the names of the voice actors in the `cast` attribute are helpful,
you don't need their birth date and place.
Therefore, you can remove them and just keep the names.
**This process [removes noise and saves room in records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/reducing-object-size) for more valuable data**.
```json JSON icon=braces theme={"system"}
{
"cast": ["Rumi Hiiragi", "Miyu Irino", "Mari Natsuki"]
}
```
### Attributes for searching
Attributes for searching are the ones that contain the terms that your users look for.
If you want to search for a movie by title, plot, genre, or cast, you need attributes that contain these terms.
In the preceding example, such attributes are `title`, `synopsis`, `director`, `cast`, and `genres`.
Algolia lets you define in which specific attributes to search, using the [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) parameter.
By default, the engine searches within the entire record, but you want to adjust this: this is better for performance and lets you remove noise.
Don't search attributes that aren't textually relevant or might generate false positives, like `images`, `release_year`, `review_scores`, or `country`.
For example, when searching for "japan", English-speaking users may want to find movies that either have the term in the title or take place in Japan rather than Japanese movies.
You can therefore set `title`, `synopsis`, `director`, `cast`, and `genres` as `searchableAttributes` and leave out the rest for displaying, filtering, and custom ranking.
You can add extra data to make your record more discoverable.
For example, some users may look for a movie by its original title or translation in their language.
Unless the translations are in the record,
searching for these terms wouldn't return any results,
so it's a good idea to retrieve them and add them to your objects.
You can fetch them from your database if you have them or a third-party source such as an API or a website.
```json JSON icon=braces theme={"system"}
{
"display_title": "Spirited Away",
"original_title": "千と千尋の神隠し",
"alternative_title": [
"Le voyage de Chihiro",
"El viaje de Chihiro",
"Chihiros Reise ins Zauberland"
]
}
```
### Attributes for display
Display attributes are those attributes that can be useful to show in the search results.
They can include:
* Titles
* Descriptions
* Attributes used for filtering and custom ranking, such as the number of likes or categories
* Images. To display images in your results, you need an image URL attribute in your records.
This way, Algolia can return them in search results for you to display on the frontend.
Some display attributes, such as `title` and `description`, can also be searchable.
Others, such as `image` or `likes`, shouldn't be set as searchable.
You can't store images directly in Algolia.
Instead, store the image on a content delivery network (CDN) or web server and add the image URL to a field in your records.
When you retrieve a record from Algolia, use this URL to display the image in your app.
### Attributes for filtering
Sometimes, you might want users to find a specific subset (category) of your records.
For example, they may want to find all movies by director Hayao Miyazaki,
find new adventure movies to watch or look for the best motion pictures of the past year.
You can do this by setting some attributes as [filters](/doc/guides/managing-results/refine-results/filtering) to narrow down search results.
The following are examples of attributes that you can filter:
* Boolean attributes (like whether an item is public)
* Lists (categories, tags)
* Numeric attributes (price, rounded rating)
* Normalized text (colors, types, or enumerated types).
For example, you could use `director`, `cast`, `country`, `content_rating`, and `genres` and display them as refinement lists in your search experience, and `release_year` to display a range slider.
Declare them with the [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting) parameter.
Filterable attributes can be anything,
but normalize your data to ensure consistency.
For example, if you have attribute `genres` with the term "Animation" in one record and "Animated picture" in another,
these would result in two different facet values.
For more information, see:
* [Filtering](/doc/guides/managing-results/refine-results/filtering)
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Declare attributes for faceting with the API](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting)
A good rule of thumb is to add attributes based on how users want to fine-tune their search.
If you have a movie reviews website, users may want to refine on review score or popularity.
### Custom ranking attributes
Custom ranking makes Algolia's search results more relevant for your users by including your business metrics.
It's a good idea to start thinking about such metrics when fetching your data and structuring your records.
For example, if a user looks for "james bond", all James Bond movies would match the same.
Without anything else to break the tie, Algolia falls back on the `objectID` in alphanumeric order, which isn't helpful.
A better way to break ties is to compare meaningful information.
For movies, you could use the `review_scores` attribute.
However, in the preceding example, you have several scores, you may want to compute them into a global one and use them in custom ranking.
The computed attribute would look like this:
```json JSON icon=braces theme={"system"}
{
"computed_score": 201.6
}
```
Custom ranking attributes must be either numeric or boolean.
For more information, see:
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking)
* [Create custom ranking attributes](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking)
* [Control precision of custom ranking metrics](/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision)
## Advanced data formatting
Algolia provides a vast collection of settings to help with relevance,
and these work in combination with how you format your content.
For example, whether to use one or several attributes for a single piece of information,
including long or short descriptions (or both),
repeat the same words in the title and description,
and how to use custom ranking attributes.
# Create nested attributes
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/creating-and-using-nested-attributes
Learn about nested attributes, and when to use them.
With nested attributes, you can add subcategories to your attributes.
For example, instead of having a single attribute `price`,
you can set up different prices as subcategories: `price.net`, `price.gross`, `price.margin`.
To refer to nested attributes, separate parent and child attribute with a `.`.
To create nested attributes,
add them as nested JSON objects to your .
For example:
```json JSON icon=braces theme={"system"}
[
{
"country": "CA",
// price is a nested attribute
"price": {
"net": 2.30,
"gross": 2.62
}
},
{
"country": "US",
// price is a nested attribute
"price": {
"net": 1.99,
"gross": 1.75
}
}
]
```
## Example: filtering nested attributes
If you want to filter a search using nested attributes,
use the [`filters`](/doc/api-reference/api-parameters/filters) parameter in your search request
and separate the parent and child attribute by a `.`.
For example, with the JavaScript API client:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Filters = "country:US AND price.gross < 2.0" })
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
filters: "country:US AND price.gross < 2.0",
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetFilters("country:US AND price.gross < 2.0"))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setFilters("country:US AND price.gross < 2.0"),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { filters: 'country:US AND price.gross < 2.0' },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(filters = "country:US AND price.gross < 2.0"),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['filters' => 'country:US AND price.gross < 2.0',
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"filters": "country:US AND price.gross < 2.0",
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(filters: "country:US AND price.gross < 2.0")
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
filters = Some("country:US AND price.gross < 2.0")
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(filters: "country:US AND price.gross < 2.0"))
)
```
This example combines two conditions on one nested object, which works because `price` holds a single object.
When a nested attribute is an array of objects, each condition is evaluated independently and can match a different element.
See [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects).
## Where you can use nested attributes
You can use nested attributes wherever you might use a regular attribute, such as [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes) and
[`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting).
Just make sure you use the appropriate dot notation such as `price.country` to refer to them.
There's no limit on the number of nested attributes apart from [the default restrictions on record size](https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records).
The maximum nesting depth is 256 levels, which is more than enough for most use cases.
For example, you could use something like `price.net.us.ca`.
# Make measures and dimensions searchable
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/dimensional-data
Transform dimension and measure formats to make them searchable.
Users enter dimensions and measures in many different ways.
For example,
users might search for `30 x 50` or `30mm x 50mm`, while your may use a different format like `30x50mm`.
Algolia's features support some formatting variations but may not handle all dimension formats reliably.
This is because:
* **User query formats vary.** A query like `30mm x 50mm` won't necessarily match a record with `30x50mm`.
* **Unit differences cause mismatches.** Queries may include `"`, `inches`, `mm`, `cm`, or `ft`, while records might use only one format.
* **Typo tolerance has limits.** While [typo tolerance](/doc/guides/managing-results/optimize-search-results/typo-tolerance) can match slight variations such as `30by50` or `30 x 50`, but not different units or separators.
* **AI doesn't consistently interpret dimensions.** Although [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started) can identify dimensions, it doesn't do so consistently, due to the ambiguity of the input.
To improve matching for dimension-based searches,
standardize and expand these values using a transformation function before sending your data to Algolia.
## Transform data into dimension-friendly formats
To address this issue,
pre-process your data with a transformation function like the one below.
For each record you pass to it, the `transform` function returns a transformed record or `undefined` if no dimensions are found.
To run this function, create a [Push to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push) connector, using the following transformation code.
```js JavaScript icon=code expandable theme={"system"}
const DIMENSIONS_RE =
/\b\d+(?:[.,]\d+)?\s*(?:(?:mm|cm|m|in(?:ch(?:es)?)?|ft|'|["″])?(?:\s*(?:x|\*|by)\s*\d+(?:[.,]\d+)?\s*(?:mm|cm|m|in(?:ch(?:es)?)?|ft|'|["″])?){1,2}|(?:mm|cm|m|in(?:ch(?:es)?)?|ft|'|["″]))\b/gi;
const PART_RE =
/(\d+(?:[.,]\d+)?)(?:\s*(mm|cm|m|in(?:ch(?:es)?)?|ft|["″'])?)?/gi;
// Convert any raw unit symbol to a standard form
function normalizeUnit(raw, fallback) {
if (!raw) return fallback.toLowerCase();
switch (raw.toLowerCase()) {
case '"':
case "″":
case "in":
case "inch":
case "inches":
return "in";
case "'":
case "ft":
return "ft";
default:
return raw.toLowerCase();
}
}
// Return every spelling for a unit
function unitForms(unit) {
if (unit === "in") return ["in", '"', "inch", "inches"];
if (unit === "ft") return ["ft", "'"];
return [unit];
}
function dimensionKeywords(attr, fallbackUnit) {
if (!attr) return [];
const out = new Set();
const entries = [...attr.matchAll(DIMENSIONS_RE)].map((m) => m[0]); // Extract all matching dimensions
for (const entry of entries) {
// Extract number and unit pairs
const parts = [];
let match;
while ((match = PART_RE.exec(entry)) !== null) {
const number = match[1];
const unit = normalizeUnit(match[2], fallbackUnit);
parts.push({ n: number, u: unit });
}
if (!parts.length) continue;
// Single numbers and number plus unit forms
parts.forEach((p) => {
out.add(p.n);
unitForms(p.u).forEach((f) => out.add(p.n + f));
});
// Combined variants when there's more than one part
if (parts.length > 1) {
const nums = parts.map((p) => p.n); // ["30","50"]
const nu = parts.map((p) => p.n + p.u); // ["30mm","50mm"]
// Join dimensions without spaces (for example, 30mm50mm), including inch variants
const tight = nu.join("");
out.add(tight);
if (tight.includes("in")) {
unitForms("in").forEach((f) => out.add(tight.replace(/in/g, f)));
}
["x", "by"].forEach((sep) => {
// Raw numbers joined
out.add(nums.join(sep));
if (sep === "by") out.add(nums.join(` ${sep} `));
// Numbers and units joined
const joinedTight = nu.join(sep);
const joinedSpc = nu.join(` ${sep} `);
[joinedTight, joinedSpc].forEach((s) => {
out.add(s);
if (s.includes("in"))
unitForms("in").forEach((f) => out.add(s.replace(/in\b/g, f)));
});
});
}
}
return [...out];
}
/**
* Transforms a record by extracting dimension keyword variants.
* @param {SourceRecord} record - A record from your dataset.
* @param {Helper} helper - A helper for accessing secrets and metadata.
* @returns {SourceRecord|undefined} - The transformed record, or undefined if no dimensions are found.
*/
async function transform(record, helper) {
record.dimension_keywords = dimensionKeywords(record.name, "mm");
return record;
}
```
### Customization
You can customize the function to support other measurement units or non-standard formats.
To add new units (for example, `yd`, `mil`, `µm`, `kg`) or handle alternative patterns (for example, `D30`, `H50`, `Ø20mm x 40mm`),
update the following:
* **`DIMENSIONS_RE`.** Extend the regular expression to detect new unit symbols or structural patterns. Consider using AI-assisted tools to build and test regular expressions.
* **`normalizeUnit(raw, fallback)`.** Map any new unit symbol or abbreviation to a standard form. For example, 'yard' and 'yd' become 'yd'.
* **`dimensionKeywords()`.** The function defaults to `mm` if it doesn't find a unit. To change this default (for example, to `cm` or `in`), update the second argument in the `dimensionKeywords()` call.
* **`unitForms(unit)`.** Add alternative spellings and symbols for each unit. For example, `["yd", "yard", "yards"]`.
## How the transformation function works
The function improves search by extracting keyword variants from dimension patterns,
by performing the following steps:
* [Identify dimensions](#identify-dimensions) with a regular expression
* [Extract numbers and units](#extract-numbers-and-units)
* [Generate variants](#generate-variants)
* [Attach keywords](#attach-keywords-to-an-attribute) to a new attribute
## Identify dimensions
The function uses the `DIMENSIONS_RE` regular expression to detect one-part, two-part, or three-part dimensions,
such as:
* One part: `600mm`, `2.4m`
* Two-part: `30x50`, `3"x6"`, `20mm x 30mm`
* Three-part: `245x148x65mm`, `30mm x 50mm x 2m`
It recognizes various separators (`x`, `*`, `by`) and units (including `mm`, `"`, `inches`, and `ft`).
Spaces are optional.
The regular expression handles a wide range of edge cases,
but test it against your data to confirm it captures the formats you use.
### Extract numbers and units
For each match, the function extracts the numbers and their associated units.
The function standardizes unit variants like `"`, `inch`,
and `inches` to `in`.
### Generate variants
Each detected dimension expands into these keyword-friendly formats:
* Bare numbers: `30`, `50`, `2`
* Normalized units: `30mm`, `2m`, `3in`
* Commonly-accepted synonyms: `30in`, `30"`, `30inch`, `30inches`
* Joined forms:
* Without spacing: `30mm50mm`
* With separators: `30x50`, `30 mm by 50 mm`, `30mmx50mm`
For example, from `30mm x 50mm`,
the function generates:
```js JavaScript icon=code theme={"system"}
[
"30",
"50",
"30mm",
"50mm",
"30x50",
"30 x 50",
"30mmx50mm",
"30 mm x 50 mm",
"30 by 50",
"30mm by 50mm",
"30mm50mm",
];
```
### Attach keywords to an attribute
The function adds a new attribute, `dimension_keywords`, to each record it processes.
## Add dimension keywords to your index
To use the generated keywords in Algolia:
* With the `taskID` generated by the Push to Algolia connector, send your data to Algolia with the Ingestion API [`pushTask`](/doc/rest-api/ingestion/push-task) method, making sure each one includes the `dimension_keywords` attribute.
* Configure `dimension_keywords` as a [searchable attribute](/doc/guides/managing-results/must-do/searchable-attributes) in your settings.
## See also
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
* [Searching in hyphenated attributes](/doc/guides/managing-results/optimize-search-results/typo-tolerance/how-to/how-to-search-in-hyphenated-attributes)
# Structure ecommerce product records
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/ecommerce-records
Find the optimal structure for your Algolia records for products
In ecommerce, you often deal with *base products* and their *variants*.
A base product, such as a t-shirt, can have variants with different colors and sizes.
You can store this data in Algolia in three ways:
* **Variant-level records:** one Algolia record for each product variation.
* **Product-level records:** one Algolia record for each base product. The record contains all variants.
* **Variation-group-level records:** one Algolia record for each variation of a specific attribute,
generally the `color`. Each record contains variations sharing this attribute value.
## Record structure examples
The following example shows records for a t-shirt in two colors.
Each color comes in two sizes.
With the **variant-level** records model,
each variant is a separate record:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "v-neck-t-shirt-white-m",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "White",
"size": "M",
"image": "v-neck-t-shirt-white.jpg",
"price": 19.99
},
{
"objectID": "v-neck-t-shirt-white-l",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "White",
"size": "L",
"image": "v-neck-t-shirt-white.jpg",
"price": 19.99
},
{
"objectID": "v-neck-t-shirt-blue-m",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "Blue",
"size": "M",
"image": "v-neck-t-shirt-blue.jpg",
"price": 22.99
},
{
"objectID": "v-neck-t-shirt-blue-l",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "Blue",
"size": "L",
"image": "v-neck-t-shirt-blue.jpg",
"price": 22.99
}
]
```
With the **product-level** records model,
all variants of a base product are in a single record.
The common attributes sit at the top level.
The varying attributes, such as color, size, images, and prices, sit in a `variants` array.
Each array item corresponds to a unique variant:
```json JSON icon=braces theme={"system"}
{
"objectID": "v-neck-t-shirt",
"name": "V-neck t-shirt",
"category": "Sport",
"variants": [
{
"variantID": "v-neck-t-shirt-white-m",
"color": "White",
"size": "M",
"image": "v-neck-t-shirt-white.jpg",
"price": 19.99
},
{
"variantID": "v-neck-t-shirt-white-l",
"color": "White",
"size": "L",
"image": "v-neck-t-shirt-white.jpg",
"price": 19.99
},
{
"variantID": "v-neck-t-shirt-blue-m",
"color": "Blue",
"size": "M",
"image": "v-neck-t-shirt-blue.jpg",
"price": 22.99
},
{
"variantID": "v-neck-t-shirt-blue-l",
"color": "Blue",
"size": "L",
"image": "v-neck-t-shirt-blue.jpg",
"price": 22.99
}
]
}
```
With the **variation-group-level** records model,
variants that share the same value of a given attribute, typically the color, sit in a single record.
For the t-shirt example, there are two records, one for each color.
The common attributes sit at the top level.
The varying attributes, such as size and price, sit in a `variants` array.
Each array item corresponds to a unique variant:
```json JSON icon="braces" theme={"system"}
[
{
"objectID": "v-neck-t-shirt-white",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "White",
"image": "v-neck-t-shirt-white.jpg",
"variants": [
{
"variantID": "v-neck-t-shirt-white-m",
"size": "M",
"price": 19.99
},
{
"variantID": "v-neck-t-shirt-white-l",
"size": "L",
"price": 19.99
}
]
},
{
"objectID": "v-neck-t-shirt-blue",
"name": "V-neck t-shirt",
"category": "Sport",
"color": "Blue",
"image": "v-neck-t-shirt-blue.jpg",
"variants": [
{
"variantID": "v-neck-t-shirt-blue-m",
"size": "M",
"price": 22.99
},
{
"variantID": "v-neck-t-shirt-blue-l",
"size": "L",
"price": 22.99
}
]
}
]
```
## Choose your record model
When choosing between the three indexing models,
consider whether you use Algolia AI or merchandising features.
**Use the variation-group-level or product-level record model if you use Algolia AI or merchandising features.**
These models can work better when your variants look alike:
* Algolia groups [click and conversion events](/doc/guides/sending-events) from each variant
by `objectID`, meaning by base product or by variation-group.
This lets Algolia re-rank or personalize results at the product or variation-group level, not just for specific variants.
* [Merchandising](/doc/guides/managing-results/rules/merchandising-and-promoting)
at the product-level or variation-group-level is simpler when you have many variants.
For example, you can [promote or hide products or variations](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#promote-or-hide-specific-hits)
rather than individual variants.
* [Search analytics](/doc/guides/search-analytics/overview) groups results by product or variation,
which makes them simpler to analyze.
**Use the variant-level record model if you don't use Algolia AI or merchandising.**
This model is more granular and better suited for precise control:
* Results match the most relevant variant directly.
For example, a search for "red shoes" returns only variants with "red" in their data.
In contrast, the product model requires more frontend work to display the correct variant.
* Faceting is more accurate.
A like `color: green AND size: 40` returns only matching variants,
not entire products with partial matches.
In the product model the two conditions can match different variants, so the results can include products where no single variant satisfies both.
See [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects).
* You can update individual variant attributes, such as price or stock.
The product model requires resending the full list of variants, because it doesn't support partial updates.
The variation-group-level model is a good compromise between the two other models.
It offers better AI and merchandising features than the variant-level model, while staying more granular than the product-level model.
For example, using `color` as the grouping attribute means a search for `red shoes` returns only records with "red" in their data.
This lets you show the matching image without extra frontend work.
### Capabilities by record model
| | Variant-level | Product-level | Variation-group-level |
| - | - | - | - |
| AI features | PER VARIANT | PER PRODUCT | PER VARIATION GROUP |
| Rules and merchandising | PER VARIANT | PER PRODUCT | PER VARIATION GROUP |
| Search analytics | PER VARIANT | PER PRODUCT | PER VARIATION GROUP |
| Textual relevance | OPTIMIZED | SUPPORTED | OPTIMIZED FOR THE VARIATION |
| Faceting support | OPTIMIZED | Requires frontend work (see below) | OPTIMIZED FOR THE VARIATION |
| Variant-powered PLP | OPTIMIZED | Requires frontend work (see below) | OPTIMIZED FOR THE VARIATION |
| Granular variant update | SUPPORTED | Not supported | Not supported |
| Average record size | Depending on further choices (see [Average record size](#average-record-size-for-the-variant-level-model)) | OPTIMIZED | OPTIMIZED |
| Number of records | HIGH | OPTIMIZED | Depends on the number of variants of each variation |
#### Average record size for the variant-level model
The average record size for the variant-level model depends on what you want to show on your results,
for example, if you want to include color swatches or images carousels.
To display those, each of your variant records needs to have information about their siblings,
which increases the average record size.
## Results display
### Group records per product
Depending on your desired end-user experience and catalog size,
showing one result per product, instead of listing each variant separately,
can offer a cleaner, friendlier interface.
With the **product-level** record model, you get one result per product by default.
The **variant-level** model requires using the [distinct](/doc/guides/managing-results/refine-results/grouping) feature to group variants under a single result.
To do this, include a shared product ID in each variant record and set the distinct parameter on that attribute:
```jsonc JSON icon=braces theme={"system"}
{
"objectID": "v-neck-t-shirt-white-m"
"name": "V-neck t-shirt",
"baseProductID": "v-neck-t-shirt",
// ...
}
```
The **variation-group-level** model gives you the choice:
* By default, you get one result per variation (for example, one per color).
* Or you can use the distinct feature the same way as for the variant-level model and display one result per product.
### Facets
The **variant-level** record model lets users refine results.
In this model, Algolia returns only the variants that match a selected ,
with no extra frontend logic required.
This also works well for the **variation-group-level** model when it's based on color.
Images are often the same for all variants of a given color, so a record's variants tend to share
the same image.
This means you don't need extra frontend logic here either.
In contrast, the **product-level** model always returns the entire product, including all variants.
To display only the matching variants, you need to post-process the search results in your InstantSearch code:
```js JavaScript icon=code theme={"system"}
hits({
// ...
transformItems(items, { results }) {
return items.map((item) => {
const colorFacets = results._state.disjunctiveFacetsRefinements['variants.color'] || [];
let selectedVariant;
if (colorFacets.length > 0) {
selectedVariant = item.variants.find(variant => {
return colorFacets.includes(variant.color);
});
} else {
selectedVariant = item.variants[0];
}
item.image = selectedVariant.image;
item.price = selectedVariant.price;
item.url = selectedVariant.url;
return item;
});
},
});
```
# Handle data relationships
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships
Model simple and complex data relationships, hierarchies, and nesting.
To index relational data into Algolia,
create a data structure that supports your desired search experience.
Algolia's schemaless nature gives you the flexibility to structure your data in the way that best fits your users' needs.
Consider a marketplace for third-parties to sell second-hand games.
You could build your search experience by displaying:
* Sellers and their full game catalogs
* Games titles and a list of people who sell them
* Classified ads, ranked by business relevance (such as seller's popularity)
Depending on the experience, you need to represent data relationships differently.
Because Algolia is schemaless,
you can tailor your data structure to support the search experience you want to create.
## Data source
When you're fetching from a relational database like as MySQL,
you usually have one entity per table and join tables for many-to-many mappings.
After making a `JOIN` request, serialized data might look like this:
```json JSON icon=braces theme={"system"}
[
{
"id": 1,
"name": "Alice",
"popularity": 1000,
"total_sales": 13,
"country": "France",
"games": [
{
"id": 1,
"title": "Grand Theft Auto V"
},
{
"id": 2,
"title": "The Legend of Zelda: Ocarina of Time"
}
]
},
{
"id": 2,
"name": "Bob",
"popularity": 900,
"total_sales": 42,
"country": "United States",
"games": [
{
"id": 3,
"title": "Mass Effect 3"
}
]
},
{
"id": 3,
"name": "Mass Effect Fan",
"popularity": 800,
"total_sales": 50,
"country": "United States",
"games": [
{
"id": 1,
"title": "Grand Theft Auto V"
}
]
}
]
```
## Display sellers and their catalog
A nested data structure would work well to create a search experience where you would display a list of sellers and their associated games.
From there, you could streamline the data by removing unnecessary attributes, and index it as follows:
```json JSON icon=braces theme={"system"}
[
{
"name": "Alice",
"popularity": 1000,
"country": "France",
"games": [
"Grand Theft Auto V",
"The Legend of Zelda: Ocarina of Time"
]
},
{
"name": "Bob",
"popularity": 900,
"country": "United States",
"games": [
"Mass Effect 3"
]
},
{
"name": "Mass Effect Fan",
"popularity": 800,
"country": "United States",
"games": [
"Grand Theft Auto V"
]
}
]
```
### Searchable attributes
This experience focuses on sellers, which means you might want to search seller data in priority.
For example, when a user searches for "mass effect", you may want seller Mass Effect Fan to show up before sellers who sell Mass Effect 3, even if Mass Effect Fan doesn't sell this game.
To do so, you can create an order of priority in your [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes), by setting `name` first and `games` second.
## Displaying game titles and their sellers
You can use the same structure to create a search experience where you would display a list of games and their associated sellers.
To achieve this, all you would need to do is to reverse the relationship: instead of nesting games per seller, you would nest sellers per game.
```json JSON icon=braces theme={"system"}
[
{
"name": "Grand Theft Auto V",
"sellers": [
{
"name": "Alice",
"popularity": 1000,
"country": "France"
},
{
"name": "Mass Effect Fan",
"popularity": 800,
"country": "United States"
}
]
},
{
"name": "The Legend of Zelda: Ocarina of Time",
"sellers": [
{
"name": "Alice",
"popularity": 1000,
"country": "France"
}
]
},
{
"name": "Mass Effect 3",
"sellers": [
{
"name": "Bob",
"popularity": 900,
"country": "United States"
}
]
}
]
```
### Searchable attributes
This experience focuses on games, meaning you might want to search game data in priority.
For example, when a user searches for "mass effect",
you may want game Mass Effect 3 to show up before seller Mass Effect Fan,
especially if they don't sell that game.
For this, you can create an order of priority in your [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes), by setting `name` first and `sellers.name` second.
## Display classified ads ranked by business relevance
The first two experiences focus on exhaustiveness.
No matter what users are looking for, they should get all possible options
(all sellers per game or all games per seller).
Another approach to building search is to be relevance-oriented:
instead of showing all possibilities, you only show the best ones.
For example, if users search for "grand theft auto",
they may not need to see all classified ads for Grand Theft Auto V.
Instead, you could only show them the ones from the most popular sellers.
In this case, you need to split your data and use a flat structure.
```json JSON icon=braces theme={"system"}
[
{
"game_title": "Grand Theft Auto V",
"seller_name": "Alice",
"seller_popularity": 1000,
"seller_country": "France"
},
{
"game_title": "Grand Theft Auto V",
"seller_name": "Mass Effect Fan",
"seller_popularity": 800,
"seller_country": "United States"
},
{
"game_title": "The Legend of Zelda: Ocarina of Time",
"seller_name": "Alice",
"seller_popularity": 1000,
"seller_country": "France"
},
{
"game_title": "Mass Effect 3",
"seller_name": "Bob",
"seller_popularity": 900,
"seller_country": "United States"
}
]
```
By breaking up games by seller, you can add more granular popularity attributes to each record.
This way, you ensure that the game with the best seller ranks better by using the [`customRanking`](/doc/api-reference/api-parameters/customRanking) attribute.
This structure involves data duplication,
so you can use Algolia's [distinct feature](/doc/guides/managing-results/refine-results/grouping) to deduplicate records on `game_title`.
For more information, see:
* [Create custom ranking attributes](/doc/guides/managing-results/must-do/custom-ranking/how-to/configure-custom-ranking)
* [Index long pages](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents)
* [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects)
### Searchable attributes
This experience focuses on classified ads, so you may want to prioritize searching on game data.
For example, when a user searches for "mass effect", you may want ads for game Mass Effect 3 to show up before ads by user Mass Effect Fan.
For this, you can create an order of priority in your [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes), by setting `game_title` first and `seller_name` second.
# Split long pages into smaller records
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents
Split long pages into smaller records by heading, section, paragraph, or another logical boundary.
Content-heavy documentation pages can contain thousands of words under a single URL.
If you index a full page as one record, a query can match unrelated parts of the page and return a result that's too broad.
To improve relevance, split long pages into smaller records.
For example, create one record for each heading, section, paragraph, or another logical boundary.
Smaller records let Algolia match queries to the most relevant part of a page instead of treating the whole page as one large block of content.
When splitting content into records, preserve the page structure.
Include attributes such as the page title, section title, URL, and content.
These attributes let you distinguish page-level matches from section-level matches and send users directly to the relevant part of the page.
Splitting long pages also helps you stay within Algolia's [record size limits](/doc/guides/scaling/algolia-service-limits#application-record-and-index-limits).
Long documentation pages or knowledge base articles might be too large to fit into single .
If you're using the Algolia Crawler and a record exceeds its size limit,
use the [`helpers.splitContentIntoRecords()`](/doc/tools/crawler/apis/configuration/actions#param-helpers-split-content-into-records) helper to split the page into smaller records.
## Example records for a long page
Each record includes page-level and section-level attributes,
such as `page_id`, `title`, `section`, `url`, and `content`.
Page-level records can omit `section`.
```json JSON icon=braces theme={"system"}
[
{
"page_id": "indexing-long-documents",
"title": "Split long pages into smaller records",
"url": "https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents",
"content": "Content-heavy documentation pages can contain thousands of words under a single URL..."
},
{
"page_id": "indexing-long-documents",
"title": "Split long pages into smaller records",
"section": "Example records for a long page",
"url": "https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents#example-records-for-a-long-page",
"content": "Each record includes page-level and section-level attributes..."
},
{
"page_id": "indexing-long-documents",
"title": "Split long pages into smaller records",
"section": "Deduplicate records in search results",
"url": "https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents#deduplicate-records-in-search-results",
"content": "A single query might return more than one record from the same page..."
}
]
```
## Deduplicate records in search results
A single query might return more than one record from the same page.
To show only the best matching record for each page, set [`attributeForDistinct`](/doc/api-reference/api-parameters/attributeForDistinct) to `page_id` and enable `distinct`.
Algolia groups records with the same `page_id` and returns the best matching record from each group.
For more information, see [Results deduplication with distinct](/doc/guides/managing-results/refine-results/grouping).
# Create parent-child relationships
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/parent-child-relationships
The indexing technique for maintaining parent-child relationships between records when you migrate data to Algolia's JSON format.
If your source data has parent-child relationships between records and you want to maintain those relationships when you migrate to Algolia's JSON format, this guide outlines the recommended indexing process.
Use parent-child relationships when you want to:
* [Filter](/doc/guides/managing-results/refine-results/filtering) on children
* Update parents without affecting children
* Index a tree-like data structure
* Model a [one-to-many](https://wikipedia.org/wiki/One-to-many_\(data_model\)) relationship.
## Model relationships in your JSON file
To model a parent-child relationship in [the records of your Algolia JSON data](/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records):
* **Create one record per child**,
and add a unique reference to their parent as a key-value pair (`parentID` in the example)
* **Don't create records for parents**.
### Dataset
```json JSON icon=braces theme={"system"}
[
{
"parentID":"ABC",
"parentName":"Parent",
"name":"Child One"
},
{
"parentID":"ABC",
"parentName":"Parent",
"name":"Child Two"
}
]
```
## Implementation notes
Since this structure doesn't have parent records, you can only query children.
To retrieve the most relevant child per parent (`parentID` in the preceding example),
use [Algolia's distinct feature](/doc/guides/managing-results/refine-results/grouping).
For more information, see:
* [Index relational data](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships)
* [Prepare your data for indexing](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/prepare-data-in-depth)
* [Filter attributes with rules](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#only-the-filtering-attributes-in-the-condition-matter)
* [Nested attributes](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/creating-and-using-nested-attributes)
# Reduce record size
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/how-to/reducing-object-size
How to reduce the size of your records by removing unused or unnecessary data.
To ensure good performance,
there's a limit on record size of 10 KB to 100 KB—based on [your plan](https://www.algolia.com/pricing) (10 KB maximum for the Free plan).
If you exceed this limit, Algolia returns the error message `Record is too big`.
Instead of upgrading your plan,
make your records smaller before [sending them to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data). Do this by:
* Only sending the necessary information by [removing unused attributes](#remove-unused-attributes)
* [Splitting long documents into smaller parts](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents).
If you're using the Algolia Crawler and the [record size exceeds the limit](/doc/tools/crawler/troubleshooting/indexing-issues#records-exceed-the-maximum-for-your-algolia-plan),
use the [`helpers.splitContentIntoRecords()`](/doc/tools/crawler/apis/configuration/actions#param-helpers-split-content-into-records) helper to split the page into smaller chunks.
## Remove unused attributes
You might not need to index every attribute from your data sources.
For example, if you're developing a website to display tweets on specific topics,
Twitter may send you a lot of data,
but only some will help when searching.
For example, an excerpt of the Twitter API response might look like this:
```json JSON icon=braces theme={"system"}
[
{
"text": "Good morning #LaraconEU ! Swing by the Algolia booth for some vintage video games, awesome stickers, and lightning-fast search 😎",
"truncated": false,
"in_reply_to_user_id": null,
"in_reply_to_status_id": null,
"pictures": true,
"pictures_urls": [
"https://pbs.twimg.com/media/Dl1UftbX4AAF1Fj.jpg",
"https://pbs.twimg.com/media/Dl1Um6OXsAAa1JS.jpg"
],
"source": "Twitter for Web",
"in_reply_to_screen_name": null,
"in_reply_to_status_id_str": null,
"retweeted": true,
"retweet_count": 10,
"like_count": 24,
"place": null,
"geolocated": false
}
]
```
You can reduce this record's size by:
* Only indexing attributes that help build your search experience
* Reusing an attribute for different purposes. For example, `retweeted=false` is the same as `retweet_count=0`
* Adding attributes that rely on others.
For example, if you want to use the `pictures` attribute to display a gallery (in the `pictures_url` array),
only include it when the `pictures_urls` array isn't empty.
### After removing attributes
Choose only the information you need for searching, displaying, and ranking.
Leave everything else out.
For example, after transforming the previous Twitter data, your record might look like this:
```json JSON icon=braces theme={"system"}
[
{
"text": "Good morning #LaraconEU ! Swing by the Algolia booth for some vintage video games, awesome stickers, and lightning-fast search 😎",
"pictures_urls": [
"https://pbs.twimg.com/media/Dl1UftbX4AAF1Fj.jpg",
"https://pbs.twimg.com/media/Dl1Um6OXsAAa1JS.jpg"
],
"retweet_count": 10,
"like_count": 24
}
]
```
Selecting only the necessary data reduces the number of attributes and record size without hurting search quality.
# Data cleaning
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/data-sanitization
Learn about data cleaning and handling the data that Algolia returns.
**Algolia accepts all your data "as is" without changing anything.**
Algolia also returns all data in your unmodified, including HTML tags and their properties.
In other words, Algolia doesn't clean your data.
## How to clean your data
You can clean data:
* **Before you send your for indexing.**
Make sure your data is safe.
This helps protect your app and users from possible security risks like [cross-site scripting (XSS) attacks](https://wikipedia.org/wiki/Cross-site_scripting).
* **When showing results.**
By default, Algolia includes all attributes in the response, even if you don't show them.
To exclude attributes from the response,
set them as [`unretrievableAttributes`](/doc/api-reference/api-parameters/unretrievableAttributes).
* **As users type into search.**
Any HTML or code they may enter in the search box exposes you to an XSS attack because Algolia sends the query back in the response.
The data cleaning process is similar wherever it's applied:
1. **Identify sensitive data.**
Determine which parts of your records could potentially be harmful if misused.
This includes any user-generated content or records that include special characters or scripts.
2. **Determine how to clean potentially harmful data.**
This could involve [preventing code injection](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
by detecting and removing scripts,
and replacing special HTML characters such as `<` and `>` with their [entity name or number](https://www.w3schools.com/html/html_entities.asp).
## How Algolia helps
**Algolia ignores HTML tags during search.**
For example, your records might contain the HTML tag `strong`:
```json JSON icon=braces theme={"system"}
{
"description": "She is amazingly powerful, deeply visionary."
}
```
But, because Algolia ignores HTML tags during search,
searching for the word "strong" wouldn't return this record.
**Algolia removes some characters from the response:**
* [Control characters](https://wikipedia.org/wiki/Control_character) (U+0000 to U+001F)
* [Delete character](https://wikipedia.org/wiki/Delete_character) (U+007F).
For more guidance, see [Security best practices](/doc/guides/security/security-best-practices).
# Prepare your index
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/prepare-data-in-depth
Learn how to prepare your data for Algolia, how to fetch it from your data source, and how to structure your index.
Before sending anything to Algolia,
consider what information you want to make searchable.
For an ecommerce store, it's products.
For a music store, it might be records and artists.
For a real estate company, it might be houses and locations.
Be selective about what goes in the record and only gather the information that helps you build a search experience.
For more information, see these blog articles:
* [12 ways to improve your search index](https://www.algolia.com/blog/engineering/12-ways-to-improve-your-search-index)
* [Building an open source index for all](https://www.algolia.com/blog/engineering/building-an-open-source-index-for-all)
## Fetch your data
Algolia doesn't directly search your data source.
Instead, you send it to Algolia's servers where it's stored in a format that's optimal for fast search.
The data may come from any format: relational databases, CSV, XML, or HTML files, or Excel spreadsheets.
You need to write code to fetch, transform, and [send your data to Algolia](#send-data-to-algolia) as JSON objects.
You might also want to refine the by changing their contents or adding new information.
For example, you have a custom PHP blog with a MySQL database and want to make your blog posts searchable.
You can create a script that fetches the posts from your database,
transforms the data, and rearranges it into records.
Then, use the [PHP API client](/doc/libraries/sdk)
to send the objects to Algolia and keep the data up to date when you add, update, or delete a post.
## Send data to Algolia
Once you have prepared your records,
send them to an on Algolia's servers using one of the API clients,
a no-code connector, Crawler, or dashboard.
The records can then be searched.
For more information, see:
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
## Algolia index
An index is where the data used by Algolia is stored.
It's the equivalent of a table in a database but optimized for search and discovery operations.
Once you've sent your data to Algolia,
you can decide how to structure your indices.
This includes how many indices you need and how to configure each one.
You can put all your records into a single index or spread them across a few smaller indices.
How you structure your indices depends on how you want to search and display your records.
### Structuring indices
An index is a collection of records.
When you perform a search,
you examine an Algolia index's records.
Flattened data is best for searching.
This applies at the index level too.
It's a good approach to create several indices and map them to your tables,
where each index represents a different kind of entity.
For example, you may want to separate movies from actors and create an index for each.
However, this might not serve the purposes of your search.
What if you want your users to search for both movies and actors simultaneously and for them to appear in the same results?
In that case, a single index works better.
The main reason for this is relevance.
Algolia searches one index at a time:
it doesn't perform cross-index searches.
Searching two indices produces two sets of results, each with a dedicated relevance configuration.
Algolia doesn't merge these results,
and trying to do this yourself would break the relevance
(since combining Algolia's results after a search requires understanding and re-implementing Algolia's ranking algorithm).
You would be undoing all the work that Algolia does for you.
It doesn't mean there are no reasons for [multiple indices](#when-to-use-multiple-indices)
but splitting data per entity isn't one of them.
### Handling record hierarchy
Simplifying and restructuring your records doesn't mean losing hierarchy or relationships.
For example, if you want your users to search for movies and see them organized by director,
store this relationship in your index.
You can organize your data in any way you want and keep it straightforward without losing complexity.
The following record represents a director and all the movies they directed (one or several records).
However, it doesn't help users search for individual movies.
While you wouldn't want to repeat data in a traditional database,
this is perfectly okay in your Algolia index.
```json JSON icon=braces theme={"system"}
{
"director": "Hayao Miyazaki",
"movies": [
{
"title": "Spirited Away",
"score": 201.6
},
{
"title": "My Neighbor Totoro",
"score": 196
},
{
"title": "Princess Mononoke",
"score": 195.4
}
]
}
```
Now take a look at that same information in three flatter, less hierarchical records:
```json JSON icon=braces theme={"system"}
[
{
"title": "Spirited Away",
"score": 201.6,
"director": "Hayao Miyazaki"
},
{
"title": "My Neighbor Totoro",
"score": 196,
"director": "Hayao Miyazaki"
},
{
"title": "Princess Mononoke",
"score": 195.4,
"director": "Hayao Miyazaki"
}
]
```
This format lets users find movies more easily.
Additionally, if you wanted to show a limited number of movies per director,
like the most popular ones,
you could use Algolia's [`distinct`](/doc/api-reference/api-parameters/distinct) feature on `director` along with custom ranking on `score`.
This way, searching for "miyazaki" would only return Hayao Miyazaki's most popular movie.
Flattening data adds more records to your index.
If you have 10,000 directors with an average of ten movies each,
this results in an index with 100,000 records.
It may sound like a lot, but it's not.
Besides what your [plan](https://www.algolia.com/pricing/) allows,
Algolia doesn't limit the number of records,
only [disk size](https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records).
For more information, see:
* [Results deduplication with distinct](/doc/guides/managing-results/refine-results/grouping)
* [Handle data relationships](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/handling-data-relationships)
### When to use multiple indices
For your UI, you need to determine [whether to use one or more indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices).
For example, if you want your search experience to display movies and actors separately,
it's better to use different indices.
Use separate indices when:
* Showcasing popular queries with Algolia's [Query Suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) feature. You need two indices: one for your content and one for common queries.
* You want to let users switch between rankings, such as ascending or descending popularity. While you can't dynamically change the ranking of an index, you can use [replica indices](/doc/guides/managing-results/refine-results/sorting/how-to/creating-replicas) with the same data and different ranking strategies.
# Algolia records
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record
Learn about Algolia records: attributes, types, limits, formatting, nesting, and more.
Algolia uses [JSON](https://www.json.org/) to model records.
Your records can have attributes with the following types:
| Type | Example |
| - | - |
| String | `"title": "Breaking Bad"` |
| Integer | `"like_count": 978` |
| Float | `"avg_rating": 1.23456` |
| Boolean | `"featured": true` |
| Object | `"lead_role": { "name": "Walter White", "portrayed_by": "Bryan Cranston" }` |
| Array | `"episodes": ["Crazy Handful of Nothin'", "Gray Matter"]` |
For example,
this record includes different data types and the `_tags` reserved attribute:
```json JSON icon=braces theme={"system"}
[
{
"objectID": 42,
"title": "Breaking Bad",
"episodes": ["Crazy Handful of Nothin'", "Gray Matter"],
"like_count": 978,
"avg_rating": 1.23456,
"air_date": 1356846157,
"featured": true,
"lead_role": {
"name": "Walter White",
"portrayed_by": "Bryan Cranston"
},
"_tags": ["tv series", "drugs"]
}
]
```
Although integer and boolean values are treated as strings by
[searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes),
they're interpreted as numbers for [numerical facets](/doc/api-reference/api-parameters/numericFilters).
For example,
Algolia treats `like_count` as a number.
## Unique record identifiers
The `objectID` attribute is a unique identifier for each record.
**Set object IDs yourself**, based on *your* data.
If you don't set object IDs, Algolia assigns them automatically:
you can check their values by browsing the .
When you retrieve records,
object IDs are in string format,
even if you set them as integers.
If you want to use integers in your app,
convert object IDs to integers after retrieving the records but
make sure that *all* your object IDs contain integer values.
Since `objectID` uniquely identifies your objects, the following applies:
* **You can search for it** by declaring it as a [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes).
* **You can't highlight or snippet it**.
If you declare `objectID` in [`attributesToHighlight`](/doc/api-reference/api-parameters/attributesToHighlight) or [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet), Algolia ignores it.
* **You can't exclude it from results**.
If you declare `objectID` in [`unretrievableAttributes`](/doc/api-reference/api-parameters/unretrievableAttributes) or omit it from [`attributesToRetrieve`](/doc/api-reference/api-parameters/attributesToRetrieve), Algolia still returns it.
For this reason, object IDs mustn't contain any sensitive data.
* **You can use it as a facet filter, but you can't facet it**.
If you declare `objectID` in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting),
Algolia ignores it. [Faceting on a unique identifier](/doc/guides/managing-results/refine-results/faceting#faceting-on-object-id) makes little sense since every facet count would equal one.
### Keep object IDs stable
Derive each `objectID` from the unique identifier of the record's source, such as a database primary key or a product SKU.
Don't derive it from the record's content, for example by hashing the attributes you send.
An object ID must identify the same item for as long as that item exists:
* **Updates and deletions target an object ID.** If the ID changes when the content changes, an update creates a second record instead of replacing the first, and the original stays in the index until you delete it.
* **[Click and conversion events](/doc/guides/sending-events) reference an object ID.** If the ID changes, the record's event history splits across two IDs, and features that build on that history, such as Dynamic Re-Ranking, Recommend, and Personalization, lose the earlier signals.
When one source item becomes several Algolia records, for example when you split a long document into sections or flatten a product's variants, build each object ID from the source identifier plus a deterministic suffix:
```json JSON icon=braces theme={"system"}
[
{ "objectID": "sku-4815-section-1", "sku": "4815", "section": 1 },
{ "objectID": "sku-4815-section-2", "sku": "4815", "section": 2 }
]
```
Use a suffix that stays the same across reindexing, such as a variant identifier or a section anchor.
Don't use a value that shifts when the source changes, such as an array position or an incrementing counter.
### Acceptable characters for object IDs
`objectID` strings can:
* Contain any character.
* Be of any length as long as the record fits within your [plan's size limit](https://support.algolia.com/hc/en-us/articles/4406981897617-Is-there-a-size-limit-for-my-index-records-).
## Dates
Date attributes must be formatted as [Unix timestamps](https://www.unixtimestamp.com) (for example, `1435735848`).
To convert dates to Unix timestamps,
use an appropriate function for your programming language,
for example:
* Android: [`Date()`](https://developer.android.com/reference/java/util/Date#Date\(\))
* Go: [`time.Unix`](https://pkg.go.dev/time#Unix)
* Java and Scala (from the `java.time` package): [`getTime()`](https://docs.oracle.com/javase/7/docs/api/java/util/Date.html#getTime%28%29)
* JavaScript: [`getTime()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime)
* Kotlin: [`kotlinx-datetime`](https://github.com/Kotlin/kotlinx-datetime#converting-instant-to-and-from-unix-time)
* C#/.Net: [`ToUnixTimeSeconds`](https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tounixtimeseconds)
* PHP: [`strtotime`](https://www.php.net/manual/en/function.strtotime.php)
* Python: [`datetime`](https://docs.python.org/3/library/datetime.html?highlight=datetime#datetime-objects)
* Ruby: [`date.to_time`](https://ruby-doc.org/stdlib-3.1.0/libdoc/date/rdoc/Date.html#method-i-to_time)
* Swift: [`NSDate(withTimeIntervalSince1970:)`](https://developer.apple.com/documentation/foundation/nstimeintervalsince1970)
Depending on the language,
the output might require conversion from milliseconds to seconds.
## Reserved attribute names
The following attribute names are reserved by Algolia.
* `_geoloc`
* `_highlightResult`
* `_rankingInfo`
* `_snippetResult`
* `_tags`
* `distinctSeqId`
The `_collections` attribute is reserved only if you're using InstantSearch to implement a [collections page](/doc/guides/solutions/ecommerce/browse/tutorials/collections#build-a-collections-page-with-instantsearch).
If you're not using InstantSearch, `_collections` behaves like any other attribute.
### In your records
In a record, you can use the `_tags` or `_geoloc` attribute names but they have an [imposed schema](/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-attributes#filter-by-tags).
Other attribute names are schema-agnostic.
Algolia doesn't search reserved attribute names unless you explicitly include them in [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes).
### In the search response
Algolia returns `_highlightResult`, `_snippetResult`, `_rankingInfo`, and `_distinctSeqID` in the search response.
They're reserved Algolia attributes tied to specific features.
To avoid conflicts, don't specify these attribute names in your records.
# Send and update your data
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data
Choose how to send data to Algolia based on where your data is stored and how you want to send updates.
Algolia doesn't query your database.
It searches a copy of your data, stored as in an .
After you've [structured your data](/doc/guides/sending-and-managing-data/prepare-your-data) into records,
choose how to send them to Algolia.
## Choose how to send your data
Start with two questions.
Where is your data stored?
How to send it to Algolia?
### Index from a platform
If your data is in one of these platforms,
use an [integration](/doc/integration):
Shopify, Adobe Commerce, BigCommerce, commercetools, Salesforce B2C Commerce, or Zendesk.
Each integration copies your catalog to an index and sends the changes.
Shopify and BigCommerce also provide a search UI and can send click and conversion events.
### Let Algolia get your data
If Algolia can get your data directly,
use a [connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors).
Examples are a hosted JSON or CSV file, MySQL, Elasticsearch, BigQuery, Supabase, or an Algolia index.
Algolia keeps the schedule, the retries, and the transformations.
Configure them in the Algolia dashboard without deploying code.
### Index a website
If your content is only web pages, and you don't have an API or database export,
use the [Crawler](/doc/tools/crawler/getting-started/overview).
The Crawler extracts content from your pages and turns it into records.
### Send records from your app
If your app controls which records to send and when, use code.
If your records need an Algolia-managed [transformation](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code),
use the [Push to Algolia connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push).
If your records don't need a transformation,
use an [API client](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api)
or a [web framework integration](/doc/framework-integration).
### Test with a local file
To test Algolia with sample data,
[upload a file in the Algolia dashboard](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard)
or use the [Algolia CLI](/doc/tools/cli/get-started).
For a source that changes, use one of the other methods.
## Update your data
After the first import, send the changes from your source to your index.
* [Incremental updates](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/incremental-updates)
* [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies)
* [Index operations are asynchronous](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/index-operations-are-asynchronous)
* [Handle concurrency with versioning](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/handling-concurrency-with-versioning)
# Connectors
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors
Index your data without writing code.
Connectors copy data from a source, such as a MySQL database or a hosted JSON file,
into an Algolia index.
You configure them in the [Algolia dashboard](https://dashboard.algolia.com/connectors)
or with the [Ingestion API](/doc/rest-api/ingestion).
## Why use connectors
A connector replaces the ingestion pipeline you would otherwise write and operate yourself:
* **Algolia hosts the ingestion service.** The schedule, the retries, and the run history all run in Algolia.
* **Transformations run in Algolia.** Use no-code transformations or JavaScript to change your records. Preview the result in the Algolia dashboard. No separate service between your source and your index.
* **Credentials stay in Algolia.** Store the credentials for a source once, then reuse them across tasks.
* **Failures are visible without extra tooling.** The [**Connector Debugger**](https://dashboard.algolia.com/connectors/debugger) shows the status of each run, and Algolia [emails you about repeated failures](#task-status-and-alerts).
* **Anyone on your team can make changes.** The schedule, the indexing strategy, and the transformations are dashboard fields, so a colleague can adjust them without a deployment.
## Available connectors
* [**Algolia Index**](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/post-indexing-transformations): reindex records that are already in an Algolia index.
* [**BigCommerce**](/doc/integration/bigcommerce/get-started/installation): index a BigCommerce store.
* [**commercetools**](/doc/integration/commercetools/get-started/installation): index a commercetools product catalog.
* [**CSV**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/csv): index a hosted CSV file.
* [**Elasticsearch**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/elasticsearch): index an Elasticsearch cluster.
* [**Google BigQuery**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/bigquery): index a BigQuery table.
* [**JSON**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json): index a hosted JSON file.
* [**MySQL**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/mysql): index a MySQL database.
* [**Push to Algolia**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push): send records from your app through an Algolia-managed transformation.
* [**Supabase**](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/supabase): index Supabase tables or views.
* [**Zendesk Knowledge Base**](/doc/integration/zendesk/indexing): index Zendesk help center articles.
## How connectors work
Connectors have these parts:
* **Sources** define where your data comes from, such as a JSON file on your server, or a MySQL database.
* **Destinations** define where your data goes, such as an Algolia index or the Insights API.
* **Tasks** define when and how a sync runs, for example, on a regular schedule.
* **[Transformations](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)** change your source data before it reaches the destination.
They're optional, and you add them to a destination.
## Authentication
Connectors authenticate with your sources and destinations.
Each source supports different authentication methods,
such as basic authentication or a Google service account.
Algolia indices require an API key with the following [ACL](/doc/guides/security/api-keys#access-control-list-acl):
* `addObject`
* `deleteIndex`
* `editSettings`
Manage your connector credentials in the [**Authentications**](https://dashboard.algolia.com/connectors/authentications) tab of the Algolia dashboard.
## Allow Algolia IP addresses
If you restrict access to your **source**, allow these Algolia IP addresses:
* `104.196.103.173`
* `35.234.69.129`
## Task status and alerts
If a task fails more than five times in a row,
Algolia deactivates it and sets its status to `critical` in the **Connector Debugger**.
Algolia also sends an email to every address associated with your Algolia application.
## See also
* [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation and fetch limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
# Google BigQuery connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/bigquery
Use the Google BigQuery connector to index data from a BigQuery table.
Use the Google BigQuery connector to index from a BigQuery table without writing an indexing script.
In this quickstart, you import a sample product dataset into BigQuery,
create a source for the BigQuery table,
use an Algolia destination,
and create an on-demand synchronization task.
## Before you begin
Make sure you have:
* **An Algolia account**.
[Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **A Google Cloud account and project**.
Select or create the project where you want to store the sample BigQuery table.
* **The BigQuery API enabled for your Google Cloud project**.
If the BigQuery API isn't enabled,
enable it from the [BigQuery API page](https://console.cloud.google.com/apis/library/bigquery.googleapis.com).
* **Permission to create and load BigQuery datasets and tables,
create a Google service account and key, and grant IAM roles**.
If your organization restricts service account keys or IAM changes,
ask your Google Cloud administrator to complete those steps.
## Set up the Google BigQuery connector
Create a source, destination, transformation, and synchronization task for the sample BigQuery table.
The source identifies the BigQuery table to read.
The destination identifies the Algolia index.
The transformation enriches the records.
The task determines when the connector runs.
Open [Cloud Hub](https://console.cloud.google.com/cloud-hub/home) and
select the Google Cloud project where you want to create the BigQuery dataset.
Make a note of the project's **ID**.
You use this value when configuring the [connector source](#source).
Select the project itself, not an App Hub app or an app-enabled folder.
You don't need to configure an App Hub app for this quickstart.
In the Google Cloud console's top toolbar,
select the **Activate Cloud Shell** (`>_`) icon.
Set the variables:
```sh Cloud Shell icon=square-terminal theme={"system"}
PROJECT_ID="$GOOGLE_CLOUD_PROJECT"
DATASET_ID="algolia_connector_demo"
TABLE_ID="apparel"
LOCATION="US"
SERVICE_ACCOUNT_ID="algolia-bigquery-reader"
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_ID}@${PROJECT_ID}.iam.gserviceaccount.com"
```
Paste these commands exactly as shown.
Cloud Shell sets `GOOGLE_CLOUD_PROJECT` to the project you selected in Cloud Hub.
Change `LOCATION` to use another [BigQuery location](https://cloud.google.com/bigquery/docs/locations).
Create a dataset for the sample table:
```sh Cloud Shell icon=square-terminal theme={"system"}
bq --location="$LOCATION" mk \
--dataset \
"$PROJECT_ID:$DATASET_ID"
```
Download the newline-delimited JSON sample data:
```sh Cloud Shell icon=square-terminal theme={"system"}
curl --fail --silent --show-error --location \
--output apparel.ndjson \
'https://raw.githubusercontent.com/algolia/quickstarts/main/sample-data/apparel.ndjson'
```
The `--replace` option overwrites any existing table named `apparel`
in the selected dataset.
Change `TABLE_ID` if you need to preserve an existing table.
```sh Cloud Shell icon=square-terminal theme={"system"}
bq --location="$LOCATION" load \
--replace \
--autodetect \
--source_format=NEWLINE_DELIMITED_JSON \
"$PROJECT_ID:$DATASET_ID.$TABLE_ID" \
apparel.ndjson
```
For more information,
see [Loading JSON data from a local file](https://cloud.google.com/bigquery/docs/batch-loading-data#loading_json_data_from_a_local_file).
Count the imported rows and unique object IDs:
```sh Cloud Shell icon=square-terminal theme={"system"}
bq --location="$LOCATION" query \
--use_legacy_sql=false \
"SELECT
COUNT(*) AS records,
COUNT(DISTINCT objectID) AS unique_object_ids
FROM \`${PROJECT_ID}.${DATASET_ID}.${TABLE_ID}\`"
```
Confirm that `records` and `unique_object_ids` have the same value.
Create a dedicated service account for the Algolia connector:
```sh Cloud Shell icon=square-terminal theme={"system"}
gcloud iam service-accounts create "$SERVICE_ACCOUNT_ID" \
--display-name="Algolia BigQuery reader" \
--project="$PROJECT_ID"
```
Grant the service account permission to run BigQuery jobs in the project:
```sh Cloud Shell icon=square-terminal theme={"system"}
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
--role="roles/bigquery.jobUser"
```
Grant the service account read access to the quickstart dataset:
```sh Cloud Shell icon=square-terminal theme={"system"}
bq --location="$LOCATION" query \
--use_legacy_sql=false \
"GRANT \`roles/bigquery.dataViewer\`
ON SCHEMA \`${PROJECT_ID}.${DATASET_ID}\`
TO \"serviceAccount:${SERVICE_ACCOUNT_EMAIL}\""
```
Create a JSON key:
```sh Cloud Shell icon=square-terminal theme={"system"}
gcloud iam service-accounts keys create \
algolia-bigquery-key.json \
--iam-account="$SERVICE_ACCOUNT_EMAIL" \
--project="$PROJECT_ID"
```
Download the key from Cloud Shell:
```sh Cloud Shell icon=square-terminal theme={"system"}
cloudshell download algolia-bigquery-key.json
```
Your browser downloads `algolia-bigquery-key.json`.
You upload this file when you configure the connector source.
If your organization prevents service account key creation,
ask your Google Cloud administrator whether they can create an approved key for this dedicated service account.
Go to the Algolia dashboard and select your Algolia application.
Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Find **Google BigQuery**, then select **Connect**.
Under **Google Service Account**,
select **Search or create an authentication**,
then **Create a new Google Service Account authentication**.
Upload `algolia-bigquery-key.json` as the **Service Account File**.
In **Name**, enter `BigQuery quickstart service account`,
then select **Create authentication**.
Under **Select your BigQuery table**,
enter or select the following BigQuery details:
| Field | Value |
| - | - |
| **Project ID** | Your project's `ID` value in Cloud Hub |
| **Dataset ID** | `algolia_connector_demo` |
| **Table name** | `apparel` |
| **Unique property identifier** | `objectID` |
Leave the custom SQL statement empty.
Without a custom SQL statement, the connector reads all rows and columns from the selected table.
In **Connector name**, enter `Quickstart products BigQuery source`,
then select **Create source**.
After you have created the source,
delete the key file from Cloud Shell:
```sh Cloud Shell icon=square-terminal theme={"system"}
rm -f algolia-bigquery-key.json
```
Delete the downloaded copy from your computer after uploading it to Algolia.
Deleting these files doesn't revoke the key used by the connector.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Under **Configure your destination**, select **Create a new destination**.
Under **Search**, enter `quickstart-products` as the index name.
If you enter the name of an existing index,
the connector overwrites the records and settings in that index.
Under **Index credentials**, select **Create one for me**.
To use an existing API key,
choose one with the `addObject`, `deleteIndex`, and `editSettings`
[ACLs](/doc/guides/security/api-keys#access-control-list-acl).
In **Name**, enter `BigQuery quickstart products destination`,
then select **Create destination**.
Under **Configure your task**, select **On demand**.
Select **Full reindexing**.
Select **Create task**, then **Run**.
Wait for the task to finish.
Open the
[`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
The index contains the imported product records,
with attributes such as `title`, `description`, `product_type`,
`price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own BigQuery data
Authenticate the connector with a dedicated Google service account.
Don't reuse a service account that can update or delete your production BigQuery data.
### Authentication
Grant the service account these roles:
| Resource | Role |
| - | - |
| Google Cloud project used to run queries | BigQuery Job User (`roles/bigquery.jobUser`) |
| BigQuery dataset you want to index | BigQuery Data Viewer (`roles/bigquery.dataViewer`) |
If your organization uses a custom IAM role instead,
include these permissions:
```txt BigQuery permissions theme={"system"}
bigquery.datasets.get
bigquery.datasets.getIamPolicy
bigquery.jobs.create
bigquery.models.export
bigquery.models.getData
bigquery.models.getMetadata
bigquery.models.list
bigquery.routines.get
bigquery.routines.list
bigquery.tables.createSnapshot
bigquery.tables.export
bigquery.tables.get
bigquery.tables.getData
bigquery.tables.getIamPolicy
bigquery.tables.list
resourcemanager.projects.get
```
### Cloud Storage data
If the connector service account reads the table's Cloud Storage objects directly,
grant it the Storage Object Viewer role (`roles/storage.objectViewer`) on each source bucket.
If your organization uses a custom IAM role instead, include these permissions:
```txt Cloud Storage permissions theme={"system"}
storage.folders.get
storage.objects.get
storage.objects.list
```
### JSON key
Create a Google service account JSON key and upload the key file when you [configure the BigQuery source](#source).
A service account JSON key is a long-lived credential.
Store it securely, don't add it to source control,
and delete local copies after uploading it to Algolia.
### Customize the data you index with SQL
By default, the connector imports all rows and columns from the selected table.
In the BigQuery source's **Custom SQL statement** field,
enter a GoogleSQL statement to filter rows, combine columns, or calculate attributes.
Use `%s` as the placeholder for the selected table.
For example:
```sql SQL icon="database" theme={"system"}
SELECT *
FROM %s
WHERE status = 'Available'
```
The query result must include the column selected as the source's unique identifier.
Its value must be present, unique, and stable for every returned row.
An existing `objectID` column is suitable.
If your table uses a composite key,
return its components as a single column.
For example:
```sql SQL icon="database" theme={"system"}
SELECT
TO_JSON_STRING(
STRUCT(
account_id AS account_id,
product_id AS product_id
)
) AS objectID,
title,
description,
price
FROM %s
```
You can instead assign `objectID` in an Algolia transformation if the BigQuery source provides a stable unique identifier.
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
To stay within your BigQuery quota,
don't schedule a Google BigQuery connector task to run more than once per day.
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Connector overview](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors)
* [JSON connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json)
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# CSV connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/csv
Use the CSV connector to index data from a hosted CSV file.
Use the CSV connector to index from a hosted CSV file without writing an indexing script.
In this quickstart, you create a source for a public product dataset, use an Algolia destination,
and an on-demand synchronization task.
## Before you begin
If you don't have an Algolia account, [create one](https://www.algolia.com/users/sign_up) for free.
## Index a sample dataset with the CSV connector
Create a source, destination, transformation, and synchronization task for the sample product dataset.
* The source tells the connector where to fetch the CSV file.
* The destination tells the connector which Algolia index to write to.
* The transformation changes records before the connector indexes them.
* The task controls when the connector runs.
Go to the Algolia dashboard and select your Algolia application.
Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Find **CSV**, then select **Connect**.
Under **Configure your data source**, select **Create a new source**.
Under **Is your data source protected?**, select **None**.
In **URL**, enter:
```txt theme={"system"}
https://raw.githubusercontent.com/algolia/quickstarts/main/sample-data/apparel.csv
```
For **HTTP Method**, select **GET**.
In **Unique column identifier**, enter `objectID`.
Under **Column type mapping**, add these columns and select the corresponding type:
| Column name | Type |
| - | - |
| `price` | **integer** |
| `taxable` | **boolean** |
| `units_sold` | **integer** |
| `weight` | **float** |
In **Connector name**, enter `Quickstart products CSV source`, then select **Create source**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Under **Configure your destination**, select **Create a new destination**.
Under **Search**, enter `quickstart-products` as the index name.
If you enter the name of an existing index, the connector overwrites the records and settings in that index.
Under **Index credentials**, select **Create one for me**.
To use an existing API key, choose one with the `addObject`, `deleteIndex`, and `editSettings` [ACLs](/doc/guides/security/api-keys#access-control-list-acl).
In **Name**, enter `CSV quickstart products destination`, then select **Create destination**.
Under **Configure your task**, select **On demand** and **Full reindexing**.
Select **Create task**, then **Run** and wait for the task to finish.
When the task completes, open the [`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
The index contains product records with attributes such as `title`, `description`, `product_type`, `price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own CSV data
To index your own records, update the connector source URL to point to a CSV file you host.
After you update the source, [run the connector task](https://dashboard.algolia.com/connectors/tasks) again to index your records.
### Source file format
The first row of your CSV file must contain the column names.
Each remaining row represents one record.
For example:
```csv CSV icon=braces theme={"system"}
id,title,description,product_type,price
product-1,Product name,"A short, searchable description",Product type,49.99
```
Each record needs a unique `objectID` attribute.
In the source configuration, use **Unique property identifier** to set this attribute.
Choose a column whose values are unique for every row (`id` in this example).
Enclose fields that contain commas, double quotation marks, or line breaks in double quotation marks.
For more information, see [Common Format and MIME Type for Comma-Separated Values (CSV) Files
](https://www.rfc-editor.org/rfc/rfc4180.html#section-2).
### Source file location
Update the connector source URL to point to your hosted file.
The source URL can use `http`, `https`, `ftp`, `ftps`, or `sftp`.
For `http` or `https` sources, choose the request method your server expects:
* **GET** if your server returns the file from a standard request.
* **POST** if your server requires a POST request.
### Source authentication
Select the authentication method for your file:
* **None** if the file is public.
* **Basic Auth** if the file requires a username and password.
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# Elasticsearch connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/elasticsearch
Use the Elasticsearch connector to sync data from an Elasticsearch index to an Algolia index.
Use the Elasticsearch connector to index from Elasticsearch 7 or later without writing an indexing script.
In this quickstart, you connect an existing Elasticsearch index,
use an Algolia destination,
transform the records,
and create an on-demand synchronization task.
## Before you begin
Make sure you have:
* **An Algolia account**.
[Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **An Elasticsearch 7 or later deployment containing the sample data in an index named `apparel`**.
Download the [Algolia apparel sample data](https://raw.githubusercontent.com/algolia/quickstarts/main/sample-data/apparel.ndjson) and add it to the index:
* For Kibana, follow Elastic's [file upload instructions](https://www.elastic.co/docs/manage-data/ingest/upload-data-files).
* For other ingestion methods, see [Ingest: Bring your data to Elastic](https://www.elastic.co/docs/manage-data/ingest).
## Set up the Elasticsearch connector
Create a source, destination, transformation, and synchronization task for the sample Elasticsearch index.
The source identifies the Elasticsearch deployment.
The destination identifies the Algolia index.
The transformation enriches the records.
The task identifies the source Elasticsearch index and determines when the connector runs.
Go to the Algolia dashboard and select your Algolia application.
Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Find **Elasticsearch**, then select **Connect**.
Under **Configure your data source**,
select **Create a new source**.
Enter the following details:
| Field | Value |
| - | - |
| **Server Endpoint** | The Elasticsearch endpoint URL |
| **Authentication method** | **Api Key/Secret** |
| **API Key ID** | The ID for an Elasticsearch API key that can access the `apparel` index |
| **API Key Secret** | The secret for that API key |
In **Connector name**,
enter `Quickstart products Elasticsearch source`,
then select **Create source**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Under **Configure your destination**,
select **Create a new destination**.
Under **Search**,
enter `quickstart-products` as the index name.
If you enter the name of an existing index,
the connector overwrites the records and settings in that index.
Under **Index credentials**, select **Create one for me**.
To use an existing API key,
choose one with the `addObject`, `deleteIndex`, and `editSettings`
[ACL permissions](/doc/guides/security/api-keys#access-control-list-acl).
In **Name**,
enter `Elasticsearch quickstart products destination`,
then select **Create destination**.
Under **Configure your task**, select **On demand**.
Select **Full reindexing**.
Under **Which table do you want to sync?**,
select `apparel` as the source Elasticsearch index and
select the `objectID` field as the **ObjectID** unique identifier.
Select **Create task**, then **Run** and wait for the task to finish.
Open the
[`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
The index contains the imported product records,
with attributes such as `title`, `description`, `product_type`,
`price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own Elasticsearch data
To index your own data,
configure the connector with your Elasticsearch server,
choose an authentication method and destination,
and create a synchronization task for the Elasticsearch indices you want to import.
### Configure the connector for your data
Use a populated Elasticsearch index that the Algolia connector can reach.
If you restrict inbound access to your Elasticsearch server,
allow the [Algolia connector IP addresses](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors#allow-algolia-ip-addresses).
Follow the quickstart steps to create the [source](#source), [destination](#destination), and [task](#task).
Replace the sample endpoint, credentials, source index, and destination index with your own values.
Review or remove the sample transformation so that it matches your record structure.
When you configure the task, select the field to use as the **ObjectID**.
The field must be present in every source document, and its value must be unique and stable.
### Elasticsearch authentication
To create an API key and secret,
use [Kibana](https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys) or
the [Elasticsearch API](https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-security-create-api-key).
Elasticsearch API keys include an ID and secret.
The Algolia connector requires these values separately.
If Elasticsearch gives you only an encoded credential,
decode it locally to recover the ID and secret.
For more information,
see [Elasticsearch API keys](https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys).
When you [configure the Elasticsearch source](#source),
choose one of these authentication methods:
* **None**: the connector reads your data without authentication.
* **Api key/Secret**: enter the decoded API key ID and API key secret.
* **Username/Password**: enter the username and password the connector should use.
For **Api key/Secret** or **Username/Password** authentication,
grant read-only access to the Elasticsearch indices you want to import.
Use the following privileges:
* **Cluster**: `monitor`
* **Indices**: `read`, `view_index_metadata`, and `monitor`
Scope the index privileges to the relevant index names or patterns.
For **Username/Password** authentication,
choose a user whose roles grant the same access.
The connector reads from Elasticsearch and writes to the Algolia destination.
It doesn't need permission to create Elasticsearch indices or documents.
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
Schedule full reindexing tasks only as often as necessary.
Each run reads all documents from the selected Elasticsearch indices
and rewrites the destination,
which increases the workload on Elasticsearch and the number of Algolia indexing operations.
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Connector overview](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors)
* [JSON connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json)
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# JSON connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json
Index your data from a hosted JSON file.
Use the JSON connector to index from a hosted JSON file without writing an indexing script.
In this quickstart, you create a source for a public product dataset, an Algolia destination,
and an on-demand synchronization task.
## Before you begin
If you don't have an Algolia account, [create one](https://www.algolia.com/users/sign_up) for free.
## Set up the JSON connector
Create a source, destination, transformation, and synchronization task for the sample product dataset.
* The source tells the connector where to fetch the JSON file.
* The destination tells the connector which Algolia index to write to.
* The transformation changes records before the connector indexes them.
* The task controls when the connector runs.
Connect the JSON source for the hosted sample product dataset.
1. Go to the Algolia dashboard and select your Algolia application.
2. Go to the [**Connectors**](https://dashboard.algolia.com/connectors) page in the Algolia dashboard.
3. Select the JSON connector and click **Connect**.
1. Select **None** as your authentication method under **Is your data source protected?**.
2. In **URL**, enter `https://dashboard.algolia.com/api/1/sample_datasets?type=apparel`.
3. Select **GET** as the **HTTP Method**.
1. In **Unique property identifier**, enter `objectID`.
2. In **Connector name**, enter `Quickstart products JSON source`.
3. Select **Create source**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Choose the Algolia index where the connector stores the product records.
1. Under **Connect your data to Algolia Search**, enter `quickstart-products`.
If you enter the name of an existing index, the connector overwrites the records and settings in that index.
2. Under **Index credentials**, select **Create one for me**.
To use an existing key, choose one with the `addObject`, `deleteIndex`, and `editSettings` [ACLs](/doc/guides/security/api-keys#access-control-list-acl).
3. In **Name**, enter `Quickstart products destination`.
4. Select **Create destination**.
Select **On demand** and **Full reindexing**.
Select **Create task**, then **Run** and wait for the task to finish.
When the task completes, open the [`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
The index contains product records with attributes such as `title`, `description`, `product_type`, `price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own JSON data
To index your own records, update the connector source URL to point to a JSON file you host.
After you update the source, [run the connector task](https://dashboard.algolia.com/connectors/tasks) again to index your records.
### Source file format
Your JSON file must contain an array of JSON objects.
For example:
```json JSON icon=braces theme={"system"}
[
{
"id": "product-1",
"title": "Product name",
"description": "Product description",
"product_type": "Product type",
"price": 49.99
}
]
```
Each object must include a property with a value that's unique across all records (`id` in this example).
In the source configuration, set **Unique property identifier** to that property so the connector can use it as the Algolia `objectID`.
### Source file location
Update the connector source URL to point to your hosted file.
The source URL can use `http`, `https`, `ftp`, `ftps`, or `sftp`.
For `http` or `https` sources, choose the request method your server expects:
* **GET** if your server returns the file from a standard request.
* **POST** if your server requires a POST request.
### Source authentication
Select the authentication method for your file:
* **None** if the file is public.
* **Basic Auth** if the file requires a username and password.
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# MySQL connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/mysql
Use the MySQL connector to index data from a MySQL database.
Use the MySQL connector to index from a MySQL table without writing an indexing script.
In this quickstart, you import a sample product dataset into MySQL, create a source for this database table,
use an Algolia destination, and create an on-demand synchronization task.
## Before you begin
Make sure you have:
* **An Algolia account**.
[Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **A MySQL host that Algolia can reach**.
Use a hosted MySQL server or configure an [SSH tunnel](https://dev.mysql.com/doc/mysql-shell/8.0/en/mysql-shell-connection-ssh.html) to your server.
A server available only at `127.0.0.1` or `localhost` isn't reachable from the hosted Algolia connector.
* **Your MySQL host's port number**.
The default is `3306` but your provider may use a different port.
* **A MySQL database**.
Create or choose a database for the sample data and note its name.
For more information,
see [Creating and selecting a database](https://dev.mysql.com/doc/refman/8.4/en/creating-database.html).
* **A MySQL username and password**.
The user must have `CREATE`, `DROP`, `INSERT`, and `SELECT` access to the target database.
For more information,
see [GRANT Statement](https://dev.mysql.com/doc/refman/8.4/en/grant.html).
## Set up the MySQL connector
Create a source, destination, transformation, and synchronization task for the sample product dataset.
* The source tells the connector which MySQL database to read.
* The destination tells the connector which Algolia index to write to.
* The transformation converts MySQL JSON values and enriches the records.
* The task controls which table to index and when the connector runs.
```sh Command line icon=square-terminal theme={"system"}
curl --fail --silent --show-error --location \
--output apparel.sql \
https://raw.githubusercontent.com/algolia/quickstarts/main/sample-data/apparel.sql
```
This script replaces any existing `apparel` table in your database.
Run the script:
```sh Command line icon=square-terminal theme={"system"}
mysql --host=MYSQL_HOST \
--port=MYSQL_PORT \
--user=MYSQL_USER \
--password \
--database=MYSQL_DATABASE \
< apparel.sql
```
Replace `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER` and `MYSQL_DATABASE` with your MySQL connection details.
When prompted, enter the password for `MYSQL_USER`.
For more information,
see [Executing SQL statements from a text file](https://dev.mysql.com/doc/refman/8.4/en/mysql-batch-commands.html).
Run:
```sh Command line icon=square-terminal theme={"system"}
mysql --host=MYSQL_HOST \
--port=MYSQL_PORT \
--user=MYSQL_USER \
--password \
--database=MYSQL_DATABASE \
--execute="SELECT COUNT(*) AS records FROM apparel;"
```
When prompted, enter the password for `MYSQL_USER`.
The SQL query should return `1000` records.
Go to the Algolia dashboard and select your Algolia application.
Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Find **MySQL**, then select **Connect**.
Under **Configure your data source**, select **Create a new source**.
Enter the connection details for the database table that contains the sample data:
| Field | Value |
| - | - |
| **Host** | `MYSQL_HOST` |
| **Port** | `MYSQL_PORT`, usually `3306` |
| **Database** | `MYSQL_DATABASE` |
| **User** | `MYSQL_USER` |
| **Password** | `MYSQL_USER_PASSWORD` |
Under **SSH Tunnel Method**, select **No Tunnel** if Algolia can connect directly to your database.
Otherwise, configure the authentication for your SSH tunnel.
In **Connector name**, enter `Quickstart products MySQL source`, then select **Create source**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Under **Configure your destination**, select **Create a new destination**.
Under **Search**, enter `quickstart-products` as the index name.
If you enter the name of an existing index, the connector overwrites the records and settings in that index.
Under **Index credentials**, select **Create one for me**.
To use an existing API key, choose one with the `addObject`, `deleteIndex`, and `editSettings`
[ACLs](/doc/guides/security/api-keys#access-control-list-acl).
In **Name**, enter `MySQL quickstart products destination`, then select **Create destination**.
Under **Configure your task**, select **On demand**.
Select the `apparel` table and keep all its columns selected.
Confirm that the connector maps the table's `objectID` primary key to each record's Algolia `objectID`.
Select **Create task**, then **Run** and wait for the task to finish.
When the task completes, open the
[`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
The index contains 1,000 product records with attributes such as `title`, `description`, `product_type`,
`price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own MySQL data
Use a dedicated read-only MySQL account with access only to the tables you want to index,
and restrict network access to Algolia's connector IP addresses.
### Set up a dedicated MySQL connector user
Connect to MySQL with an account that can create users and grant privileges.
Run the following statements in the MySQL client or your hosting provider's SQL console.
Create a dedicated connector user and grant it `SELECT` access to the table you want to index.
For example:
```sql SQL icon="database" theme={"system"}
CREATE USER 'algolia_connector'@'%' IDENTIFIED BY 'MYSQL_USER_PASSWORD';
GRANT SELECT ON MYSQL_DATABASE.TABLE_NAME TO 'algolia_connector'@'%';
```
Replace `MYSQL_USER_PASSWORD` and `MYSQL_DATABASE.TABLE_NAME` with your connector user's password, database, and table name.
If the user already exists, run only the [`GRANT` statement](https://dev.mysql.com/doc/refman/8.4/en/grant.html).
### Allow the Algolia connector to reach your MySQL server
The `%` host permits remote connections.
For production databases,
restrict inbound connections to [Algolia's connector IP addresses](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors#allow-algolia-ip-addresses) or use an SSH tunnel.
instead of allowing connections from all IP addresses.
### Configure the connector for your data
1. Follow the steps to [configure the MySQL source](#source) and create a [synchronization task](#task): replace the sample connection details and `apparel` table with your own database details and tables.
2. Use the dedicated connector user you created.
3. Review the [transformation](#transform) and [destination index](#destination) to make sure they're appropriate for your data.
### Secure the connection
Configure connection security (**SSL modes**) in your [MySQL source](https://dashboard.algolia.com/connectors/sources).
SSL encrypts the connection to your MySQL server and can verify the server's identity.
An SSH tunnel lets the connector reach a database that isn't directly accessible from Algolia.
Depending on your setup, you can use SSL, an SSH tunnel, or both.
#### Configure SSL
Choose an SSL mode that matches your MySQL server's TLS configuration:
* **Preferred.**
The connector uses SSL if the MySQL server supports it.
Otherwise, it connects without SSL.
* **Required** requires an encrypted connection but doesn't verify the server certificate.
* **Verify CA** requires encryption and verifies the server certificate against a trusted certificate authority.
Add the CA certificate supplied by your database provider to **CA certificate**.
* **Verify Identity** verifies that the certificate identifies the host.
Add the provider's **CA certificate**.
Leave **Client certificate** and **Client key** empty unless your MySQL server
requires mutual TLS authentication.
If it does, add the client certificate supplied by your database provider to **Client certificate** and its corresponding private key to **Client key**.
#### Connect through an SSH tunnel
If Algolia can't connect directly to your MySQL database,
connect through an SSH host.
Select an authentication method:
To authenticate with an RSA private key:
1. Generate an RSA key pair in PEM format:
```sh Command line icon=square-terminal theme={"system"}
ssh-keygen -t rsa -m PEM -f algolia_mysql_rsa
```
2. Add `algolia_mysql_rsa.pub` to the SSH user's authorized keys on the bastion host.
3. In the MySQL source configuration, select **SSH Key Authentication** under **SSH Tunnel Method**.
4. Enter the **SSH Tunnel Jump Server Host**, SSH port, and **SSH Login Username**.
5. Add the contents of `algolia_mysql_rsa` to **SSH Private Key**.
To authenticate with a password:
1. In the MySQL source configuration, select **Password Authentication** under **SSH Tunnel Method**.
2. Enter the **SSH Tunnel Jump Server Host**, SSH port, and **SSH Login Username**.
3. Enter the SSH user's password.
Use the credentials for the SSH host, not the MySQL username and password.
### MySQL data types
The connector maps MySQL values to JSON-compatible Algolia attributes.
In particular:
* `DECIMAL` values become numbers.
* `TINYINT(1)` values become booleans by default.
* Native MySQL `JSON` values become serialized JSON strings.
* `TEXT` and `LONGTEXT` columns remain strings, including columns that contain JSON-encoded data.
For JSON-encoded strings, use a transformation to convert them into arrays or objects before indexing.
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Connector overview](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors)
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# Push to Algolia connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push
Push records from your app through an Algolia-managed transformation to an index.
Use the Push to Algolia connector when your app controls which records to index and when to send them.
Your app sends records to a connector task with an API client.
Algolia transforms the records and writes them to the destination index.
If Algolia can retrieve your data directly, use another [connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors).
For example, use the [JSON connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json) if your records are available as a hosted JSON file and
you want Algolia to fetch them on demand or on a schedule without an indexing script.
If your records don't need an Algolia-managed transformation,
[send them directly with an API client](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/sending-records-in-batches).
## Quickstart
Create a transformation, destination, and task for a public product dataset,
then run a Node.js script that pushes the records through the connector.
The script uses `pushTask` to identify the connector task explicitly by its task ID.
### Before you begin
Make sure you have:
* [An Algolia account](https://dashboard.algolia.com/users/sign_up). Create one for free if you don't already have one.
* Node.js 22 or later and npm.
* Your Algolia [Write API key](https://dashboard.algolia.com/account/api-keys).
* Your Algolia application's [analytics region](https://dashboard.algolia.com/account/infrastructure/analytics): `us` for the United States or `eu` for Europe.
### Set up the Push to Algolia connector
Go to the Algolia dashboard and select your Algolia .
On the left sidebar, select **Data sources**,
then open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Select **Push to Algolia**, then select **Connect**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Select **Create a new destination** to choose an for the connector to store records.
Under **Connect your data to Algolia Search**, enter `quickstart-products` and,
under **Index credentials**,
select **Create one for me**.
Use a new index for this quickstart to avoid adding new records to it.
In **Name**, enter `Quickstart push destination` and select **Create destination**.
Select **Create task**.
The dashboard opens **API Client & Code snippets** with a generated `pushTask` example.
Copy the `taskID` value from the example, then select **Close**.
Continue with the next step instead of copying the generated records or code.
The next steps use environment variables to keep your API key out of the source code.
In a terminal, create a project and install the Algolia API client and `dotenv`:
```sh Command line icon=square-terminal theme={"system"}
mkdir algolia-push-quickstart
cd algolia-push-quickstart
npm init -y
npm install algoliasearch@5 dotenv
```
Create a `.env` file in the project directory:
```dotenv .env icon=lock-keyhole theme={"system"}
ALGOLIA_APPLICATION_ID=
ALGOLIA_API_KEY=
ALGOLIA_APPLICATION_REGION=
ALGOLIA_TASK_ID=
```
Set the variables to your Algolia application ID, write API key, analytics region, and task ID.
To keep your Write API key secret,
create a `.gitignore` file in the project directory:
```text .gitignore icon=file-cog theme={"system"}
.env
node_modules/
```
Create a `push.mjs` file with this code:
```js push.mjs icon=code expandable theme={"system"}
import "dotenv/config";
import { algoliasearch } from "algoliasearch";
const datasetUrl =
"https://dashboard.algolia.com/api/1/sample_datasets?type=apparel";
async function main() {
const {
ALGOLIA_APPLICATION_ID: appId,
ALGOLIA_API_KEY: apiKey,
ALGOLIA_APPLICATION_REGION: region,
ALGOLIA_TASK_ID: taskID,
} = process.env;
if (!appId || !apiKey || !region || !taskID) {
throw new Error(
"Set ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY, " +
"ALGOLIA_APPLICATION_REGION, and ALGOLIA_TASK_ID in your .env file.",
);
}
if (region !== "us" && region !== "eu") {
throw new Error(
"ALGOLIA_APPLICATION_REGION must be either 'us' or 'eu'.",
);
}
const client = algoliasearch(appId, apiKey).initIngestion({ region });
// Confirm that the task belongs to this application and region.
await client.getTask({ taskID });
const response = await fetch(datasetUrl);
if (!response.ok) {
throw new Error(
`Couldn't fetch the sample dataset: ` +
`${response.status} ${response.statusText}`,
);
}
const products = await response.json();
if (!Array.isArray(products)) {
throw new Error("The sample dataset didn't return an array of records.");
}
const result = await client.pushTask({
taskID,
pushTaskPayload: {
action: "addObject",
records: products,
},
watch: true,
});
console.log(`Submitted ${products.length} records.`);
console.log(`Run ID: ${result.runID}`);
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`Push failed: ${message}`);
process.exitCode = 1;
});
```
Run the script:
```sh Command line icon=square-terminal theme={"system"}
node push.mjs
```
Copy the **Run ID** printed by the script.
Open the [Connector Debugger](https://dashboard.algolia.com/connectors/debugger) and search for the run ID printed by `push.mjs`.
Confirm that the operation completed successfully.
Open the [`quickstart-products` index](https://dashboard.algolia.com/explorer/browse/quickstart-products) in the Algolia dashboard.
Confirm that it contains product records with attributes such as `title`, `description`, `product_type`, `price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Use the connector with your app
In production, replace the quickstart data request in `push.mjs` with code that selects records from your app or data pipeline.
For example, supply records from a processing job or a source for which Algolia doesn't provide another connector.
### Prepare your records
Send only the records required by the indexing operation.
For example, your app can send a changed record after updating its source data
instead of exporting and synchronizing the full dataset.
Ensure each record has a [stable, unique `objectID`](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#keep-object-ids-stable).
If your source uses a different unique property, map it before pushing the records:
```js JavaScript icon=code theme={"system"}
const records = sourceRecords.map((record) => ({
...record,
objectID: String(record.id),
}));
```
Update your transformation to match your record structure.
If you want to make a transformed attribute searchable, use it for faceting, or use it for ranking, update the corresponding index setting:
[`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes),
[`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting), or
[`customRanking`](/doc/api-reference/api-parameters/customRanking).
### Choose an API client method
Choose a method based on how you want to identify the connector task and update the index.
| Use case | Method |
| - | - |
| Target a specific connector task by its task ID | [`pushTask`](/doc/libraries/sdk/methods/ingestion/push-task) |
| Push one request by index name when one task matches | [`push`](/doc/libraries/sdk/methods/ingestion/push) |
| Push large record sets in batches, with control over the action and batch size | [`chunkedPush`](/doc/libraries/sdk/methods/ingestion/chunked-push) |
| Add or replace complete records by adapting an existing Search API implementation | [`saveObjectsWithTransformation`](/doc/libraries/sdk/methods/search/save-objects-with-transformation) |
| Add or update selected attributes by adapting an existing Search API implementation | [`partialUpdateObjectsWithTransformation`](/doc/libraries/sdk/methods/search/partial-update-objects-with-transformation) |
| Replace every record in an index by adapting an existing Search API implementation | [`replaceAllObjectsWithTransformation`](/doc/libraries/sdk/methods/search/replace-all-objects-with-transformation) |
* `push`, `chunkedPush`, and the `WithTransformation` helper methods select a user-created Push to Algolia task by index name. They support one user-created Push to Algolia connector, including when the index also uses [Collections](/doc/guides/solutions/ecommerce/browse/tutorials/collections).
* If multiple destinations link to the index or multiple user-created Push to Algolia connectors target it, use `pushTask` and specify the task ID. To send records through the Collections pipeline, use its task ID with `pushTask`.
### Push with the Ingestion API
Use the Ingestion API client to choose the indexing action or identify a connector task by ID.
#### Push by task ID
Use `pushTask` to select a connector task by its task ID.
```cs C# theme={"system"}
var response = await client.PushTaskAsync(
"6c02aeb1-775e-418e-870b-1faccd4b2c0f",
new PushTaskPayload
{
Action = Enum.Parse("AddObject"),
Records = new List
{
new PushTaskRecords
{
ObjectID = "o",
AdditionalProperties = new Dictionary
{
{ "key", "bar" },
{ "foo", "1" },
},
},
new PushTaskRecords
{
ObjectID = "k",
AdditionalProperties = new Dictionary
{
{ "key", "baz" },
{ "foo", "2" },
},
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.pushTask(
taskID: "6c02aeb1-775e-418e-870b-1faccd4b2c0f",
pushTaskPayload: PushTaskPayload(
action: Action.fromJson("addObject"),
records: [
PushTaskRecords(
objectID: "o",
additionalProperties: {
'key': "bar",
'foo': "1",
},
),
PushTaskRecords(
objectID: "k",
additionalProperties: {
'key': "baz",
'foo': "2",
},
),
],
),
);
```
```go Go theme={"system"}
response, err := client.PushTask(client.NewApiPushTaskRequest(
"6c02aeb1-775e-418e-870b-1faccd4b2c0f",
ingestion.NewEmptyPushTaskPayload().SetAction(ingestion.Action("addObject")).SetRecords(
[]ingestion.PushTaskRecords{
*ingestion.NewEmptyPushTaskRecords().SetAdditionalProperty("key", "bar").SetAdditionalProperty("foo", "1").SetObjectID("o"),
*ingestion.NewEmptyPushTaskRecords().SetAdditionalProperty("key", "baz").SetAdditionalProperty("foo", "2").SetObjectID("k"),
}),
))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
WatchResponse response = client.pushTask(
"6c02aeb1-775e-418e-870b-1faccd4b2c0f",
new PushTaskPayload()
.setAction(Action.ADD_OBJECT)
.setRecords(
Arrays.asList(
new PushTaskRecords().setAdditionalProperty("key", "bar").setAdditionalProperty("foo", "1").setObjectID("o"),
new PushTaskRecords().setAdditionalProperty("key", "baz").setAdditionalProperty("foo", "2").setObjectID("k")
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.pushTask({
taskID: '6c02aeb1-775e-418e-870b-1faccd4b2c0f',
pushTaskPayload: {
action: 'addObject',
records: [
{ key: 'bar', foo: '1', objectID: 'o' },
{ key: 'baz', foo: '2', objectID: 'k' },
],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.pushTask(
taskID = "6c02aeb1-775e-418e-870b-1faccd4b2c0f",
pushTaskPayload =
PushTaskPayload(
action = Action.entries.first { it.value == "addObject" },
records =
listOf(
PushTaskRecords(
objectID = "o",
additionalProperties =
mapOf("key" to JsonPrimitive("bar"), "foo" to JsonPrimitive("1")),
),
PushTaskRecords(
objectID = "k",
additionalProperties =
mapOf("key" to JsonPrimitive("baz"), "foo" to JsonPrimitive("2")),
),
),
),
)
```
```php PHP theme={"system"}
$response = $client->pushTask(
'6c02aeb1-775e-418e-870b-1faccd4b2c0f',
['action' => 'addObject',
'records' => [
['key' => 'bar',
'foo' => '1',
'objectID' => 'o',
],
['key' => 'baz',
'foo' => '2',
'objectID' => 'k',
],
],
],
);
```
```python Python theme={"system"}
response = client.push_task(
task_id="6c02aeb1-775e-418e-870b-1faccd4b2c0f",
push_task_payload={
"action": "addObject",
"records": [
{
"key": "bar",
"foo": "1",
"objectID": "o",
},
{
"key": "baz",
"foo": "2",
"objectID": "k",
},
],
},
)
```
```ruby Ruby theme={"system"}
response = client.push_task(
"6c02aeb1-775e-418e-870b-1faccd4b2c0f",
Algolia::Ingestion::PushTaskPayload.new(
action: "addObject",
records: [
Algolia::Ingestion::PushTaskRecords.new(key: "bar", foo: "1", algolia_object_id: "o"),
Algolia::Ingestion::PushTaskRecords.new(key: "baz", foo: "2", algolia_object_id: "k")
]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.pushTask(
taskID = "6c02aeb1-775e-418e-870b-1faccd4b2c0f",
pushTaskPayload = PushTaskPayload(
action = Action.withName("addObject"),
records = Seq(
PushTaskRecords(
objectID = "o",
additionalProperties = Some(List(JField("key", JString("bar")), JField("foo", JString("1"))))
),
PushTaskRecords(
objectID = "k",
additionalProperties = Some(List(JField("key", JString("baz")), JField("foo", JString("2"))))
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.pushTask(
taskID: "6c02aeb1-775e-418e-870b-1faccd4b2c0f",
pushTaskPayload: PushTaskPayload(
action: IngestionAction.addObject,
records: [
PushTaskRecords(from: [
"objectID": AnyCodable("o"),
"key": AnyCodable("bar"),
"foo": AnyCodable("1"),
]),
PushTaskRecords(from: [
"objectID": AnyCodable("k"),
"key": AnyCodable("baz"),
"foo": AnyCodable("2"),
]),
]
)
)
```
#### Push by index name
Use `push` when one task targets the specified index.
The request returns an error if no task or more than one task matches the index name.
```cs C# theme={"system"}
var response = await client.PushAsync(
"INDEX_NAME",
new PushTaskPayload
{
Action = Enum.Parse("AddObject"),
Records = new List
{
new PushTaskRecords
{
ObjectID = "o",
AdditionalProperties = new Dictionary
{
{ "key", "bar" },
{ "foo", "1" },
},
},
new PushTaskRecords
{
ObjectID = "k",
AdditionalProperties = new Dictionary
{
{ "key", "baz" },
{ "foo", "2" },
},
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.push(
indexName: "INDEX_NAME",
pushTaskPayload: PushTaskPayload(
action: Action.fromJson("addObject"),
records: [
PushTaskRecords(
objectID: "o",
additionalProperties: {
'key': "bar",
'foo': "1",
},
),
PushTaskRecords(
objectID: "k",
additionalProperties: {
'key': "baz",
'foo': "2",
},
),
],
),
);
```
```go Go theme={"system"}
response, err := client.Push(client.NewApiPushRequest(
"INDEX_NAME",
ingestion.NewEmptyPushTaskPayload().SetAction(ingestion.Action("addObject")).SetRecords(
[]ingestion.PushTaskRecords{
*ingestion.NewEmptyPushTaskRecords().SetAdditionalProperty("key", "bar").SetAdditionalProperty("foo", "1").SetObjectID("o"),
*ingestion.NewEmptyPushTaskRecords().SetAdditionalProperty("key", "baz").SetAdditionalProperty("foo", "2").SetObjectID("k"),
}),
))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
WatchResponse response = client.push(
"INDEX_NAME",
new PushTaskPayload()
.setAction(Action.ADD_OBJECT)
.setRecords(
Arrays.asList(
new PushTaskRecords().setAdditionalProperty("key", "bar").setAdditionalProperty("foo", "1").setObjectID("o"),
new PushTaskRecords().setAdditionalProperty("key", "baz").setAdditionalProperty("foo", "2").setObjectID("k")
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.push({
indexName: 'foo',
pushTaskPayload: {
action: 'addObject',
records: [
{ key: 'bar', foo: '1', objectID: 'o' },
{ key: 'baz', foo: '2', objectID: 'k' },
],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.push(
indexName = "INDEX_NAME",
pushTaskPayload =
PushTaskPayload(
action = Action.entries.first { it.value == "addObject" },
records =
listOf(
PushTaskRecords(
objectID = "o",
additionalProperties =
mapOf("key" to JsonPrimitive("bar"), "foo" to JsonPrimitive("1")),
),
PushTaskRecords(
objectID = "k",
additionalProperties =
mapOf("key" to JsonPrimitive("baz"), "foo" to JsonPrimitive("2")),
),
),
),
)
```
```php PHP theme={"system"}
$response = $client->push(
'INDEX_NAME',
['action' => 'addObject',
'records' => [
['key' => 'bar',
'foo' => '1',
'objectID' => 'o',
],
['key' => 'baz',
'foo' => '2',
'objectID' => 'k',
],
],
],
);
```
```python Python theme={"system"}
response = client.push(
index_name="INDEX_NAME",
push_task_payload={
"action": "addObject",
"records": [
{
"key": "bar",
"foo": "1",
"objectID": "o",
},
{
"key": "baz",
"foo": "2",
"objectID": "k",
},
],
},
)
```
```ruby Ruby theme={"system"}
response = client.push(
"INDEX_NAME",
Algolia::Ingestion::PushTaskPayload.new(
action: "addObject",
records: [
Algolia::Ingestion::PushTaskRecords.new(key: "bar", foo: "1", algolia_object_id: "o"),
Algolia::Ingestion::PushTaskRecords.new(key: "baz", foo: "2", algolia_object_id: "k")
]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.push(
indexName = "INDEX_NAME",
pushTaskPayload = PushTaskPayload(
action = Action.withName("addObject"),
records = Seq(
PushTaskRecords(
objectID = "o",
additionalProperties = Some(List(JField("key", JString("bar")), JField("foo", JString("1"))))
),
PushTaskRecords(
objectID = "k",
additionalProperties = Some(List(JField("key", JString("baz")), JField("foo", JString("2"))))
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.push(
indexName: "INDEX_NAME",
pushTaskPayload: PushTaskPayload(
action: IngestionAction.addObject,
records: [
PushTaskRecords(from: [
"objectID": AnyCodable("o"),
"key": AnyCodable("bar"),
"foo": AnyCodable("1"),
]),
PushTaskRecords(from: [
"objectID": AnyCodable("k"),
"key": AnyCodable("baz"),
"foo": AnyCodable("2"),
]),
]
)
)
```
#### Push records in batches
Use `chunkedPush` to split a large record set into batches and send each batch with `push`.
The default batch size is 1,000 records.
Use it directly when you need more control over the indexing action or batch size.
### Search API `WithTransformation` methods
Use the Search API client helpers to adapt an existing implementation that calls `saveObjects`, `partialUpdateObjects`, or `replaceAllObjects`.
The helpers route records through the Push to Algolia connector and automatically split them into batches.
They're subject to the [connector limits](/doc/guides/scaling/algolia-service-limits#connectors-limits).
To run the examples, [install the latest API client](/doc/libraries/sdk/install).
#### Set up transformation options
Before calling a `WithTransformation` method, configure `transformationOptions` with your Algolia application's analytics region.
The API client throws an error if you omit `transformationOptions`.
```cs C# theme={"system"}
namespace Algolia;
using System;
using System.Collections.Generic;
using Algolia.Search.Clients;
using Algolia.Search.Http;
using Algolia.Search.Models.Search;
class SetUpTransformationOptions
{
async Task Main(string[] args)
{
// Set TransformationOptions with your transformation region to use the `WithTransformation` helper methods.
// Replace "us" with "eu" if your Algolia application uses the Europe analytics region.
var client = SearchClient.WithTransformation(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_API_KEY",
new TransformationOptions("us")
);
// Save records, transforming them through the Push connector
try
{
var result = await client.SaveObjectsWithTransformationAsync(
"INDEX_NAME",
new List
`transformationOptions` creates a dedicated ingestion transporter that starts from the Ingestion API defaults:
25-second timeouts, hosts derived from the region, and no compression.
It only overrides the options you set and doesn't inherit the Search API client's configuration.
For more information, see
[Transformation options and the ingestion transporter](/doc/libraries/sdk/customize#transformation-options-and-the-ingestion-transporter).
`transformationOptions` replaces the deprecated `setTransformationRegion` method in [Java](/doc/libraries/sdk/upgrade/java),
[C#](/doc/libraries/sdk/upgrade/csharp),
and [PHP](/doc/libraries/sdk/upgrade/php),
the `set_transformation_region` method in [Python](/doc/libraries/sdk/upgrade/python),
and the `transformation` option in [JavaScript](/doc/libraries/sdk/upgrade/javascript).
#### Add or replace records with a transformation
To add or replace complete records with a transformation, call `saveObjectsWithTransformation`.
```cs C# theme={"system"}
var response = await client.SaveObjectsWithTransformationAsync(
"INDEX_NAME",
new List
#### Add or update record attributes with a transformation
To add or update selected attributes with a transformation, call [`partialUpdateObjectsWithTransformation`](/doc/libraries/sdk/methods/search/partial-update-objects-with-transformation).
```cs C# theme={"system"}
var response = await client.PartialUpdateObjectsWithTransformationAsync(
"INDEX_NAME",
new List
#### Replace all records with a transformation
To replace all records with a transformation, call `replaceAllObjectsWithTransformation`.
```cs C# theme={"system"}
var response = await client.ReplaceAllObjectsWithTransformationAsync(
"INDEX_NAME",
new List
### Supported indexing actions
The Push to Algolia connector supports all values of the [`action`](/doc/rest-api/search/batch) property for batch indexing operations.
For `deleteObject`, `delete`, and `clear` actions,
the connector skips the transformation and uses
[traditional indexing](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/sending-records-in-batches).
## Connector Debugger
To debug push operations:
* Check incoming events in the [Connector Debugger](https://dashboard.algolia.com/connectors/debugger).
The `runID` returned by a successful `push` or `pushTask` request identifies the indexing operation.
* To wait for a `push` or `pushTask` operation and receive its result in the response,
add the [`watch`](/doc/rest-api/ingestion/push-task) parameter.
The response reports successful and failed operations.
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# Supabase connector
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/supabase
Use the Supabase connector to index data from a Supabase table or view.
Use the Supabase connector to index from a Supabase table or view without writing an indexing script.
In this quickstart, you create a Supabase source, transform the records, create an Algolia destination, and configure a synchronization task.
## Before you begin
Make sure you have:
* **An Algolia account**. [Create one for free](https://www.algolia.com/users/sign_up) if you don't already have one.
* **A Supabase project with the sample data in a `public.apparel` table**.
Download the [Algolia apparel sample data](https://raw.githubusercontent.com/algolia/quickstarts/main/sample-data/apparel.csv)
and follow Supabase's [data import instructions](https://supabase.com/docs/guides/database/import-data).
Use `objectID` as the table's primary key.
## Set up the Supabase connector
Create a source, transformation, destination, and synchronization task.
Go to the Algolia dashboard and select your Algolia application.
Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
Find [**Supabase** and select **Connect**](https://dashboard.algolia.com/connectors/supabase).
Under **Quick setup**, select **Connect Supabase**.
For **Select a project**,
choose the project containing the `public.apparel` table.
The connector copies **Host**, **Port**, **Session Pooler Port**, **Database Name**, **Username**, and **Password** values from your Supabase project.
For **SSL modes**, select **Prefer**.
In **Connector name**,
enter `Quickstart products Supabase source`,
then select **Create source**.
This transformation adds a `price_range` attribute to each record.
After the connector indexes your records, you can display `price_range` or configure it as a facet.
Select **Transform using the code editor** and replace the placeholder **Transformation code** with this function:
```js JavaScript icon=code expandable theme={"system"}
async function transform(record, helper) {
const price = Number(record.price);
if (!Number.isFinite(price)) {
return record;
}
if (price < 25) {
record.price_range = "Under $25";
} else if (price < 50) {
record.price_range = "$25 to $49";
} else if (price < 100) {
record.price_range = "$50 to $99";
} else {
record.price_range = "$100 and up";
}
return record;
}
```
Select **Save**.
Under **Configure your destination**,
select **Create a new destination**.
Under **Search**,
enter `quickstart-products` as the index name.
If you use an existing index,
running the connector overwrites the records and settings in that index.
Under **Index credentials**,
select **Create one for me**.
To use an existing API key,
choose one with the `addObject`, `deleteIndex`, and `editSettings`
[access control list permissions](/doc/guides/security/api-keys#access-control-list-acl).
In **Name**,
enter `Supabase quickstart products destination`,
then select **Create destination**.
Under **Configure your task**,
select **On demand** and clear **Scheduled**.
Select **Full reindexing**.
Select `apparel` as the source table.
Select all columns.
For **ObjectID**, select `objectID`.
Select **Create task**,
then **Run**,
and wait for the task to finish.
Open the
[`quickstart-products` index in the Algolia dashboard](https://dashboard.algolia.com/explorer/browse/quickstart-products).
Check that the index contains the imported product records,
with attributes such as `title`, `description`, `product_type`,
`price`, `price_range`, and `showcase_image`.
You can use this index as the data source for
[Build your first search experience](/doc/guides/get-started/quickstart).
Before you build the UI,
[configure `product_type` as an attribute for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard)
in the `quickstart-products` index.
## Index your own Supabase data
To index your own data:
* Use a populated Supabase table or view that the connector credentials can read.
* Follow the quickstart steps to create the [source](#source), [destination](#destination), and [task](#task). Replace the sample project, table, credentials, and destination index with your own values.
* Review or remove the sample transformation so that it matches your record structure. When you configure the task, select the columns you want to include in your Algolia records.
* Select the column to use as the Algolia `objectID`. This column must be present in every row, and its value must be unique and stable.
### Supabase database credentials
The quickstart uses the database settings imported from your Supabase project.
For your own data, create a dedicated Postgres login role for the connector.
Grant the role permission to:
* Connect to the database.
* Use the schema that contains the source data.
* Run `SELECT` queries on each table or view the connector needs to read.
If Row Level Security (RLS) is enabled,
add a `SELECT` policy that lets the connector role read the rows you want to index.
For more information, see [Postgres roles](https://supabase.com/docs/guides/database/postgres/roles) and
[Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security).
### Configure the connector username
Supabase's session pooler details show the username for the default `postgres` role.
When you configure the source,
replace `postgres` with the dedicated connector role name,
but keep the project reference after the period.
For example, for a connector role named `algolia_connector`,
replace `postgres.PROJECT_REFERENCE` with `algolia_connector.PROJECT_REFERENCE`.
### Combine data with a Postgres view
An Algolia record can combine data from several Supabase tables.
For example, a product record might combine product details with review ratings and recent order data.
If your source data spans multiple tables,
create a Postgres view that joins and aggregates the required data.
Then, select the view as the source when you configure the synchronization [task](#task).
Make sure each row in the view includes a unique, stable column that you can use as the Algolia `objectID`.
This example creates a view that combines data from the `apparel` and `reviews` tables.
```sql SQL icon="database" theme={"system"}
create view apparel_search
with (security_invoker = true)
as
select
p."objectID",
p.title,
p.description,
p.price,
avg(r.rating) as average_rating,
count(distinct r.id) as review_count
from apparel as p
left join reviews as r on r.product_id = p."objectID"
group by p."objectID", p.title, p.description, p.price;
```
Create a separate synchronization task for each table or view you want to index.
For more information,
see [Prepare your data in Supabase](https://supabase.com/blog/algolia-connector-for-supabase#1-prepare-your-data-in-supabase).
### Transformations
Use transformations to add computed attributes or change values before the connector indexes your records.
To use a transformed attribute for search, faceting, or ranking,
update the relevant [index settings](/doc/api-reference/settings-api-parameters),
such as `searchableAttributes`, `attributesForFaceting`, or `customRanking`.
Test transformations with repeated task runs.
Transformations that read from and write to the same attribute,
or extract a value and then delete the source attribute,
can produce different results when the task runs again.
### Synchronization schedule
After you create a task,
you can [edit it in the Algolia dashboard](https://dashboard.algolia.com/connectors/tasks) to choose when the connector runs:
* **On demand**. Run the connector manually.
* **Scheduled**. Select a predefined schedule or enter a custom [cron expression](https://crontab.guru/).
### Indexing strategy
You can choose how the connector updates your index.
For information about full reindexing, full record updates, and partial record updates,
see [Data synchronization strategies](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies).
## Limitations
This connector is subject to the following limitations:
* [Connectors limits](/doc/guides/scaling/algolia-service-limits#connectors-limits)
* [Transformation limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits)
## See also
* [Connector overview](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors)
* [Prepare your records for indexing](/doc/guides/sending-and-managing-data/prepare-your-data)
* [Transform your data with code](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code)
# AI-assisted transformations
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/ai-assisted-transformations
How AI can help build no-code transformations.
Instead of using the form-based [no-code transformation builder](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-without-code),
describe the transformation you need in plain English,
and AI will generate the configuration for you.
This is useful for complex conditions or computed values.
## When to use AI-Assisted transformations
This feature is for users who need to adjust data without writing code.
It helps when you want to:
* **Add new attributes.** For example, add a discount percentage field from the full price and the sale price.
* **Remove fields** that are no longer relevant.
* **Filter records.** For example, to exclude test products or filter by status.
* **Compute new values** from existing attributes. For example, create a ranking signal by combining popularity and newness.
### Key benefits
* **Simplifies complex logic.** You don't need to build nested "if/then" rules manually: AI handles it.
* **Faster iteration.** Quickly test and refine transformations using natural language.
* **You stay in control.** AI fills in the form, but you can review and adjust it before applying.
* **Preview before applying.** See how your data will look before committing changes.
## Use AI to build a transformation
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Data sources**.
3. Open the [**Connectors**](https://dashboard.algolia.com/connectors) page and select a connector.
4. Configure your data source by selecting the appropriate options.
5. Click **Create source**
6. Select **Create a new transformation** and then select **Transform without code**.
7. Click **AI Assist**
8. Describe your transformation by typing it in plain English. For example: "If price is greater than 100, add the label 'expensive'."
9. **Review the suggestion.** AI generates a transformation configuration based on your prompt. You'll see a preview showing how your data will change before you apply it. Refine the transformation either by adjusting your prompt or by editing the form fields.
10. Name the transformation and save it.
11. Complete the remaining connector workflow: select a destination, create the task that will schedule the job (and its indexing strategy), and then click **Run**. The connector then runs the transformation as part of the workflow.
## Limitations
The AI assistant has a few constraints to keep in mind when using it with No Code data transformations.
* **Limited transformation types.**
Only these transformation types are supported:
*Add Attribute*, *Remove Attribute*, *Filter Records*, and *Compute New Value*. It doesn't cover advanced operations like joins, type conversions, or hierarchical restructuring.
* **Accuracy of suggestions.**
The perfect transformation may not always be generated. Review and, if needed, refine the suggestion before applying it. This "human in the loop" step is essential.
* **Prompt constraints.**
Prompts must be concise and specific. Long or ambiguous descriptions may lead to incomplete or incorrect transformations.
* **Performance limits.**
Transformations are still bound by Algolia's [service limits](/doc/guides/scaling/algolia-service-limits) (for example, record size, memory, and execution timeout). AI can't bypass these constraints.
* **Security and data handling.**
Only the record schema and your written prompt are sent to the AI model, not full record data. This means if you include sensitive data in a prompt, it *could* be processed, so avoid adding personally identifiable information or confidential values.
* **Over-reliance.**
Use AI to help, not replace your judgment. Confirm that the previewed changes are correct, as incorrect logic could affect search relevance.
* **Language coverage.**
Prompts are optimized for English. Support for other languages is limited.
# Enrich your records with Fetch
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/enrich-your-records-with-fetch
Enrich your records with Fetch Helper Functions.
Fetch lets you enrich your Algolia data directly within a code transformation.
Using a JavaScript function within your transformation, you can retrieve information from third-party APIs to enhance the quality and relevance of your search results.
Using Fetch affects your record ingestion rate.
To avoid slowing down your indexing process, ensure you use third-party APIs that are fast and reliable.
Use Fetch for operations that can be completed quickly.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
## Create a code transformation
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Data sources**.
3. Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
4. Select a connector and click **Connect** to start creating your task.
5. Create a new source or select an existing one.
6. To create a transformation function, click **Create transformation**.
## Retrieve and store secrets
To call an external API, you typically need an API key or other credential type (such as basic access authentication or OAuth 2.0).
You can store these credentials as a secret and reference them in your transformation code.
1. Retrieve the secret from the third-party API.
2. In [your code transformation](#create-a-code-transformation), click **Secret**.
3. Paste in the third-party API key, give it name, then click **Add**.
You can now add the secret to your transformation code.
```js JavaScript theme={"system"}
const mySecretValue = helper.secrets.get("mySecretName");
```
## How to use fetch helper functions
1. Click **Helper Functions**.
2. Select **Fetch / API**
3. Select a helper and click **Add**.
You can either use third-party APIs directly or use a generic Fetch function.
### Third-party APIs
| Helper | Description |
| - | - |
| OpenAI - Text Generation | Generate text based on a prompt. You need an [OpenAI](https://openai.com/api/) API key to use this function. For more information, see the [OpenAI text generation documentation](https://platform.openai.com/docs/guides/text-generation). |
| OpenAI - Vision | Generate a description or tags for an image. You need an OpenAI API key to use this function. For more information, see the [OpenAI vision documentation](https://platform.openai.com/docs/guides/vision). |
| Convert currency | Convert a currency to another. You need a [CurrencyAPI](https://currencyapi.net/) API key to use this function. For more information, see the [CurrencyAPI documentation](https://currencyapi.net/documentation) . |
| Translate text | Translate text from one language to another. You need a [DeepL](https://www.deepl.com/en/products/api) API key to use this function. For more information, see the [DeepL documentation](https://currencyapi.net/documentation). |
### Generic function
This function lets you connect to any service with an API.
| Helper type | Description |
| - | - |
| Fetch with API key (in header) | Connect to any service with an API key in the header. |
| Fetch with API key (in URL) | Connect to any service with an API key in the query. |
| Fetch with Basic Auth | Connect to any service with [basic access authentication](https://wikipedia.org/wiki/Basic_access_authentication). |
There's a [timeout and response size limit](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits) on all transformations that include Fetch requests.
# Import data from the dashboard
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard
Use a no-code connector or a dashboard file upload to index your data in Algolia.
The Algolia dashboard has two methods to index without code:
* A **connector** reads your data from a source you host, then updates your on a schedule or on demand. Use a connector for data that changes.
* A **file upload** sends one file from your computer to your index. Use a file upload for sample data.
## Index your data with connectors
You point a connector at your data and choose the index it keeps up to date.
Algolia manages the ingestion pipeline, so you don't have to write your own.
You can also apply [transformations](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-without-code) to your records before indexing.
For files, use one of these connectors:
* The [CSV connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/csv) indexes a hosted CSV file.
* The [JSON connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/json) indexes a hosted JSON file.
For more information, see [Connectors](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors).
## Upload a file for a one-time import
Use a file upload to index a local file—you don't need to host it.
Keep your files smaller than 100 MB.
Larger files can make your browser unresponsive.
To index them, use a connector or the [API](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api).
### Import file formats
You can import JSON, CSV, and TSV files.
```json JSON theme={"system"}
[
{
"firstname": "Jimmie",
"lastname": "Barninger"
},
{
"firstname": "Warren",
"lastname": "Speach"
}
]
```
```csv CSV theme={"system"}
firstname,lastname
Jimmie,Barninger
Warren,Speach
```
```tsv TSV theme={"system"}
"firstname" "lastname"
"Jimmie" "Barninger"
"Warren" "Speach"
```
### Upload the file
An upload adds to the index you're viewing, and never deletes records.
Every record in your index has a unique [object ID](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#unique-record-identifiers).
If a record in your file has the same object ID as a record in your index, the upload replaces it.
If a record in your file doesn't have an object ID, Algolia generates one from the record's content.
1. Click **Add records > Upload file**. On an empty index, click **Upload records > Upload file**.
2. Drop the file into the **Upload records** box, or select the file on your file system. Then click **Upload**.
When Algolia indexes your data, the dashboard shows a success notification.
### Create a new index
To upload your file to a new index, create the index first:
1. In the Algolia dashboard, go to the [**Indices** tab](https://dashboard.algolia.com/explorer/browse). Click **New > Index**.
2. Enter a name for your index. Click **Create**.
# Importing with the API
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api
How to index your data to Algolia using an API client.
Algolia doesn't directly search your data sources.
Instead, you upload the parts of your data that are relevant for search to Algolia.
Algolia stores this data in an :
a data structure optimized for fast search.
* For more information about preparing your data for indexing, see [Format and structure your data](/doc/guides/sending-and-managing-data/prepare-your-data).
* For alternatives to the API for pushing data, see [Send your data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data).
## Required credentials
To send data to Algolia, you need an Application ID and a valid API key (with [`addObjects`](/doc/guides/security/api-keys#access-control-list-acl) permission).
You can find them in the **API Keys** section of [Algolia's dashboard](https://dashboard.algolia.com/account/api-keys/all).
* **Application ID**. Your Application ID is what Algolia uses to identify your app, where all your indices live.
* **API key**. [API keys](/doc/guides/security/api-keys) control access to Algolia's API and determine what you're allowed to do, such as searching an index, or adding new . For better security, create specific API keys with minimal [permissions](/doc/guides/security/api-keys#access-control-list-acl) for indexing tasks, and use them only in server-side code. Keep your indexing API keys secret.
**Only use the [Admin API key](/doc/guides/security/api-keys#admin-api-key) to create other API keys**.
Don't use the Admin API key in your apps.
## Fetch your data
Before sending anything to Algolia, **you need to retrieve your data**.
You can do this in several ways, depending on the nature of your app.
### From a database
```cs C# theme={"system"}
IEnumerable FetchDataFromDataBase() {
// Fetch data from your database
}
var actors = FetchDataFromDataBase();
```
```go Go theme={"system"}
func fetchDataFromDatabase() []Actor {
// Fetch data from your database
}
func main() {
records := fetchDataFromDatabase()
}
```
```java Java theme={"system"}
public List fetchDataFromDatabase() {
// Fetch data from your database
}
List actors = fetchDataFromDataBase();
```
```js JavaScript theme={"system"}
const fetchDataFromDatabase = () => {
// Fetch data from your database.
};
const records = fetchDataFromDatabase();
```
```kt Kotlin theme={"system"}
fun fetchFromDatabase(): List {
// Fetch data from your database
}
val actors: List = fetchFromDatabase()
```
```php PHP theme={"system"}
function fetchDataFromDatabase() {
// Fetch data from your database
}
$records = fetchDataFromDatabase();
```
```python Python theme={"system"}
def fetch_data_from_database():
data = {}
# Fetch data from your database
return data
records = fetch_data_from_database()
```
```ruby Ruby theme={"system"}
def fetch_data_from_database
# Fetch data from your database
end
records = fetch_data_from_database
```
```scala Scala theme={"system"}
def fetchDataFromDatabase(): Iterable[Actor] = {
// Fetch data from your database
}
val actors = fetchDataFromDatabase()
```
```swift Swift theme={"system"}
func fetchDataFromDataBase() -> [[String: Any]] {
// Fetch data from your database
}
let records = fetchDataFromDataBase()
```
### From a file
You can use this [actors dataset](https://github.com/algolia/datasets/blob/master/movies/actors.json) to test this out.
```cs C# theme={"system"}
using System.Collections.Generic;
using System.IO;
using Algolia.Search.Clients;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class Actor
{
public string Name { get; set; }
public string ObjectID { get; set; }
public int Rating { get; set; }
public string ImagePath { get; set; }
public string AlternativePath { get; set; }
}
public class AlgoliaActorsIndexer
{
private SearchClient client;
private SearchIndex index;
public AlgoliaActorsIndexer(string appId, string apiKey, string indexName)
{
client = new SearchClient(appId, apiKey);
index = client.InitIndex(indexName);
}
public void IndexActorsFromFile(string filePath)
{
var settings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy()
}
};
IEnumerable actors = JsonConvert.DeserializeObject>(File.ReadAllText(filePath), settings);
index.SaveObjects(actors);
}
}
// Usage
public class Program
{
public static void Main(string[] args)
{
AlgoliaActorsIndexer indexer = new AlgoliaActorsIndexer("undefined", "undefined", "actors");
indexer.IndexActorsFromFile("actors.json");
}
}
```
```go Go theme={"system"}
package main
import (
"encoding/json"
"io/ioutil"
)
type Actor struct {
Name string `json:"name"`
Rating int `json:"rating"`
ImagePath string `json:"image_path"`
AlternativeName string `json:"alternative_name"`
ObjectID string `json:"objectID"`
}
func main() {
var actors []Actor
data, _ := ioutil.ReadFile("actors.json")
_ = json.Unmarshal(data, &actors)
}
```
```java Java theme={"system"}
import java.io.FileInputStream;
import java.io.InputStream;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Actor {
// Getters/Setters omitted
private String name;
private String objectID;
private int rating;
private String imagePath;
private String alternativePath;
}
ObjectMapper objectMapper = Defaults.getObjectMapper();
InputStream input = new FileInputStream("actors.json");
Actor[] actors = objectMapper.readValue(input, Actor[].class);
```
```js JavaScript theme={"system"}
const records = require("./actors.json");
```
```kt Kotlin theme={"system"}
@Serializable
data class Actor(
val name: String,
val rating: Int,
val imagePath: String,
val alternativePath: String,
override val objectID: ObjectID
) : Indexable
val string = File("actors.json").readText()
val actors: List = Json.plain.parse(Actor.serializer().list, string)
```
```php PHP theme={"system"}
$records = json_decode(file_get_contents('actors.json'), true);
```
```python Python theme={"system"}
import json
with open('actors.json') as f:
records = json.load(f)
```
```ruby Ruby theme={"system"}
require 'json'
file = File.read('actors.json')
records = JSON.parse(file)
```
```scala Scala theme={"system"}
import org.json4s._
import org.json4s.native.JsonMethods._
case class Actor(name: String,
rating: Int,
image_path: String,
alternative_path: Option[String],
objectID: String)
object Main {
def main(args: Array[String]): Unit = {
val json = parse(new FileInputStream("actors.json")).extract[Seq[Actor]]
}
}
```
```swift Swift theme={"system"}
let filePath = Bundle.main.path(forResource: "actors", ofType: "json")!
let contentData = FileManager.default.contents(atPath: filePath)!
let records = try! JSONSerialization.jsonObject(with: contentData, options: []) as! [[String: Any]]
```
### From the source code directly
Only use this method for exploration purposes or if you have a small amount of data.
```cs C# theme={"system"}
using System.Collections.Generic;
public class Actor
{
public string Name { get; set; }
}
public class Program
{
public static void Main()
{
IEnumerable records = new List
{
new Actor { Name = "Tom Cruise" },
new Actor { Name = "Scarlett Johansson" }
};
}
}
```
```go Go theme={"system"}
actors := []Actor{
{Name: "Tom Cruise"},
{Name: "Scarlett Johansson"},
}
```
```java Java theme={"system"}
public class Person {
private String name;
public Person() {}
public String getName() {
return name;
}
public Person setName(String name) {
this.name = name;
return this;
}
}
ArrayList persons = new ArrayList() {{
add(new Person().setName("Tom Cruise"));
add(new Person().setName("Scarlett Johansson"));
}};
```
```js JavaScript theme={"system"}
const records = [{ name: "Tom Cruise" }, { name: "Scarlett Johansson" }];
```
```kt Kotlin theme={"system"}
val records = listOf(json { "name" to "Tom Cruise" }, json { "name" to "Scarlett Johansson" })
```
```php PHP theme={"system"}
$records = [
['name' => 'Tom Cruise'],
['name' => 'Scarlett Johansson']
];
```
```python Python theme={"system"}
records = [
{'name': 'Tom Cruise'},
{'name': 'Scarlett Johansson'}
]
```
```ruby Ruby theme={"system"}
records = [
{ name: 'Tom Cruise' },
{ name: 'Scarlett Johansson' }
]
```
```scala Scala theme={"system"}
case class Person(name: String)
val records = Seq(
Person("Tom Cruise"),
Person("Scarlett Johansson"),
)
```
```swift Swift theme={"system"}
let records: [[String: Any]] = [
["name": "Tom Cruise"],
["name": "Scarlett Johansson"],
]
```
## Send the data to Algolia
Once the records are ready, you can push them to Algolia using the [`saveObjects`](/doc/libraries/sdk/v1/methods/save-objects) method.
```cs C# theme={"system"}
var response = await client.SaveObjectsAsync(
"INDEX_NAME",
new List
### Send your data in batches
For better performance, send several records at once instead of one by one.
If you have many records to index,
[send them in batches](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/sending-records-in-batches).
## Next steps
After indexing your data, you can [configure relevance settings](/doc/guides/managing-results/relevance-overview).
# Incremental updates
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/incremental-updates
How to keep your data in sync with Algolia.
After initially uploading your data,
*you need to keep your up to date as the data changes*\*.
One way of doing so is with incremental updates,
by tracking changes and forwarding them to Algolia.
Whenever you add, edit, or delete data from your source,
update the corresponding so they reflect the latest state of your data.
## Track each record with a unique identifier
Incremental updates rely on the `objectID` attribute to match your source data to the right record.
Derive it from the unique identifier of the source item and keep it stable, otherwise an update adds a second record instead of replacing the first.
For more information, see [Unique record identifiers](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#unique-record-identifiers).
## Add records
Whenever you add new data in your data source,
you can add them to Algolia with the [`saveObjects`](/doc/libraries/sdk/methods/search/save-objects) method.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
```cs C# theme={"system"}
var response = await client.SaveObjectsAsync(
"INDEX_NAME",
new List
## Update records
You can update records in two ways:
* Fully replace the old record with a new one
* Update only a subset of the record (attributes) with new data
### Replace old records
When saving a record in Algolia, if a record with the specified `objectID` already exists in the index,
the engine replaces it.
You can replace an existing record by using the `saveObjects` method and specifying the `objectID`.
This technique is useful when you're updating data in a single place in your system, or when you don't know what changed.
```cs C# theme={"system"}
var response = await client.SaveObjectsAsync(
"INDEX_NAME",
new List
### Update a subset of the record
When you don't need to replace the entire record,
you can also update specific attributes of a record.
This is useful when you know which attribute to update,
but you don't know the other attributes,
for example, when different systems manage different aspects of your data.
For this, you can use the [`partialUpdateObjects`](/doc/libraries/sdk/methods/search/partial-update-objects) method and only pass the changed data.
Anything you don't specify remains untouched.
```cs C# theme={"system"}
var response = await client.PartialUpdateObjectsAsync(
"INDEX_NAME",
new List
## Delete records
Whenever you delete data in your data source,
you can delete the corresponding record from Algolia with [`deleteObjects`](/doc/libraries/sdk/methods/search/delete-objects).
```cs C# theme={"system"}
var response = await client.DeleteObjectsAsync(
"INDEX_NAME",
new List { "1", "2" }
);
```
```go Go theme={"system"}
response, err := client.DeleteObjects(
"INDEX_NAME",
[]string{"1", "2"})
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
List response = client.deleteObjects("INDEX_NAME", Arrays.asList("1", "2"));
```
```js JavaScript theme={"system"}
const response = await client.deleteObjects({ indexName: 'INDEX_NAME', objectIDs: ['1', '2'] });
```
```kotlin Kotlin theme={"system"}
var response =
client.deleteObjects(indexName = "INDEX_NAME", objectIDs = listOf("1", "2"))
```
```php PHP theme={"system"}
$response = $client->deleteObjects(
'INDEX_NAME',
[
'1',
'2',
],
);
```
```python Python theme={"system"}
response = client.delete_objects(
index_name="INDEX_NAME",
object_ids=[
"1",
"2",
],
)
```
```ruby Ruby theme={"system"}
response = client.delete_objects("INDEX_NAME", ["1", "2"])
```
```scala Scala theme={"system"}
val response = Await.result(
client.deleteObjects(
indexName = "INDEX_NAME",
objectIDs = Seq("1", "2")
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.deleteObjects(indexName: "INDEX_NAME", objectIDs: ["1", "2"])
```
### Delete by query
Sometimes, you may need to delete all records matching a certain filter.
Back to the book example, if you stop selling books from a specific publisher, you might want to delete all records matching this `publisher` in your index.
To do this, you can use the [`deleteBy`](/doc/libraries/sdk/methods/search/delete-by) method.
The [`deleteBy`](/doc/libraries/sdk/methods/search/delete-by) method is an expensive operation for the engine.
For better performance, use the [`deleteObjects`](/doc/libraries/sdk/methods/search/delete-objects) method instead.
```cs C# theme={"system"}
var response = await client.DeleteByAsync(
"INDEX_NAME",
new DeleteByParams { Filters = "brand:brandName" }
);
```
```dart Dart theme={"system"}
final response = await client.deleteBy(
indexName: "INDEX_NAME",
deleteByParams: DeleteByParams(
filters: "brand:brandName",
),
);
```
```go Go theme={"system"}
response, err := client.DeleteBy(client.NewApiDeleteByRequest(
"INDEX_NAME",
search.NewEmptyDeleteByParams().SetFilters("brand:brandName")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.deleteBy("INDEX_NAME", new DeleteByParams().setFilters("brand:brandName"));
```
```js JavaScript theme={"system"}
const response = await client.deleteBy({ indexName: 'theIndexName', deleteByParams: { filters: 'brand:brandName' } });
```
```kotlin Kotlin theme={"system"}
var response =
client.deleteBy(
indexName = "INDEX_NAME",
deleteByParams = DeleteByParams(filters = "brand:brandName"),
)
```
```php PHP theme={"system"}
$response = $client->deleteBy(
'INDEX_NAME',
['filters' => 'brand:brandName',
],
);
```
```python Python theme={"system"}
response = client.delete_by(
index_name="INDEX_NAME",
delete_by_params={
"filters": "brand:brandName",
},
)
```
```ruby Ruby theme={"system"}
response = client.delete_by("INDEX_NAME", Algolia::Search::DeleteByParams.new(filters: "brand:brandName"))
```
```scala Scala theme={"system"}
val response = Await.result(
client.deleteBy(
indexName = "INDEX_NAME",
deleteByParams = DeleteByParams(
filters = Some("brand:brandName")
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.deleteBy(
indexName: "INDEX_NAME",
deleteByParams: DeleteByParams(filters: "brand:brandName")
)
```
Any attribute you're using to delete by needs to be in your [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes).
# Post-indexing transformations
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/post-indexing-transformations
Apply post-indexing transformations to your data already in an Algolia index with the Algolia Index connector.
Post-indexing [transformations](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code) can help you optimize your data:
* **Data cleaning.** Correct inconsistencies or errors discovered after indexing, rename attributes, or change data types.
* **A/B testing.** Create variations of attributes for testing different ranking strategies.
* **Index optimizations.** Bucket, compute new values, convert units of measure.
To apply post-indexing transformations, use the Algolia Index connector.
The Algolia Index connector transforms the records in your index, not your source data.
It can't repair a transformation that wrote incorrect values, because those values are its input.
## Get started
1. Go to the Algolia dashboard and select your Algolia .
2. On the left sidebar, select **Data sources**.
3. On the [**Connectors**](https://dashboard.algolia.com/connectors/) page,
select **Algolia Index** connector, and click **Connect**.
4. Choose the index to transform.
5. Configure your [transformation](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code): create a new transformation or reuse an existing one.
6. Create the task. You can select two types of tasks:
* **On demand**. You can manually trigger the task from the [**Task**](https://dashboard.algolia.com/connectors/tasks) page in the dashboard, or using the [API](/doc/rest-api/ingestion/run-task).
* **Scheduled**. You can select a schedule from the list, or enter a custom schedule with a [cron expression](https://en.wikipedia.org/wiki/Cron).
### Costs
The Algolia Index connector browses your and re-applies the changes.
Depending on the indexing strategy you choose, this **adds read and write operations**.
These operations might lead to costs, depending on your plan.
To control how often your connector task runs, change the schedule in the task configuration.
## Performance considerations
If you don't need [an Algolia-managed transformation step](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code),
[send records directly to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/sending-records-in-batches) instead.
The Algolia Index connector adds overhead to your ingestion process.
### Filter the source index
If your transformation doesn't need to run over the whole index, set the `filters` property on the Algolia Index connector source to restrict the records the connector browses.
Filtering at the source reduces extraction time and the number of read operations on your index and the overall run duration.
Use the same syntax as the [Search API `filters` parameter](/doc/api-reference/api-parameters/filters).
Filtered attributes must be declared in [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting) on the source index.
## Constraints
* Index size: no limit on the index size.
* Duration: each run can't take longer than 60 minutes.
## How to select the right indexing strategy
Data drift happens when you have different sources of truth for your data.
To avoid **data drift**, select your indexing strategy when you use the Algolia Index connector.
The connector reads records from an Algolia index and applies transformations.
Make sure this workflow doesn't conflict with your primary ingestion method, such as the API or another connector.
### Key limitation for real-time updates and full record updates
Be careful when using the Algolia Index connector with ingestion methods that use the full record updates action with real-time updates.
Full record updates replace the entire record.
If a real-time update runs after the connector reads data from your index but before it completes the transformations, **any transformation applied by the connector is lost**.
That's because a real-time update will overwrite the entire record, including the transformed fields.
The following Algolia ecommerce integrations use the full record updates by default:
* [Shopify](/doc/integration/shopify/getting-started/quick-start)
* [Magento](/doc/integration/magento-2/getting-started/quick-start)
* [Salesforce](/doc/integration/salesforce-commerce-cloud-b2c/getting-started/introduction)
* [BigCommerce](/doc/integration/bigcommerce/get-started/installation)
* [commercetools](/doc/integration/commercetools/get-started/installation)
### Other connectors and API clients using partial updates
Connectors and API clients using partial updates work with the Algolia Index connector.
Partial updates ensure that only the specified attributes are modified, preserving other attributes and avoiding conflicts.
To avoid data loss, select your indexing strategy when using the Algolia Index connector and prefer partial updates whenever possible.
* [**Partial record updates (recommended)**](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies#partial-record-updates) allow creating, updating, and deleting during the connector run. However, concurrent field updates will be overwritten.
* [**Full record updates**](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies#full-record-updates) replace individual records, overriding changes made during the connector run. Avoid this strategy.
* [**Full reindexing**](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies#full-reindexing) replaces your index, overriding any changes made during the connector run. Avoid this indexing strategy.
### Example scenario of conflicts
The following scenario illustrates a potential issue when two connectors try to update the same index:
1. API client:
* Ingests product data, including `price`, `category`, and `inventory`.
* Uses "Partial Update" action.
* Runs on a cron job every hour.
2. Algolia Index connector:
* Reads records from the Algolia index and adds a new field, `discounted_price`, based on the `price`.
* Uses "Partial Update" action.
### Potential issue
An issue can arise if the Algolia Index connector runs for a long time.
For example, if the API client updates a product's price at 10 AM,
the Algolia Index connector starts at 10:07 AM, and finishes after 11 AM,
the calculated `discounted_price` attribute will be outdated.
At 11 AM, the API client already updates the price again, which means the `discounted_price` attribute will be based on an outdated `price` attribute.
### Recommendation
To avoid such issues, ensure that the Algolia Index connector never runs concurrently with any other indexing operation.
You can also [programmatically trigger your connector](/doc/libraries/sdk/methods/ingestion/run-task) after your other indexing operations.
# Sending records in batches
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/sending-records-in-batches
Learn how to send record updates to Algolia for faster indexing and less network calls.
When [sending data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data),
it's best to send several simultaneously instead of individually.
It reduces network calls and speeds up indexing,
especially when you have a lot of records,
but everyone should send indexing operations in batches whenever possible.
For example, you might decide to send all the data from your database and end up with a million records to index.
That's too big to send all at once because Algolia limits you to 1 GB per batch per request.
In reality, sending that much data in a single network call would fail before reaching the API.
You could loop over each record and send them with the [`saveObjects`](/doc/libraries/sdk/v1/methods/save-objects) method.
The problem is that **you would perform a million individual network calls**,
which would take way too long and saturate your Algolia cluster with indexing jobs.
A better approach is to **split your collection of records into smaller collections, then send each chunk one by one.**
For optimal indexing performance, aim for a batch size of about 10 MB,
representing between 1,000 and 10,000 records, depending on the average record size.
* **Batching records doesn't reduce your operations count.** [Algolia counts indexing operations per record](https://support.algolia.com/hc/en-us/articles/18138875086865), not per method call, so from a pricing perspective, batching records is the same as indexing records individually.
* **Be careful when approaching your plan's [maximum number of records](https://www.algolia.com/pricing/).** If you're close to the record limit,
batch operations may fail.
The error message
"You have exceeded your Record quota" means the engine doesn't know if the batch operation will update records or add new ones.
If this happens, upgrade to a plan with a higher record limit or reduce your batch size.
## Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
When using the [`saveObjects`](/doc/libraries/sdk/v1/methods/save-objects) method,
the API clients automatically chunk your records into batches of 1,000.
With this approach, you would make 100 API calls instead of 1,000,000.
Depending on your records' sizes and your network speed, you could create bigger or smaller chunks.
```cs C# theme={"system"}
var response = await client.SaveObjectsAsync(
"INDEX_NAME",
new List
If you use an Algolia-managed transformation to process records,
use [`saveObjectsWithTransformation`](/doc/libraries/sdk/methods/search/save-objects-with-transformation) instead of `saveObjects`.
Like `saveObjects`, it uploads records in batches, but sends each batch through the configured
[Push to Algolia connector](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push).
For more control over the batch size or indexing action,
call [`chunkedPush`](/doc/libraries/sdk/methods/ingestion/chunked-push) on the Ingestion client directly.
For more information, see:
* [Large indexing jobs](/doc/guides/scaling/scaling-to-larger-datasets)
* [Import with the API](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api)
## Using the dashboard
You can also send your records in your Algolia dashboard.
### Add records manually
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Open the **Add records** menu and select **Add manually**.
5. Paste your records in the JSON editor and click **Save**.
### Upload a file
1. Go to your dashboard and select your index.
2. Click the **Add records** tab and select **Upload file**.
3. Select the file you want to upload and click **Upload**.
For more information, see [Import from the dashboard](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard).
# Transform your data with code
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-with-code
Transform your data with JavaScript before writing to your Algolia index.
You can add transformations to your connector's destination to manipulate data before it's written to your Algolia indices.
To use transformations, create a task that specifies the data source and the destination (usually an Algolia ).
Add transformations during task creation or apply them directly to existing destinations.
To learn more about connectors, read the [Connectors overview](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors).
Code transformations are small JavaScript functions.
You can only add one transformation to a destination.
## Get started with code transformations
1. Go to the Algolia dashboard and select your Algolia .
2. On the left sidebar, select **Data sources**.
3. Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
4. Select a connector and click **Connect** to start creating your task.
5. Configure your data source: create a new source or select an existing one.
6. To create your transformation function, click **Create transformation**.
When writing a transformation, you can:
* Use the code editor, which includes autocompletion and syntax highlighting.
For more information, see [Write your transformation](#write-your-transformation).
* Use predefined [helper functions](#helper-functions) for common Algolia-specific tasks.
To use a helper function, click **Helper functions** and select from the list.
7. Configure your destination: create a new destination or select an existing one.
8. Configure when your task should run and specify the operation type for writing your Algolia .
9. Confirm whether you want to run your task immediately.
To see your transformation in action, click **Run** and inspect your index records after the task finishes.
## Write your transformation
The signature of the transformation function is as follows.
You can't change its names or its arguments:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// Your transformation code here
}
```
The transformation function returns a record or an array of records and receives two arguments:
* `record`. The record to transform.
It's an editable JavaScript object representing a data source record,
like a JSON array item or a CSV row.
* `helper`. A helper object that provides access to utility methods and contextual information to help you build your transformation.
The term **helper** can refer to two related but distinct concepts:
* **Helper object methods.**
Methods available on the `helper` object passed to the transformation function.
These give access to secrets and metadata.
* **Helper functions.**
Pre-built utilities that help you change or enrich your records during transformation.
### Helper object methods
The `helper` object includes utility methods for accessing secrets and metadata:
* `helper.secrets.get(secretName)`: retrieve a secret value from the vault.
* `helper.getMetadata().appID`: retrieve the Algolia for the running task.
* `helper.getMetadata().indexName`: retrieve the target index name for the record.
* `helper.getMetadata().taskID`: retrieve the ID of the running task.
* `helper.getMetadata().actionType`: retrieve the action type for this record. For more information about possible values, see the [API reference documentation](/doc/libraries/sdk/v1/methods/batch#param-action).
## Helper functions
Use these pre-built functions to change or enrich your records:
| Helper function | Description |
| - | - |
| Reshape | Add or remove record attributes. |
| Filters | Exclude records based on conditions. |
| Convert | Change attribute data types, trim or normalize values, and convert to ISO formats. |
| Compute | Create new attributes, such as a calculated discount price. |
| Ranking | Optimize [custom ranking](/doc/guides/managing-results/must-do/custom-ranking) by prioritizing recent activity and adjusting attribute importance. |
| Query Categorization | Convert a flat list into a hierarchical category structure. |
| Fetch/API | [Enrich records with data from external APIs.](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/enrich-your-records-with-fetch) |
## Preview transformation results
When you create a transformation, you can preview the function on the right side of the code editor.
The preview includes the following information:
* **Input Record**.
A sample record, taken from the data source before applying the transformation.
* **Transformation Preview**.
Click **Try it out** to see the results of your transformation on the sample record.
Not all connectors support fetching sample records.
## Debug transformations
Use the [**Connector Debugger**](https://dashboard.algolia.com/connectors/debugger) for insights into the status of your tasks,
including transformations.
## Error handling
If your transformation function throws an error, it may fail the task depending on the task type and setup.
Take care to avoid errors when writing transformation functions.
Use `try` and `catch` blocks to handle errors gracefully.
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
try {
// Your transformation code here.
} catch (error) {
// Return the original record to avoid breaking the task,
// or return undefined to delete and ignore the record.
return record;
}
}
```
Also make allowances for when your transformation function doesn't work as expected.
For example, use [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) to access missing properties.
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// Accessing a property that may not exist.
const value = record?.property?.nestedProperty;
// Your transformation code here.
}
```
### Broken transformations and consecutive failures
Algolia stops your transformation if it encounters several consecutive failures within a short period.
The transformation restarts if its code changes or after a specific time.
To avoid this, write transformation functions to [avoid errors](#error-handling) and [preview the transformation](#preview-transformation-results) before running it.
## Performance considerations
Adding a transformation adds an overhead to the ingestion process.
Try to keep the JavaScript transformation as simple as possible.
## What transformations can't do
Transformations process one record at a time.
They can return zero, one, or more records.
Transformations have two limits:
* **External data.**
To add data from a different system to a record, a transformation can call a third-party API.
The request must complete within the [fetch timeout and response-size limits](/doc/guides/scaling/algolia-service-limits#data-transformation-and-fetch-limits).
For more information, see [Enrich your records with fetch](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/enrich-your-records-with-fetch).
* **Operations across records.**
Some operations need access to more than one record, for example, deduplicating records or calculating an attribute from your full index.
Transformations process each record independently and can't do these operations.
## Indexing strategy impact
Data transformations change your records before indexing.
By default, the entire record is returned after transformation.
However, the correct approach depends on the [indexing strategy](/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies) you choose.
The following example adds a new property to the record and returns it.
This is the default behavior.
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
record.newProperty = "foo";
return record;
}
```
If your data has several sources such as API clients, connectors, or ecommerce integrations, choose an indexing strategy that avoids data conflicts.
Sometimes, returning the whole record can lead to data loss or other unwanted behavior.
The following sections describe various synchronization strategies and how they influence output.
### Full reindexing and full record updates
Since these methods completely replace an existing record,
your transformation must return the entire modified record, including any additions, deletions, or changes.
Change an existing or a new record:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// Add a property.
record.newProperty = "foo";
// Remove a property.
delete record.existingProperty;
// Modify a property.
record.existingProperty2 = "a new value";
// Return the full modified record.
return record;
}
```
Delete record:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
return undefined;
}
```
Every record your transformation returns needs its own `objectID`.
Build it from the source record's identifier plus a deterministic suffix so the same input always produces the same object ID.
For more information, see [Keep object IDs stable](/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/what-is-in-a-record#keep-object-ids-stable).
Create new records:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// Create a new record with an object ID derived from the source record
newRecord = {};
newRecord.objectID = `${record.objectID}-summary`;
// Create a new object from the current record. Change the objectID!
copyRecord = { ...record };
copyRecord.objectID = `${record.objectID}-copy`;
// Return the initial, copy, and new record
return [record, newRecord, copyRecord];
}
```
### Partial record updates
This method only updates specific attributes
Your transformation should only return the object ID and the modified or added attributes.
Existing attributes not included in the returned object remain unchanged.
You can't delete records with partial updates.
Change an existing record:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// We only keep the objectID
let partialRecord = { objectID: record.objectID };
// We only add or modify what we need.
partialRecord.newProperty = "foo";
partialRecord.existingProperty = "new value";
return partialRecord;
}
```
Add new records:
```js JavaScript icon=code theme={"system"}
async function transform(record, helper) {
// Create a brand new record with an object ID derived from the source record
newRecord = {};
newRecord.objectID = `${record.objectID}-summary`;
// Return the new record. The original record will be untouched.
return [newRecord];
}
```
# Transform your data without code
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/transform-your-data-without-code
Transform your data before writing to your Algolia index.
You can add *transformations* to your connector's destination to manipulate data before it's written to your Algolia indices.
To use transformations, create a task that specifies the data source *and* the destination (usually an Algolia ).
Add transformations during task creation or apply them directly to existing destinations.
To learn more about connectors, read the [Connectors](/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors) overview.
## Get started with no-code transformations
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Data sources**.
3. Open the [**Connectors**](https://dashboard.algolia.com/connectors) page.
4. Select a connector and click **Connect** to start creating your task.
5. Configure your data source: create a new source or select an existing one.
6. To create your transformation, click **Create a new transformation** and select **Transform without code**.
To learn more about the available transformation steps, see [Create your transformation](#create-your-transformation).
7. Configure your destination: create a new destination or select an existing one.
8. Configure when your task should run and specify the operation type for writing your Algolia .
9. Confirm whether you want to run your task immediately.
To see your transformation in action, click **Run** and inspect your index records after the task finishes.
## Create your transformation
A no-code transformation consists of one or more steps.
Each step can include a condition,
so it only applies to records that meet specific criteria.
If any of the steps fail, the entire transformation will be marked as failed.
### Available step types
| Step type | Description |
| - | - |
| [Add record attribute](#add-attribute) | Adds a new attribute to a record. The value can be a fixed value or calculated from other attributes in the record. |
| [Remove record attribute](#delete-attribute) | Removes an attribute from a record. |
| [Remove records](#remove-records) | Removes records based on a condition. For example, you can remove records with a price below a certain threshold. |
#### Add attribute
Use the **Add Attribute** step to create a new attribute in your records,
or to update the value of an existing one:
1. On the **Transformation** page, select **Create Attribute**.
2. Enter the name of the attribute you want to add or update.
If the attribute doesn't exist yet, its added to each matching record.
If the attribute already exists, its value is replaced.
If you enter an existing attribute, the values are updated.
Use *dot* notation for nested attributes, such as `prices.discount_pct`.
3. Optional: [Add a condition](#conditions) to select the records for adding or updating attributes.
4. Specify the value for the new or updated attribute:
* To assign the same value to each record matching the conditions,
enter a value, for example, `true`, `10`, or `new`.
* To assign a computed value based on other attributes,
enter a formula, for example, `price * discount_pct`.
**Computed values** are based on other record attributes.
For example, to calculate the discounted price of a product, use the formula:
`price * (1 - discount_pct)`,
where `price` and `discount_pct` are attributes in the record.
For a list of available functions you can use to compute values,
see [Function reference](#function-reference).
### Delete attribute
Use the **Delete Attribute** step to delete an attribute from your records.
This is useful for cleaning up data or removing fields you don't want to send to your Algolia index.
1. Select **Delete Attribute** as the step type.
2. Enter the name of the attribute you want to delete.
For nested attributes, use dot notation, for example, `prices.discount_pct`.
Only that nested field will be deleted, not the entire attribute.
3. Optional: [Add a condition](#conditions) to determine from which records the attribute is removed.
### Remove records
Use the **Remove Records** step to remove records from your index based on specific conditions.
This is useful for excluding records that don't meet your criteria before sending them to your Algolia index.
1. Choose **Remove Records** as the step type.
2. [Add a condition](#conditions) to determine which records to remove.
## Conditions
Conditions let you control when a transformation step runs for a particular record:
* **Add Attribute:** Only add or modify an attribute to records that meet the conditions.
* **Delete Attribute:** Only delete attributes from records that meet the conditions.
* **Remove Records:** Only remove records that meet the conditions.
### Parts of conditions
Conditions consist of the following elements:
* **Field.**
The attribute to check, for example, `category` or `price`.
* **Operator.**
The comparison to perform, for example, `equals`, `does not equal`, or `greater than`.
* **Attribute type.**
The data type of the field: String, Number, or Boolean.
This specifies *how* to compare the field to the value.
* **Value.**
The value to compare the field against, for example, `"food"`, `200`, or `true`.
### Example: a basic condition
* Field: `category`
* Operator: `Equals`
* Type: `String`
* Value: `Food`
This condition selects records where the `category` attribute has the value `Food`.
### Multiple conditionals
You can combine multiple conditionals using **AND** and **OR** logic.
For example:
* `category equals "food" AND price < 200`
For more complex logic, you can **group** conditionals.
This is similar to using parentheses in a formula.
* `category equals "food" AND (price < 200 OR discount == true)`
Grouping conditions changes the order in which conditions are evaluated,
allowing for complex logic.
### Supported attribute types
| Type | Description |
| - | - |
| String | A string of text, for example: `food` |
| Number | A number, for example: `200` |
| Boolean | A boolean value: true or false |
Array and object types aren't supported in no-code transformations.
## Preview your transformation
At any point during setup, you can preview your transformation.
The preview feature lets you apply your transformation to a sample of your data,
so you can compare the results before and after the changes.
If you data source doesn't support previewing,
you can still preview your transformation by copying a sample record into the text area available in preview.
Not all data sources support previewing.
## Limitations
* You can only add one transformation to a destination.
* Support for data sampling in preview is limited to these data sources:
* Algolia index
* CSV
* JSON
* Edits to a transformation don't support sampling.
* In a given task, you can use either no-code or code transformations, not both.
* Transforming complex records structures (arrays and objects) isn't supported.
## Function reference
The following table lists available functions for [computed values](#add-attribute).
For more information, see the [Expr](https://expr-lang.org/docs/language-definition) documentation.
| Function | Description | Example usage |
| - | - | - |
| `mean([x, y, ...)]` | Returns the mean of the numbers. | `mean([1, 2, 3])` → `2` |
| `median([x,y, ...])` | Returns the median of the numbers. | `median([1, 2, 3])` → `2` |
| `round(x)` | Rounds `x` to the nearest integer. | `round(2.5)` → `3` |
| `max(a, b)` | Returns the largest of two values | `max(1, 5)` → `5` |
| `min(a, b)` | Returns the smallest of two values. | `min(1, 5, 3)` → `1` |
| `len(x)` | Returns the length of a string. | `len("abc")` → `3` |
| `lower(x)` | Converts a string to lowercase. | `lower("HELLO")` → `"hello"` |
| `upper(x)` | Converts a string to uppercase. | `upper("hello")` → `"HELLO"` |
| `replace(s, old, new)` | Replace all occurrences of `old` with `new` in `s`. | `replace("foo", "o", "a")` → `"faa"` |
| `trim(str[,chars])` | Trim whitespace from start and end of string. Optionally trim `chars`. | `trim(" foo ")` → `"foo"` |
| `split(s, sep)` | Splits string `s` into an array using separator `sep`. | `split("a,b,c", ",")` → `["a","b","c"]` |
| `join(arr, sep)` | Joins elements of array `arr` into a string, separated by `sep`. | `join(["a","b"], ",")` → `"a,b"` |
| `uniq([x,y,...]`) | Removes duplicate values from an array. | `uniq([1, 2, 2, 3])` → `[1, 2, 3]` |
| `now()` | Returns the current time. | `now()` → `"2024-06-07T12:34:56Z"` |
# Handling concurrency with versioning
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/handling-concurrency-with-versioning
Learn how to use versioning and optimistic locking in your indexing pipeline to guarantee data consistency, even in concurrent environments.
If you have multiple indexing processes running, one process could try to update a record at the same time as another.
If this happens, your might turn out differently than expected.
If you have an ecommerce website, and your backend runs two processes to index its changes to Algolia with the [`partialUpdateObjects`](/doc/libraries/sdk/v1/methods/partial-update-objects) method.
One process sends an update to set the stock of a product from `10` to `20`, because you just received ten new items in a shipment.
Simultaneously, another process sets the stock for this product from `10` to `9` because of an order that just came in.
Depending on which job wins the race condition, the stock is now either `9` or `20`, even though the correct stock is `19`.
## Versioning
One way to handle concurrency is the [`IncrementSet`](/doc/libraries/sdk/v1/methods/partial-update-objects#update-only-if-the-value-is-greater-than-a-numeric-attribute) [operation](/doc/libraries/sdk/v1/methods/partial-update-objects#built-in-operations).
It lets you update a record only if you provide a higher numerical value than already exists in the record,
or a value higher than 0 if the record doesn't exist.
A great use-case for this is adding a Unix timestamp to your records.
With [`IncrementSet`](/doc/libraries/sdk/v1/methods/partial-update-objects#update-only-if-the-value-is-greater-than-a-numeric-attribute),
Algolia only updates your record if the timestamp you sent is more recent than the existing one.
If you update in the meantime,
you prevent Algolia from overriding your record:
**only the latest update is taken into account**.
## Optimistic locking
Another way to handle concurrency is with the [`IncrementFrom`](/doc/libraries/sdk/v1/methods/partial-update-objects#update-only-if-the-value-matches-a-numeric-attribute) [operation](/doc/libraries/sdk/v1/methods/partial-update-objects#built-in-operations).
It lets you update a record only if you provide the current version of that record, or 0 if the record doesn't exist.
A typical use-case is when you perform a read-modify-write sequence:
1. Read a record from your .
2. Make the changes you want to make.
3. Provide the [`IncrementFrom`](/doc/libraries/sdk/v1/methods/partial-update-objects#update-only-if-the-value-matches-a-numeric-attribute) operation with the old value of your versioning attribute.
With this approach, Algolia only updates your record if the value from your [`IncrementFrom`](/doc/libraries/sdk/v1/methods/partial-update-objects#update-only-if-the-value-matches-a-numeric-attribute) operation matches the current value currently in the record,
and increments it.
**If another process incremented this value in the meantime, Algolia ignores the update.**
Algolia doesn't provide feedback when it rejects an update because of versioning or optimistic locking.
You have to wait for the indexing operation to complete,
then fetch the record to compare it to the desired result.
An ignored operation won't necessarily show up as published with the [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) API method.
# Index operations are asynchronous
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/index-operations-are-asynchronous
Understand that all indexing operations are asynchronous and learn how to wait for a task to finish.
**All indexing operations run asynchronously.**
When you call indexing methods, you add a new job to a queue.
The job, *not* the method itself, performs the desired action.
Jobs usually finish within seconds or even milliseconds but processing time depends on the queue length.
When the queue has many pending tasks, the job waits its turn.
## When to wait for tasks
To help manage asynchronous jobs, each method returns a unique `taskId`, which you can use with the [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) method.
This ensures the job completes before the next request runs.
Some common scenarios where `waitTask` is useful include:
* **Managing dependencies**: you want to use `waitTask` to manage dependencies, for example, when deleting an before creating a new one with the same name or clearing an index before adding new .
* **Atomic reindexing**: atomic reindexing is a way to update all your records without any downtime, by populating a temporary index and replacing the destination index with it. **You don't need to implement this by yourself.** Instead, you can use the [`replaceAllObjects`](/doc/libraries/sdk/v1/methods/replace-all-objects) method which does it all for you.
* **Frontend events**: if you're building a frontend interface that performs indexing operations, you may want to react to completed tasks.
For example, you may want to display a success message when an indexing operation has completed, or refresh a page, or move on with some following, co-dependent operations, etc.
* **Debugging**: use `waitTask` when testing a search immediately after updating an index.
## When not to wait for tasks
In most scenarios, you don't need to wait for tasks to complete before moving on with new ones.
Using [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) makes your indexing operations synchronous, which slows them down.
Don't use `waitTask` instead of your language's native asynchronous features.
Ensure that tasks are queued in order by using promises or callbacks.
# Data synchronization strategies
Source: https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies
Learn about the different ways to reindex your data into Algolia whenever your content changes.
Once you've indexed your data, **you need to keep it up to date as the data changes**.
Most of the time, you need to reindex in the following scenarios:
* You've changed your structure.
* Your source has changed since the last update.
* You need to resolve "data drift" which happens when you have different sources of data, making it difficult to track changes.
You can update your index in one of several ways:
* **Full reindexing**: replace your entire index with a new set of .
* **Full record updates**: replace an entire record with a new one.
* **Partial record updates**: update only a subset of attributes within a record.
The appropriate method depends on your use case.
## Choose an update frequency
How often you update depends on **how often your content changes and how fast you need it to be searchable**.
Take the example of an ecommerce store:
* Update price changes and product availability in real time.
* Send the number of sales, used for custom ranking, periodically in batches.
Aim for a balance between getting fresh information into the search experience as fast as possible
and [reducing the number of operations](https://support.algolia.com/hc/en-us/articles/18138875086865),
which affects your indexing performance and, potentially, your Algolia bill.
## Full reindexing
When performing a full reindex, **you're removing all existing records and replacing them with a fresh set.**
This is ideal when your data changes a lot or when there's no way for you to track it.
For example, if you have a content site that constantly changes,
such as technical documentation,
[split long pages into multiple records](/doc/guides/sending-and-managing-data/prepare-your-data/how-to/indexing-long-documents) to improve the relevance of your search results.
This makes tracking changes difficult, because adding a single sentence in the middle of a page can shift the entire record structure.
Also, depending on the structure of the site and how you update it, it might be impossible to know what exact pages were impacted.
**It's a typical scenario where full reindexing is necessary.**
You can fully reindex your data with API clients or from the Connectors page in the Algolia dashboard:
* **API client**: use the [`replaceAllObjects`](/doc/libraries/sdk/v1/methods/replace-all-objects) method.
This ensures there's no downtime during the reindex, and your search is always available.
* **[Connectors](https://dashboard.algolia.com/connectors/tasks)**: edit the task and choose **Full reindexing** (or choose that option during setup).
A full reindex [can significantly increase your indexing operations count](https://support.algolia.com/hc/en-us/articles/18138875086865).
It's also slower than incremental updates, because you're reindexing every single record.
This is a powerful way of keeping your data up to date, but **be mindful of the impact on your operations count and indexing pipeline.**
## Full record updates
If you can track changes to your data and are able to identify affected records, then you can update records individually.
For example, suppose you're running a forum with a search to look for members.
Your records have a one to one relationship with member entities in your data.
In this scenario, you can update each record whenever the entity changes.
Whenever you add, update, or delete a member, you could also perform an incremental update to your Algolia index.
You can update individual records with an API client or from the Connectors page in the Algolia dashboard:
* **API client**: use the [`saveObjects`](/doc/libraries/sdk/v1/methods/save-objects) method and specify the `objectID` of the record to update.
* **[Connectors](https://dashboard.algolia.com/connectors/tasks)**: edit the task and choose **Full record updates** (or choose that option during setup).
## Partial record updates
Full record updates work when you're able to specify the full data to put in the record.
Yet, there are cases when you only know the part of the data to update, and not the rest of the data.
In those situations, you can perform partial record updates with only the data to change.
Consider an online store with independent systems for content management and user reviews.
When a product's description changes, your CMS doesn't know how many reviews the product has.
Conversely, whenever a user posts a review, the API of your third-party service can only provide you with this data.
In this case, you would probably want to centralize updates and perform partial record updates based on the data you receive.
You can update individual attributes in records with an API client or from the Connectors page in the Algolia dashboard:
* **API client**: use the [`partialUpdateObjects`](/doc/libraries/sdk/v1/methods/partial-update-objects) method.
* **[Connectors](https://dashboard.algolia.com/connectors/tasks)**: edit the task and choose **Partial record updates** (or choose that option during setup).
## See also
To learn more, see also [Incremental updates](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/incremental-updates)
# NeuralSearch A/B testing
Source: https://www.algolia.com/doc/guides/ai-relevance/neuralsearch/ab-testing
Measure the impact of NeuralSearch by running controlled experiments before rolling out to all traffic.
Before you enable NeuralSearch for all search traffic,
run an A/B test to measure how it affects your search metrics.
NeuralSearch uses Algolia's standard A/B testing features.
You can create tests with the A/B testing API or from the Algolia dashboard.
## Choose a test type
You can run one of these test types:
| Test type | Control | Variant | When to use |
| - | - | - | - |
| Keyword vs NeuralSearch | Keyword search | NeuralSearch | Compare keyword search with NeuralSearch |
| Adaptive Intent test | NeuralSearch | NeuralSearch with Adaptive Intent | Compare NeuralSearch to NeuralSearch with [Adaptive Intent](/doc/guides/ai-relevance/neuralsearch/adaptive-intent) |
### Compare keyword search with NeuralSearch
Your index must have NeuralSearch enabled before you can launch an A/B test.
NeuralSearch can be in one of these states:
* **Inactive**: NeuralSearch isn't configured for the index.
* **Preview**: NeuralSearch is configured for testing but doesn't serve *all* search traffic.
* **Active**: NeuralSearch is enabled for the index's search traffic.
To change the NeuralSearch state for an index, set [`neuralSearchMode`](/doc/guides/ai-relevance/neuralsearch/get-started#parameters) appropriately.
This A/B test evaluates whether NeuralSearch improves your existing search experience.
* The control uses the index's default keyword search.
* The variant enables NeuralSearch.
```sh Command line expandable icon=square-terminal theme={"system"}
curl --request POST \
--url https://analytics.algolia.com/3/abtests \
--header 'content-type: application/json' \
--header 'x-algolia-api-key: {ALGOLIA_ANALYTICS_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}' \
--data '{
"name": "Keyword vs NeuralSearch",
"variants": [
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"description": "Keyword search (control)"
},
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"customSearchParameters": {
"enableNeuralSearch": true
},
"description": "NeuralSearch (variant)"
}
],
"metrics": [
{
"name": "conversionRate"
}
],
"endAt": "2026-12-31T00:00:00.000Z"
}'
```
### Compare NeuralSearch to NeuralSearch with Adaptive Intent
This A/B test measures whether [Adaptive Intent](/doc/guides/ai-relevance/neuralsearch/adaptive-intent) improves NeuralSearch results.
* The control uses NeuralSearch without Adaptive Intent.
* The variant enables Adaptive Intent.
```sh Command line expandable icon=square-terminal theme={"system"}
curl --request POST \
--url https://analytics.algolia.com/3/abtests \
--header 'content-type: application/json' \
--header 'x-algolia-api-key: {ALGOLIA_ANALYTICS_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}' \
--data '{
"name": "NeuralSearch vs NeuralSearch with Adaptive Intent",
"variants": [
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"description": "NeuralSearch (control)"
},
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"customSearchParameters": {
"adiConfig": {
"enabled": true
}
},
"description": "NeuralSearch with Adaptive Intent (variant)"
}
],
"metrics": [
{
"name": "conversionRate"
}
],
"endAt": "2026-12-31T00:00:00.000Z"
}'
```
## Start tests from the Algolia dashboard
To start NeuralSearch A/B tests from the Algolia dashboard:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/) and select your Algolia application.
2. Select your index.
3. Click the **NeuralSearch** tab.
4. Click **Launch an A/B test**.
5. Select the test type: **Keyword vs NeuralSearch** or **Adaptive Intent test**.
6. Set the traffic split and test duration.
7. Click **Create test**.
The Algolia dashboard pre-fills the required `customSearchParameters` for each test type, so you don't need to configure the API parameters manually.
## Test metrics
Algolia tracks these metrics automatically for every NeuralSearch A/B test:
* Conversion rate
* Click-through rate
* Add-to-cart rate
* No-results rate
* Average click position
For more information, see [Click and conversion events](/doc/guides/sending-events/concepts/event-types).
## Test duration
Run tests long enough to reach statistical significance:
* Tests should run for at least 14 days to account for weekly traffic patterns.
* Aim for a minimum increase of 3% on conversion rate.
* Indices with more traffic usually reach statistical significance faster.
## Stopping a test
Stop tests from the [Algolia dashboard](/doc/guides/ab-testing) or with the [A/B Testing API](/doc/rest-api/abtesting-v3/stop-ab-test).
After you stop a test, traffic returns to the index's active configuration automatically.
# Adaptive Intent
Source: https://www.algolia.com/doc/guides/ai-relevance/neuralsearch/adaptive-intent
Improve NeuralSearch results for high-traffic queries using your engagement data.
**Adaptive Intent** improves NeuralSearch results on high-volume, high-value queries:
the searches that drive the most engagement on your site.
Without Adaptive Intent, NeuralSearch relies on general semantic similarity.
Terms specific to *your* data, such as brand names, niche terminology, or regional product names, might not match the records users expect.
Adaptive Intent solves this by learning from the records your users actually click and convert on.
## How Adaptive Intent works
For each query with enough engagement, Adaptive Intent builds a **bag**.
The bag combines records associated with clicks and conversions for that query.
The bag takes the form of a vector, weighted by engagement signal strength:
* Conversions count 10× more than clicks, so records with stronger engagement have more influence
* The bag refreshes regularly as new engagement data becomes available.
* At query time, NeuralSearch uses the bag instead of the vector generated from the query text. This helps search results reflect user intent, not just semantic similarity.
Queries without enough engagement data continue to use NeuralSearch as usual.
### Where Adaptive Intent helps most
Adaptive Intent addresses problems that standard NeuralSearch struggles with:
* **Brand names and niche terminology.** Generic language models might not recognize terms specific to your data: sporting goods brands, regional product names, specialty vocabulary. Adaptive Intent learns these from your data without manual work.
* **Ambiguous queries.** Short or vague queries such as "bottle" or "hydro" can match unintended records when relying on generic semantic similarity. Adaptive Intent resolves ambiguity from users' past behavior.
* **Conversion misalignment.** Semantically relevant results aren't always the ones that convert. Adaptive Intent bridges this gap by learning which results users engage with for each query.
## Before you begin
To use Adaptive Intent, your index must:
* Use NeuralSearch
* Send click and conversion events to Algolia
Adaptive Intent activates automatically for queries that meet a minimum engagement threshold.
At least 60 engagement points (1 point for each click and 10 points for each conversion) across at least three records.
Queries below this threshold fall back to standard NeuralSearch with no degradation.
If you're using Dynamic Re-Ranking or Algolia A/B testing,
you're probably already sending the required events.
## Enable Adaptive Intent from the dashboard
1. Go to the Algolia dashboard and select your Algolia application and index.
2. Click the **NeuralSearch** tab.
3. Open **Settings**.
4. Turn on **Adaptive Intent**.
Adaptive Intent trains on your engagement data and activates for qualifying queries automatically.
## Enable Adaptive Intent with the API
Adaptive Intent is controlled through the `adiConfig` extension in the Search API.
To enable it on a per-query basis or within an A/B test:
```sh Command line expandable icon=square-terminal theme={"system"}
curl --request POST \
--url https://{ALGOLIA_APPLICATION_ID}.algolia.net/1/indexes/{INDEX_NAME}/query \
--header 'content-type: application/json' \
--header 'x-algolia-api-key: {ALGOLIA_ANALYTICS_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}' \
--data '{
"query": "your search query",
"adiConfig": {
"enabled": true
}
}'
```
## Monitor and manage Adaptive Intent
Use the Algolia dashboard to check which queries use Adaptive Intent and compare their results with other retrieval strategies.
### Review trained queries in Query Explorer
To see the impact of Adaptive Intent on individual queries, go to the **NeuralSearch** tab, click **Compare**, and open the **Query Explorer**.
For each trained query, the Query Explorer shows:
* **Number of documents.** How many records are in the training set for that query.
* **Similarity score** across training documents. A high score may indicate overfitting, while a lower score suggests a more diverse training set.
* **Training status:** whether Adaptive Intent is active for that query.
Turn Adaptive Intent on or off for any query with the toggle in the Query Explorer.
When you turn off Adaptive Intent for a query, it uses standard NeuralSearch vector retrieval.
### Compare retrieval strategies
To see how Adaptive Intent affects a specific query's results:
1. Go to the **Compare** tab in the NeuralSearch section of the Algolia dashboard.
2. Enter a query that has Adaptive Intent training.
3. Use the toggles on each side to switch between keyword search, vector search, and NeuralSearch with Adaptive Intent.
This lets you compare all three retrieval strategies side by side for the same query.
## Run an A/B test with Adaptive Intent
To measure Adaptive Intent's impact before you enable it for all traffic, run an A/B test.
### Launch an Adaptive Intent test
Before starting the test, check that **Adaptive Intent** in your NeuralSearch settings is set to **Disabled**.
This ensures the control variant doesn't use Adaptive Intent.
```sh Command line expandable icon=square-terminal theme={"system"}
curl --request POST \
--url https://analytics.algolia.com/2/abtests \
--header 'content-type: application/json' \
--header 'x-algolia-api-key: {ALGOLIA_ANALYTICS_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}' \
--data '{
"name": "NeuralSearch vs NeuralSearch with Adaptive Intent",
"variants": [
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"description": "NeuralSearch (control)"
},
{
"index": "{INDEX_NAME}",
"trafficPercentage": 50,
"customSearchParameters": {
"adiConfig": {
"enabled": true
}
},
"description": "NeuralSearch with Adaptive Intent (variant)"
}
],
"endAt": "2026-12-31T00:00:00.000Z"
}'
```
For guidance on test duration and metrics,
see [NeuralSearch A/B testing](/doc/guides/ai-relevance/neuralsearch/ab-testing).
## Limitations
* Adaptive Intent requires click and conversion events. Queries with insufficient engagement fall back to standard NeuralSearch.
* You can't manually edit bags, such as by adding or removing records or adjusting weights.
# Get started with NeuralSearch
Source: https://www.algolia.com/doc/guides/ai-relevance/neuralsearch/get-started
Activate NeuralSearch to combine keyword and semantic search on your Algolia index.
NeuralSearch adds vector search to your keyword search, then merges and ranks the results.
This means your index returns results that match concepts and intent, not just exact keywords.
## Before you begin
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
To get started, [your index must contain records](/doc/guides/sending-and-managing-data/send-and-update-your-data).
Click and conversion events aren't required.
If you have them, NeuralSearch uses them to choose which record attributes to use for vectors.
Without events, NeuralSearch selects attributes automatically from your record structure.
## Activate NeuralSearch
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your index.
4. Click the **NeuralSearch** tab.
5. Click **Configure NeuralSearch**.
6. Select the source index for events.
7. Click **Train NeuralSearch**.
* **If your index has events**, NeuralSearch analyzes the relationships between queries, events, and records to select and weight the attributes that produce the most relevant vector representations.
* **If your index doesn't have events**, NeuralSearch automatically selects attributes from your record structure. For example, attributes such as `title`, `description`, `color`, or `category`. Once you have events, you can retrain NeuralSearch to use them.
Building the NeuralSearch index can take longer for larger datasets.
## Preview your results
When training is complete, NeuralSearch enters **Preview** mode automatically.
In preview mode you can:
* Compare NeuralSearch and keyword results using the **Compare** tab
* Launch an A/B test to measure impact on real traffic
* Apply NeuralSearch to live traffic
### Compare results
Use the Compare tab to evaluate NeuralSearch result quality.
Go to the **NeuralSearch** tab in your index and click **Compare**.
The Compare tab shows:
* **Top queries:** queries from your analytics that you can click to run immediately in compare mode
* **No-result queries:** queries that don't return any results in keyword search, so you can see NeuralSearch's impact
* **Training coverage:** which queries NeuralSearch has trained on
Compare mode shows keyword and NeuralSearch results side by side.
Turn vector results on and off to isolate the impact of semantic search.
The graph between the two columns shows which results have been re-ranked or moved as a result of vector search:
lines crossing indicate records that shifted position.
### Inspect vector results
You can also inspect individual results in the **Index** tab.
1. Run a query and look for results with a blue **Vector** badge: these were retrieved by vector search.
2. Click a result, then click the compass icon to open its details.
3. Look for **Vector similarity:** a score between 0 and 1. Closer to 1 means a stronger semantic match.
## Understand search result changes
NeuralSearch expands what your index can match:
| Capability | Keyword search | NeuralSearch |
| - | - | - |
| Retrieval | Keyword matches | Keyword matches and semantic matches |
| Results | Exact or similar keyword matches | Relevant results even when the query doesn't match keywords |
| Intent understanding | Limited | Infers intent from the query |
For example, a query for "something to keep coffee hot" can return "insulated travel mug",
even with no keyword overlap.
## How NeuralSearch ranks results
NeuralSearch ranks results in three stages:
1. Retrieval
2. Tie-breaking
3. Merge results
### Retrieve results
NeuralSearch retrieves keyword results and vector results independently.
Vector results are initially ordered by similarity:
how close the record's vector is to the query's vector.
### Apply tie-breaking
Tie-breaking is applied independently to each result list.
The keyword list goes through Algolia's full [tie-breaking algorithm](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria), including textual relevance steps like typo tolerance, attribute ordering, and proximity.
The vector list uses a subset of tie-breaking steps, but only those that apply to non-textual relevance:
* Geographical distance
* Optional filters
* Custom ranking
Text-based tie-breaking steps don't apply to vector results.
### Merge results
NeuralSearch merges the ranked vector and keyword lists,
then normalizes their scores to a common scale so they can be combined.
The [presets](#configure-presets) determine final result order.
## Configure presets
Presets control how keyword and semantic results are blended.
NeuralSearch uses the `default` preset unless you configure otherwise.
| Preset | Behavior |
| - | - |
| `conservative` | Semantic search activates only for queries that don't return any keyword results. This preset minimizes changes to existing keyword results and is a good starting point. |
| `expanded_reach` | Semantic search activates when keyword result counts are low. |
| `append_only` | Semantic results are appended after all keyword results. |
| `default` | Semantic search runs on every query and blends with keyword results, with keyword results taking priority. |
| `custom` | Lets you configure `semanticBlendWeight`, `minHitsForSemantic`, and `enableNeuralSearchSortBy`. |
Configure presets with the [`semanticSearch/settings` API endpoint](#configure-neuralsearch-with-the-semantic-settings-endpoint).
## Semantic Precision
NeuralSearch retrieves up to 100 vector results per query by default.
Semantic Precision lets you control the trade-off between precision and recall by filtering those results based on similarity.
Configure Semantic Precision in your NeuralSearch settings.
Choose from these options:
| Setting | Behavior |
| - | - |
| **More Recall** (default) | Allows a broader set of results, including broader semantic matches. |
| **Balanced** | A midpoint between recall and precision. |
| **More Precision** | Filters out results with lower similarity scores, keeping only the closest semantic matches. |
To configure Semantic Precision with the API, see [`semanticSearch/settings`](#configure-neuralsearch-with-the-semantic-settings-endpoint).
## Configure NeuralSearch with the semantic settings endpoint
The semantic settings endpoint gives you programmatic control over NeuralSearch configuration for an index.
Use it to set presets, adjust blending behavior, configure Semantic Precision filtering, and configure positional ranking.
Changes take effect immediately.
No reindexing is required unless you change `neuralExpression` or `vectorModelId`.
```txt Base URL theme={"system"}
https://{ALGOLIA_APPLICATION_ID}.algolia.net/1/indexes/{INDEX_NAME}/semanticSearch/settings
```
### Retrieve settings
To inspect the current NeuralSearch configuration for an index, send a `GET` request to the `semanticSearch/settings` endpoint:
```sh Command line icon=square-terminal theme={"system"}
curl --request GET \
--url https://{ALGOLIA_APPLICATION_ID}.algolia.net/1/indexes/{INDEX_NAME}/semanticSearch/settings \
--header 'x-algolia-api-key: {ALGOLIA_ADMIN_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}'
```
### Update settings
To update the NeuralSearch configuration for an index, send a `PUT` request to the `semanticSearch/settings` endpoint.
This example sets the NeuralSearch preset to `custom` and configures Semantic Precision.
```sh Command line expandable icon=square-terminal theme={"system"}
curl --request PUT \
--url https://{ALGOLIA_APPLICATION_ID}.algolia.net/1/indexes/{INDEX_NAME}/semanticSearch/settings \
--header 'content-type: application/json' \
--header 'x-algolia-api-key: {ALGOLIA_ADMIN_API_KEY}' \
--header 'x-algolia-application-id: {ALGOLIA_APPLICATION_ID}' \
--data '{
"neuralSearchPreset": "custom",
"semanticBlendWeight": 0.5,
"dynamicThreshold": {
"enabled": true,
"aggression": 0.3,
"lowerLimit": 0.64,
"upperLimit": 0.85
}
}'
```
### Parameters
| Parameter | Type | Description |
| - | - | - |
| `neuralSearchMode` | `string` | `"active"`, `"preview"`, or `"inactive"`. Controls whether NeuralSearch is on for live traffic. |
| `neuralSearchPreset` | `string` | One of `"default"`, `"conservative"`, `"expanded_reach"`, `"append_only"`, or `"custom"`. For more information, see [Configure presets](#configure-presets). |
| `semanticBlendWeight` | `float` | From `0.0001` to `0.999`. Relative weight given to semantic results. `custom` preset only. |
| `minHitsForSemantic` | `integer` | From `0` to `20`. Minimum keyword results before semantic results are included. `custom` preset only. |
| `enableNeuralSearchSortBy` | `boolean` | When `true`, index ranking and sorting settings apply to the full NeuralSearch result set. `custom` preset only. |
| `dynamicThreshold` | `object` | Semantic Precision filter configuration. See [Semantic Precision](#semantic-precision). |
| `usePositionalSemanticRanking` | `boolean` | When `true`, vector results use Algolia's standard tie-breaking algorithm. See [Positional semantic ranking](#positional-semantic-ranking). |
### Positional semantic ranking
By default, vector results are ranked by how closely they match the meaning of the query.
Algolia's ranking formula doesn't apply to them.
Setting `usePositionalSemanticRanking: true` in the semantic settings endpoint changes this behavior.
With positional ranking enabled:
* Geographical distance is factored into semantic result ranking
* Optional filters apply to vector results
* Custom ranking attributes affect the order of semantic results
## See also
* **Measure the impact.** Run an [A/B test](/doc/guides/ai-relevance/neuralsearch/ab-testing) to compare NeuralSearch against keyword search on real traffic before you enable it for all traffic.
* **Improve results for your top queries.** After you have enough engagement data, enable [Adaptive Intent](/doc/guides/ai-relevance/neuralsearch/adaptive-intent) to continuously learn from clicks and conversions.
* **Configure NeuralSearch with the API.** Use [`semanticSearch/settings`](#configure-neuralsearch-with-the-semantic-settings-endpoint) for programmatic control over presets, blending, and ranking behavior.
* **Understand what's not supported.** Review [Limitations](/doc/guides/ai-relevance/neuralsearch/limitations) before building on top of NeuralSearch.
# NeuralSearch limits and unsupported features
Source: https://www.algolia.com/doc/guides/ai-relevance/neuralsearch/limitations
Features, configurations, and index sizes that NeuralSearch doesn't support.
NeuralSearch uses different infrastructure than Algolia's keyword search.
Because of this, some features work differently or aren't supported.
Review these limits before you turn on NeuralSearch or use it in production.
## Virtual replicas
NeuralSearch doesn't support [virtual replicas](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas).
You use virtual replicas for relevant sorting.
To use NeuralSearch with sorted results, use a standard replica instead of a virtual replica.
If you want custom ranking criteria to affect vector results, enable `usePositionalSemanticRanking`.
For more information, see [Configure NeuralSearch with the semantic settings endpoint](/doc/guides/ai-relevance/neuralsearch/get-started#configure-neuralsearch-with-the-semantic-settings-endpoint).
## Natural language processing (NLP) features
NLP features apply only to the keyword search part of NeuralSearch.
Semantic (vector) retrieval doesn't use these features:
* **[Synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms)** configured in your index expand keyword queries. They don't change how semantic retrieval represents vectors.
* **Decompounding** applies only to keyword matching. Algolia uses it for German and other languages with compound words.
* **Plurals and word forms.** Stemming and plural normalization apply to keyword matching. The semantic model handles word variation through embeddings.
* **Algolia Rules** only affect keyword retrieval.
If you need precise NLP behavior, keyword results in NeuralSearch work the same way as keyword-only search.
Semantic results supplement keyword results rather than replacing them.
## Case sensitivity and query normalization
Semantic (vector) retrieval doesn't normalize queries.
Differences in case, spelling, and special characters can change semantic results or rankings.
For example, `table`, `Table`, and `TABLE` might produce different results.
The same applies to `canapé` and `canape`.
NeuralSearch merges keyword and semantic results. These differences can change the final ranking.
The keyword part of the query still applies your index's normalization settings.
This behavior only affects the semantic part of the results.
## Sort by filter or geographic distance
NeuralSearch supports sort-by replicas for sorting results by a fixed ranking order.
By default, these sorting settings don't apply filter-based ranking or geographic distance to vector results.
To apply geographic distance and filter-based ranking to vector results, enable `usePositionalSemanticRanking`.
For more information, see [Configure NeuralSearch with the semantic settings endpoint](/doc/guides/ai-relevance/neuralsearch/get-started#positional-semantic-ranking).
## Index size limits
NeuralSearch indexes follow the same [size limits](/doc/guides/scaling/algolia-service-limits#application-record-and-index-limits) as other Algolia indexes:
* Maximum index size: 100 GB
* Maximum record size: 100 KB
Indexes that exceed these limits aren't supported by either keyword search or NeuralSearch.
## Other considerations
* **Custom dictionaries.** NeuralSearch doesn't support custom dictionaries for stop words, plurals, or compounds.
* **Log IP forwarding.** NeuralSearch indices don't show user IP addresses from `X-Forwarded-For` in search logs.
# Manage model training preferences
Source: https://www.algolia.com/doc/guides/ai-relevance/neuralsearch/model-training
Learn how Algolia uses data to improve AI models and how you can manage your organization's participation.
NeuralSearch's AI models improve by learning from anonymized, combined usage patterns across connected apps.
This shared learning helps NeuralSearch better interpret language,
understand intent, and surface more relevant results over time.
You don't need to build or maintain custom training pipelines.
By default, all applications in your organization are automatically included in model training.
Application owners can change this setting at any time in the dashboard.
Opting out excludes that application's data from future training cycles.
Search continues to function normally,
but it won't receive improvements from shared model updates.
You can opt back in at any time.
To manage your organization's training preference:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com).
2. On the left sidebar, select **Settings > Organization Settings**.
3. In the **General** section, click **Model Training**.
A table lists all applications in your organization.
Filter the table and review the details to decide which applications should participate.
You can manage preferences individually or in bulk:
* **Toggle switch**. Turn training on or off for an application.
* **Checkboxes**. Select multiple applications to apply bulk actions.
* **Select all**. Apply changes to every eligible application at once.
* To confirm your selection, select **Opt In Selected** or **Opt Out Selected**.
To apply your changes, click **Save Settings**.
To undo any changes before saving, click **Discard**.
## What happens after opting out
* Future model training runs won't use data from excluded applications.
Previously trained models remain unchanged.
* You'll continue to receive NeuralSearch feature updates,
but not incremental improvements from shared model training.
* You can opt back in at any time to receive the latest shared model improvements.
## See also
To learn more about how Algolia trains language models,
including data usage, privacy safeguards, and retention policies, see:
* [AI Trust Center](https://www.algolia.com/trust-center)
* [How Algolia trains language models](https://www.algolia.com/blog/ai/language-model-training)
# Dynamic Facets
Source: https://www.algolia.com/doc/guides/algolia-ai/dynamic-facets
Dynamic Facets helps users find relevant results by automatically reordering facets and facet values based on past filtering patterns.
Instead of a static list, users see the most relevant facets first for their query.
## How dynamic facets work
Dynamic Facets uses [query aggregation data](/doc/guides/search-analytics/concepts/query-aggregation) to reorder facets and facet values based on filtering patterns in past searches.
Dynamic Facets:
* Recomputes facet and facet value orderings daily and whenever you update the feature configuration.
* Uses the last 30 days of search data.
## Get started
To use Dynamic Facets in your UI, read facet and facet value ordering from the `renderingContent.facetOrdering` property in the search response.
You can use this ordering with InstantSearch or in a custom UI.
### With InstantSearch
Use the [`DynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/js) component to render refinement lists in the order returned by Dynamic Facets.
To apply facet value ordering automatically from the search response, you need **InstantSearch.js 4.72.0 or later** or **React InstantSearch v6 or later**.
### With a custom UI
To use Dynamic Facets without InstantSearch:
1. Read the facet ordering from the `renderingContent.facetOrdering` property in the search response.
2. Combine it with the `facets` property to build the final facet order in your UI.
3. Render only facets that are present in the response so you don't show outdated attributes.
For example:
```json JSON icon=braces expandable wrap theme={"system"}
{
"renderingContent": {
"facetOrdering": {
"facets": {
"order": ["brand", "categories", "color"]
},
"values": {
"brand": {
"order": ["Nike", "Adidas", "Puma"],
"sortRemainingBy": "count"
},
"categories": {
"order": ["Shoes", "Jacket", "Jogging"],
"sortRemainingBy": "count"
},
"color": {
"order": ["Red", "Green", "Blue"],
"sortRemainingBy": "count"
}
}
}
}
}
```
## Settings
Use these settings to configure how Dynamic Facets orders facets and facet values.
### Coverage
You can use another index as the source of events.
For example, if a replica doesn't receive events, you can reorder its facets and facet values based on events from its primary index.
### Browsing facets
If you [use Algolia for navigation](/doc/guides/solutions/ecommerce/filtering-and-navigation), you can select a browsing facet for empty queries.
This means that instead of reordering facets based on a specific query, Dynamic Facets can reorder them for a single filter used for browsing, such as `categories:"Shoes"`.
### Where to apply
Choose where Dynamic Facets applies.
You can A/B test these options independently.
* **All**: apply to search queries and category pages.
* **Queries only**: apply only to search queries.
* **Categories only**: apply only to category browsing.
### Pinned facets
Pin up to 10 facets to keep them at the top of the list.
Dynamic Facets orders all other facets automatically.
Use pinned facets for globally relevant facets that should always stay visible, for example, for commercial or policy reasons.
* Only facets already present in your `Facet display` index setting can be pinned.
* Pinning applies to **all queries**. To override it for a specific query, use a [rule](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-facets) that replaces the full `facetOrdering`.
* Only facets can be pinned, not individual facet values.
* If a pinned facet is also part of the dynamically reordered set, Dynamic Facets removes it from that set and places it in its pinned position.
### Preserve value order for selected facets
Use this option for facets such as `size` or `date`, where facet values follow a logical order.
The facet can still move within the list, but its values keep the order inherited from [`Facet display`](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js).
## Interaction with `Facet display` index setting
If your index has facets listed in the `Facet display` index setting, Dynamic Facets merges its output with that setting at search time instead of replacing it.
### Facet ordering
`Facet display` acts as a list of allowed facets.
Dynamic Facets reorders only facets listed there.
If Dynamic Facets generates a facet that isn't in the index setting, that facet is removed.
Facets present in the index setting but not in the Dynamic Facets output are appended at the end in the same order as the index setting.
This guarantees that:
* No unwanted facets appear in the response.
* All configured facets are always present.
If no facets are listed in the `Facet display` index setting, the Dynamic Facets output is used as-is.
### Facet value ordering
For each facet in the final list:
* If the facet is in the **Preserve value order** list, Dynamic Facets ignores its own value reordering and uses the index setting values as-is, including any pinned values, hidden values, and the `sortRemainingBy` property.
* If the facet *isn't* in the **Preserve value order** list, Dynamic Facets uses its own value ordering (up to 20 values per facet) and sets `sortRemainingBy` to `count`.
* If a facet doesn't have any Dynamic Facets values or index setting values, it's included without value ordering. The default frontend behavior applies.
## Interaction with other features
* **Rules and merchandising.** If a [rule](/doc/guides/managing-results/rules/rules-overview) provides a `renderingContent` query parameter, it takes full precedence. Dynamic Facets is bypassed for that query.
* **Personalization.** Dynamic Facets and personalization don't work together. If personalization is enabled for a signed-in user, the personalized order overrides Dynamic Facets.
## Limitations
* Only the top ** distinct queries** are considered.
* Only queries between **3 and 100 characters** with at least **100 searches** are considered for facet reordering.
* A maximum of ** facets** and ** facet values** per facet are reordered per query.
* Remaining items follow the `sortRemainingBy` behavior in `renderingContent`. `count` is the default.
* Deleted facets can still appear in Dynamic Facets output because the ordering is based on historical data. Check that a facet exists in the `facets` list before you display it.
# AI Synonyms
Source: https://www.algolia.com/doc/guides/algolia-ai/dynamic-synonym-suggestions
Use AI to automate finding the best synonyms for your search.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
Synonyms are words that share a similar meaning.
Adding synonyms to your search helps your users find relevant results
even if they use different words to express the same concept.
For example, by adding a synonym for trousers and pants,
a search for either term returns identical results.
## Get started
To activate AI Synonyms for one of your indices:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia .
4. Open the [**AI Synonyms**](https://dashboard.algolia.com/ai-synonyms) page and select **Activate for this index**.
## Send a user token with your searches
To find suggestions, Algolia determines [how often users rewrite](#user-query-rewrites) a search term.
To make that more accurate, send user tokens with your search requests.
For more information, see [Distinguish users for analytics](/doc/guides/search-analytics/guides/usertoken).
## Review synonyms
The **Suggested Synonyms** tab shows new suggestions.
To show all suggestions, turn off the **Show only new synonyms** option.
As you examine a suggested synonym, you'll see metrics to help you select the better option: the original term or the suggestion.
These metrics include:
* Conversion rate (CVR)
* Click-through rate (CTR)
* Result count
* Confidence score
If you're [sending click and conversion events](/doc/guides/sending-events),
you can compare click-through and conversion rates for both alternatives.
#### What the confidence score means
Each suggestion includes a **confidence score** between 0 and 100.
It helps you find the synonym suggestions with the highest probability of improving your search results.
Two machine learning models determine the score:
* **A semantic similarity model** which measures how closely related the terms are.
* **A confidence scoring model** that converts this semantic similarity into a probability that the suggested synonym will enhance search relevance.
For example:
* A score of **90** means there's a 90% chance the suggested synonym will improve your search results.
* A score of **10** means it's unlikely to help.
## Accept or decline suggestions
To accept a suggestion, click **Add to configuration** (`✓`).
If you don't want to add this suggestion to all replicas of this index, turn off the **Forward to replicas** option at the top.
You can edit accepted suggestions in the **Accepted suggestions** tab.
To decline a suggestion, click **Decline this suggestion** (`x`).
You can edit and accept declined suggestions in the **Declined suggestions** tab.
After accepting a suggestion, only edit it in the dashboard, not with the API.
## Change synonym types
By default, the AI suggests adding regular two-way synonyms.
To change to another [synonym type](/doc/guides/managing-results/optimize-search-results/adding-synonyms),
hover over the suggestion and click the **Edit** button.
Then, click **Select Type** (`⇔`) and select a synonym type.
## Edit suggestions
To edit a suggestion, hover over it and click the **Edit** button.
Then, you can edit both synonyms or add more.
## Alternative suggestions
Sometimes, there are several suggestions for the same search query.
To see the alternative suggestions, click **More suggestions**.
## How AI Synonyms work
AI Synonyms provides a list of synonyms that may improve your search experience by considering how users rewrite their queries and the historical performance of different searches.
### User query rewrites
Algolia analyzes user sessions to detect when people change their search query.
If a user searches for a word and then changes the query to use a different word, that implies a potential synonym.
To qualify as a synonym suggestion:
* Users must search for one word and then change the query to another word (within about a minute).
* The two words must seldom appear together in the same query, suggesting they're potential synonyms rather than companions. For example, since **"sneakers"** and **"trainers"** are often synonyms, they're unlikely to appear in the same query. However, **"sneakers"** and **"laces"** may frequently co-occur and, hence, are companions.
For each index, the evaluation considers analytics data from the last 30 days.
For multi-word terms, Algolia examines the frequency of these terms across different search queries to confirm they're potential synonyms and not part of a set phrase.
#### Rewrite count
Each synonym suggestion includes a **rewrite count**, which reflects how often users switched from one word to another during the preceding week.
The more frequent this behavior is among your users, the higher the count.
### Search performance analysis
Algolia uses your analytics data to see how well each search term performs.
It only considers terms that users have typed into the search box.
Low-performing terms are terms that lead to low engagement (few clicks, low conversions) or poor result quality (few or zero results).
For each of these low-performing terms, Algolia suggests synonyms that are linguistically similar and likely to improve search outcomes.
For example, if users frequently search for "denim trousers" but that term leads to few clicks, "jeans" might be a synonym suggestion.
## See also
* [Synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms)
* [Merchandising Playbook: AI Synonyms](https://www.algolia.com/ecommerce-merchandising-playbook/ai-synonyms)
# Optimize search settings with AI Assist
Source: https://www.algolia.com/doc/guides/algolia-ai/optimize-search-settings
Use AI Assist in the Algolia dashboard to review an index's relevance settings and apply recommended configuration changes.
This is a **beta feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/).
AI Assist reviews index settings that affect [relevance](/doc/guides/managing-results/relevance-overview) and recommends changes.
Use AI Assist before you launch a search experience, after you change your record structure, or when search quality declines.
AI Assist tailors its recommendations to your [Algolia plan](https://www.algolia.com/pricing) and only suggests features included in your plan.
You can review each suggested change before you apply it.
## Before you begin
Make sure you have:
* An Algolia index with records.
* Permission to [update index settings](/doc/guides/security/api-keys#rights-and-restrictions) (the `editSettings` ACL).
### Plan how to test and apply changes
Before applying recommendations, consider how the index is used:
* If this is a production index, test changes on a test index or with [A/B testing](/doc/guides/ab-testing/what-is-ab-testing) before applying them broadly.
* If the index is part of an active A/B test, wait until the test ends before changing a setting that the test is evaluating.
* If the index has replicas, decide whether each change should apply to the primary index, its replicas, or both.
If you must change a setting during an active A/B test, keep that setting consistent across all variants.
Otherwise, the change can affect the comparison.
## Review and apply index setting recommendations
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia application.
2. On the left sidebar, select **Search**.
3. Select the index you want to review, then open the **Configuration** tab.
4. To review settings, select **Optimize Search** for any setting or, on the **Configuration** tab, select **AI Assist**, then enter `Optimize my search configuration`.
5. Answer the questions about your industry, search use case, and goals. AI Assist uses your answers to tailor its recommendations.
6. Review the **What's working well** and **Needs optimization** sections. For each proposed change, review the explanation.
7. Select the checkbox for each recommendation you want to use, then select **Apply**.
8. Wait for AI Assist to confirm that the changes are applied.
If your Algolia plan includes Activity Logs, review the applied changes in the
[logs](https://dashboard.algolia.com/account/audit-logs).
## Supported settings
AI Assist can suggest changes for these index settings:
| Setting | What AI Assist reviews |
| - | - |
| [Searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes) | Which record attributes Algolia searches and in what order. |
| [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking) | The attributes Algolia uses to break ties between records that otherwise rank the same. |
| [Ranking](/doc/api-reference/api-parameters/ranking) and [sorting](/doc/guides/managing-results/refine-results/sorting) | How the index ranks records and supports sort orders. |
| [Typo tolerance](/doc/guides/managing-results/optimize-search-results/typo-tolerance) | How Algolia handles spelling mistakes. |
# Query Categorization
Source: https://www.algolia.com/doc/guides/algolia-ai/query-categorization
Predict the categories to which a search query belongs with AI.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
The Query Categorization feature uses an AI model to predict the appropriate category for a search query.
It predicts the most likely category for a query based on past search and click and conversion data.
For example, the query "banana" might be categorized under "Food > Fruits."
Query Categorization also groups similar queries into categories.
For example, both "blue jeans" and "denim" could be classified in the "Clothing > Pants" category.
"Denim" could also belong to the "Clothing" category.
## Features
**Query Categorization** gives you access to these features:
* **Dashboard control** lets you set up and check AI model predictions from the Algolia dashboard.
* **Automatic filtering and boosting** improves the relevance of results without writing code.
* **Category predictions:** use the Search API to generate real-time category predictions for user queries. Use these predictions to customize their search and discovery experience.
## Set up Query Categorization
To get started with Query Categorization:
1. **[Send click and conversion data](#send-click-and-conversion-events)** to Algolia: this helps the AI model learn user behavior.
2. **[Create a facet hierarchy](#create-a-facet-hierarchy)** to define the categories and subcategories your belong to. This lets the AI model classify them.
After setup, the [AI model](#the-ai-model) analyzes popular queries and categorizes them.
It also predicts categories for new queries.
### Send click and conversion events
To train the AI model, you must [send click or conversion events](/doc/guides/sending-events) to Algolia.
The model uses this data to predict the categories for new queries.
For a query to be eligible for Query Categorization model training, it must:
* Be longer than three characters
* Have returned at least 10 records
* Have received events for at least three different records.
The model is re-trained every 24 hours, using data from the last 90 days.
### Create a facet hierarchy
To enable Query Categorization, use the [dashboard](https://dashboard.algolia.com/query-categorization/) to define the facets.
These are the categories and subcategories the AI model uses.
These facets should accurately represent your data hierarchy, up to five levels deep.
Once you've entered your facets, click **Save** to start the model-building process.
Depending on the number of categories and traffic, this can take several minutes to half an hour.
#### Example facet hierarchy of nested categories
Assuming a record structure like this:
```json JSON icon=braces theme={"system"}
{
"name": "banana",
"description": "...",
"price": 3.45,
"hierarchicalCategories":
{
"lvl0": "Food",
"lvl1": "Fruits"
}
}
```
Set `hierarchicalCategories.lvl0` as the first level used by the model and `hierarchicalCategories.lvl1` as the second level.
#### Example facet hierarchy of flat categories
Assuming a record structure like this:
```json JSON icon=braces theme={"system"}
{
"name": "banana",
"description": "...",
"price": 3.45,
"group": "Food",
"section": "Fruits"
}
```
Set `group` as the first level used by the model and `section` as the second level.
Your records might belong to several categories at the same time.
If you use arrays to represent each level of depth, the model expects shared prefixes.
For example, `Food` is the first level facet value and `Food > Fruits` is the second level.
#### Unsupported hierarchical facet formats
The model doesn't support records structured with only one attribute for all depth levels.
For example:
```json JSON icon=braces theme={"system"}
{
"name": "banana",
"description": "...",
"price": 3.45,
"categories": ["Food", "Food > Fruits"]
}
```
### Manage categories
After model training, view the generated categories tree.
Find it in the **Categories Tree View** tab in the [dashboard](https://dashboard.algolia.com/query-categorization/).
This lets you review and adjust the structure as needed.
#### Exclude categories
From the **Categories Tree View** tab,
you can also exclude non-categories like "Black Friday" or "On sale" to increase the model's accuracy.
### Manage events source
You can use different data sources for your events.
For example, use data from a production to improve predictions for a test index that hasn't had any user interactions.
To use a different data source:
1. Go to the **Categories Setting** tab in the [dashboard](https://dashboard.algolia.com/query-categorization/).
2. In **Events source index**, select the new data source (it must be a [replica](/doc/guides/managing-results/refine-results/sorting/in-depth/replicas) or a copy of the existing source index).
3. Click **Save** to regenerate the category tree and make predictions using events from the new data source.
## The AI model
After the facet hierarchy has been saved, the AI model performs these tasks:
1. Build a "categories tree" based on your data.
2. Identify top categories for popular queries.
3. Predict categories for new queries.
### Model output
The AI model assigns a confidence level and a type to each prediction:
* **Confidence level:** `very low`, `low`, `high`, `very high`, or `certain`.
* **Type:**
* `narrow`: the query matches a specific category.
* `broad`: the query matches a category with subcategories.
* `ambiguous`: the query matches several unrelated categories.
* `none`: the model can't determine a category.
## Use predictions at query time
Use Query Categorization predictions to enhance search results in your frontend at query time to:
* **Expand results:** if results are limited, expand them. Add more items from the same category.
* **Refine broad search terms:** if a query is classified as `broad`, suggest different categories. This helps users narrow their search.
* **Customize search**: offer a tailored search experience by providing a specific layout for some categories.
Query Categorization populates your search results with the predicted categories for the search query.
[Algolia normalizes](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp#normalization) the query used for prediction, not the raw query.
### Turn on Query Categorization at query time
To retrieve Query Categorization results at query time, activate the option from the dashboard or in query parameters.
* **In the [dashboard](https://dashboard.algolia.com/query-categorization/)**, enable the **Categories with Search API** toggle in the **Categories Settings** tab.
* **In query parameters** as a JSON object or a [URL encoded string](https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding).
For example, `extensions%3D%7B%22queryCategorization%22%3A%7B%22enableCategoriesRetrieval%22%3Atrue%7D%7D`
Find Query Categorization parameters in the `extensions` field:
```jsonc JSON icon=braces theme={"system"}
{
// Other standard query parameters
"extensions": {
"queryCategorization": {
"enableCategoriesRetrieval": true
// Other options to control automatic filtering and boosting are available
}
}
}
```
#### Search response format
The [search response](/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response) shows predictions in the attribute `extensions.queryCategorization`.
```jsonc JSON icon=braces theme={"system"}
{
// Regular search answer (like hits)
"extensions": {
"queryCategorization": {
"normalizedQuery": "banana",
"count": 2,
"type": "narrow",
"categories": [
{
"bin": "very high",
"hierarchyPath": [
{
"facetName": "category.lvl0",
"facetValue": "Food",
"depth": 0
},
{
"facetName": "category.lvl1",
"facetValue": "Fruits",
"depth": 1
}
]
}
]
}
}
}
```
Sometimes, `extensions.queryCategorization` is empty if the Query Categorization model can't categorize a query.
## How to override predictions
You can override the AI model's predictions from the [dashboard's](https://dashboard.algolia.com/query-categorization/) **Predictions Explorer** tab.
* To change the override or replace the predicted categories, click the edit (pencil) icon.
* To revert an override to the predicted categories, click the zap (lightning) icon.
* To remove a prediction, click the trash icon.
Algolia displays changes in the predictions list.
To confirm them, click **Save changes** at the bottom of the page.
Changing the index classification in **Categories Settings** deletes any override affected by this change.
For example, removing the second facet level from the index classification deletes overrides like `Food > Fruits`,
reverting the query to automatic predictions.
## Automatic filtering and boosting
Automatic filtering and boosting applies filters for user queries based on Query Categorization predictions.
* **Automatic filtering** [filters out](/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters) items that don't match the predicted category.
* **Automatic boosting** uses an [optional filter](/doc/guides/managing-results/rules/merchandising-and-promoting/in-depth/optional-filters) to boost items that match the predicted category.
This moves matching items to the top of search results.
Based on [confidence levels](#model-output), the Query Categorization model decides how to apply predictions.
It can apply them as filters, as boosts, or not at all.
### Use automatic filtering and boosting
To use this feature, click **Enable Automatic Filtering & Boosting**.
You'll find this option in the **Automatic filtering & boosting Settings** tab in the [dashboard](https://dashboard.algolia.com/query-categorization/).
By default, only automatic boosting is activated.
To enable automatic filtering, see [Configure automatic filtering and boosting](#configure-automatic-filtering-and-boosting).
Once activated, Algolia automatically adds boosts to your search parameters at query time.
This doesn't require frontend changes.
#### Exclude queries
From the **Automatic filtering & boosting Settings** tab,
you can also exclude queries that shouldn't be automatically filtered or boosted.
Anything specified here overrides your index's configuration.
### Configure automatic filtering and boosting
Adjust the impact of automatic filtering and boosting by modifying two parameters in the **Automatic filtering & boosting Settings** tab:
* The minimum expected confidence level for filtering
* The minimum expected confidence level for boosting
These parameters let you configure when to apply filters or boosts based on the predictions' [confidence levels](#model-output).
The feature:
* Boosts predictions with a confidence level equal to or above the boosting level, but below the filtering level.
* Filters predictions with a confidence level equal to or above the filtering level.
For instance, say the boosting confidence level is `high` and the filtering confidence level is `certain`.
Algolia then boosts `high` and `very high` predictions, and filters on `certain` predictions.
The confidence level for boosting must always be lower than the level for filtering.
Turn off filtering or boosting with the corresponding options.
### Override automatic filtering and boosting at query time
You can override the default configuration for automatic filtering and boosting with query parameters:
```jsonc JSON icon=braces theme={"system"}
{
// Other query parameters ...
"extensions": {
"queryCategorization": {
"enableAutoFiltering": true
}
}
}
```
To let users remove filters applied by automatic filtering and boosting, you must turn off automatic filters and boosts on the search query.
Target the query at your index.
Do this when users clear the automatic filter.
[Create an InstantSearch widget to handle this behavior](/doc/guides/building-search-ui/going-further/automatic-filtering/js).
### Detect the impact of automatic filtering and boosting at query time
When automatic filtering and boosting is active for a query, the `extensions.queryCategorization.autofiltering` section has the following content:
```jsonc JSON icon=braces theme={"system"}
{
// ... Regular search response (including hits) ...
"extensions": {
"queryCategorization": {
"normalizedQuery": "banana",
"count": 14870,
"type": "narrow",
"categories": [
{
"bin": "certain",
"hierarchyPath": [
{
"facetName": "categories.lvl0",
"facetValue": "Food",
"depth": 0
},
{
"facetName": "categories.lvl1",
"facetValue": "Food > Fruits",
"depth": 1
}
]
}
],
"autofiltering": {
"enabled": true,
"maxDepth": 5,
"facetFilters": [
[
"categories.lvl0:Food"
],
[
"categories.lvl1:Food > Fruits"
],
],
"optionalFilters": []
}
}
}
}
```
You can activate automatic filtering and boosting without it having an impact.
Other fields don't appear in your [search response](#search-response-format).
### Preview automatic filtering and boosting
You can preview automatic filtering and boosting for any index.
Find this preview in the **Automatic filtering & boosting Preview** tab of the [**Query categorization** section in the dashboard](https://dashboard.algolia.com/query-categorization/).
If you have category predictions for the selected index, you can preview results for any query with predicted categories.
This shows how automatic filtering and boosting affects results, without activating it on your production traffic.
The automatic filtering and boosting preview shows how promotion [rules](/doc/guides/managing-results/rules/rules-overview) and [Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking) affect results.
You can turn these features off in the preview with the **Rules** and **Dynamic Re-Ranking** options.
### A/B test automatic filtering and boosting
You can use A/B testing to test automatic filtering and boosting on an index.
This helps you measure the effect on your search accurately.
To do this, open the [**Query Categorization**](https://dashboard.algolia.com/query-categorization/) page in the Algolia dashboard.
Then, select the **Automatic filtering & boosting Settings** tab and click **Launch an A/B test**.
## Analytics grouped by categories
After setting up the Query Categorization model, you can view queries grouped by their predicted categories.
Find this view in the dashboard's **Grouped Searches** tab, under **Observe > Analytics**.
This view doesn't include browsing queries:
"empty" queries generated when a user clicks a filter or other UI element.
Compare categories or click them to inspect their queries.
Algolia automatically flags queries with low click-through or conversion rates.
It labels them "underperforming" within each category.
For example, Algolia flags `blue jeans` and `denim` as belonging to the same category (`pants`).
Grouped analytics displays the performance of the category `pants`.
This aggregates data for `blue jeans`, `denim`, and other queries in the `pants` category.
You can then compare the performance of the two.
For example, the `pants` category's click-through rate is 10%, but the click-through rate for `blue jeans` is only 4%.
Algolia identifies `blue jeans` as underperforming.
You can improve the performance of the query by, for example, adding a synonym or a Rule.
With grouped analytics, you can combine your search analytics to uncover new insights.
This helps you optimize your search and discovery experience.
It simplifies search analysis.
It also helps manage the [long tail](https://wikipedia.org/wiki/Long_tail) of search queries.
## See also
To learn more, see these blog articles:
* [Query understanding](https://www.algolia.com/blog/product/query-understanding-101)
* [12 ways to improve your search (including Query Categorization)](https://www.algolia.com/blog/engineering/12-ways-to-improve-your-search-index)
# Dynamic Re-Ranking
Source: https://www.algolia.com/doc/guides/algolia-ai/re-ranking
Dynamic Re-Ranking adjusts ranking based on changing user behavior.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
The relevance of your search results can change over time, sometimes quickly.
Consider a drugstore.
Before the COVID-19 pandemic, users who searched for "mask" could have been looking to buy facial masks for skincare.
During the pandemic, the same query could refer to surgical masks.
To respond to changing user expectations,
you might have to adjust your results with [Rules](/doc/guides/managing-results/rules/rules-overview) or [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters).
This can become time-consuming because trends come and go.
A better, more scalable approach is to use the Dynamic Re-Ranking feature.
## What is Dynamic Re-Ranking?
Dynamic Re-Ranking is an Algolia feature that uses AI to detect trends in user behavior.
It learns from click and conversion events for a given and can promote records that are becoming more popular.
Dynamic Re-Ranking only reorders records that pass textual relevance and your filters.
For example, if users increasingly choose surgical masks when searching for "mask", Dynamic Re-Ranking can move surgical masks higher in future results.
This can improve relevance and increase without requiring rules or manual changes.
For more information,
see [Merchandising Playbook: AI re-ranking](https://www.algolia.com/ecommerce-merchandising-playbook/ai-re-ranking/)
## How Dynamic Re-Ranking works
Learn when Dynamic Re-Ranking can reorder results, how ranking works, and how it interacts with Personalization.
### Effect on relevance
Dynamic Re-Ranking runs after Algolia has built the result set using textual relevance], filters, and
custom ranking.
However, there are some constraints on the feature:
* Dynamic Re-Ranking only reorders that are already eligible to appear in the results (it doesn't add new records).
* Most Rules consequences (such as boosting or burying categories) run *before* Dynamic Re-Ranking.
* Pin and hide consequences run *after* Dynamic Re-Ranking, so pinned records stay in place and hidden records don't appear.
Dynamic Re-Ranking may also change ordering when you use [geographical filtering or sorting](/doc/guides/managing-results/refine-results/geolocation#geographical-filtering-and-sorting),
depending on how you configure [the Geo ranking criterion](/doc/guides/managing-results/refine-results/geolocation/how-to/geo-ranking-info).
In a typical search, Algolia applies these ranking steps in this order:
1. [Textual ranking criteria](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria)
2. [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking)
3. [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization) (if you've enabled it and Algolia has enough information to personalize results for a particular user)
4. Rules ([boosting](/doc/guides/managing-results/must-do/custom-ranking/how-to/boost-or-penalize-some-records) and [burying](https://support.algolia.com/hc/en-us/articles/4406981935761#bury-categories)) or [Automatic Boosting from Query Categorization](/doc/guides/algolia-ai/query-categorization#automatic-filtering-and-boosting)
5. Dynamic Re-Ranking
6. Rules ([pinning](https://support.algolia.com/hc/en-us/articles/4406981935761#pin-items) and [hiding](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-hide-hits) specific items).
By default,
Algolia doesn't apply Dynamic Re-Ranking to empty searches but you can
[enable re-ranking for empty searches filtered on a particular facet value](#enable-re-ranking-on-browsing-pages-browsing-facet).
### Allow Dynamic Re-Ranking when Personalization applies
By default, Dynamic Re-Ranking doesn't run if [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization) reorders results on the first page for that user.
To enable Dynamic Re-Ranking even when Personalization affects the first page of results,
set [`allowReRankingOnPersonalization`](/doc/api-reference/api-parameters/allowReRankingOnPersonalization) to `true` at query time.
```json JSON icon=braces theme={"system"}
{
"query": "mask",
"enablePersonalization": true,
"personalizationImpact": 80,
"enableReRanking": true,
"allowReRankingOnPersonalization": true,
"distinct": 0
}
```
Dynamic Re-Ranking doesn't apply when [`distinct`](/doc/api-reference/api-parameters/distinct) > 1,
even if `allowReRankingOnPersonalization` is `true`.
Set `distinct` to `0` or `1` to use Dynamic Re-Ranking with Personalization.
## Dynamic Re-Ranking preparation
Before you enable Dynamic Re-Ranking,
make sure you send the right events and meet the data requirements.
### Send the required click and conversion events
To use Dynamic Re-Ranking, you must send [click and conversion events](/doc/guides/sending-events).
Dynamic Re-Ranking uses this data to detect trends and adjust ranking accordingly.
Dynamic Re-Ranking may work with only click events (not conversion events)
but performs better if you send *both* click and conversion events.
Dynamic Re-Ranking refreshes the [ordering](#ordering-settings) for each query every 24 hours by default.
It uses a sliding 30-day window of recent analytics data.
To promote a record for a given query,
Dynamic Re-Ranking needs at least 20 clicks or two conversions for that record during the 30-day window.
Events must contain a [`queryID`](/doc/libraries/sdk/v1/methods/send-events#param-query-id) property that corresponds to a valid search.
For each query and record (`objectID`), Dynamic Re-Ranking counts at most one click and one conversion per [`userToken`](#security).
If the `userToken` in the event differs from the `userToken` in the search request, Dynamic Re-Ranking uses the search `userToken`.
Events must include an `objectID` property for a record in the specified index.
This `objectID` needs to belong to one of the returned records of the search with the specified `queryID`.
For example:
1. On the first day of the month, a search returns record `X`.
2. Your app sends an event for record `X` in the production index with the corresponding `queryID`.
3. Dynamic Re-Ranking uses record `X` in its next ordering computation.
4. On the fifth day of the month, your app deletes record `X` from the production index.
5. Dynamic Re-Ranking stops using record `X` in future ordering computations.
Dynamic Re-Ranking might not be suitable for indices with frequent record deletions,
such as those used by a second-hand car dealer where each car's record is deleted upon sale.
Instead, use [Automatic boosting from Query Categorization](/doc/guides/algolia-ai/query-categorization#automatic-filtering-and-boosting).
#### Security
To prevent malicious users from manipulating the ranking of your records,
Dynamic Re-Ranking counts at most one click and one conversion per [`userToken`](/doc/api-reference/api-parameters/userToken) for a given user, query, and record.
For a given [user-query-record combination](/doc/guides/sending-events/concepts/event-types#events-requirements-for-dynamic-re-ranking), Dynamic Re-Ranking counts at most one click and one conversion per `userToken`.
For example, if the same user (same `userToken`) clicks on the same record 100 times after the same query, it counts as one click.
If the same user clicks on the same record after different queries, they count as separate events.
To prevent users from crafting several user tokens,
use [secured API keys](/doc/libraries/sdk/v1/methods/generate-secured-api-key).
With secured API keys, you can include the `userToken` as an embedded parameter when you generate the key on your backend.
This gives each user a unique key, but they can't change its `userToken`.
### Validate with the simulator
Simulate re-ranking for any from the [**Dynamic Re-Ranking** page in the dashboard](https://dashboard.algolia.com/reranking/).
Once you've [sent enough click and conversion events](#send-the-required-click-and-conversion-events),
try out any query and see how Dynamic Re-Ranking affects the results (without activating re-ranking on your live index).
In the simulator, each record has an attractiveness score per query.
This score reflects the number of events the record received in the last 30 days compared with other records
and it explains why re-ranking boosts one record over another.
With time decay enabled, recent events matter more,
so a record with fewer recent events can outrank a record with more historical events.
Turn off the rules in the simulator with the **Rules** toggle to assess the benefits of Dynamic Re-Ranking.
Consider removing these rules if you're satisfied with the records that Dynamic Re-Ranking promotes.
If you use rules to [pin top records for popular queries](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits),
Dynamic Re-Ranking has less impact because pinned records stay in place.
## Dynamic Re-Ranking settings
Configure the following settings from the [**Dynamic Re-Ranking** page in the dashboard](https://dashboard.algolia.com/reranking/).
### General settings
General settings control the goal, event source, and refresh frequency.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
The **Re-Ranking Revenue Goal** option is part of the Dynamic Re-Ranking for Revenue feature.
By default, Dynamic Re-Ranking reorders your search results to maximize the number of conversions on your index (CVR).
To optimize for total revenue instead,
send [Purchased object IDs after search events](/doc/libraries/sdk/v1/methods/purchased-object-ids-after-search) with `price`, `quantity`, and `currency` for each purchased item.
Dynamic Re-Ranking optimizes revenue for the currency that appears most often in an index's events and ignores events in other currencies.
By default, Dynamic Re-Ranking uses the [events](#send-the-required-click-and-conversion-events) sent to the re-ranked index.
You can use another index as the source of the events by changing the **Events source index** option.
The indices must be replicas of each other.
Use this parameter to experiment with your Dynamic Re-Ranking configuration without affecting production search results:
* Create a replica of your production index
* Enable Dynamic Re-Ranking on the replica
* Set the event source index to your production index to use its events to generate the replica's re-ranking, which would otherwise be empty because the replica doesn't have any events
* Test changes to your Dynamic Re-Ranking configuration in the simulator on the replica
* Start an A/B test by comparing your production index with the replica
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
Refreshing every hour is an Advanced Dynamic Re-Ranking feature.
By default, Dynamic Re-Ranking refreshes every 24 hours.
When you enable this option, Dynamic Re-Ranking refreshes every hour instead.
**Refreshing every hour helps you respond to shorter-lived search trends.**
### Coverage settings
Coverage settings determine which queries Dynamic Re-Ranking can affect and the number of results it can reorder.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
By default, Dynamic Re-Ranking uses your events to re-rank top queries.
The Multi-Signal Ranking feature improves the ordering of less popular queries:
it doesn't affect the ordering of your top queries.
For less popular queries, Algolia trains an AI model on your events.
The model considers each record's textual relevance to the query and the record's content.
Algolia retrains the model every day, and training can take up to an hour.
After training finishes, Dynamic Re-Ranking applies the updated ordering to those queries in search results (as it does for any re-ranked query).
To train the model, choose up to 20 record attributes.
Choose attributes that influence the "attractiveness" of records for users.
Think about what influences user choices when they browse your site.
For example, `brand`, `price`, and `category.id` are good candidates but `SKU`, `imageURL`, or `margin` aren't.
The attributes must be booleans, numbers, strings, or lists.
Objects aren't supported.
You can mark each attribute as:
* `date` for numbers expressed as a [Unix timestamp](https://www.unixtimestamp.com/).
* `category` for string or number values shared across your records.
Good examples are `brand` and `color`.
Don't use `category` for unique fields like `productDescription` or `title`.
* `unclassified` for anything else.
To see which attributes affected ranking the most, download a CSV from the dashboard.
The closer the value is to 0, the less impact the attribute has on re-ranking.
Negative values show that the attribute lowers ranking.
For example, the attribute `isOutOfStock` can be negative because out of stock items aren't desirable.
Multi-Signal Ranking learns to demote attributes with a negative impact.
By default, Algolia doesn't dynamically re-rank empty queries.
Setting a **Browsing Facet** re-ranks empty queries that include facet filters
(either with the [`facetFilters`](/doc/api-reference/api-parameters/facetFilters) or [`filters`](/doc/api-reference/api-parameters/filters) parameter)
for that attribute.
For example, if you use Algolia to populate your website's category pages by performing an empty query with a `categoryPageID` facet, set `categoryPageID` as a browsing facet.
Re-ranking reorders records in each `categoryPageID` based on their popularity.
If you set the root of a nested faceting attribute as a browsing facet, Dynamic Re-Ranking reorders all its sub-attributes.
For example, if you have an attribute named `hierarchicalCategories` with several nested levels (like `hierarchicalCategories.level0` and `hierarchicalCategories.level1`), using only `hierarchicalCategories` as a browsing facet re-ranks empty queries filtered on `hierarchicalCategories.level0` and `hierarchicalCategories.level1` as well.
Set up to five browsing facets if your [plan](https://www.algolia.com/pricing) includes Advanced Dynamic Re-Ranking.
Otherwise, you can use only one browsing facet.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
This is an Advanced Dynamic Re-Ranking feature.
By default, Algolia doesn't dynamically re-rank empty queries.
When you enable this option, Dynamic Re-Ranking always re-ranks the empty query and promotes the most popular records.
Dynamic Re-Ranking uses events to sort re-ranked items only when the query is empty and no filters apply.
If you set up [browsing facets](/doc/guides/algolia-ai/re-ranking#enable-re-ranking-on-browsing-pages-browsing-facet),
Dynamic Re-Ranking uses the empty-query fallback only when the search request doesn't include any browsing facets.
You can increase the number of records at the top of search results from the default of 20 to 100.
Dynamic Re-Ranking may promote fewer results than this value if Dynamic Re-Ranking doesn't gather significant traffic on enough records for some queries, or if filters remove records that were about to be re-ranked.
### Ordering settings
Ordering settings control how Dynamic Re-Ranking weighs events and shares signals across similar queries.
To give more weight to recent events in the ranking computation, enable the **Event Freshness** option.
Otherwise, an event from four weeks ago affects re-ranking the same way as one from yesterday.”
**Enabling event freshness better handles seasonality and shorter search trends.**
By default, each query is re-ranked independently.
**Group Similar Queries** helps to re-rank less frequent query variations.
Dynamic Re-Ranking groups queries that contain the same words, in any order.
Setting a language for grouping allows Dynamic Re-Ranking to ignore plurals, conjugations, and stop words during the grouping phase.
Grouped queries share events and therefore use the same ordering.
If you set a re-ranking filter, Dynamic Re-Ranking only promotes records matching the configured filters.
For example, if you select `inStock = true`, Dynamic Re-Ranking only promotes items that are in stock.
## Enable Dynamic Re-Ranking for production
Enable Re-Ranking on the [**Dynamic Re-Ranking** page in the dashboard](https://dashboard.algolia.com/reranking/).
Choose the index you want to activate re-ranking for and select **Activate Re-Ranking**.
### A/B test Dynamic Re-Ranking
Use [A/B testing](/doc/guides/ab-testing/what-is-ab-testing) to measure how Dynamic Re-Ranking affects search performance on your index.
To create an A/B test from the [**Dynamic Re-Ranking** page in the dashboard](https://dashboard.algolia.com/reranking/), select **Launch an A/B test** in the top-right corner.
This lets you choose the traffic split and duration to compare search performance with and without Dynamic Re-Ranking.
### Turn off Dynamic Re-Ranking for specific queries
To turn Dynamic Re-Ranking off for a specific query,
create a [rule](/doc/guides/managing-results/rules/rules-overview) with `{"enableReRanking": false}` as a consequence.
# Enable Dynamic Re-Ranking on category pages
Source: https://www.algolia.com/doc/guides/algolia-ai/re-ranking/tutorials/re-rank-category-pages
Use Dynamic Re-Ranking to re-rank and promote popular products on category pages.
You can use [Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking) to detect trends in your users' behavior.
Based on their search and browse interactions,
Dynamic Re-Ranking boosts trending or popular results to improve relevance on category pages.
Once you've configured your category pages and sent [click and conversion events](/doc/guides/sending-events/getting-started) to Algolia,
Dynamic Re-Ranking uses this data to identify trends.
You don't need to write any code to enable Dynamic Re-Ranking on category pages.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
## Before you begin
Create [category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages) before enabling Dynamic Re-Ranking.
## Enable Dynamic Re-Ranking on category pages
Set the following options in the [Dynamic Re-Ranking page in the dashboard](https://dashboard.algolia.com/reranking/):
* **Events source index.** Select the that receives your click and conversion events.
You can also select an alternative source index.
This is helpful if you have a staging index that replaces the production index every day for atomic reindexing.
In this case, enable re-ranking on both the staging and production indices as source events, given that the production index already receives events.
* **Browsing facets.** Set this to the category page identifier in your index, such as `collections`, `categories`, or `categoryPageId`.
Check with your team to make sure the browsing is referencing the correct filter used in the to generate the category page in the frontend UI.
* **(Optional) Event freshness.** [Event freshness](/doc/guides/algolia-ai/re-ranking#give-more-weight-to-recent-events-event-freshness) gives more weight to recent events to account for seasonality and short-term trends. This setting is on by default, and in most cases, we recommend keeping it enabled.
* **(Optional) Re-Ranking filter.** You can turn off re-ranking for specific to prevent promoting popular items that are out of stock or no longer listed. To do this, select a filter value, for example `instock=true`.
## Turn on Dynamic Re-Ranking
To turn the feature on,
click **Activate Re-Ranking** from the [Dynamic Re-Ranking page in the dashboard](https://dashboard.algolia.com/reranking/).
Dynamic Re-Ranking refreshes each query's ranking every 24 hours.
It always takes the last 30 days into account,
meaning it's based on a sliding window of the most recent analytics data.
You can see re-ranked results only after Algolia has received enough search traffic and click and conversion events.
To be promoted for a given query, a record must receive **at least 20 clicks or two conversions within the 30-day window.**
## A/B test Dynamic Re-Ranking on category pages
To ensure accurate measurement,
launch your A/B test from the Dynamic Re-Ranking page in the dashboard:
not from the dashboard's A/B test page.
This applies the configuration as intended and limits results to category pages with enough traffic.
## See also
* [Dynamic Re-Ranking's effect on relevance](/doc/guides/algolia-ai/re-ranking#effect-on-relevance).
* [Send click and conversion events with InstantSearch.js](/doc/guides/building-search-ui/events/js).
* [Choose how to send events](/doc/guides/sending-events/getting-started).
* [Security best practice on sending click and conversion events](/doc/guides/algolia-ai/re-ranking#security).
# Send click and conversion events with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/events/js
Unlock Algolia's most powerful features by sending click and conversion events from your InstantSearch app.
## What are events?
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
## How to send events
InstantSearch widgets can automatically send certain events if you enable the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights)
option when setting up your app.
For a full guide to implementing events, see [Send events with InstantSearch](/doc/guides/sending-events/instantsearch/send-events).
## Default events
With the `insights` middleware, your InstantSearch widgets send default events.
To check the default events, go to the [**Events Debugger**](https://dashboard.algolia.com/events/debugger).
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
### Default click events for refinement widgets
The following widgets send `click` events ("Filter Applied") when users select a refinement.
[Custom widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js) using the connectors send the same events.
| Widget | Connector |
| - | - |
| [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js#param-send-event) | `connectHierarchicalMenu` |
| [`menu`](/doc/api-reference/widgets/menu/js#param-send-event) | `connectMenu` |
| [`menuSelect`](/doc/api-reference/widgets/menu-select/js#param-send-event) | `connectMenu` |
| [`ratingMenu`](/doc/api-reference/widgets/rating-menu/js#param-send-event) | `connectRatingMenu` |
| [`refinementList`](/doc/api-reference/widgets/refinement-list/js#param-send-event) | `connectRefinementList` |
| [`toggleRefinement`](/doc/api-reference/widgets/toggle-refinement/js#param-send-event) | `connectToggleRefinement` |
Numeric refinement widgets don't send `clicked` events as the `clickedFilters` event doesn't apply to numeric filters.
This is relevant for `connectNumericMenu`- and `connectRange`-based widgets.
### Default view events for results widgets
The following widgets send `view` events ("Hits Viewed") for the visible items in the search results.
[Custom widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js) using the connectors send the same events.
| Widget | Connector |
| - | - |
| [`autocomplete`](/doc/api-reference/widgets/autocomplete/js#param-indices) | `connectAutocomplete` |
| [`geoSearch`](/doc/api-reference/widgets/geo-search/js#param-send-event) | `connectGeoSearch` |
| [`hits`](/doc/api-reference/widgets/hits/js#param-send-event) | `connectHits` |
| [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js#param-send-event) | `connectInfiniteHits` |
### Default click events for results widgets
The following widgets send `click` events ("Hit Clicked") when users click a search result.
| Widget | Connector |
| - | - |
| [`hits`](/doc/api-reference/widgets/hits/js#param-send-event) | [`connectHits`](/doc/api-reference/widgets/hits/js#customize-the-ui-with-connecthits) |
| [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js#param-send-event) | [`connectInfiniteHits`](/doc/api-reference/widgets/infinite-hits/js#customize-the-ui-with-connectinfinitehits) |
Because of technical limitations, the default `click` events aren't sent when using `connectHits` or `connectInfiniteHits`.
If you're using connectors, make sure to set up `click` events on them.
```js JavaScript theme={"system"}
function render(renderOptions, isFirstRender) {
const { items, sendEvent, widgetParams } = renderOptions;
widgetParams.container.innerHTML = `
`;
[...widgetParams.container.querySelectorAll('li')]
.forEach((element, index) => {
element.addEventListener('click', (event) => {
sendEvent('click', hits[index], 'Hit Clicked');
});
});
}
const customHits = connectHits(render);
// or
const customInfiniteHits = connectInfiniteHits(render);
```
# Get started with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/js
Use InstantSearch.js to create a dynamic, filterable search UI.
## Before you begin
This quickstart requires:
* Basic JavaScript knowledge.
* **[Node.js](https://nodejs.org/en/download)** version 16 or later with **npm** version 5.2 or later.
## Quickstart
In this quickstart, you'll add an Algolia search interface to some starter code.
The code:
* Displays and formats a search box and results
* Uses InstantSearch's pre-built UI components (widgets) to filter results
Run and edit the InstantSearch.js quickstart example in CodeSandbox.
Browse the source for the InstantSearch.js quickstart example on GitHub.
To generate some starter code, use the `create-instantsearch-app` tool.
It provides the data, all necessary code, and predefined credentials ( and ).
In a terminal, paste:
```sh Command line icon=square-terminal theme={"system"}
npx create-instantsearch-app@latest ais-ecommerce-demo-app \
--template "InstantSearch.js" \
--app-id "B1G2GM9NG0" \
--api-key "aadef574be1f9252bb48d4ea09b5cfe5" \
--index-name demo_ecommerce \
--attributes-to-display name \
--attributes-for-faceting ''
```
This command creates a folder structure on your machine:
```txt theme={"system"}
ais-ecommerce-demo-app/
├── node_modules
├── src
│ ├── app.css
│ └── app.js
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── favicon.png
├── index.html
├── package.json
└── README.md
```
1. In `index.html`, remove the `header`, and replace `
` with:
```html HTML icon=code-xml theme={"system"}
InstantSearch.js e-commerce demo
```
2. Open `src/app.css` and replace its content with:
```css CSS icon=paintbrush theme={"system"}
body {
font-family: sans-serif;
padding: 1em;
}
.ais-SearchBox {
margin: 1em 0;
}
```
1. In your terminal, type:
```sh Command line icon=square-terminal theme={"system"}
cd ais-ecommerce-demo-app
npm start
```
2. Open your browser and go to [http://localhost:3000](https://localhost:3000).
The app displays the search box, results, and pagination components.
1. Open `index.html` and update it to:
```html HTML icon=code-xml theme={"system"}
InstantSearch.js e-commerce demo
Brands
```
2. Open `src/app.js`, and add widgets to these placeholders:
```js JavaScript icon=code theme={"system"}
// Before `search.start()`
search.addWidgets([
instantsearch.widgets.clearRefinements({
container: "#clear-refinements",
}),
instantsearch.widgets.refinementList({
container: "#brand-list",
attribute: "brand",
}),
instantsearch.widgets.configure({
hitsPerPage: 8,
}),
]);
```
The widgets come with a [predefined style](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#style-your-widgets) but you can customize it.
1. Open `src/app.css` and update it to the following to apply a two-column layout:
```css CSS icon=paintbrush theme={"system"}
body {
font-family: sans-serif;
padding: 1em;
}
.ais-SearchBox {
margin: 1em 0;
}
.ais-Pagination {
margin-top: 1em;
}
.left-panel {
float: left;
margin-top: 1em;
width: 250px;
}
.right-panel {
margin-left: 260px;
}
```
1. In your browser, after a page refresh, the layout includes a **Brands** .
Open the `src/app.js` and replace the content of the `hits` widget with:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: (hit, { html, components }) => html`
`,
},
}),
]);
```
1. Open `src/app.css` and replace its content with:
```css CSS icon=paintbrush theme={"system"}
body {
font-family: sans-serif;
padding: 1em;
}
.ais-SearchBox {
margin: 1em 0;
}
.ais-Pagination {
margin-top: 1em;
}
.left-panel {
float: left;
margin-top: 1em;
width: 250px;
}
.right-panel {
margin-left: 260px;
}
.ais-InstantSearch {
max-width: 960px;
margin: 0 auto;
}
.ais-Hits-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 1em;
}
.ais-Hits-item img {
margin-right: 1em;
}
.hit-name {
margin-bottom: 0.5em;
}
.hit-description {
color: #888;
font-size: 14px;
margin-bottom: 0.5em;
}
```
In your browser, after a page refresh, you'll see product images.
## The widgets
This quickstart uses several InstantSearch widgets:
* [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) is the root InstantSearch.js component. You must wrap all other widgets inside this component.
* [`searchBox`](/doc/api-reference/widgets/search-box/js) displays a search box for users to type queries into.
* [`hits`](/doc/api-reference/widgets/hits/js) displays the results from Algolia, based on the .
* [`pagination`](/doc/api-reference/widgets/pagination/js) adds navigation controls to browse through pages of results.
* [`clearRefinements`](/doc/api-reference/widgets/clear-refinements/js) displays a button that clears the current refinements.
* [`refinementList`](/doc/api-reference/widgets/refinement-list/js) displays a list of brands that users can use to filter the search.
* [`configure`](/doc/api-reference/widgets/configure/js) passes [search parameters](/doc/api-reference/search-api-parameters). In this quickstart, it sets `hitsPerPage` to 8.
## Next steps
Instead of the credentials from `create-instantsearch-app`,
use your own Algolia [credentials](https://www.algolia.com/users/sign_up).
You can send your own data or [a sample dataset](https://github.com/algolia/datasets/blob/master/ecommerce/records.json) to Algolia using the JavaScript API client that comes preinstalled with `create-instantsearch-app`.
Then, configure the index with the JavaScript API client.
For example, to configure the sample dataset,
use the following code:
```js JavaScript icon="code" theme={"system"}
index
.setSettings({
searchableAttributes: [
"brand",
"name",
"categories",
"unordered(description)",
],
customRanking: ["desc(popularity)"],
attributesForFaceting: ["searchable(brand)", "type", "categories", "price"],
})
.then(() => {
// done
});
```
## See also
* [What is InstantSearch?](/doc/guides/building-search-ui/what-is-instantsearch/js)
* [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
* [InstantSearch widgets](/doc/api-reference/widgets/js)
* [Customize an existing widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js)
* [Send click and conversion events](/doc/guides/building-search-ui/events/js)
# Access state outside the lifecycle
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/access-state-outside-lifecycle/js
InstantSearch.js provides `renderState`, which you can use to render custom widgets or refine the search outside the InstantSearch.js lifecycle.
Each InstantSearch widget owns its part of the search state.
While this fits the majority of use cases, **sometimes you might want to read the state or refine the search outside of the InstantSearch lifecycle**.
For example, you may want to let users refine on a specific category from a product page, and not just from a refinement list.
InstantSearch.js lets you access the render state of each widget, which you can use to create custom widgets or refine the search outside the InstantSearch.js lifecycle.
The `renderState` property is available starting in InstantSearch.js v4.9.
## Refining a search from the outside
In InstantSearch, the [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget controls refinements for a given attribute.
You can access its state from the `renderState` property.
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript theme={"system"}
const search = instantsearch({
indexName,
searchClient,
});
search.addWidgets([
// ...
instantsearch.widgets.refinementList({
container: '#brand',
attribute: 'brand',
sortBy: ['isRefined'],
}),
]);
search.renderState[indexName].refinementList.brand; // returns an object containing the render state
```
The exposed state matches the render options exposed to the [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#customize-the-ui-with-connectrefinementlist) render options.
You can access the state of `brand` and use the [`refine`](/doc/api-reference/widgets/refinement-list/js#param-refine) method to programmatically set a refinement.
```js JavaScript theme={"system"}
search.renderState[indexName].refinementList.brand.refine('Apple');
```
This lets you refine on a brand from anywhere in your app, even outside of the InstantSearch lifecycle.
For example, you might want to let users search in the related brand whenever they visit a product page.
```html HTML theme={"system"}
```
```js JavaScript theme={"system"}
document
.querySelector('#apple-search-button')
.addEventListener('click', () => {
search.renderState[indexName].refinementList.brand.refine('Apple');
});
```
## Accessing the state of other widgets
When refining on a brand, you might end up with no results at all.
By default, the [`hits`](/doc/api-reference/widgets/hits/js) widget displays a "No results" message.
You can customize it using the [`connectHits`](/doc/api-reference/widgets/hits/js#customize-the-ui-with-connecthits) connector along with the [`refinementList`](/doc/api-reference/widgets/refinement-list/js) render state.
First, remove the default template for `empty` hits from the `hits` widget so it doesn't display anything when there are no hits.
```js JavaScript theme={"system"}
instantsearch.widgets.hits({
container: '#hits',
templates: {
// ...
empty: ''
}
});
```
Second, add a button to reproduce the no results situation.
```html HTML theme={"system"}
```
```js JavaScript theme={"system"}
document
.querySelector('#pear-search-button')
.addEventListener('click', () => {
search.renderState[indexName].refinementList.brand.refine('Pear');
});
```
Then, you can add a dedicated custom widget to handle the no results situation.
```js JavaScript theme={"system"}
const renderer = ({ items, widgetParams }) => {
const container = document.querySelector(widgetParams.container);
if (items.length > 0) {
container.innerHTML = '';
return;
}
const brandState = search.renderState[indexName].refinementList.brand;
const isPearRefined =
brandState.items.filter((item) => item.label === 'Pear' && item.isRefined)
.length > 0;
if (!isPearRefined) {
container.innerHTML = 'No results';
return;
}
container.innerHTML = `
No results for Pear
`;
container.querySelector('button').addEventListener('click', () => {
search.renderState[indexName].refinementList.brand.refine('Pear');
});
};
const emptyHits = instantsearch.connectors.connectHits(renderer);
search.addWidgets([
emptyHits({
container: '#empty-hits',
})
]);
```
Even though you're using the `connectHits` connector,
you can access the render state of the `brand` [`refinementList`](/doc/api-reference/widgets/refinement-list/js).
This lets you check whether "Pear" is a selected value and display a button to remove it.
The `refine` method is a toggle function.
When you call it with a value that's already refined, it removes it from the search state, and vice versa.
If you're not familiar with customizing widgets with connectors, see the guide on [customizing the complete UI of widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#customize-the-complete-ui-of-the-widgets).
## Using the panel widget
The [`panel`](/doc/api-reference/widgets/panel/js) widget gives you easier access to the render state.
This is the basic implementation of a [`panel`](/doc/api-reference/widgets/panel/js).
```js JavaScript theme={"system"}
const categories = panel({
templates: {
header: 'Categories',
},
})(instantsearch.widgets.hierarchicalMenu);
search.addWidgets([
categories({
container: '#categories',
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
'hierarchicalCategories.lvl3',
],
}),
])
```
The `renderState` of [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js) for each attribute are available in the `collapsed` and `hidden` methods of widget.
```js JavaScript theme={"system"}
const categories = panel({
templates: {
header: 'Categories',
},
collapsed(options) {
// ...
},
hidden(options) {
// ...
}
})(instantsearch.widgets.hierarchicalMenu);
```
The provided `options` include the same state as you'd find under `search.renderState[indexName].hierarchicalMenu['hierarchicalCategories.lvl0']`.
For example, when there are no facets to display, you can hide or collapse the [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js):
```js JavaScript theme={"system"}
const categories = panel({
// ...
hidden({ items }) {
return items.length === 0;
},
})(instantsearch.widgets.hierarchicalMenu);
```
You can see a [live demo on CodeSandbox](https://codesandbox.io/p/sandbox/github/algolia/doc-code-samples/tree/master/instantsearch.js/render-state).
# Secured API keys in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/api-keys-security/js
Use secured API keys in your InstantSearch.js app to keep your data safe.
## Search-only API keys in web apps
You can use the [search-only API key](/doc/guides/security/api-keys#search-only-api-key) in your frontend code.
This key is safe to expose because it only allows search operations.
If you need additional restrictions,
such as limiting the access to specific indices,
or applying rate limits,
generate a **secured API key** on your backend.
## Secured API keys
Secured API keys add restrictions and search parameters that your users can't change.
You must generate them on your server, or anyone can remove the restrictions again.
This guide shows how to use secured API keys in an InstantSearch app.
For mode details, see [User-restricted access to data](/doc/guides/security/api-keys/how-to/user-restricted-access-to-data).
The aim is to build an app that generates a secured API key with restrictions on the server,
and then uses it on the client.
See the full example on
[GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/secured-api-keys).
### Server
#### Generate a secured API key
Generate the secured API key on your server.
This example uses [Express](https://expressjs.com), but the same approach works with any server.
To generate a key, you need the Algolia JavaScript client and a parent API key with the
[search ACL](/doc/guides/security/api-keys#access-control-list-acl).
In this example, the generated key only allows searching the `demo_ecommerce` .
You can also apply
[additional restrictions](/doc/guides/security/api-keys/in-depth/api-key-restrictions),
including
[search parameters](/doc/guides/security/api-keys/in-depth/api-key-restrictions#query-parameter-restrictions).
```js JavaScript icon=code theme={"system"}
const algoliasearch = require("algoliasearch");
const client = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const securedApiKey = client.generateSecuredApiKey({
parentApiKey: "ALGOLIA_SEARCH_API_KEY",
restrictions: {
restrictIndices: "demo_ecommerce",
},
});
```
Send the secured API key to the client in one of these ways:
* Return it from an endpoint, then fetch it before you create your InstantSearch app.
* Inline it in the HTML, then read it from a global object in the client.
### Inline the API key
If your build tool generates the HTML, add a placeholder value and replace it on the server before sending the page.
If you control the HTML template, inline the value directly.
```html HTML icon=code-xml theme={"system"}
```
Replace the placeholder with the generated API key:
```js JavaScript icon=code theme={"system"}
const path = require("path");
const fs = require("fs");
const util = require("util");
const express = require("express");
const app = express();
const readFileAsync = util.promisify(fs.readFile);
app.get("/", async (_, res) => {
const index = await readFileAsync(
path.join(__dirname, "dist", "index.html"),
"utf-8",
);
const indexWithServerData = index.replace(
"__SERVER_DATA__",
JSON.stringify({
ALGOLIA_API_KEY: securedApiKey,
}),
);
res.send(indexWithServerData);
});
```
Start the server and read the key from `window.SERVER_DATA` in the client.
The next section explains how to use this key in an InstantSearch app.
### Client
#### Retrieve the API key
Now that you have the API key on the global object, you can retrieve it from the client code and inject it into [`searchClient`](/doc/api-reference/widgets/instantsearch/js).
Make sure you clean up the global object.
Otherwise, this value will stay in memory.
```js JavaScript icon=code theme={"system"}
const SERVER_DATA = window.SERVER_DATA;
// clean up the global object
delete window.SERVER_DATA;
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
SERVER_DATA.ALGOLIA_API_KEY,
);
const search = instantsearch({
indexName: "demo_ecommerce",
searchClient,
});
```
That's it for the client.
Your app can now only target the `demo_ecommerce` index.
You can try to target a different one like `demo_media` but the API will return an error.
* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/instantsearch.js/secured-api-keys)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/secured-api-keys)
## Prevent cross-site scripting vulnerabilities
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js) helps users understand how their query matches the results.
It's a key visual cue.
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/vue) helps users understand how their query matches the results.
It's a key visual cue.
Algolia wraps matching words in HTML tags.
By default, it uses tags like [`em`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em), which the browser renders as HTML.
If your site includes user-generated content, this can expose your app to cross-site scripting (XSS) attacks.
The same risk applies to the snippeting feature.
To prevent this, InstantSearch restricts highlighting to a predefined set of HTML tags and escapes all others.
If you set a custom tag in the Algolia dashboard,
it's overridden and replaced by [`mark`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark).
To use a different tag, such as [`strong`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong),
set the `highlightedTagName` props in [`Highlight`](/doc/api-reference/widgets/highlight/js) or [`Snippet`](/doc/api-reference/widgets/snippet/js).
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js).
### Usage with template functions
You can also provide the argument to the `Highlight` and `Snippet` components from [template functions](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#using-html-strings).
```js JavaScript theme={"system"}
const search = instantsearch({
indexName: 'instant_search'
searchClient,
});
search.addWidgets([
instantsearch.widgets.hits({
container: '#hits',
templates: {
item(hit, { html, components }) {
return html`
`;
},
},
})
]);
```
# Turn off automatic filtering in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/automatic-filtering/js
Build a widget to turn off automatic filtering and boosting with InstantSearch.js.
Automatic Filtering and Boosting applies a category filter when users enter a .
You can use [Query Categorization](/doc/guides/algolia-ai/query-categorization) to
predict categories for your most popular queries.
If you enable [Automatic Filtering and Boosting](/doc/guides/algolia-ai/query-categorization#automatic-filtering-and-boosting)
the categories are automatically applied as filters.
## Control Automatic Filtering and Boosting in your UI
If you want to have Automatic Filtering and Boosting on or off **for all searches**, you only need to enable the feature to filter the results automatically.
If you want to let users turn Automatic Filtering and Boosting on or off, you can build an InstantSearch widget. This widget should inform users that the results are filtered. The widget should also let users remove any applied filters.
## Build a widget for Automatic Filtering and Boosting
The search response includes the following properties if [Automatic Filtering and Boosting is enabled](/doc/guides/algolia-ai/query-categorization#detect-the-impact-of-automatic-filtering-and-boosting-at-query-time):
```json JSON icon="braces" theme={"system"}
{
"extensions": {
"queryCategorization": {
"autofiltering": {
"facetFilters": [],
"optionalFilters": []
}
}
}
}
```
If filters are applied to the query, they're listed in the `facetFilters` property.
```json JSON icon="braces" theme={"system"}
{
"facetFilters": [
"category.level0:Fresh products",
"category.level1:Fresh vegetables",
"category.level2:Tomatoes"
]
}
```
To remove filters applied with Automatic Filtering and Boosting, you need to [turn the feature off for the current query](/doc/guides/algolia-ai/query-categorization#override-automatic-filtering-and-boosting-at-query-time) using the `enableAutoFiltering` API parameter.
```json JSON icon="braces" theme={"system"}
{
"extensions": {
"queryCategorization": {
"enableAutoFiltering": false
}
}
}
```
To keep Automatic Filtering and Boosting for other queries,
check for a query change and then set `enableAutoFiltering` to `true`.
## Implementing the widget
The following custom connector implements all steps to let users turn off Automatic Filtering and Boosting for a search query.
The connector requires the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
```JavaScript JavaScript theme={"system"}
//connectAutoFiltering.js
// util function to set the query parameter (step 2)
function setAutoFiltering(value, helper) {
return helper.setQueryParameter('extensions', {
queryCategorization: {
enableAutoFiltering: value,
},
});
}
export function connectAutoFiltering(renderFn, unmountFn = () => {}) {
return function autoFiltering(widgetParams) {
const connectorState = {};
return {
$$type: 'algolia.beta.autoFiltering',
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
true
);
},
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
false
);
},
dispose() {
unmountFn();
},
getWidgetSearchParameters(searchParameters) {
return searchParameters;
},
getRenderState(renderState, renderOptions) {
return {
...renderState,
autoFiltering: this.getWidgetRenderState(renderOptions),
};
},
// this is where the logic happens
getWidgetRenderState({ results, helper, state }) {
if (!connectorState.cancelAutoFiltering) {
// exposing a function to disable autofiltering
connectorState.cancelAutoFiltering = () => {
// Disable auto filtering for next search
setAutoFiltering(false, helper);
helper.search();
// storing in the state the disabled query
connectorState.disabledQuery = helper.getQuery().query;
};
}
// empty results case
if (!results) {
return {
appliedFilters: [],
cancelAutoFiltering: () => {},
widgetParams,
};
}
// enabling back auto filtering if the query has changed (step 3)
if (
// "state" stores the current query parameters
state.extensions &&
state.extensions.queryCategorization.enableAutoFiltering === false &&
connectorState.disabledQuery &&
results.query !== connectorState.disabledQuery
) {
setAutoFiltering(true, helper);
if (
results.extensions &&
results.extensions.queryCategorization.normalizedQuery
) {
// if the current query has predicted categories, we refine the search with autofiltering enabled
helper.search();
}
}
// Retrieving the applied filters (step 1)
const facetFilters =
(results.extensions &&
results.extensions.queryCategorization.autofiltering &&
results.extensions.queryCategorization.autofiltering
.facetFilters) ||
[];
return {
appliedFilters:
facetFilters.map((facetFilter) => ({
name: facetFilter.split(':')[0],
value: facetFilter.split(':')[1],
})),
cancelAutoFiltering: connectorState.cancelAutoFiltering,
widgetParams,
};
},
};
};
}
```
```TypeScript TypeScript theme={"system"}
// connectAutoFiltering.ts
import type { Connector } from 'instantsearch.js';
export type AutoFilteringConnectorParams = {};
export type AutoFilteringRenderState = {
appliedFilters: Array<{
name: string;
value: string;
}>;
cancelAutoFiltering(): void;
};
export type AutoFilteringWidgetDescription = {
$$type: 'algolia.beta.autoFiltering';
renderState: AutoFilteringRenderState;
indexRenderState: {};
indexUiState: {};
};
type AutoFilteringConnector = Connector<
AutoFilteringWidgetDescription,
AutoFilteringConnectorParams
>;
type ConnectorState = {
disabledQuery?: string;
cancelAutoFiltering?: AutoFilteringRenderState['cancelAutoFiltering'];
};
// util function to set the query parameter (step 2)
function setAutoFiltering(value, helper) {
return helper.setQueryParameter('extensions', {
queryCategorization: {
enableAutoFiltering: value,
},
});
}
type WidgetRenderStateWithResultsExtensions = Parameters<
ReturnType>['getWidgetRenderState']
>[0] & {
state: {
extensions?: {
queryCategorization?: {
enableAutoFiltering?: boolean;
};
};
};
results: {
extensions?: {
queryCategorization?: {
normalizedQuery?: string;
autofiltering?: {
facetFilters?: string[];
};
};
};
};
};
export const connectAutoFiltering: AutoFilteringConnector = (
renderFn,
unmountFn = () => {}
) => {
return function autoFiltering(widgetParams) {
const connectorState: ConnectorState = {};
return {
$$type: 'algolia.beta.autoFiltering',
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
true
);
},
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
false
);
},
dispose() {
unmountFn();
},
getWidgetUiState(uiState) {
return uiState;
},
getWidgetSearchParameters(searchParameters) {
return setAutoFiltering(true, searchParameters);
},
getRenderState(renderState, renderOptions) {
return {
...renderState,
autoFiltering: this.getWidgetRenderState(renderOptions),
};
},
// this is where the logic happens
getWidgetRenderState({
results,
helper,
state,
}: WidgetRenderStateWithResultsExtensions) {
if (!connectorState.cancelAutoFiltering) {
connectorState.cancelAutoFiltering = () => {
// Disable auto filtering for next search
setAutoFiltering(false, helper);
helper.search();
// storing in the state the disabled query
connectorState.disabledQuery = helper.getQuery().query;
};
}
// empty results case
if (!results) {
return {
appliedFilters: [],
cancelAutoFiltering: () => {},
widgetParams,
};
}
// enabling back auto filtering if the query has changed (step 3)
if (
// "state" stores the current query parameters
state.extensions?.queryCategorization?.enableAutoFiltering ===
false &&
connectorState.disabledQuery &&
results.query !== connectorState.disabledQuery
) {
setAutoFiltering(true, helper);
if (results.extensions.queryCategorization.normalizedQuery) {
// if the current query has predicted categories, we refine the search with autofiltering enabled
helper.search();
}
}
// Retrieving the applied filters (step 1)
const facetFilters = results.extensions?.queryCategorization?.autofiltering
?.facetFilters || [];
return {
appliedFilters:
facetFilters.map((facetFilter) => ({
name: facetFilter.split(':')[0],
value: facetFilter.split(':')[1],
})),
cancelAutoFiltering: connectorState.cancelAutoFiltering,
widgetParams,
};
},
};
};
};
```
Use this [InstantSearch connector](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js#build-a-custom-connector) to create a widget.
```JavaScript JavaScript theme={"system"}
// autofiltering.js
import { connectAutoFiltering } from './connectAutoFiltering';
export function autoFiltering(widgetParams) {
const { container: containerSelector } = widgetParams;
if (!containerSelector) {
console.error('The `container` option is required.');
}
const container = document.querySelector(containerSelector);
// the widget render function
const renderer = (renderOptions, isFirstRender) => {
const { appliedFilters, cancelAutoFiltering } = renderOptions;
/**
* Here we choose to display the applied filters in a banner with a button to disable autofiltering
* We are displaying only the last filter of the hierarchy, but you can chose to display all of them if you want to
* It is entirely up to you how you choose to render the filters
*/
container.innerHTML = appliedFilters.length
? `
Applied filter:
${appliedFilters.pop().value}
×
`
: '';
const button = container.querySelector('button');
if (button) {
// attaching our cancel function to our button
button.addEventListener('click', () => {
cancelAutoFiltering();
});
}
};
const makeWidget = connectAutoFiltering(renderer, () => {
container.innerHTML = '';
});
return {
...makeWidget({}),
$$widgetType: 'algolia.beta.autoFiltering',
};
}
```
```TypeScript TypeScript theme={"system"}
// autofiltering.ts
import { connectAutoFiltering } from './connectAutoFiltering';
import type { AutoFilteringRenderState } from './connectAutoFiltering';
type WidgetParams = {
container: string;
};
export function autoFiltering(widgetParams: WidgetParams) {
const { container: containerSelector } = widgetParams;
if (!containerSelector) {
console.error('The `container` option is required.');
}
const container = document.querySelector(containerSelector);
// the widget render function
const renderer = (
renderOptions: AutoFilteringRenderState,
isFirstRender: boolean
) => {
const { appliedFilters, cancelAutoFiltering } = renderOptions;
/**
* Here we choose to display the applied filters in a banner with a button to disable autofiltering
* We are displaying only the last filter of the hierarchy, but you can chose to display all of them if you want to
* It is entirely up to you how you choose to render the filters
*/
container.innerHTML =
appliedFilters && appliedFilters.length
? `
Applied filter:
${appliedFilters.pop().value}
×
`
: '';
const button = container.querySelector('button');
if (button) {
// attaching our cancel function to our button
button.addEventListener('click', () => {
cancelAutoFiltering();
});
}
};
const makeWidget = connectAutoFiltering(renderer, () => {
container.innerHTML = '';
});
return {
...makeWidget({}),
$$widgetType: 'algolia.beta.autoFiltering',
};
}
```
If you copy the code snippets into your implementation,
check the widget render function to adapt to your design.
You can add the widget to your UI:
```js JavaScript theme={"system"}
import { autoFiltering } from './autoFiltering';
// ...
const search = instantsearch({
indexName,
searchClient,
});
search.addWidgets([
// ...
autoFiltering({
container: '#autofiltering-container',
}),
]);
```
# Implement backend search with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js
Implement server-side search with InstantSearch.js and Algolia's API clients.
You may wish to query Algolia's servers from your backend instead of the frontend, while still being able to use InstantSearch widgets.
Possible motivations could be for security restrictions, for SEO purposes or to enrich the Algolia data with data from your servers.
Keep in mind though that [Algolia recommends frontend search](https://support.algolia.com/hc/en-us/articles/4406981933457-Searching-from-the-frontend-or-the-backend-What-do-you-recommend-) for performance and high availability reasons.
## How backend search works
InstantSearch is the UI on top of a search client.
Its state is managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
However, you can use a different search client.
### Algolia search client
Algolia's [search client](https://github.com/algolia/algoliasearch-client-javascript) queries Algolia's backend whenever users refine the search.
The response from Algolia's search client is a JSON object that includes:
* Results already formatted for display (full record content, embedded HTML, image URLs, and so on).
* Highlighting and snippeting
* Pagination
For more information, see [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/js).
### Custom search client
You can implement your own search client to query your backend which, in turn, uses the Algolia search client to query Algolia's backend.
To create your own client, you must implement an interface that receives and returns formatted data that InstantSearch can understand.
## On the backend: create the necessary routes
This guide assumes that you have an existing server running on [`http://localhost:3000`](http://localhost:3000) with the route `POST /search` that takes the default Algolia parameters as JSON.
This backend could be using the [JavasScript API client](/doc/libraries/sdk/install) to query Algolia, on top of any other operations you want to perform.
The `algoliasearch` package lets you query Algolia from your backend. Here's an example using [Express](https://expressjs.com/):
```js JavaScript icon="code" theme={"system"}
// Instantiate an Algolia client
const algoliasearch = require("algoliasearch");
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
// Add the search endpoint
app.post("/search", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.search(requests);
res.status(200).send(results);
});
// Add the recommendations endpoint
app.post("/getRecommendations", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.getRecommendations(requests);
});
```
Once your new route is ready, get back to the frontend and create a search client that can communicate with this server.
## On the frontend: call your new backend routes
### Searching for results
The `search()` method runs whenever users search or refine results.
Because the server accepts requests in the InstantSearch format,
forward the from this method to your backend and return the response.
```js JavaScript icon="code" theme={"system"}
const requestsCache = new Map();
const resultsCache = new Map();
function makeCacheKey(requests, target) {
// You can go further here, like sorting individual values inside the requests.
// This makse sure the key is always the same for the same requests.
return target + JSON.stringify(requests);
}
const customSearchClient = {
search(requests) {
const key = makeCacheKey(requests, 'search');
if (resultsCache.has(key)) {
return Promise.resolve(resultsCache.get(key));
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/search', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
getRecommendations(requests) {
const key = makeCacheKey(requests, 'getRecommendations');
if (resultsCache.has(key)) {
return resultsCache.get(key);
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/getRecommendations', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
clearCache() {
requestsCache.clear();
resultsCache.clear();
},
};
```
This example uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to query the server.
Check the [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility) requirements before using it in production.
If you want to transform the data passed to your server,
you can learn more about the parameters that the `search` and `getRecommendations` methods support:
* [Search API parameters](/doc/api-reference/api-parameters)
* [Recommendations API parameters](/doc/rest-api/recommend/get-recommendations)
### Search for facet values
The same `search()` method is used by InstantSearch to fetch values from refinement lists.
You must set the option [`searchable`](/doc/api-reference/widgets/refinement-list/js#param-searchable) to `true` in your refinement list to make it searchable.
## Use the search client with InstantSearch
Now, you need to tell InstantSearch to use the search client you created.
This is possible with the `searchClient` option.
This parameter turns off all Algolia requests coming from the frontend and proxies them to your backend and displays the UI.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "YourIndexName",
searchClient: customSearchClient,
});
search.start();
```
## Enrich data from the backend
Now that InstantSearch is querying your backend before fetching results from Algolia,
you could merge Algolia's data with yours (to offer your users more exhaustive results).
A recurring problem with ecommerce websites using Algolia managing the remaining stock for each product.
It's sometimes hard to keep track of the exact number of items.
An approach made possible with a custom backend is to only store the item's availability on each Algolia record (`none`, `low`, `medium`, `high`) and to fetch the exact stock on your database.
You need to make a few changes to your backend:
```js JavaScript icon=code theme={"system"}
app.post('/search', async ({body}, res) => {
const requests = body;
const algoliaResults = await algoliaClient.search(requests);
const results = {
...algoliaResults,
results: algoliaResults.results.map(result => ({
...result,
hits: result.hits.map(async hit => ({
...hit,
// `getStock()` retrieves a product's exact stock from your database
stock: await getStock(hit.productID),
})),
})),
};
res.status(200).send(results);
});
app.post('/getRecommendations', async ({body, res) => {
/* The same logic as above can be applied here */
});
```
You can now access the property `stock` on each hit with InstantSearch on the frontend.
# Backend search with an API client
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/js
Understand the search Algolia provides out of the box.
To query your Algolia , you need an Application ID and a valid API key.
You can find them in the Algolia dashboard's [API Keys](https://dashboard.algolia.com/account/api-keys/) section.
These credentials let you connect to Algolia and perform operations on your data.
Use one of Algolia's [API clients](/doc/libraries/sdk) to guarantee end-to-end reliability.
## Basic queries
To retrieve results from Algolia, use the [Search index](/doc/libraries/sdk/v1/methods/search) method. Querying Algolia involves:
* Choosing the index to query against
* Sending the query string
* Adding optional parameters
Along with the user's text, you can provide any number of [query parameters](/doc/api-reference/api-parameters) in your .
Some override an index's settings, such as disabling typo tolerance.
Others enrich the search, such as filtering.
Adding optional parameters can alter textual relevance behavior at query-time.
For example, it's possible to define stop words or change plurals behavior only for certain queries.
Algolia recommends querying directly from the user's browser, mobile device, or client to ensure optimal performance by reducing latency (and benefiting from offloading your servers).
Here's a basic query example:
```js JavaScript icon="code" theme={"system"}
index.search("query", {
ignorePlurals: true,
});
```
### Empty queries
Sometimes, you might not need to pass in a textual query.
It's possible to retrieve results from Algolia by specifying an index and other optional parameters (most likely, a ).
In this way, it's possible to use Algolia beyond a standard "search results" experience.
For example, one common use case is to use filtering capabilities to generate category landing pages.
### Browsing your index
Algolia lets you fetch all in an index using the [`browse`](/doc/libraries/sdk/v1/methods/browse) method.
If you need to retrieve the full contents of your index (for backup, SEO purposes or for running a script on it),
use the [`browse`](/doc/libraries/sdk/v1/methods/browse) method instead of [`search`](/doc/libraries/sdk/v1/methods/search), as [`search`](/doc/libraries/sdk/v1/methods/search) is optimized for speed.
Results are returned ranked by attributes and custom ranking. Additionally, [`browse`](/doc/libraries/sdk/v1/methods/browse) supports most search parameters, meaning you can easily apply filters to retrieve a subset of results.
### Multiple indices / multiple queries
[Multiple indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices) are used when you have different kinds of data (movies + actors), or you need to display results with different rankings. You can do this by using the same query / response logic, one for each index or query.
## Response format
If a query is successful, your will generate a JSON response in return.
This response contains both the matching results (*hits*) as well as other information useful for building a full-fledged search experience (for example, facets, highlighting, hits count, paging, and more).
For more information, see [Understand the API response](/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/vue)
### Highlighting and snippeting
Highlighting is an important tool to demonstrate to searchers *why* a result matched their query by providing different styling to all matched query words.
By default, highlighting is enabled on all searchable attributes.
Snippets only return a portion of the matched attribute: the matched words and some words around them. Unlike highlighting, snippeting must be proactively enabled for each attribute you wish to snippet, although you can set the value `*` to snippet all attributes.
### Pagination
Pagination gives you full control over how you retrieve query results. You can implement standard paging or retrieve specific record subsets that ignore page boundaries (infinite scrolling).
You can set pagination defaults at indexing time and override them at query time.
### Faceting
Facets are used to create categories on a select group of attributes.
For example, useful facets for an index of books might be author and genre.
Additionally, Algolia calculates the count of records for each .
You can display facets and facet counts in the UI to let users filter results (for example, by author or genre).
### Search for facet values
Closely tied to *faceting* is *searching* for facet values.
With this powerful feature,
you give your user the ability to navigate through maybe hundreds of different facet values.
And because they can search, you are no longer concerned about creating too many values—they will find what they need.
## See also
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Paginate results](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/js)
* [Highlighting](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js)
* [Search for facet values](/doc/libraries/sdk/v1/methods/search-for-facet-values)
# Understand the API response
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/js
Learn more about the Algolia API response
Algolia search responses look similar to:
```json JSON icon="braces" theme={"system"}
{
"hits": [
{
"firstname": "Jimmie",
"lastname": "Barninger",
"objectID": "433",
"_highlightResult": {
"firstname": {
"value": "Jimmie",
"matchLevel": "partial"
},
"lastname": {
"value": "Barninger",
"matchLevel": "none"
},
"company": {
"value": "California Paint & Wlpaper Str",
"matchLevel": "partial"
}
}
}
],
"page": 0,
"nbHits": 1,
"nbPages": 1,
"hitsPerPage": 20,
"processingTimeMS": 1,
"query": "jimmie paint"
}
```
## Hits
The most important field in the search response is `hits`, an array of objects matching the active search state.
Each hit contains the object's **retrievable attributes**,
those attributes that have been configured to be returned in search responses.
Two additional fields may be present if **highlighting** and **snippeting** have been enabled:
* `_highlightResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a highlighted version of the original attribute, as well as information describing the quality of the match.
* `_snippetResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a snippeted version of the original attribute, as well as information describing the quality of the match.
If **highlighting** or **snippeting** is enabled, the highlighted and snippeted values of `firstname`,
for example, will be available as `_highlightResult.firstname.value` and `_snippetResult.firstname.value`, respectively.
These values can then be used, instead of their original values, to support highlighting and snippeting in a results interface.
## Pagination
**A single response doesn't return all hits**—the hits are paginated,
with the current pagination state reflected by a few response fields:
* `nbHits`: the total number of hits matching the search state
* `nbPages`: the total number of pages in the search response
* `hitsPerPage`: the configurable number of hits presented per page
* `page`: the current, zero-indexed page of responses
In a search interface, these fields can be used to render appropriate pagination controls:
* Increment `page` when a "Next" button is clicked, or a specific page when a number is clicked.
* Don't show a "Next" button if `page` == `nbPages`.
* Don't show a "Back" button if `page` == `0`.
InstantSearch.js handles these cases with minimal configuration.
## Ranking information
By default, the search response doesn't include ranking information to keep the response as small as possible.
To include the ranking information, use the [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) parameter with your .
For example:
```json JSON icon="braces" theme={"system"}
{
"_rankingInfo": {
"nbTypos": 0,
"firstMatchedWord": 0,
"proximityDistance": 0,
"userScore": 7,
"geoDistance": 1600,
"geoPrecision": 1,
"nbExactWords": 0,
"words": 0,
"filters": 0,
"matchedGeoLocation": {
"lat": 37.3688,
"lng": -122.036,
"distance": 1600
}
}
}
```
For more information, see [Search API response](/doc/rest-api/search/search-single-index#response-hits)
# Backend search with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/js
Set up search on your backend with InstantSearch.js.
## Frontend versus backend search
Use frontend search because:
* **It offers a better response speed**. Speed alone justifies the choice. Backend search requires two more trips between a user's browser and Algolia's servers. This decreases the effectiveness of Algolia's engine and infrastructure optimizations.
* **UI libraries aid development**. Algolia's UI libraries ([InstantSearch](/doc/guides/building-search-ui/getting-started/js) and [Autocomplete](/doc/ui-libraries/autocomplete/introduction/getting-started)) help build a frontend solution so that you can focus on configuring relevance and creating the best possible search experience.
* **It frees up your resources**. Algolia reduces the need for development and eliminates the requirement for additional server deployments and its associated infrastructure.
## Why backend search?
* Server-side pre-processing. Perhaps the most important reason for a backend solution is to make complex calculations or format your data before displaying the results.
* Real-time availability updates (like Airbnb)
* In some situations, SEO is easier to manage.
## How to implement backend search
You only need a few lines of code to send a query to Algolia and receive a response.
The response contains everything needed to build your UI.
You can perform backend search with either InstantSearch or one of the Algolia API clients.
### Backend InstantSearch
[Backend InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js) offers a good compromise.
You perform the search on your servers, carry out any required pre-processing, and then send your results to InstantSearch's widgets.
This enables you to control the query and response but saves you the work of building a custom frontend.
### Backend search with an Algolia API client
This option requires a server-side solution and custom frontend development. You manage all queries and responses, and you build the frontend UI.
You need to parse the API response: a JSON object.
The response includes:
* Results already formatted for display (full record content, embedded HTML, or image URLs)
* Highlighting and snippeting
* Pagination
For more information, see:
* [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/js)
* [Pagination](/doc/api-reference/api-parameters/page)
* [Highlighting](/doc/api-reference/api-parameters/attributesToHighlight)
* [Snippeting](/doc/api-reference/api-parameters/attributesToSnippet)
# Build an AI-powered search experience
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/ai-search-experience/js
Combine the search box, chat, and autocomplete widgets to build an AI-powered search experience with InstantSearch.js.
Pair classic InstantSearch widgets with an [Agent Studio](/doc/guides/algolia-ai/agent-studio) chat agent to give users a conversational entry point alongside their search results. This guide wires together:
* An "AI Mode" button on the search box that opens a chat panel loaded with the user's query.
* The `chat` widget itself, with hidden context to share session metadata.
* AI-generated prompt suggestions in the autocomplete drop-down menu.
* Streaming controls so you can stop or resume an in-flight response.
## Before you begin
* An [Agent Studio agent ID](https://dashboard.algolia.com/generativeAi/agent-studio/agents).
* An `instantsearch` instance with an Algolia search client.
* `instantsearch.js` installed at a version that ships AI Mode (for example, the release from PR [`algolia/instantsearch#6982`](https://github.com/algolia/instantsearch/pull/6982) or later).
Add the [`chat`](/doc/api-reference/widgets/chat/js) widget on the same `instantsearch` instance as your search UI.
The other widgets in this guide rely on it being present.
```js JavaScript icon=code theme={"system"}
import algoliasearch from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { chat } from "instantsearch.js/es/widgets";
const search = instantsearch({
indexName: "instant_search",
searchClient: algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey"),
});
search.addWidgets([
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
}),
]);
search.start();
```
The `chat` widget renders the chat panel but doesn't open it on its own.
In this guide, the AI Mode button you add in the next step is the entry point.
If you don't enable AI Mode, add the [`chatTrigger`](/doc/api-reference/widgets/chat-trigger/js) widget instead, or the `chat` widget logs a development warning.
Set [`aiMode`](/doc/api-reference/widgets/search-box/js#param-ai-mode) on `searchBox` to render an AI Mode button next to the input.
Clicking it opens the `chat` widget and forwards the current query.
AI Mode is a valid chat entry point, so you don't also need a [`chatTrigger`](/doc/api-reference/widgets/chat-trigger/js) widget.
```js JavaScript icon=code theme={"system"}
import { chat, searchBox } from "instantsearch.js/es/widgets";
search.addWidgets([
searchBox({
container: "#searchbox",
aiMode: true,
}),
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
}),
]);
```
If you use [`autocomplete`](/doc/api-reference/widgets/autocomplete/js), surface AI-generated prompt suggestions alongside hits.
See [`showPromptSuggestions`](/doc/api-reference/widgets/autocomplete/js#param-show-prompt-suggestions) for the full configuration surface.
```js JavaScript icon=code theme={"system"}
import { autocomplete } from "instantsearch.js/es/widgets";
search.addWidgets([
autocomplete({
container: "#autocomplete",
showPromptSuggestions: {
indexName: "prompt_suggestions",
},
indices: [
{
indexName: "instant_search",
templates: {
item: ({ item }, { html }) => html`
${item.name}
`,
},
},
],
}),
]);
```
Selecting a prompt suggestion opens the `chat` widget on the same index and sends the suggestion as the first message.
Use the [`context`](/doc/api-reference/widgets/chat/js#param-context) option to share metadata with the agent on every message.
The widget serializes the value as JSON and adds it to the user message as a hidden text part.
Users don't see it in the chat UI, but the agent does.
The widget sends `context` to the agent in plain text.
Don't put secrets, access tokens, or personally identifiable information you don't intend to share with the model in this field.
```js Static object theme={"system"}
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
context: { locale: "en", segment: "logged-in" },
});
```
```js Function theme={"system"}
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
context: () => ({
locale: navigator.language,
currentPage: window.location.pathname,
}),
});
```
Listen for response completion with [`onFinish`](/doc/api-reference/widgets/chat/js#param-on-finish), and use the [`connectChat`](/doc/api-reference/widgets/chat/js#streaming-and-resumption) connector if you need a custom Stop button or to call `resumeStream()` manually.
See [Streaming and resumption](/doc/api-reference/widgets/chat/js#streaming-and-resumption) on the `chat` widget reference for the full status surface.
```js JavaScript icon=code theme={"system"}
search.addWidgets([
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
resume: true,
onFinish: ({ message, isError }) => {
if (!isError) {
analytics.track("chat_message_completed", { id: message.id });
}
},
}),
]);
```
Set [`resume: true`](/doc/api-reference/widgets/chat/js#param-resume) to reconnect to an in-flight assistant response after a page reload.
## End-to-end example
```js JavaScript icon=code expandable theme={"system"}
import algoliasearch from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import {
autocomplete,
chat,
hits,
searchBox,
} from "instantsearch.js/es/widgets";
const search = instantsearch({
indexName: "instant_search",
searchClient: algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey"),
});
search.addWidgets([
searchBox({
container: "#searchbox",
aiMode: true,
}),
autocomplete({
container: "#autocomplete",
showPromptSuggestions: { indexName: "prompt_suggestions" },
indices: [
{
indexName: "instant_search",
templates: {
item: ({ item }, { html }) => html`
${item.name}
`,
},
},
],
}),
hits({
container: "#hits",
templates: {
item: (hit, { html }) => html`${hit.name}`,
},
}),
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
resume: true,
context: () => ({
locale: navigator.language,
currentPage: window.location.pathname,
}),
onFinish: ({ message, isError }) => {
if (!isError) {
analytics.track("chat_message_completed", { id: message.id });
}
},
}),
]);
search.start();
```
## Related
* [`searchBox` widget reference](/doc/api-reference/widgets/search-box/js)
* [`chat` widget reference](/doc/api-reference/widgets/chat/js)
* [`autocomplete` widget reference](/doc/api-reference/widgets/autocomplete/js)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
# Conditional display in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-display/js
Learn how to display content conditionally in InstantSearch.js for empty queries, searches without results, and errors.
This guide describes what to do when there are [no results](#handle-searches-without-results), when there's [no query](#handle-empty-queries), or when there are [errors](#handle-search-errors).
If results haven't loaded, the search may still be in progress or the device may be offline.
For more information,
see [No hits or is the search still in progress?](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/js#no-hits-or-is-the-search-still-in-progress)
If you want to feature content in your search results based on a set of conditions, you can use Algolia Rules to:
* [Feature specific data from within your records](https://www.algolia.com/ecommerce-merchandising-playbook/merchandise-search-results-page/) to, for example, show promotional prices during a sales period
* [Display advertisements or promotional banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
To learn how to suppress InstantSearch's initial ,
see [Conditional requests](/doc/guides/building-search-ui/going-further/conditional-requests/js).
## Handle searches without results
When a query doesn't return results,
display a message and suggest changing the query or clearing filters.
### Display a message
To display a fallback message when a query doesn't return results,
use the [`templates.empty`](/doc/api-reference/widgets/hits/js#param-empty) option.
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript theme={"system"}
search.addWidgets([
instantsearch.widgets.hits({
container: 'hits',
templates: {
empty: ({ query }, { html }) =>
html`
No results have been found for ${ query }.
`
}
})
]);
```
The preceding example also works with [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js).
### Let users reset filters and facets
If users apply an overly specific configuration, they may not find any results.
Account for this by letting them reset filters from the "no results" display so they can start another search.
Do this with the [`clearRefinements`](/doc/api-reference/widgets/clear-refinements/js) widget.
However, you can't use it inside the `empty` template, so must use [routing](/doc/guides/building-search-ui/going-further/routing-urls/js) instead.
First, activate the URL sync mechanism:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
routing: true
});
```
Routing makes your InstantSearch app aware of changes in the URL.
By removing URL parameters, you can influence search parameters and reset filters and facets.
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.hits({
container: 'hits',
templates: {
empty: ({ query }, { html }) =>
html`
`,
},
}),
]);
```
## Handle empty queries
By default, InstantSearch sends search requests even when the query is empty.
To hide results until users enter a query,
use the following code.
This hides the results without preventing search requests.
### Using `transformItems`
Using `transformItems` to display no results when the query is empty:
```js JavaScript icon=code theme={"system"}
hits({
transformItems(items, { results }) {
if (results.query === '') return [];
return items;
},
templates: {
empty(results, { html }) {
if (results.query === '') return null;
return html`No results found for "${results.query}"`;
},
}
});
```
### Using a connector
The following example uses the [`connectHits`](/doc/api-reference/widgets/hits/js#customize-the-ui-with-connecthits) connector.
```js JavaScript icon=code theme={"system"}
const customHits = instantsearch.connectors.connectHits(
(renderOptions, isFirstRender) => {
const { results, widgetParams } = renderOptions;
const { container } = widgetParams;
container.innerHTML =
results && results.query
? `
Searching for query "${results.query}".
`
: `
No query
`;
}
);
search.addWidgets([
customHits({
container: document.querySelector('#hits')
})
]);
```
## Handle search errors
If an error occurs, you can display a specific piece of content to help users return to the standard state.
### Making an error-handling widget
You can build a custom widget to handle errors.
If you want to learn more about custom widgets, [check out the guide](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js).
```js JavaScript icon=code theme={"system"}
search.addWidgets([
{
$$type: 'error-display',
render({ status, error }) {
if (status === 'error' && error) {
console.error('Error', error);
}
}
}
]);
```
You can improve this custom widget by writing a more meaningful error message, adapting it to your UI, figuring out if users are online, and logging the error in your monitoring system.
# Conditional requests with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/js
Learn how to make conditional requests with InstantSearch.js
By default, InstantSearch sends an initial to Algolia's servers with an empty .
This connection helps speed up later requests.
However, sometimes you don't want to perform more network calls than are necessary.
For example, you may want to limit the number of search requests and reduce your overall Algolia usage.
This guide helps you build a UI that prevents this initial request.
## How a search client works
InstantSearch is the UI layer that sits on top of [Algolia's search client layer](https://github.com/algolia/algoliasearch-client-javascript),
with a state managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper) layer.
These three layers are interchangeable so that you can use the InstantSearch widgets with a different search client.
The search client queries Algolia's servers whenever users refines a search.
You can build a custom search client to add custom behavior.
For example,
to perform [backend searches with InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js).
## Implementing a proxy
To prevent the initial empty query, you must wrap a custom search client around Algolia's, and pass it to [`instantsearch`](/doc/api-reference/widgets/instantsearch/js). The custom search client acts as a proxy for search requests:
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript theme={"system"}
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
const searchClient = {
...algoliaClient,
search(requests) {
return algoliaClient.search(requests);
},
};
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
search.start();
```
This proxy lets you add some logic before calling the `search` method.
In this case, you only call it when performing a query.
You can also use a proxy to only begin searching after a certain number of characters have been typed by adding a condition, for example, `query.length > 3`.
## Detecting empty search requests
If you don't want to perform a search request when the query is empty (`""`),
you need to detect it:
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
// Here we have to do something else
}
return algoliaClient.search(requests);
},
};
```
Sometimes, one user action results in multiple queries, for example, clicking on an [`refinementList`](/doc/api-reference/widgets/refinement-list/js) or using multiple [`index`](/doc/api-reference/widgets/index-widget/js) widgets.
Make sure that every query is empty before intercepting the function call.
When an empty search is detected, you must return a [formatted response](/doc/libraries/sdk/v1/methods/search#response) as an array of objects of the same length as the `requests` array.
Each object requires at least these properties: `hits`, `nbHits`, and `processingTimeMS`.
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
```
Now you can use the proxy with the [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) widget, like this:
```js JavaScript theme={"system"}
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
search.start();
```
# Improve performance
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/js
Learn how to optimize your InstantSearch.js app.
Algolia is fast by default.
But network speed and bandwidth can vary.
This page lists best practices you can implement to adapt to your users' network conditions.
## Prepare the connection to Algolia
When sending the first network request to a domain, a security handshake must happen, consisting of several round trips between the client and the Algolia server.
If the handshake first happened when users typed their first keystroke, the speed of that first request would be slower.
Use a [preconnect link](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preconnect) to carry out the handshake after loading the page but before any user interaction.
To do this, add a link tag with your Algolia domain in the `head` of your page.
```html HTML icon=code-xml theme={"system"}
```
## Add a loading indicator
Consider a user accessing your app in a subway:
1. They type some characters
2. Nothing happens
3. They wait, but still, nothing happens
However, you can enhance the user experience by displaying a loading indicator to indicate something is happening.
To display a loading indicator in the `searchBox`, use the [`showLoadingIndicator`](/doc/api-reference/widgets/search-box/js#param-show-loading-indicator) option.
The indicator will display slightly after the last query has been sent to Algolia.
Change the duration of the delay with [`stalledSearchDelay`](/doc/api-reference/widgets/instantsearch/js#param-stalled-search-delay) (on the `instantSearch` widget).
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, you're using a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js).
For example:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
stalledSearchDelay: 200, // this is the default value for the delay
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchBox",
placeholder: "Search for products",
showLoadingIndicator: true, // this add the loading indicator
}),
]);
```
### Make your own loading indicator
You can also use the loading indicator with other widgets.
The following example shows how to make a custom component that writes `Loading...` when search stalls.
If network conditions are optimal, users won't see this message.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
// The container to use
const loadingContainer = document.querySelector("#loading");
search.addWidgets([
{
render({ searchMetadata = {} }) {
const { isSearchStalled } = searchMetadata;
loadingContainer.innerHTML = isSearchStalled ? "Loading..." : "";
},
},
]);
```
## Debouncing
Another way of improving the perception of performance is by preventing lag.
InstantSearch generates one query per keystroke by default.
This can lead to a lag in the worst network conditions because browsers can only make a limited number of parallel requests.
By reducing the number of requests, you can prevent this lag.
Debouncing limits the number of requests and avoids processing unnecessary ones by avoiding sending requests before a timeout.
Implement debouncing at the `searchBox` level with the [`queryHook`](/doc/api-reference/widgets/search-box/js#param-query-hook) option.
For example:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
let timerId;
let timeout = 0;
search.addWidgets([
instantsearch.widgets.searchBox({
container: document.querySelector("#searchBox"),
placeholder: "Search for products",
queryHook(query, refine) {
clearTimeout(timerId);
timerId = setTimeout(() => refine(query), timeout);
},
}),
]);
```
This function uses the option `queryHook` and is called on every keystroke.
In the example, the code debounces the call to `refine`.
When network speeds improve, restore search-as-you-type to offer users the best possible experience.
You can use the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) to detect connection changes.
The [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) is unavailable on some browsers.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
let timerId;
let timeout = 0;
search.addWidgets([
instantsearch.widgets.searchBox({
container: document.querySelector("#searchBox"),
placeholder: "Search for products",
queryHook(query, refine) {
clearTimeout(timerId);
timerId = setTimeout(() => refine(query), timeout);
},
}),
]);
const connection = navigator.connection;
if (connection) {
connection.addEventListener("change", function updateTimeout() {
timeout = ["slow-2g", "2g"].includes(connection?.effectiveType) ? 400 : 0;
});
}
```
### Select a debounce delay
The optimal debouncing delay should match your audience's typing speed
(typically 30 words per minute (WPM) on mobile devices and 40 WPM on desktop devices).
If the delay is too short, users still see flashes of content.
If the delay is too long, the time between key presses and results becomes too long.
200 ms is the preferred debounce delay.
Delays of over 300 ms will start degrading the user experience.
## Optimize build size
InstantSearch supports [dead code elimination through tree shaking](https://webpack.js.org/guides/tree-shaking/), but you must follow these rules for it to work:
* Bundle your code using a module bundler that supports tree shaking with the `sideEffects` property in `package.json`, such as [Rollup](https://rollupjs.org/) or [webpack 4+](https://webpack.js.org).
* Ensure you pick the ES module build of InstantSearch by targeting the `module` field in `package.json` ([`resolve.mainFields` option in webpack](https://webpack.js.org/configuration/resolve/#resolvemainfields), [`mainFields` option in `@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve#mainfields)). This is the default configuration in most popular bundlers: you only need to change something if you have a custom configuration.
* Keep [Babel](https://babeljs.io) or other transpilers from transpiling ES6 modules to CommonJS modules. Tree shaking is much less optimal on CommonJS modules, so it's better to let your bundler handle modules itself.
If you're using Babel, you can configure [`babel-preset-env`](https://babeljs.io/docs/en/babel-preset-env) not to process ES6 modules:
```js JavaScript icon=code theme={"system"}
// babel.config.js
module.exports = {
presets: [
[
"env",
{
modules: false,
},
],
],
};
```
If you're using the [TypeScript compiler (`tsc`)](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#tsc-the-typescript-compiler):
```jsonc JSON icon=braces theme={"system"}
// tsconfig.json
{
"compilerOptions": {
"module": "esnext",
}
}
```
Import only what you need. For this, use the multiple entry points from the ES module build:
```js JavaScript icon=code theme={"system"}
// instantsearch() function without reference to the widgets or connectors
import instantsearch from 'instantsearch.js/es';
// Import connectors individually
import { connectSearchBox } from 'instantsearch.js/es/connectors';
// Import widgets individually
import { searchBox } from 'instantsearch.js/es/widgets';
const search = instantsearch({ ... });
search.addWidgets([searchBox({ ... })]);
search.addWidgets([connectSearchBox(() => { ... })({ ... })])
```
### Troubleshooting
To check if tree shaking works, try to import InstantSearch into your project without using it.
```js JavaScript icon=code theme={"system"}
import "instantsearch.js/es"; // Unused import
```
Build your app, then look for the unused code in your final bundle (for example, "InstantSearch").
If tree shaking works, you shouldn't find anything.
## Caching
### Caching by default (and how to turn it off)
By default, Algolia caches the search results of the queries, storing them locally in the cache. This cache only persists during the current page session, and as soon as the page reloads, the cache clears.
If users type a search (or part of it) that's already been entered, the results will be retrieved from the cache instead of requesting them from Algolia, making the app much faster.
While it's a convenient feature, sometimes you may want to clear the cache and make a new request to Algolia.
For instance, when changes are made to some in your , you should update your app's frontend to reflect that change (and avoid displaying stale results retrieved from the cache).
The `refresh` function, available for custom connectors, lets you clear the cache and trigger a new search.
The [`refresh()`](/doc/api-reference/widgets/instantsearch/js#param-refresh) method on the `instantsearch` instance lets you clear the cache and trigger a new search.
### When to discard the cache
Consider discarding the cache when your app's data is updated by:
* Your users (for example, in a dashboard). In this case, refresh the cache based on an app state, such as the last user modification.
* Another process you don't manage (for example, a cron job that updates users inside Algolia). In this case, refresh your app's cache periodically.
### Refresh the cache triggered by a user action
The following code triggers a refresh based on a user action (such as adding a new product or clicking a button).
Run and edit the Improve performance example in CodeSandbox.
Browse the source for the Improve performance example on GitHub.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "demo_ecommerce",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
]);
search.addWidgets([
instantsearch.widgets.hits({
container: "#hits",
}),
]);
document.querySelector("button").addEventListener("click", () => {
search.refresh();
});
search.start();
```
### Refresh the cache periodically
You can set an interval to determine how often the app clears the cache.
Use this approach if you can't trigger cache clearance based on user actions.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "demo_ecommerce",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
setInterval(() => {
search.refresh();
}, 5000);
search.start();
```
If you need to wait for an action from Algolia, use [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) to avoid refreshing the cache before the task completes.
Run and edit the Improve performance example in CodeSandbox.
Browse the source for the Improve performance example on GitHub.
### Disable the cache
If you need the most current data and the performance impact of this isn't an issue,
[turn off caching](/doc/libraries/sdk/caching#requests-and-responses).
```js JavaScript icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { createNullCache } from "@algolia/cache-common";
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
{
// Disable caching for completed requests
responsesCache: createNullCache(),
// Disable caching for in-flight requests
requestsCache: createNullCache(),
},
);
```
## Queries per second (QPS)
Search operations aren't limited by a fixed "search quota".
Instead, they're limited by your plan's [maximum QPS](https://support.algolia.com/hc/en-us/articles/4406975224721) and
[operations limit](https://support.algolia.com/hc/en-us/articles/18138875086865).
Every keystroke in InstantSearch using the `searchBox` counts as one operation.
Then, depending on the widgets you add to your search interface,
you may have more operations being counted on each keystroke.
For example, if you have a search interface with a `searchBox`, a `hierarchicalMenu`, and a `refinementList`,
then every keystroke triggers one operation.
Upon each user change to a `menu` or `refinementList`, a new operation is executed.
If you experience QPS limitations, consider implementing [a debounced `searchBox`](#debouncing).
For more information, see:
* [`searchBox`](/doc/api-reference/widgets/search-box/js)
* [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js)
* [`refinementList`](/doc/api-reference/widgets/refinement-list/js)
# Integrate Google Analytics
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/integrate-google-analytics/js
Get insights from user searches in your InstantSearch.js app.
Although Algolia provides analytics tailored to your search implementation,
you might want to send your search data to your existing analytics tools.
To do this, implement some custom middleware:
See [Set up the Google tag with gtag.js](https://developers.google.com/tag-platform/gtagjs).
In the event listener, send events to Google Analytics.
This example debounces the event for 3 seconds but adjust this to your needs.
```js JavaScript icon="code" theme={"system"}
function googleAnalyticsMiddleware() {
let timer;
const sendEventDebounced = () => {
clearTimeout(timer);
timer = setTimeout(() => {
gtag('event', 'page_view', {
page_location: window.location.pathname + window.location.search,
});
}, 3000);
});
return {
onStateChange() {
sendEventDebounced();
},
subscribe() {},
unsubscribe() {},
};
}
```
```js JavaScript theme={"system"}
const search = instantsearch({
searchClient,
indexName,
});
search.use(googleAnalyticsMiddleware);
```
# Using InstantSearch.js on mobile
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/native/js
InstantSearch.js doesn't support native platforms. Use the dedicated InstantSearch libraries for iOS and Android instead.
InstantSearch.js doesn't support native platforms at the moment.
# Optimize InstantSearch.js search requests
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/optimize-search-request-usage/js
Find areas to optimize the number of search requests and reduce billing in InstantSearch.js.
Each keystroke sent to an Algolia counts as one search request.
To reduce the number of requests sent to Algolia, and potentially reduce billing, you can ["debounce searches"](/doc/guides/building-search-ui/going-further/improve-performance/js#debouncing).
## What influences search request usage
Your usage depends on factors that influence the volume of search requests, such as:
* **Where do you plan to implement Algolia?** If you implement both mobile and desktop apps, your search request usage will be more than if you just had a desktop app.
* **Will you just capture searches or also capture other elements?** Expect more search request usage if you also use Algolia to capture other elements such as navigation on [category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages), discovery solutions, and other implementations like marketing landing pages and shared widgets.
## How to optimize search request usage
You can make changes to both your **frontend and backend implementations** to ensure your search request usage is as intended.
### Optimize search requests on your frontend implementation
To find out how to optimize the number of search requests, use the browser's developer tools console to inspect Algolia API calls.
#### Check per keystroke
Type in the search box and **see how many API calls are sent to Algolia at each keystroke**.
* By default, there is one request for each keystroke and index.
* If you use [federated search](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices), you have separate requests for each queried index.
**Compare the number of search requests for three elements** - the API calls, the number of keystrokes, and the results.
* If you have more requests than your displayed indices, they could be duplicating queries to the same index or querying indices that aren't displayed.
* If you have a non-optimized multi-index search request, you are already successfully using the multi-query endpoint (federated search uses multiple indices).
* If you don't group the requests into one network request, all requests are on separate lines and billed separately.
* If you have an optimized multi-index search request and you have five different requests, the multi-query endpoint groups all five different requests to different indices.
#### Check per facet click
If your results page includes a , check whether applying it triggers an additional .
There should only be one.
#### Check on page load
* **Clear your browser's cache and reload the page**. Were any queries sent to Algolia? If yes, check to see if any Algolia results are being displayed on the page by default. It could be that a is sent to populate the page's contents, like on category pages.
* If Algolia-delivered results aren't visible on the page, **this could be a redundant query**. You can use your discretion to remove the query.
#### Check if you are using single or multi-query endpoint
How you query Algolia is important and can make a big difference to your billing.
Each line in your browser developer tool's **Network** tab is called a "network request".
* **A network request that starts with "query"** means only one request was sent to one index.
* **A network request starting with "queries"** means that two or more queries are grouped in a single network request, and it's either used to search through multiple indices or it's searching through one index but needs to make multiple queries. An example of a search through one index with multiple queries is when using [`dynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/js) which could [cause an extra search request](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js#optimize-dynamic-widget-network-requests).
**You need to optimize if you have a multi-index search implementation** and there are few calls to the multi-query endpoint.
Algolia has [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) to help you optimize the usage of multi-query endpoints,
including [an API for InstantSearch](/doc/api-reference/widgets/index-widget/js) and for frontend libraries like [Magento](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
#### Check for expected signs of usage
* Check for linear growth in **the number of queries over time**. Study the graph in the **Monitoring** > **Usage** section in your dashboard.
* Check for linear growth in Analytics for **searches in comparison to the number of users**. A high number of searches per user can mean:
* You have a [backend implementation](#optimize-search-requests-on-your-backend-and-native-mobile-implementations).
* You have a low number of users.
* You have a bug that is sending too many queries per keystroke.
* Your search is used more extensively than initially scoped. For example, after an initial scope to power your search, has Algolia now been implemented on the category pages or has it been extended to a new country? This could result in an increase in search requests.
* Check for **abnormal spikes** in search requests. For example, is there an increase during holiday or sale seasons?
* Compare **search queries and the average number of visitors**. This comparison depends on your use case and search experience. For example, if you use Algolia to browse and search, you will have more queries per user. And the number of as-you-type queries depends on how prominently the search box is displayed.
#### Check if you are making extended use of Algolia
Check if Algolia is **used across multiple domains** beyond the initial scope.
#### Check Analytics on your dashboard to see the top searches
* Check if a high number of searches are labeled as "empty search". This means that a majority of the usage is going towards browsing the content page.
* Check the **Searches** tab to see if the filters on the "empty search" queries have a category-based . This implies that Algolia is used extensively for filters and category pages. If this wasn't scoped for initially, it could explain a high search request usage.
#### Check for bots crawling your website
[Google bots](https://support.algolia.com/hc/en-us/articles/4406981900433) could cause an increase in search queries.
### Optimize search requests on your backend and native mobile implementations
Depending on your implementation, you may need to use your internal tools to investigate.
**Check the usage of each search endpoint** by looking at the incoming API operations on the Algolia dashboard's **Monitoring** > **Operations** > **API Calls** section
If you are using a multi-index search and few calls are made to the multi-query endpoint, there is room for optimization.
To start using multi-query endpoint, use Algolia's [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) or frontend libraries like
[InstantSearch for federated search](/doc/api-reference/widgets/index-widget/js) or [Magento 2](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
# Routing and URL syncing in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/js
How to synchronize your URLs with InstantSearch.js.
Synchronizing your UI with the browser URL is considered good practice.
It lets your users take one of your results pages, copy the URL, and share it.
It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.
InstantSearch provides the necessary API entries to let you synchronize the state of your search UI (your refined widgets and current ) with any kind of storage.
Use the [`routing`](/doc/api-reference/widgets/instantsearch/js#param-routing) option to synchronize UI state with the browser URL, so users can bookmark, share, and revisit a search.
This guide covers enabling default routing, customizing URLs, and creating SEO-friendly URLs.
Don't configure [`initialUiState`](/doc/api-reference/widgets/instantsearch/js#param-initial-ui-state) and `routing` together.
* Use `initialUiState` to set the UI state when the search first loads, such as for a default query or filter.
* Use `routing` to keep the UI state synchronized with the URL, so users can bookmark or share a search.
## Default URLs
Set `routing` to `true` to use the default routing configuration:
a history router with simple state mapping.
This stores routing-compatible UI state in URL query parameters.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
searchClient,
indexName: "instant_search",
routing: true,
});
```
Setting `routing` to `true` is equivalent to this configuration:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
searchClient,
indexName: "instant_search",
routing: {
router: instantsearch.routers.history(),
stateMapping: instantsearch.stateMappings.simple(),
},
});
```
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple", "Samsung"
* **Page:** 2
This produces the following URL:
```txt theme={"system"}
https://example.org/?instant_search[query]=galaxy&instant_search[menu][categories]=Cell Phones&instant_search[refinementList][brand][0]=Apple&instant_search[refinementList][brand][0]=Samsung&instant_search[page]=2
```
By default, routing includes state from routing-compatible widgets.
With many widgets, this can create long URLs.
To create shorter or more descriptive URLs, customize the URL routing.
## Customize URL routing
You can customize which values appear and rename the URL parameters.
The `stateMapping` option maps between InstantSearch's `uiState` and the `routeState` used by the router.
Use it to rename parameters or omit values that you don't want to include in the URL.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
searchClient,
indexName: "instant_search",
routing: {
stateMapping: {
stateToRoute(uiState) {
// ...
},
routeToState(routeState) {
// ...
},
},
},
});
```
InstantSearch stores widget state in [`uiState`](/doc/api-reference/widgets/ui-state/js).
The following example maps that state to shorter URL parameters.
The state contains information about the user's search, including the query,
the selection,
the page being viewed,
and the widget hierarchy.
`uiState` only stores modified widget values, not defaults.
To persist this state in the URL,
InstantSearch converts the `uiState` into an object called `routeState`:
this `routeState` then becomes a URL.
Conversely, when InstantSearch reads the URL and applies it to the search,
it converts `routeState` into `uiState`.
This logic lives in two functions:
* `stateToRoute`: converts `uiState` to `routeState`.
* `routeToState`: converts `routeState` to `uiState`.
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple" and "Samsung"
* **Page:** 2
This translates into the following `uiState`:
```json JSON icon="braces" theme={"system"}
{
"indexName": {
"query": "galaxy",
"menu": {
"categories": "Cell Phones"
},
"refinementList": {
"brand": ["Apple", "Samsung"]
},
"page": 2
}
}
```
Implement `stateToRoute` to flatten this object into a URL, and `routeToState` to restore the URL into a UI state:
```js JavaScript icon=code expandable theme={"system"}
const indexName = "instant_search";
const search = instantsearch({
searchClient,
indexName,
routing: {
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState[indexName];
return {
q: indexUiState.query,
categories: indexUiState.menu && indexUiState.menu.categories,
brand:
indexUiState.refinementList && indexUiState.refinementList.brand,
page: indexUiState.page,
};
},
routeToState(routeState) {
return {
[indexName]: {
query: routeState.q,
menu: {
categories: routeState.categories,
},
refinementList: {
brand: routeState.brand,
},
page: routeState.page,
},
};
},
},
},
});
```
### Keep unrelated URL parameters
If you enable InstantSearch routing, the URL includes only parameters from widgets.
To keep other parameters unrelated to InstantSearch, add them when implementing `createURL`.
For example, to keep all URL parameters that start with `utm_`, use the following code:
```js JavaScript icon=code expandable theme={"system"}
history({
// ... other options
parseURL({ qsModule, location }) {
return qsModule.parse(location.search.slice(1));
},
createURL({ qsModule, location, routeState }) {
const { origin, pathname, hash } = location;
const queriesFromUrl = qsModule.parse(location.search.slice(1));
// Preserve unrelated URL parameters that start with "utm_".
const utmQueries = Object.fromEntries(
Object.entries(queriesFromUrl).filter(
([key]) =>
!Object.keys(routeState).includes(key) &&
// Add here a condition to keep the parameters you want, for example starting with "utm_"
key.startsWith("utm_"),
),
);
// Combine InstantSearch state with the preserved parameters.
const queryString = qsModule.stringify(
{
...routeState,
...utmQueries,
},
{
addQueryPrefix: true,
arrayFormat: "repeat",
},
);
return `${origin}${pathname}${queryString}${hash}`;
},
});
```
## SEO-friendly URLs
To create more descriptive URLs, move search state from query parameters into the URL path.
This is a common pattern for ecommerce category and search pages.
```txt theme={"system"}
https://example.org/search/Cell+Phones/?query=galaxy&page=2&brands=Apple&brands=Samsung
```
### Store categories in the URL path
This example stores the category in the path and the query, page, and brands as query parameters.
```js JavaScript icon=code expandable theme={"system"}
// Convert a category name to a URL slug.
// Replace spaces with "+" and encode other characters.
function getCategorySlug(name) {
return name.split(" ").map(encodeURIComponent).join("+");
}
// Convert a category slug to a category name.
// Replace "+" with spaces and decode other characters.
function getCategoryName(slug) {
return slug.split("+").map(decodeURIComponent).join(" ");
}
const search = instantsearch({
searchClient,
indexName: "instant_search",
routing: {
router: instantsearch.routers.history({
windowTitle({ category, query }) {
const queryTitle = query ? `Results for "${query}"` : "Search";
if (category) {
return `${category} – ${queryTitle}`;
}
return queryTitle;
},
createURL({ qsModule, routeState, location }) {
const urlParts = location.href.match(/^(.*?)\/search/);
const baseUrl = `${urlParts ? urlParts[1] : ""}/`;
const categoryPath = routeState.category
? `${getCategorySlug(routeState.category)}/`
: "";
const queryParameters = {};
if (routeState.query) {
queryParameters.query = encodeURIComponent(routeState.query);
}
if (routeState.page !== 1) {
queryParameters.page = routeState.page;
}
if (routeState.brands) {
queryParameters.brands = routeState.brands.map(encodeURIComponent);
}
const queryString = qsModule.stringify(queryParameters, {
addQueryPrefix: true,
arrayFormat: "repeat",
});
return `${baseUrl}search/${categoryPath}${queryString}`;
},
parseURL({ qsModule, location }) {
const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
const category = getCategoryName(
(pathnameMatches && pathnameMatches[1]) || "",
);
const {
query = "",
page,
brands = [],
} = qsModule.parse(location.search.slice(1));
// `qs` does not return an array when there's a single value.
const allBrands = Array.isArray(brands)
? brands
: [brands].filter(Boolean);
return {
query: decodeURIComponent(query),
page,
brands: allBrands.map(decodeURIComponent),
category,
};
},
}),
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState["instant_search"] || {};
return {
query: indexUiState.query,
page: indexUiState.page,
brands:
indexUiState.refinementList && indexUiState.refinementList.brand,
category: indexUiState.menu && indexUiState.menu.categories,
};
},
routeToState(routeState) {
return {
instant_search: {
query: routeState.query,
page: routeState.page,
menu: {
categories: routeState.category,
},
refinementList: {
brand: routeState.brands,
},
},
};
},
},
},
});
```
This example uses [`instantsearch.routers.history`](/doc/api-reference/widgets/history-router/js) to configure the default history router.
The router reads and writes URLs, while `stateMapping` maps `uiState` to `routeState` and back.
When you configure the history router, you can customize these functions:
* `windowTitle`: returns the browser window title for a `routeState`.
* `createURL`: creates a URL from `routeState`. InstantSearch calls it when synchronizing the browser URL, rendering links in the `menu` widget, or when a connector calls `createURL`.
* `parseURL`: creates `routeState` from the URL when users load or reload the page or use the browser's back or forward navigation.
### Make URLs more discoverable
Shorter category URLs can be more readable and memorable.
Use a mapping object to map category names to shorter URL slugs.
Given the dataset in this guide, you can make some categories more discoverable:
* "Cameras and camcorders" → `/Cameras`
* "Car electronics and GPS" → `/Cars`
When users open `https://example.org/search/Cameras`, InstantSearch selects the "Cameras and camcorders" category.
Define mappings between category names and URL slugs:
```js JavaScript icon=code expandable theme={"system"}
// Map URL slugs to category names.
const encodedCategories = {
Cameras: "Cameras and camcorders",
Cars: "Car electronics and GPS",
Phones: "Phones",
TV: "TV and home theater",
};
// Map category names to URL slugs.
const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
const newKey = encodedCategories[key];
const newValue = key;
return {
...acc,
[newKey]: newValue,
};
}, {});
// Use the mappings when converting category names and slugs.
function getCategorySlug(name) {
const encodedName = decodedCategories[name] || name;
return encodedName.split(" ").map(encodeURIComponent).join("+");
}
function getCategoryName(slug) {
const decodedSlug = encodedCategories[slug] || slug;
return decodedSlug.split("+").map(decodeURIComponent).join(" ");
}
```
You can build these dictionaries from your Algolia .
With such a solution, you have full control over what categories are discoverable from the URL.
### About SEO
For your search results to be part of a public search engine's results, you must be selective.
Trying to index too many search results pages could be considered spam.
To do that, create a [`robots.txt`](http://www.robotstxt.org/) and host it at `https://example.org/robots.txt`.
Here's an example based on the URL scheme you created.
```txt robots.txt theme={"system"}
User-agent: *
Allow: /search/Audio/
Allow: /search/Phones/
Disallow: /search/
Allow: *
```
Explore basic InstantSearch routing in CodeSandbox.
Explore SEO-friendly routing in CodeSandbox.
## Group facet values
To group facet values such as "turquoise", "ocean", and "sky" under "blue", add the group at indexing time.
Either add a separate grouping attribute or store both the individual value and group value in the same attribute.
For example, with the following dataset:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": "turquoise"
},
{
"objectID": "2",
"color": "ocean"
},
{
"objectID": "3",
"color": "sky"
}
]
```
To facet on a separate grouping attribute, add `colorGroup` to each record:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": "turquoise",
"colorGroup": "blue"
},
{
"objectID": "2",
"color": "ocean",
"colorGroup": "blue"
},
{
"objectID": "3",
"color": "sky",
"colorGroup": "blue"
}
]
```
To facet on both individual colors and their group, store both values in the `color` attribute:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": ["turquoise", "blue"]
},
{
"objectID": "2",
"color": ["ocean", "blue"]
},
{
"objectID": "3",
"color": ["sky", "blue"]
}
]
```
# Dynamic rendering with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/js
InstantSearch.js doesn't support server-side rendering, but you can get the same benefits by implementing dynamic rendering.
Dynamic rendering is now considered a workaround instead of a recommended approach and Rendertron, the tool mentioned in this guide, is deprecated.
You can use server-side rendering with [React InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/react) and [Vue InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/vue).
InstantSearch.js doesn't support server-side rendering (SSR).
However, it's possible to get some of the SSR benefits using another technique: [dynamic rendering](https://developers.google.com/search/docs/guides/dynamic-rendering).
## Dynamic rendering
[Dynamic rendering](https://developers.google.com/search/docs/guides/dynamic-rendering) lets you switch between client-side rendered and pre-rendered content for specific user agents, typically search engine user agents.
This means that whenever your server receives a request, it first looks at the user agent to determine whether it's coming from a search engine or a regular user.
* If the request is coming from a search engine, the response is proxied through a headless browser that pre-renders it.
* If the request is coming from a regular user, the response is rendered client-side.
### Build an InstantSearch app
The easiest way to setup a basic InstantSearch app is using [`create-instantsearch-app`](https://github.com/algolia/create-instantsearch-app/).
Install [create-instantsearch-app](https://github.com/algolia/create-instantsearch-app/) and create your app.
```bash Shell theme={"system"}
npm install -g create-instantsearch-app@latest
create-instantsearch-app my-app
```
Feel free to pick any InstantSearch flavor your like.
Pick the default values for the rest of the options.
Run the build command and confirm it produced a `dist/` folder.
```bash Shell theme={"system"}
cd my-app
npm run build
```
Add a basic [Express.js](https://expressjs.com/) server that will serve static files.
```bash Shell theme={"system"}
npm install --save express
touch index.js
```
```js JavaScript theme={"system"}
// index.js
const express = require('express');
const app = express();
app.use(express.static('dist'));
const port = process.env.PORT || 3000;
app.listen(port, err => {
if (err) {
console.error('failed to start server.');
return;
}
console.log(`server started at http://localhost:${port}/`);
});
```
Now run the server and visit [http://localhost:3000](http://localhost:3000) to make sure everything is working.
```bash Shell theme={"system"}
node index.js
```
### Set up and configure a dynamic renderer
Google recommends using either [rendertron](https://github.com/GoogleChrome/rendertron) (free, self-hosted) or [prerender.io](https://prerender.io/) (fully hosted).
This example uses [rendertron](https://github.com/GoogleChrome/rendertron).
```bash Shell theme={"system"}
npm install -g rendertron
PORT=3001 rendertron &
```
The pre-renderer is now listening on port `3001` and ready to render JavaScript generated content into HTML.
### Proxy your requests through Rendertron
You need to send every request with the `Content-Type: text/html` header and the relevant user agent to the URL of Rendertron: [`http://localhost:3001/render`](http://localhost:3001/render).
Depending on your stack, there are multiple ways you can achieve this kind of proxying, but if you're using an [Express.js](https://expressjs.com/) server, [rendertron-middleware](https://github.com/GoogleChrome/rendertron/blob/master/middleware/) can handle this proxying for you.
```bash Shell theme={"system"}
npm install --save rendertron-middleware
```
```js JavaScript theme={"system"}
const rendertronMiddleware = require('rendertron-middleware');
/* ... */
app.use(
rendertronMiddleware.makeMiddleware({
proxyUrl: `http://localhost:3001/render/`, // rendertron address with a /render/ suffix
timeout: 60 * 1000,
userAgentPattern: /googlebot|bingbot/i // by default
})
);
app.use(express.static('dist'));
/* ... */
```
Feel free to adjust the `userAgentPattern` or use the [default values](https://github.com/GoogleChrome/rendertron/blob/4f396d4d522cd39ceaa0b33b203afd81371e28f5/middleware/src/middleware.ts#L25-L40).
Close and restart your server, then run this curl command to confirm pre-rendering works.
```bash Shell theme={"system"}
curl --user-agent "googlebot" http://localhost:3000/
```
# How to install InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/installation/js
Learn how to install InstantSearch.js with a package manager or use it directly from a content delivery network.
Install InstantSearch.js with a packaging system, with a direct link in your web page, or with `create-instantsearch-app`.
## With a packaging system
If you have a JavaScript build tool, you can install InstantSearch.js from npm:
```sh Command line icon=square-terminal theme={"system"}
npm install algoliasearch instantsearch.js
```
Then in your module, you can load the main package:
```js JavaScript theme={"system"}
// Import InstantSearch.js
import { liteClient as algoliasearch } from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { searchBox, hits } from "instantsearch.js/es/widgets";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const indexName = "INDEX_NAME";
const searchClient = algoliasearch(appID, apiKey);
const search = instantsearch({
indexName,
searchClient,
});
search.addWidgets([
searchBox({
container: "#searchbox",
}),
hits({
container: "#hits",
}),
]);
search.start();
```
[`instantsearch`](/doc/api-reference/widgets/instantsearch/js) is the root InstantSearch.js component.
All widgets must be wrapped within it
In a production environment, [secure your API keys](/doc/guides/security/security-best-practices#secure-your-api-keys) as environment variables.
## Directly in your page
This method uses the version of **InstantSearch.js** from the [jsDelivr](https://www.jsdelivr.com/) CDN:
```html HTML icon=code-xml theme={"system"}
```
```html HTML icon=code=xml theme={"system"}
```
You then have access to the [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) function in the global scope (`window`).
```js JavaScript icon=code theme={"system"}
const { liteClient: algoliasearch } = window["algoliasearch/lite"];
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const indexName = "INDEX_NAME";
const searchClient = algoliasearch(appID, apiKey);
const search = instantsearch({
indexName,
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
search.start();
```
[`instantsearch`](/doc/api-reference/widgets/instantsearch/js) is the root InstantSearch.js component.
All widgets must be wrapped within it.
In a production environment, [secure your API keys](/doc/guides/security/security-best-practices#secure-your-api-keys) as environment variables.
### Load the styles
You need to manually load the companion [CSS file](https://cdn.jsdelivr.net/npm/instantsearch.css@8.5.1/themes/reset-min.css) into your page:
```html HTML icon=code-xml theme={"system"}
```
Or you can load the [satellite theme](https://cdn.jsdelivr.net/npm/instantsearch.css@8.5.1/themes/satellite-min.css) widget styles into your page.
```html HTML icon=code-xml theme={"system"}
```
You can customize the themes using CSS variables.
For a complete reference of customization options, see [Styling and theming](/doc/guides/building-search-ui/styling/js).
## With `create-instantsearch-app`
Use [`create-instantsearch-app`](https://github.com/algolia/instantsearch/tree/master/packages/create-instantsearch-app#create-instantsearch-app) to create a working InstantSearch app.
Similar to other interactive command-line apps,
run it with `npm` or `yarn`:
```sh npm theme={"system"}
npx create-instantsearch-app@latest 'getting-started'
```
```sh yarn theme={"system"}
yarn create instantsearch-app 'getting-started'
```
To launch the app, type inside your terminal:
```sh Command line icon=square-terminal theme={"system"}
cd getting-started
npm start # or yarn start
```
## Send click and conversion events
Capturing real-time user interactions as events gives you actionable insights with [click and conversion metrics](/doc/guides/search-analytics/concepts/metrics#click-through-rate),
and they help you increase your customer engagement and conversions.
Events are used to activate Algolia features and products like [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started),
[Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking),
[Query Categorization](/doc/guides/algolia-ai/query-categorization),
[Recommend](/doc/guides/algolia-recommend/overview),
and [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization).
To send click and conversion events when users interact with your search UI, set the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option to `true`.
## Browser support
Algolia supports the last two versions of the major browsers: Chrome, Edge, Firefox, Safari.
To support [Internet Explorer 11](https://wikipedia.org/wiki/Internet_Explorer_11) you need [polyfills](https://wikipedia.org/wiki/Polyfill_\(programming\)) for:
`Array.prototype.find`, `Array.prototype.includes`, `Promise`, `Object.entries`, and `Object.assign`.
The code samples in this documentation use a JavaScript syntax that isn't natively supported by older browsers like Internet Explorer 11.
If your site needs to support older browsers, use a tool like [Babel](https://babeljs.io/repl) to make your code work in the browsers you target.
# Styling and theming
Source: https://www.algolia.com/doc/guides/building-search-ui/styling/js
Learn how to customize InstantSearch.js appearance using CSS variables and themes.
InstantSearch.css provides a set of themes to style your search experiences with minimal effort.
The themes are designed to be customizable through [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) (CSS custom properties), allowing you to match your brand identity while maintaining the core layout and features.
CSS variables are currently only supported for [`chat`](/doc/api-reference/widgets/chat/js) and [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widgets.
## Loading themes
InstantSearch.css includes two main themes:
* **`satellite.css`** - A complete theme with modern styling
* **`algolia.css`** - Algolia-branded theme
Both themes include `reset.css`, a minimal reset theme that provides base styles.
### Use a CDN
The themes are available on jsDelivr:
```html HTML icon=code-xml theme={"system"}
```
### Using npm
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
Then import it in your project:
```js JavaScript icon=code theme={"system"}
// Satellite theme
import 'instantsearch.css/themes/satellite.css';
// Or Algolia theme
// import 'instantsearch.css/themes/algolia.css';
```
### Loading individual component styles
If you only need specific components, you can import their styles individually to reduce bundle size. This is currently available for `chat` and `autocomplete` components:
```js JavaScript icon=code theme={"system"}
// Import only chat component styles
import 'instantsearch.css/components/chat.css';
// Or import the minified version
// import 'instantsearch.css/components/chat-min.css';
```
```js JavaScript icon=code theme={"system"}
// Import only autocomplete component styles
import 'instantsearch.css/components/autocomplete.css';
// Or import the minified version
// import 'instantsearch.css/components/autocomplete-min.css';
```
Individual component styles include all CSS variables and theming capabilities of the full themes.
You can still customize them using the same CSS variable overrides.
## Development tools
InstantSearch.css provides developer tools that help you customize CSS variables visually in your browser. The developer tools display an interactive panel where you can:
* Modify CSS variables in real-time
* Toggle between theme modes (Auto, Light, Dark)
* Export your customized variables to clipboard
### Use the developer tools
Import the developer tools in your application:
```js JavaScript icon=code theme={"system"}
import 'instantsearch.css/devtools/inject';
```
The developer tools panel will appear in the bottom-left corner of your browser, allowing you to interactively customize colors, spacing, typography, and more.
The developer tools are intended for development only.
Don't include them in your production build.
### Programmatic usage
You can also create the developer tools programmatically with custom options:
```js JavaScript icon=code theme={"system"}
import { createInstantSearchDevtools } from 'instantsearch.css/devtools';
createInstantSearchDevtools({
// Optional: custom container
container: document.getElementById('devtools'),
// Optional: custom styling
style: {
position: 'fixed',
top: '1rem',
right: '1rem',
zIndex: '1000',
},
});
```
## Customization example
You can override any CSS variable to match your brand:
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 124, 58, 237;
/* Spacing */
--ais-spacing-factor: 1.5;
}
```
## Dark mode
The themes support dark mode in two ways:
### Using data-theme attribute
```html HTML icon=code-xml theme={"system"}
```
### Using CSS class
```html HTML icon=code-xml theme={"system"}
```
Both approaches automatically adjust the following variables:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
/* Text colors */
--ais-text-color-rgb: 255, 255, 255;
--ais-primary-color-rgb: 110, 160, 255;
--ais-muted-color-rgb: 190, 190, 190;
--ais-button-text-color-rgb: 255, 255, 255;
/* Border color */
--ais-border-color-rgb: 100, 100, 100;
/* Background color */
--ais-background-color-rgb: 38, 38, 38;
/* Shadow color */
--ais-shadow-color-rgb: 0, 0, 0;
}
```
### Custom dark mode colors
You can override dark mode colors:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
--ais-primary-color-rgb: 147, 197, 253;
--ais-background-color-rgb: 15, 23, 42;
}
```
## Complete variable reference
For a complete list of all available CSS variables with detailed descriptions, see the tables below:
### Text colors
| Variable | Default | Description |
| - | - | - |
| `--ais-text-color-rgb` | `38, 38, 38` | RGB values for the base text color |
| `--ais-text-color-alpha` | `1` | Alpha value for the base text color |
| `--ais-primary-color-rgb` | `30, 89, 255` | RGB values for the accent color |
| `--ais-primary-color-alpha` | `1` | Alpha value for the accent color |
| `--ais-muted-color-rgb` | `82, 82, 82` | RGB values for muted/secondary text |
| `--ais-muted-color-alpha` | `1` | Alpha value for muted/secondary text |
| `--ais-button-text-color-rgb` | `255, 255, 255` | RGB values for button text |
| `--ais-button-text-color-alpha` | `1` | Alpha value for button text |
### Border, background, and shadow colors
| Variable | Default | Description |
| - | - | - |
| `--ais-border-color-rgb` | `150, 150, 150` | RGB values for borders |
| `--ais-border-color-alpha` | `1` | Alpha value for borders |
| `--ais-background-color-rgb` | `255, 255, 255` | RGB values for backgrounds |
| `--ais-background-color-alpha` | `1` | Alpha value for backgrounds |
| `--ais-shadow-color-rgb` | `23, 23, 23` | RGB values for shadow colors |
### Spacing
| Variable | Default | Description |
| - | - | - |
| `--ais-base-unit` | `16` | Base unit value to calculate font sizes and spacing |
| `--ais-spacing-factor` | `1` (tap: `1.2`) | Multiplier for spacing calculations |
| `--ais-spacing` | calculated | Computed spacing value |
### Typography
| Variable | Default | Description |
| - | - | - |
| `--ais-font-size` | `16px` | Base font size derived from `--ais-base-unit` |
| `--ais-font-weight-medium` | `500` | Medium font weight |
| `--ais-font-weight-semibold` | `600` | Semibold font weight |
| `--ais-font-weight-bold` | `700` | Bold font weight |
### Icons
| Variable | Default | Description |
| - | - | - |
| `--ais-icon-size` | `20px` | Icon size |
| `--ais-icon-stroke-width` | `1.6` | Icon stroke width |
### Shadows
| Variable | Default | Description |
| - | - | - |
| `--ais-shadow-sm` | `0px 0px 0px 1px rgba(..., 0.05), 0px 1px 3px 0px rgba(..., 0.25)` | Small shadow for subtle depth |
| `--ais-shadow-md` | `0px 0px 0px 1px rgba(..., 0.05), 0px 4px 8px -2px rgba(..., 0.25)` | Medium shadow for cards |
| `--ais-shadow-lg` | `0 0 0 1px rgba(..., 0.05), 0 6px 16px -4px rgba(..., 0.15)` | Large shadow for modals |
### Border radius
| Variable | Default | Description |
| - | - | - |
| `--ais-border-radius-sm` | `4px` | Small border radius |
| `--ais-border-radius-md` | `8px` | Medium border radius |
| `--ais-border-radius-lg` | `16px` | Large border radius |
| `--ais-border-radius-full` | `9999px` | Full border radius for pills |
### Transitions
| Variable | Default | Description |
| - | - | - |
| `--ais-transition-duration` | `0.3s` | Duration for transitions |
| `--ais-transition-timing-function` | `cubic-bezier(0.4, 0, 0.2, 1)` | Timing function for transitions |
### Z-index
| Variable | Default | Description |
| - | - | - |
| `--ais-z-index-chat` | `9999` | Z-index for chat component |
| `--ais-z-index-autocomplete` | `10000` | Z-index for autocomplete component |
### Component-specific
#### Chat component
| Variable | Default | Description |
| - | - | - |
| `--ais-chat-width` | `22.5rem` | Default width of chat panel |
| `--ais-chat-height` | `70%` | Default height of chat panel |
| `--ais-chat-maximized-width` | `70%` | Width when maximized |
| `--ais-chat-maximized-height` | `100%` | Height when maximized |
| `--ais-chat-margin` | `1.5rem` | Margin around chat panel |
| `--ais-chat-carousel-item-width` | `calc(var(--ais-spacing) * 10)` | Width of carousel items |
#### Autocomplete component
| Variable | Default | Description |
| - | - | - |
| `--ais-autocomplete-search-input-height` | `44px` | Height of search input |
| `--ais-autocomplete-panel-max-height` | `650px` | Maximum height of autocomplete panel |
# Federated two-column autocomplete
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/federated/js
Build a two-column autocomplete with recent searches, Query Suggestions, and product previews.
This example builds on the [`autocomplete`](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js) widget guide.
It creates a federated autocomplete experience with recent searches and Query Suggestions on the left, and product previews on the right.
Run and edit the federated autocomplete example in CodeSandbox.
Browse the source code for the federated autocomplete example on GitHub.
The example has three states. Each one arranges the same two columns differently:
| Search state | Left column | Right column |
| - | - | - |
| **Empty query** | Recent searches and popular searches | Shortcuts |
| **Results** | Suggestions and [a category](#add-a-category-suggestion) | Product (and article) previews |
| **No results** | A "no results" message | Popular categories. To show popular categories, add a source that returns category facet values |
The sections below build each piece, then combine them in a single `panel` template.
The `panel` template arranges the sources into two columns, and each source brings its own header and item templates:
```js JavaScript icon=code expandable theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { autocomplete } from "instantsearch.js/es/widgets";
import "instantsearch.css/themes/satellite.css";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
autocomplete({
container: "#autocomplete",
placeholder: "Search for products",
// Left column, top: recent searches from local storage
showRecent: {
templates: {
header: (_, { html }) =>
html`Recent searches`,
},
},
// Left column, bottom: popular Query Suggestions
showQuerySuggestions: {
indexName: "instant_search_demo_query_suggestions",
searchParameters: { hitsPerPage: 5 },
templates: {
header: (_, { html }) =>
html`Suggestions`,
},
},
// Right column: product previews with image, name, price, and highlight
indices: [
{
indexName: "instant_search",
searchParameters: { hitsPerPage: 5 },
templates: {
header: ({ items }, { html }) =>
items.length === 0
? null
: html`Products`,
noResults: (_, { html }) =>
html`
`,
},
},
],
// Access built-in sources by their element names and index sources by index name.
templates: {
panel: ({ elements }, { html }) =>
html`
${elements.recent} ${elements.suggestions}
${elements["instant_search"]}
`,
},
}),
]);
search.start();
```
Add CSS for the columns, product rows, and state-specific elements.
The [example style sheet](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/autocomplete/src/app.css) includes classes for:
* Two-column grids
* Product rows
* Shortcut cards
* "See all" and no-results states
## Add a shortcuts panel
You can show promotional cards or shortcuts when the query is empty.
Define them as static data and render them in the `panel` template.
When users type a query, show product previews instead:
```js JavaScript icon=code expandable theme={"system"}
// Quick-access cards shown in the empty state.
const QUICK_ACCESS = [
{
title: "Spring sale",
subtitle: "up to 60% off",
image: "https://picsum.photos/seed/spring-sale/300/200",
href: "#",
},
{
title: "New collection",
subtitle: "spring / summer",
image: "https://picsum.photos/seed/new-collection/300/200",
href: "#",
},
];
templates: {
panel: ({ elements, indices }, { html }) => {
const products = indices.find(
(index) => index.indexName === "instant_search",
);
const isEmptyQuery = products?.results?.query === "";
return html`
`;
},
},
```
For dynamic cards, return custom data from an [Algolia rule](/doc/guides/managing-results/rules/rules-overview).
In the `panel` template, read the data from `results.userData` for the products index instead of using a static array.
## Add a category suggestion
To show a category suggestion in the left column, find the products index and read the top product's `categories` from `results.hits[0]` in the `panel` template.
Guard against a missing or empty `categories` attribute, then render the category after the suggestions:
```js JavaScript icon=code theme={"system"}
const products = indices.find(
(index) => index.indexName === "instant_search",
);
// Derive a category from the top product, if any.
const categories = products?.results?.hits?.[0]?.categories ?? [];
// In the left column, after the suggestions:
html`${elements.suggestions}
${categories.length > 0
? html`
${categories.join(" › ")}
`
: null}`;
```
## Show a "See all" link
The index source doesn't have a `footer` template.
To show a **See all** link with the total number of matching results,
read the products index's `results.nbHits` in the `panel` template and render the link after the product previews:
```js JavaScript icon=code theme={"system"}
const products = indices.find(
(index) => index.indexName === "instant_search",
);
const nbHits = products?.results?.nbHits ?? 0;
// In the right column, after the product elements:
html`${elements["instant_search"]}
${nbHits > 5
? html`
See all ${nbHits.toLocaleString()} results
`
: null}`;
```
## Show the no-results state
When a result source doesn't have any items, the widget doesn't render its `item` template.
Use the source's `noResults` template to show a message such as "No products found", as shown in the main example.
## Combine the empty and typing states
The final `panel` template combines two states:
* For an empty query, show recent and popular searches in the left column and shortcut cards in the right column.
* After users enter a query, show Query Suggestions and a category in the left column and product previews in the right column.
To switch between the states:
* Remove the `header` template from `showQuerySuggestions`.
* Render the suggestions heading in the `panel` template. Use "Popular searches" for an empty query and "Suggestions" for a non-empty query.
* Show the heading only when the Query Suggestions source has results.
For the assembled `panel` template, see [`src/app.js`](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/autocomplete/src/app.js).
## See also
* [Autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js) guide for the widget basics.
* [Rich text box with mentions](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/mentions/js) for an example of a headless, connector-based pattern.
# Build a mentions text box
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/mentions/js
Build a headless social media mentions text box with the `connectAutocomplete` connector.
This example builds on the [`autocomplete`](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js) widget guide.
Use this guide to build a headless mentions text box with the `connectAutocomplete` connector.
This widget is and is subject to change in minor versions.
## Build a custom UI with the connector
The widget renders its own input and panel.
If you need full control over the markup, a custom input element, a different panel structure, or an inline type-ahead like `@` mentions, use the `connectAutocomplete` connector instead.
It turns a render function into a widget, leaving all the markup to you.
Your render function receives the current query, the matching hits per index, and a `refine` function to run a new search:
```js JavaScript icon=code expandable theme={"system"}
import instantsearch from "instantsearch.js";
import { connectAutocomplete } from "instantsearch.js/es/connectors";
// `connectAutocomplete` builds a custom widget from a render function.
const customAutocomplete = connectAutocomplete(
(renderOptions, isFirstRender) => {
const { indices, refine, widgetParams } = renderOptions;
const { input, list } = widgetParams;
// Connect the input once, so typing doesn't lose focus on re-render
if (isFirstRender) {
input.addEventListener("input", (event) => {
refine(event.currentTarget.value);
});
return;
}
// Render your own markup from the hits on every result
const hits = indices[0]?.hits ?? [];
list.innerHTML = hits
.map((hit) => `
${hit._highlightResult.name.value}
`)
.join("");
},
);
search.addWidgets([
customAutocomplete({
input: document.querySelector("#autocomplete-input"),
list: document.querySelector("#autocomplete-list"),
}),
]);
```
Each entry in `indices` has the shape `{ indexName, indexId, hits, results, sendEvent }`.
The connector searches the Algolia indices in your widget tree,
the root index and any nested [`index`](/doc/api-reference/widgets/index-widget/js) widgets,
so you don't pass index names to the connector.
Because you render the markup, you're responsible for keyboard navigation, active-item state, and ARIA attributes.
If you need the same accessible combobox behavior without building it yourself, use the [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget instead of the connector.
## Build a rich text box with mentions
Autocomplete can do more than redirect to a search page.
In a text box, it can help people find and insert usernames as they type.
For example, the social media mentions feature lets users mention another user with the `@` character so they can complete the message with the right username.
The text box provides type-ahead suggestions.
The panel doesn't block typing.
Users can keep typing and ignore the suggestions or select one to complete the message.
The compose box doesn't process a query from a search input.
Instead, it parses the content of a text box and detects when you're trying to mention someone.
To replicate this, you need full control over the markup, so you use the `connectAutocomplete` connector rather than the widget.
This example searches the public `autocomplete_twitter_accounts` index, whose records include a `name`, a `handle`, and an `image`.
Run and edit the mentions example in CodeSandbox.
Browse the source code for the mentions example on GitHub.
### Install dependencies
Install InstantSearch.js, the Algolia API client, and [`textarea-caret`](https://www.npmjs.com/package/textarea-caret) (used later to position the panel at the caret):
```sh npm theme={"system"}
npm install instantsearch.js algoliasearch textarea-caret
```
```sh yarn theme={"system"}
yarn add instantsearch.js algoliasearch textarea-caret
```
### Render the text box
Render a `textarea` for the message and an empty container for the suggestions panel.
This example uses a `textarea` element instead of an `input`, which is better for free-form plain text spanning multiple lines.
```html HTML icon=code-xml theme={"system"}
```
### Detect a mention
If you pass the full text box value to `refine`, Algolia searches the entire message.
You only want to search when the caret sits inside a mention, and you only want to send the mention itself, not the whole message.
To do that, tokenize the text and find the word under the caret, then check whether it's a valid username:
```js JavaScript icon=code expandable theme={"system"}
// The word under the caret, with its [start, end] range in the text.
function getActiveToken(input, cursor) {
const re = /\S+/g;
let match;
while ((match = re.exec(input))) {
const [start, end] = [match.index, match.index + match[0].length];
if (start <= cursor && cursor <= end) {
return { word: match[0], range: [start, end] };
}
}
return null;
}
// A mention is "@" followed by 1–15 word characters.
const isMention = (word) => /^@\w{1,15}$/.test(word);
```
### Position the panel
When users mention someone, the panel should follow the caret instead of sitting at the bottom of the text box.
Use `getCaretCoordinates(textarea, position)` from [`textarea-caret`](https://www.npmjs.com/package/textarea-caret) to get the caret's `top`, `left`, and `height` at a given offset.
Use it to place the panel just below the `@` of the active mention.
### Search for accounts and render the panel
Attach `connectAutocomplete` to the text box.
On every input, find the active token.
If it's a mention, call `refine` with the text after `@` and show the matching accounts.
Otherwise, hide the panel.
The connector gives you `indices[0].hits` and a `refine` function.
You manage the remaining behavior, including the active token, panel state, and selection handling.
This excerpt shows the connector wiring.
The setup (search client and instance, element references) and the `renderHits`, `positionPanel`, and `hidePanel` helpers are in [the complete `src/app.js`](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/autocomplete-mentions/src/app.js).
```js JavaScript icon=code expandable theme={"system"}
// Only search when the caret sits inside a mention.
function onInput() {
activeToken = getActiveToken(textarea.value, textarea.selectionEnd);
if (activeToken && isMention(activeToken.word)) {
refine(activeToken.word.slice(1));
} else {
hidePanel();
}
}
const customAutocomplete = connectAutocomplete(
(renderOptions, isFirstRender) => {
refine = renderOptions.refine;
if (isFirstRender) {
textarea.addEventListener("input", onInput);
textarea.addEventListener("click", onInput);
textarea.addEventListener("keyup", onInput);
return;
}
const hits = renderOptions.indices[0]?.hits ?? [];
if (!activeToken || !isMention(activeToken.word) || hits.length === 0) {
hidePanel();
return;
}
// `positionPanel` places the panel at the caret; `renderHits` builds the
// suggestion rows with DOM APIs
positionPanel();
panel.hidden = false;
renderHits(hits);
},
);
search.addWidgets([customAutocomplete({})]);
search.start();
```
When users type `@` followed by a name, highlighted results appear.
### Show a loading state
On slow connections, the panel can appear empty while results load.
The InstantSearch instance exposes a `status` (`"idle"`, `"loading"`, `"stalled"`, or `"error"`), and it re-renders your widget when the search stalls.
Show an indicator when the search is stalled and the caret is inside a mention:
```js JavaScript icon=code theme={"system"}
// Inside the connectAutocomplete render function, before rendering the hits:
if (activeToken && isMention(activeToken.word) && search.status === "stalled") {
positionPanel();
panel.hidden = false;
const loading = document.createElement("li");
loading.className = "account-loading";
loading.textContent = "Searching…";
panel.replaceChildren(loading);
return;
}
```
### Select an account
The goal of the mention feature is to help users find an account and autocomplete its handle.
For example, when users type a few letters after `@`,
the panel opens with matching accounts.
Selecting an account replaces the typed text with the account's handle, such as "@jwestlakedc", and closes the panel.
When the user picks an account, replace the active token with the correct handle and move the caret after it.
Use `mousedown` (with `preventDefault`) rather than `click` so the `textarea` keeps focus:
```js JavaScript icon=code theme={"system"}
const [start, end] = activeToken.range;
const replacement = `@${button.dataset.handle} `;
textarea.value =
textarea.value.slice(0, start) + replacement + textarea.value.slice(end);
const caret = start + replacement.length;
textarea.setSelectionRange(caret, caret);
textarea.focus();
hidePanel();
```
### Navigate in the text box
Typing isn't the only action in a text box.
Users can also edit their text or move the caret to a different position.
When the caret lands on a mention, the panel should open.
When it leaves a mention, the panel should close.
Listen for `click` and `keyup` in addition to `input`, and run the same active-token check again:
```js JavaScript icon=code theme={"system"}
textarea.addEventListener("input", onInput);
textarea.addEventListener("click", onInput);
textarea.addEventListener("keyup", onInput);
```
### Add styles
Style the text box, the suggestions panel, and the account rows with your own CSS.
For a complete style sheet, see [`src/app.css`](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/autocomplete-mentions/src/app.css).
Users can move the caret through the text, and the panel updates when the caret enters or leaves a mention.
### Next steps
This pattern also applies to collaborative editing (such as Google Docs), email composition (such as Gmail), and chat apps (such as Slack).
To extend this pattern:
* Reuse the same logic to add hashtags by changing the `isMention` function to detect hashtags and search a hashtag index instead.
* Add [synonyms](/doc/guides/managing-results/optimize-search-results/adding-synonyms) or [Algolia Rules](/doc/guides/managing-results/rules/rules-overview) so people are found by their nicknames.
* Render mentions and hashtags as interactive tokens with [`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
## See also
* [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget reference for the full list of options.
* [Autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js) guide for the widget basics.
* [Federated two-column autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/federated/js) for an example of a widget-based, multi-source panel.
# Autocomplete
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js
Learn how to build an autocomplete search experience with InstantSearch.js.
**Autocomplete is a ubiquitous part of most search experiences.** Search providers like Google, ecommerce sites like Amazon, and messaging apps like Slack all offer autocomplete experiences on mobile and desktop.
Algolia provides a full-featured solution to build autocomplete experiences with InstantSearch.
The [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget lets you build an accessible, as-you-type autocomplete user interface that you can integrate anywhere on your site or app.
Run and edit the Autocomplete example in CodeSandbox.
Browse the source for the Autocomplete example on GitHub.
A common pattern in search is to implement a search box with an autocomplete as a first step of the search experience.
**Adding an autocomplete to a search page lets you enhance the search experience and create a richer, more contextual search.**
You can use the [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget in an existing or a new InstantSearch.js implementation to create this kind of experience.
## Install InstantSearch.js
The `autocomplete` widget is part of InstantSearch.js.
Install InstantSearch.js and the Algolia API client from the [npm](https://www.npmjs.com/) registry:
```sh npm theme={"system"}
npm install algoliasearch instantsearch.js instantsearch.css
```
```sh yarn theme={"system"}
yarn add algoliasearch instantsearch.js instantsearch.css
```
Import a theme to style the widget out of the box:
```js JavaScript icon=code theme={"system"}
import "instantsearch.css/themes/satellite.css";
```
## Add a container
The widget renders into a container element.
Add one to your markup:
```html HTML icon=code-xml theme={"system"}
```
Provide a container (for example, a `div`), not an `input`.
The widget generates a fully accessible search box for you.
## Add the widget to InstantSearch
Unlike the standalone [Autocomplete](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete) library,
the `autocomplete` widget runs inside an InstantSearch instance.
Create the instance, add the widget with [`addWidgets`](/doc/api-reference/widgets/instantsearch/js#param-add-widgets), then [`start`](/doc/api-reference/widgets/instantsearch/js#param-start) the search:
```js JavaScript icon=code expandable theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { autocomplete } from "instantsearch.js/es/widgets";
import "instantsearch.css/themes/satellite.css";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
autocomplete({
container: "#autocomplete",
placeholder: "Search for products",
}),
]);
search.start();
```
The widget is added to InstantSearch but it doesn't show results until you configure the indices to query.
## Display results from an index
Use the [`indices`](/doc/api-reference/widgets/autocomplete/js#param-indices) option to define which indices to query and how to display their results.
Each index can declare its own [`templates`](/doc/api-reference/widgets/autocomplete/js#templates) and a [`getURL`](/doc/api-reference/widgets/autocomplete/js#param-indices) function to navigate when a user selects an item:
```js JavaScript icon=code expandable theme={"system"}
autocomplete({
container: "#autocomplete",
placeholder: "Search for products",
indices: [
{
indexName: "instant_search",
getURL: (item) => `/products/${item.objectID}`,
searchParameters: {
hitsPerPage: 5,
},
templates: {
header: ({ items }, { html }) =>
html`
`,
},
},
],
});
```
You can query several indices at once by adding more objects to the `indices` array.
To learn more, see [Including multiple result types](/doc/ui-libraries/autocomplete/guides/including-multiple-result-types).
## Customize result templates
Each index renders with its own `header`, `item`, and `noResults` templates, and the widget accepts a top-level `panel` template for the overall layout.
The [Display results from an index](#display-results-from-an-index) section covers `header` and `item`.
### Highlight the matched query
Item templates receive a `components` object with `Highlight`, `ReverseHighlight`, `Snippet`, and `ReverseSnippet` helpers that emphasize the part of an attribute matching the user's query:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
indices: [
{
indexName: "instant_search",
templates: {
item: ({ item }, { html, components }) =>
html`
`,
},
},
],
});
```
Use `Snippet` to highlight within a longer, truncated attribute, and the `Reverse` variants to emphasize the text that doesn't match.
This is a common style for Query Suggestions.
### Show an empty state
The `noResults` template renders when an index returns nothing.
If any index defines it, the panel stays open even when every index is empty, so you can show a helpful message:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
indices: [
{
indexName: "instant_search",
templates: {
noResults: (_, { html }) => html`
No products found.
`,
},
},
],
});
```
### Customize the panel layout
The top-level `panel` template controls the layout of the whole panel, including the order and grouping of recent searches, query suggestions, and each index.
It receives an `elements` object whose keys are `recent`, `suggestions`, and each index's `indexName`, so you can arrange sections into rows or columns—for example, suggestions on the left and results on the right:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
templates: {
panel: ({ elements }, { html }) =>
html`
${elements.suggestions}
${elements["instant_search"]}
`,
},
// ...
});
```
The template only sets the markup.
Style the columns with your own CSS:
```css CSS icon=code theme={"system"}
.aa-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
}
```
## Filter results
To narrow what an index returns, set Algolia [search parameters](/doc/api-reference/search-api-parameters) on that index with `searchParameters`.
You can apply [`filters`](/doc/api-reference/api-parameters/filters), [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), [`numericFilters`](/doc/api-reference/api-parameters/numericFilters), and other parameters per index:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
indices: [
{
indexName: "instant_search",
searchParameters: {
hitsPerPage: 5,
filters: "categories:Phones",
facetFilters: [["brand:Apple", "brand:Samsung"]],
},
},
],
});
```
The widget applies these parameters to every query for that index.
For an interactive filter UI inside the autocomplete—such as selectable tags or facet values that users add and remove—use the standalone [Autocomplete](/doc/ui-libraries/autocomplete/guides/filtering-results) library, which provides the tags plugin and facet sources.
## Reshape and deduplicate results
Use `transformItems` to adjust results before they're displayed—for example, to remove duplicates across indices or limit the number of items each index shows.
The function receives an array with one entry per index, each shaped as `{ indexName, indexId, hits, results }`, and must return the same shape:
```js JavaScript icon=code expandable theme={"system"}
autocomplete({
container: "#autocomplete",
indices: [
{ indexName: "instant_search" },
{ indexName: "instant_search_demo_query_suggestions" },
],
transformItems(indices) {
const seen = new Set();
return indices.map((index) => ({
...index,
hits: index.hits
.filter((hit) => {
if (seen.has(hit.objectID)) {
return false;
}
seen.add(hit.objectID);
return true;
})
.slice(0, 5),
}));
},
});
```
Unlike the standalone library's [reshape](/doc/ui-libraries/autocomplete/guides/reshaping-sources) API, the widget doesn't group or sort sources for you—deduplication and per-index limits are manual, as shown above.
## Add Query Suggestions and recent searches
The widget can display query suggestions from a [Query Suggestions index](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) and recent searches stored in the browser's local storage.
Enable them with [`showQuerySuggestions`](/doc/api-reference/widgets/autocomplete/js#param-show-query-suggestions) and [`showRecent`](/doc/api-reference/widgets/autocomplete/js#param-show-recent):
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
placeholder: "Search for products",
showRecent: true,
showQuerySuggestions: {
indexName: "instant_search_demo_query_suggestions",
getURL: (item) => `/search?q=${item.query}`,
},
indices: [
{
indexName: "instant_search",
// ...
},
],
});
```
To customize suggestions and recent searches, set a custom storage key, or refine your main index when a user selects a suggestion,
see [Build a Query Suggestions UI](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/query-suggestions/js).
## Send click and conversion events
To send [click and conversion events](/doc/guides/sending-events/getting-started) when users interact with the autocomplete,
enable [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) on the InstantSearch instance:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
insights: true,
});
```
The widget then sends view and click events automatically when users see and select results.
Conversion events, like `add-to-cart`, usually happen after the user leaves the autocomplete, on the destination page.
Send those where the conversion occurs with the [Insights client](/doc/guides/sending-events/getting-started).
The autocomplete item template doesn't expose a `sendEvent` function.
## Support keyboard navigation
The widget renders an accessible combobox, so users can operate it entirely from the keyboard—no extra configuration:
* **Up and Down arrows** move through the items. The highlight wraps around at the ends of the list.
* **Enter** opens the highlighted item, or submits the typed query when nothing is highlighted.
* **Escape** closes the panel, then clears the highlight.
* **Tab** closes the panel and moves focus to the next element.
What happens on **Enter** depends on how you handle navigation:
* If the highlighted item's index defines [`getURL`](#display-results-from-an-index), the browser opens that URL.
* Otherwise, when you set `getSearchPageURL`, submitting the query sends the user to your search results page.
* To use client-side routing instead of a full page load, provide an `onSelect` function and handle navigation in your app.
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
// On Enter with no highlighted item, open the search results page.
getSearchPageURL: ({ query }) => `/search?q=${query}`,
indices: [
{
indexName: "instant_search",
// On Enter or click with a highlighted item, open its URL.
getURL: (item) => `/products/${item.objectID}`,
},
],
});
```
The widget also sets the matching ARIA roles (`combobox`, `aria-expanded`, `aria-activedescendant`), so screen readers announce the active item as users move through the list.
## Enable detached (mobile) mode
On small screens, the widget can switch to a full-screen overlay.
Use the [`detachedMediaQuery`](/doc/api-reference/widgets/autocomplete/js#param-detached-media-query) option to control when this happens.
By default, detached mode activates at `(max-width: 680px)`.
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
detachedMediaQuery: "(max-width: 1024px)",
// ...
});
```
To turn off detached mode, set `detachedMediaQuery` to an empty string.
## Explore advanced examples
* [Federated two-column autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/federated/js): a layout that combines recent searches, Query Suggestions, product previews, a shortcuts panel, and category suggestions.
* [Rich text box with mentions](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/mentions/js): a headless "@" mentions text box, built with the [`connectAutocomplete`](/doc/api-reference/widgets/autocomplete/js) connector, that reproduces the X compose experience.
## See also
* [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget reference for the full list of options.
* [Build a Query Suggestions UI](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/query-suggestions/js).
# Build a Query Suggestions UI
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/query-suggestions/js
Use the Autocomplete widget to show Query Suggestions and recent searches.
Use [Query Suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) to help users find popular searches as they type.
Query Suggestions creates an from popular searches.
You can use this index to show search suggestions.
After you configure the Query Suggestions index, use the [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget to show suggestions.
When users select a suggestion, the widget navigates to the suggestion URL or refines the search query, depending on your configuration.
Run and edit the Query Suggestions UI example in CodeSandbox.
Browse the source code for the Query Suggestions UI example on GitHub.
## Add Query Suggestions and recent searches
The widget displays suggestions from a [Query Suggestions index](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) and recent searches stored in the browser's local storage.
Enable them with [`showQuerySuggestions`](/doc/api-reference/widgets/autocomplete/js#param-show-query-suggestions) and [`showRecent`](/doc/api-reference/widgets/autocomplete/js#param-show-recent):
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
placeholder: "Search for products",
showRecent: true,
showQuerySuggestions: {
indexName: "instant_search_demo_query_suggestions",
getURL: (item) => `/search?q=${item.query}`,
},
indices: [
{
indexName: "instant_search",
// ...
},
],
});
```
## Refine results with suggestions
To keep users on the same page, set up a multi-index search experience that uses the selected suggestion to search the main index.
The [`autocomplete`](/doc/api-reference/widgets/autocomplete/js) widget targets the index that contains the suggestions.
The other widgets target the main index that contains your records.
For more information, see [Multi-index search](/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/js).
```js JavaScript icon=code expandable theme={"system"}
// ...
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
autocomplete({
container: '#autocomplete',
showQuerySuggestions: {
indexName: 'instant_search_demo_query_suggestions',
},
}),
hits({
container: "#hits",
templates: {
// ...
},
}),
]);
search.start();
```
## Customize recent searches
Pass an object to `showRecent` to customize how each recent search renders:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
showRecent: {
templates: {
item: ({ item, onSelect }, { html }) =>
html`
${item.query}
`,
},
},
// ...
});
```
## Customize suggestions
Pass an object to `showQuerySuggestions` to customize each suggestion and apply [search parameters](/doc/api-reference/search-api-parameters) to the suggestions query:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
showQuerySuggestions: {
indexName: "instant_search_demo_query_suggestions",
searchParameters: { hitsPerPage: 5 },
templates: {
item: ({ item, onSelect }, { html, components }) =>
html`
`,
},
},
// ...
});
```
Use `ReverseHighlight` to emphasize the text that doesn't match the query.
This is a common style for Query Suggestions.
The widget doesn't have built-in category grouping for suggestions and there's no `categoryAttribute`.
To show a category next to a suggestion, render the category in the suggestion's `item` template.
## Filter suggestions on category pages
After you [configure category page filtering](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js#filter-suggestions-on-category-pages), set the `filters` search parameter for Query Suggestions.
Use the current category page value in the filter.
For example, to show suggestions with matching hits in the `Accessories` category of the `products` source index:
```js JavaScript icon=code theme={"system"}
autocomplete({
container: "#autocomplete",
showQuerySuggestions: {
indexName: "products_query_suggestions",
searchParameters: {
filters: "products.browsing_facet_values:Accessories",
},
},
// ...
});
```
Update the filter whenever the category page changes.
Replace `products` with the name of your configured source index.
## See also
* [Autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/js) guide for the widget basics.
# Facet display
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js
Learn how to configure and order facets in InstantSearch.js.
Learn how to configure, order, hide, and display facets and facet values using the dashboard, API, and your choice of InstantSearch or a custom solution.
## Manage facet display from the Algolia dashboard
Control the facets to display and their order by sending a [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) parameter with your search query.
Your UI must interpret this setting so your users can interact with the index.
The main implementation steps are:
1. [Configure the attributes to use for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting) on your search index.
2. Configure your facet display.
3. [Build a UI capable of interpreting the `renderingContent` setting](#build-the-ui-with-instantsearch).
For this step, use InstantSearch library widgets.
You can create a UI with other tools but must write the interpretation logic yourself.
1) Select the **Search** product icon on your dashboard and then select your index.
2) Click the **Configuration** tab.
3) Under the **Filtering and Faceting** category, click **Facet display**.
4) Click **Add facet to display** to choose the facets you want to display in your UI.
5) Use the `=` icon next to each facet to drag them into the correct position. The order you set here determines how facets display in your UI.
6) For each facet, click the pen icon to configure how the engine should display the facet's values. You can:
* Pin some values at the top of the list if you want to display them first.
* Hide some values from the list entirely.
* Choose how to display the remaining facet values: by count, alphabetically, or only display pinned values.
7) To remove a facet from the list and stop displaying it in your UI, click the trash icon.
8) Save your changes.
If you want to only display the facets "brand", "size", and "color" in your UI, declare them as [attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting) and add them to the list of facets to display.
With those three facets added, you want to display the brands alphabetically.
However:
* Since you have a specific partnership with the brand "Lacoste", you want the brand to always display on top.
* Sizes need to be in a specific order: S, M, L, XL. You may have sizes in different formats in your data, but for the sake of clarity, you only want to display those four. You pin them at the top in the correct order and choose not to display other values.
* For colors, you want to display them by count and not pin any specific value.
As soon as you save the changes, the UI adapts to this new configuration.
This approach lets you configure a static facet display.
If you want a more dynamic approach that customizes the display of the facets and values for a specific query or category, see the [Merchandising facets](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-facets).
To replicate the dashboard example,
use the API client's [`setSettings`](/doc/rest-api/search/set-settings) method with the following settings applied to your index:
```cs C# expandable theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
RenderingContent = new RenderingContent
{
FacetOrdering = new FacetOrdering
{
Facets = new Facets
{
Order = new List { "brand", "size", "color" },
},
Values = new Dictionary
{
{
"brand",
new Value
{
Order = new List { "Lacoste" },
SortRemainingBy = Enum.Parse("Alpha"),
}
},
{
"size",
new Value
{
Order = new List { "S", "M", "L", "XL" },
SortRemainingBy = Enum.Parse("Hidden"),
}
},
{
"color",
new Value
{
SortRemainingBy = Enum.Parse("Count"),
}
},
},
},
},
}
);
```
```dart Dart expandable theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
renderingContent: RenderingContent(
facetOrdering: FacetOrdering(
facets: Facets(
order: ["brand", "size", "color"],
),
values: {
'brand': Value(
order: ["Lacoste"],
sortRemainingBy: SortRemainingBy.fromJson("alpha"),
),
'size': Value(
order: ["S", "M", "L", "XL"],
sortRemainingBy: SortRemainingBy.fromJson("hidden"),
),
'color': Value(
sortRemainingBy: SortRemainingBy.fromJson("count"),
),
},
),
),
),
);
```
```go Go expandable theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetRenderingContent(
search.NewEmptyRenderingContent().SetFacetOrdering(
search.NewEmptyFacetOrdering().SetFacets(
search.NewEmptyFacets().SetOrder([]string{"brand", "size", "color"}),
).SetValues(map[string]search.Value{
"brand": *search.NewEmptyValue().SetOrder(
[]string{"Lacoste"},
).SetSortRemainingBy(search.SortRemainingBy("alpha")),
"size": *search.NewEmptyValue().SetOrder(
[]string{"S", "M", "L", "XL"},
).SetSortRemainingBy(search.SortRemainingBy("hidden")),
"color": *search.NewEmptyValue().SetSortRemainingBy(search.SortRemainingBy("count")),
}),
),
),
))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java expandable theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setRenderingContent(
new RenderingContent().setFacetOrdering(
new FacetOrdering()
.setFacets(new Facets().setOrder(Arrays.asList("brand", "size", "color")))
.setValues(
new HashMap() {
{
put("brand", new Value().setOrder(Arrays.asList("Lacoste")).setSortRemainingBy(SortRemainingBy.ALPHA));
put("size", new Value().setOrder(Arrays.asList("S", "M", "L", "XL")).setSortRemainingBy(SortRemainingBy.HIDDEN));
put("color", new Value().setSortRemainingBy(SortRemainingBy.COUNT));
}
}
)
)
)
);
```
```js JavaScript expandable theme={"system"}
const response = await client.setSettings({
indexName: "INDEX_NAME",
indexSettings: {
renderingContent: {
facetOrdering: {
facets: {
order: ["brand", "size", "color"],
},
values: {
brand: {
order: ["Lacoste"],
sortRemainingBy: "alpha",
},
size: {
order: ["S", "M", "L", "XL"],
sortRemainingBy: "hidden",
},
color: {
sortRemainingBy: "count",
},
},
},
},
},
});
```
```kotlin Kotlin expandable theme={"system"}
val response = client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
renderingContent = RenderingContent(
facetOrdering = FacetOrdering(
facets = Facets(order = listOf("brand", "size", "color")),
values = mapOf(
"brand" to Value(
order = listOf("Lacoste"),
sortRemainingBy = SortRemainingBy.entries.first { it.value == "alpha" },
),
"size" to Value(
order = listOf("S", "M", "L", "XL"),
sortRemainingBy = SortRemainingBy.entries.first { it.value == "hidden" },
),
"color" to Value(
sortRemainingBy = SortRemainingBy.entries.first { it.value == "count" },
),
),
),
),
),
)
```
```php PHP expandable theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['renderingContent' => ['facetOrdering' => ['facets' => ['order' => [
'brand',
'size',
'color',
]],
'values' => ['brand' => ['order' => [
'Lacoste',
],
'sortRemainingBy' => 'alpha',
],
'size' => ['order' => [
'S',
'M',
'L',
'XL',
],
'sortRemainingBy' => 'hidden',
],
'color' => [
'sortRemainingBy' => 'count',
],
],
]]],
);
```
```python Python expandable theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"renderingContent": {
"facetOrdering": {
"facets": {
"order": ["brand", "size", "color"],
},
"values": {
"brand": {
"order": ["Lacoste"],
"sortRemainingBy": "alpha",
},
"size": {
"order": ["S", "M", "L", "XL"],
"sortRemainingBy": "hidden",
},
"color": {
"sortRemainingBy": "count",
},
},
},
},
},
)
```
```ruby Ruby expandable theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
rendering_content: Algolia::Search::RenderingContent.new(
facet_ordering: Algolia::Search::FacetOrdering.new(
facets: Algolia::Search::Facets.new(order: ["brand", "size", "color"]),
values: {
brand: Algolia::Search::Value.new(order: ["Lacoste"], sort_remaining_by: "alpha"),
size: Algolia::Search::Value.new(order: ["S", "M", "L", "XL"], sort_remaining_by: "hidden"),
color: Algolia::Search::Value.new(sort_remaining_by: "count")
}
)
)
)
)
```
```scala Scala expandable theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
renderingContent = Some(
RenderingContent(
facetOrdering = Some(
FacetOrdering(
facets = Some(
Facets(
order = Some(Seq("brand", "size", "color"))
)
),
values = Some(
Map(
"brand" -> Value(
order = Some(Seq("Lacoste")),
sortRemainingBy = Some(SortRemainingBy.withName("alpha"))
),
"size" -> Value(
order = Some(Seq("S", "M", "L", "XL")),
sortRemainingBy = Some(SortRemainingBy.withName("hidden"))
),
"color" -> Value(
sortRemainingBy = Some(SortRemainingBy.withName("count"))
)
)
)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift expandable theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(renderingContent: SearchRenderingContent(facetOrdering: SearchFacetOrdering(
facets: SearchFacets(order: ["brand", "size", "color"]),
values: [
"brand": SearchValue(order: ["Lacoste"], sortRemainingBy: SearchSortRemainingBy.alpha),
"size": SearchValue(order: ["S", "M", "L", "XL"], sortRemainingBy: SearchSortRemainingBy.hidden),
"color": SearchValue(sortRemainingBy: SearchSortRemainingBy.count),
]
)))
)
```
### Hide facet values
The following example shows three facets: `author`, `brand`, and `on_sale` (in that order).
The `author` facet is sorted by number of hits, the `brand` facet is ordered alphabetically, always shows the facet value "Kensington" in the first position (pinned) and never shows the value "Bosch" (hidden). The `on_sale` facet only shows the pinned true value.
```js JavaScript icon="code" expandable theme={"system"}
const response = await client.setSettings({
indexName: "INDEX_NAME",
indexSettings: {
renderingContent: {
facetOrdering: {
facets: {
order: ["author", "brand", "on_sale"],
},
values: {
author: {
sortRemainingBy: "count",
},
brand: {
order: ["Kensington"],
hide: ["Bosch"],
sortRemainingBy: "alpha",
},
on_sale: {
order: ["true"],
sortRemainingBy: "hidden",
},
},
},
},
},
});
```
## Build the UI with InstantSearch
The simplest way to control facet display is to use InstantSearch.
This UI library has built-in widgets that understand the [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) data returned by searches.
This data includes everything you need to show facets how you want.
### Facet display requirements for InstantSearch
To make your UI compatible with the facet display feature:
* InstantSearch.js version 4.72.0 or later (see [Upgrade guide](/doc/guides/building-search-ui/upgrade-guides/js)).
* Add refinement widgets for the different facets.
* Add a [`dynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/js) container around the widgets you want to sort.
* In those widgets, remove any custom `sortBy` or set `facetOrdering` to `true`.
```js JavaScript icon=code theme={"system"}
// This example assumes you're including InstantSearch.js in your web
// page via a CDN. If you're using it with a package manager, you
// should adjust the way you import InstantSearch.js and its widgets.
const { dynamicWidgets, menu, hierarchicalMenu } = instantsearch.widgets;
search.addWidgets([
dynamicWidgets({
container: "#dynamic-widgets",
fallbackWidget: ({ container, attribute }) =>
menu({ container, attribute, limit: 10 }),
widgets: [
(container) =>
hierarchicalMenu({
limit: 10,
attributes: [
"hierarchicalCategories.lvl0",
"hierarchicalCategories.lvl1",
],
}),
],
}),
]);
```
### Optimize dynamic widget network requests
Algolia automatically mounts the appropriate widgets for the search results.
* In versions 4.32 to 4.35, dynamic widgets request all facets and generate an extra network request for this.
To avoid this, set `facets` to `[]` (empty).
* In version 4.36 and later, to avoid an extra request, `facets` is set to `[*]` by default.
If you prefer to do two network requests with only the relevant facets returned, set `facets` to `[]`.
```js JavaScript icon=code theme={"system"}
// This example assumes you're including InstantSearch.js in your web
// page via a CDN. If you're using it with a package manager, you
// should adjust the way you import InstantSearch.js and its widgets.
const { dynamicWidgets, menu, hierarchicalMenu } = instantsearch.widgets;
search.addWidgets([
dynamicWidgets({
container: "#dynamic-widgets",
fallbackWidget: ({ container, attribute }) =>
menu({ container, attribute, limit: 10 }),
widgets: [
(container) =>
hierarchicalMenu({
limit: 10,
attributes: [
"hierarchicalCategories.lvl0",
"hierarchicalCategories.lvl1",
],
}),
],
// do two network requests
facets: [],
}),
]);
```
### Widget limitation
This applies to InstantSearch.js version 4.36 and later.
Dynamic widgets can show up to 1,000 facets.
If you do have more than that, here's how to display the ones with the most results.
1. Add an `applicable_facets` facet to every object:
```json JSON icon=braces theme={"system"}
{
"applicable_facets": ["special_type", "color"],
"special_type": "stretch",
"color": "blue"
}
```
2. Configure the dynamic widget so it doesn't ask for all facets.
Many of them will be hidden anyway because of the 1,000 facet limit.
To do this, set `facets` to `[]` (empty).
```js JavaScript icon=code theme={"system"}
dynamicWidgets({
facets: [],
});
```
3. Ensure that `applicable_facets` is always requested.
Such as from a hidden menu:
```js JavaScript icon=code theme={"system"}
connectMenu(() => {})({ attribute: "applicable_facets" });
```
4. Use `transformItems` of `dynamicWidgets` to use `applicable_facets` for dynamic widget rendering:
```js JavaScript icon=code theme={"system"}
dynamicWidgets({
facets: [],
transformItems(_items, { results }) {
return Object.keys(results._rawResults[0].facets.applicable_facets);
},
});
```
## Build the UI without InstantSearch
If you aren't using InstantSearch to build your UI, you need to build frontend logic to:
* Interpret the [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) parameter returned along search results,
* Order facets and values based on your interpretation of the [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) parameter values.
A custom implementation of frontend search requires:
* Reading the facet order
* Reading the facet value order
### Read the facet order
If you hardcode the list of facets to display on a page, you can read that from `result.renderingContent.facetOrdering.facets.order` (all keys are optional).
This is the list of attributes that you chose to display and can loop over to display individual facet lists.
### Read the facet value order
You need to list the attribute you want to display facets of in `searchParameters.facets`.
Read the possible results from `result.facets[facetName]`.
After fetching those values, you can read the `result.renderingContent.facetOrdering.values[facetName]` object to sort them.
This object has the following keys:
* `order`: an array of facet values to pin at the start of the list
* `hide`: an array of facet values to hide from the list
* `sortRemainingBy`: a string that describes how to sort the remaining values. Either "alpha" (alphabetically, ascending), "count" (value retrieved from `facets[facetName][facetValue]`, descending), or "hidden", which only displays the ordered items.
# Facet drop-down menus
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/facet-dropdown/js
Learn how to display your facets as dropdowns with InstantSearch.js.
All Search and Discovery user interfaces display, in addition to a search box, a series of filters that allow users to narrow down their search.
These onscreen filters are called [facets](/doc/guides/managing-results/refine-results/faceting).
A typical facet is an attribute like "brand" or "price", and facet values are the individual brands and prices.
By clicking on a facet value, users can include and exclude whole categories of products.
For example, by selecting "Blue" in the "Color" facet filter, a user can exclude every product except blue ones.
For a long time, ecommerce websites have been displaying facets on the left side of the screen, providing direct access.
But this placement reduces the space available to display products.
With the rise of mobile-first design and touchscreen, online businesses have started to display facets at the top of their product listing.
That's the case of [Lacoste](https://www.lacoste.com/gb/polo-shirts/all-polo-shirts/#query=Polo) which [increased its sales by +150% sales with Algolia's search](https://resources.algolia.com/customer-stories/casestudy-lacoste-en).
Dropdown faceting offers two benefits:
* It increases facet visibility and accessibility, encouraging more usage.
* It simplifies the screen, leaving more room for products and creating more on-screen breathing space, an important UX design choice.
This guide shows how to turn a facet filter, commonly represented by a [`refinementList`](/doc/api-reference/widgets/refinement-list/js) or a [`hierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/js), widget, into a [custom widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js) with a drop-down menu layout.
The provided code uses InstantSearch.js, which can be taken as-is or as a reference if you want to replicate the same pattern using [Vue](/doc/guides/building-search-ui/what-is-instantsearch/vue) InstantSearch.
Run and edit the Facet drop-down menus example in CodeSandbox.
Browse the source for the Facet drop-down menus example on GitHub.
## Wrapper elements
By design, each drop-down menu refinement widget wrapper has two elements:
* A button with a label and facet count
* A drop-down menu box
Both can be customized using [settings](#settings).
Each of the drop-down menu come wrapped in its own `div`,
which allows multiple facets in the same container, thus creating a horizontal list.
To ease the creation of a drop-down menu widget, this guide provides a factory wrapper function called `createDropdown` that takes two parameters:
* The refinement widget you want to turn into a drop-down menu
* An object for optional settings
This function, located in the [`src/Dropdown.js`](https://codesandbox.io/p/sandbox/github/algolia/doc-code-samples/tree/master/instantsearch.js/facet-dropdown?file=/src/Dropdown.js) file, returns an [InstantSearch widget](/doc/guides/building-search-ui/what-is-instantsearch/js#predefined-widgets) that can be placed alongside other widgets on your search screen.
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript icon=code theme={"system"}
const myFacetDropdown = createDropdown(instantsearch.widgets.refinementList, {
/* optional settings */
});
```
### Quickstart
First, you create your own drop-down menu widget.
Later, you customize it.
To create the drop-down menu, use a [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget on your `type` facet.
* Import all JavaScript files, such as Algolia search, InstantSearch.js, and your own JS files.
* Declare a placeholder `div` with the ID `type` for the drop-down menu widget for the `type` facet.
In [`index.html`](https://codesandbox.io/p/sandbox/github/algolia/doc-code-samples/tree/master/instantsearch.js/facet-dropdown?file=/index.html):
```html HTML icon=code-xml theme={"system"}
```
Import the `createDropdown` function from the [`src/dropdown.js`](https://codesandbox.io/p/sandbox/github/algolia/doc-code-samples/tree/master/instantsearch.js/facet-dropdown?file=/src/Dropdown.js)
file before initializing an InstantSearch instance with your Algolia credentials and create the drop-down menu widget.
In this case, use a [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget for the type facet.
As a result, you create a customized `refinementListDropdown` widget for the `type` facet that can also be re-used for other facets.
In [`src/app.js`](https://codesandbox.io/p/sandbox/github/algolia/doc-code-samples/tree/master/instantsearch.js/facet-dropdown?file=/src/app.js):
```js JavaScript icon=code theme={"system"}
import { createDropdown } from "./Dropdown";
// Create the refinementListDropdown widget
const refinementListDropdown = createDropdown(
instantsearch.widgets.refinementList,
{ closeOnChange: true },
);
```
Here, `instantsearch.widgets.refinementList` refers to InstantSearch's [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget.
See the [widget reference guide](/doc/api-reference/widgets/refinement-list/js).
Now that you've created your `refinementListDropdown` widget, you're ready to add it to your InstantSearch instance.
```js JavaScript icon=code theme={"system"}
// Initialize InstantSearch and your widgets
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
}),
instantsearch.widgets.hits({...}),
// Adding the refinementListDropdown widget on the `type` facet
refinementListDropdown({
container: '#type', // The CSS Selector of the DOM element inside which the widget is inserted.
attribute: 'type', // The name of the attribute in the records.
searchable: true // Whether to add a search input to let users search for more facet values.
}),
...
]);
search.start();
```
By design, the `createDropdown` function creates a widget that acts as a wrapper around the initial widget, which is passed as a parameter.
That's why the created `refinementListDropdown` widget can take any of the settings available in the `refinementList` widget.
For example, `{ limit: 5, showMore: true }` limits the default number of displayed facet values to "5".
## Settings
The `createDropdown` function takes two parameters:
* Required: a refinement widget to turn into a drop-down menu
* Optional: an object with settings
```js JavaScript icon=code theme={"system"}
createDropdown(
baseWidget,
({ cssClasses, buttonText, buttonClassName, closeOnChange } = {}),
);
```
### `cssClasses`
**Type:** `object`
An object that lets you [override the CSS class](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#style-your-widgets) in your code.
Here's an example with default values:
```js JavaScript icon=code theme={"system"}
cssClasses = {
root: "ais-Dropdown",
button: "ais-Dropdown-button",
buttonRefined: "ais-Dropdown-button--refined",
closeButton: "ais-Dropdown-close",
};
```
### `buttonText`
**Type:** `string | function`
This is the text displayed in the DropDown button.
It can be a string or a function.
By default, it shows the price of the active price refinement.
Here's an example from the `priceMenuDropdown` widget.
```js JavaScript icon=code theme={"system"}
buttonText({ items }) {
const refinedItem = (items || []).find(
(item) => item.label !== "All" && item.isRefined
);
return refinedItem ? `Price (${refinedItem.label})` : "Price menu";
},
```
### `buttonClassName`
**Type:** `string`
Same as `buttonText`, but for the button CSS class name.
Here's an example from the `priceMenuDropdown` widget.
```js JavaScript icon=code theme={"system"}
buttonClassName({ items }) {
const isRefined = (items || []).find(
(item) => item.label !== "All" && item.isRefined
);
return isRefined && "ais-Dropdown-button--refined";
},
```
### `closeOnChange`
**Type:** `boolean | function`
This argument can be a boolean or a function that returns `true` if you want the drop-down menu to close as soon as a user selects a value.
If it's `false`, it doesn't close automatically, thus enabling users to select more than one facet value.
Here's an example in which the code returns `true` if users are using a mobile device.
```js JavaScript icon=code theme={"system"}
const MOBILE_WIDTH = 375;
/* ... */
closeOnChange: () => window.innerWidth >= MOBILE_WIDTH;
```
## Customize the UI
The generated markup lets you customize the look and feel to your needs,
to change the default aspect of the drop-down menu with a few lines of CSS.
Here's the default version:
You can customize two aspects:
* **Inline facet values**
The `brandDropdown` widget uses an inline list.
See the code in `app.css`.
```css CSS icon=paintbrush theme={"system"}
.my-BrandDropdown .ais-RefinementList-list {
width: 20rem;
display: flex;
flex-wrap: wrap;
}
```
* **Fixed height drop-down menu**
The hierarchical `categoriesDropdown` widget uses a fixed height list.
See the code in `app.css`.
```css CSS icon=paintbrush theme={"system"}
#category .ais-HierarchicalMenu {
height: 195px;
overflow: auto;
}
```
## Mobile support
On mobile devices, more than anywhere else,
displaying facet refinements over the results is a must-have,
considering the limited real estate.
Drop-down widgets are a great fit,
but you need to tweak the display so they can take advantage of the screen's width and height.
The mobile version uses the following CSS media query:
```css CSS icon=paintbrush theme={"system"}
@media only screen and (max-width: 375px) {
/* ... */
}
```
It uses the following JavaScript settings at each widget level:
```js JavaScript icon=code theme={"system"}
const MOBILE_WIDTH = 375;
const refinementListDropdown = createDropdown(
instantsearch.widgets.refinementList,
{ closeOnChange: () => window.innerWidth >= MOBILE_WIDTH },
);
```
This demo gets you started by providing a basic look and feel for mobile devices.
Refactor the provided code to match your current design and offer the best experience to your mobile users.
# Geo search
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/geo-search/js
Learn how to use geographical search in your InstantSearch.js app.
You can use Algolia's [geographical search capabilities](/doc/guides/managing-results/refine-results/geolocation#geo-search) of Algolia with the [`geoSearch`](/doc/api-reference/widgets/geo-search/js) widget.
The widget is implemented on top of [Google Maps](https://developers.google.com/maps/documentation/javascript/overview) but the core logic isn't tied to any map provider.
You can build your own map widget with the [`connectGeoSearch`](/doc/api-reference/widgets/geo-search/js#customize-the-ui-with-connectgeosearch) connector and a different provider (such as [Leaflet](https://leafletjs.com)).
Before diving into the usage keep in mind that you are responsible for loading the Google Maps library.
You can find more information about that [in the Google Maps documentation](https://developers.google.com/maps/documentation/javascript/tutorial).
## Dataset
This guide use a dataset of over 3,000 of the biggest airports in the world.
```json JSON icon=braces theme={"system"}
{
"objectID": "3797",
"name": "John F Kennedy Intl",
"city": "New York",
"country": "United States",
"iata_code": "JFK",
"links_count": 911,
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
}
}
```
Latitude and longitude are stored in the record to allow hits to be displayed on the map.
You should store them in the `_geoloc` attribute to enable geo-filtering and geo-sorting.
You can download the dataset on [GitHub](https://github.com/algolia/datasets/tree/master/airports).
Have a look at how to import it in [Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard).
### Configure index settings
When displaying on a map, you still want the relevance to be good.
For that, configure the as follows:
* [Searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes) should be set to enable search in the four textual attributes: `name`, `city`, `country`, and `iata_code`.
* [Custom ranking](/doc/guides/managing-results/must-do/custom-ranking): use the **number of other connected airports** `links_count` as a ranking metric. The more connections the better.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
SearchableAttributes = new List { "name", "country", "city", "iata_code" },
CustomRanking = new List { "desc(links_count)" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: [
"name",
"country",
"city",
"iata_code",
],
customRanking: [
"desc(links_count)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSearchableAttributes(
[]string{"name", "country", "city", "iata_code"}).SetCustomRanking(
[]string{"desc(links_count)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setSearchableAttributes(Arrays.asList("name", "country", "city", "iata_code"))
.setCustomRanking(Arrays.asList("desc(links_count)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
searchableAttributes: ['name', 'country', 'city', 'iata_code'],
customRanking: ['desc(links_count)'],
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
searchableAttributes = listOf("name", "country", "city", "iata_code"),
customRanking = listOf("desc(links_count)"),
),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['searchableAttributes' => [
'name',
'country',
'city',
'iata_code',
],
'customRanking' => [
'desc(links_count)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"searchableAttributes": [
"name",
"country",
"city",
"iata_code",
],
"customRanking": [
"desc(links_count)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
searchable_attributes: ["name", "country", "city", "iata_code"],
custom_ranking: ["desc(links_count)"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
searchableAttributes = Some(Seq("name", "country", "city", "iata_code")),
customRanking = Some(Seq("desc(links_count)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
searchableAttributes: ["name", "country", "city", "iata_code"],
customRanking: ["desc(links_count)"]
)
)
```
## Hits on the map
This is the most simple use case for the [`geoSearch`](/doc/api-reference/widgets/geo-search/js) widget.
It displays your results on a Google Maps.
In the example, the `height` of the parent container is explicitly set.
This is a requirement of Google Maps: **if you don't set it, the map won't be displayed.**
The widget will set the zoom and position of the map according to the hits retrieved by the search.
In case the search doesn't return any results the map will fall back to its [`initialZoom`](/doc/api-reference/widgets/geo-search/js#param-initial-zoom) and [`initialPosition`](/doc/api-reference/widgets/geo-search/js#param-initial-position) .
By default, once you move the map the widget will use [the bounding box](/doc/api-reference/api-parameters/insideBoundingBox) of the map to filter the results.
You can find all the available options of the widget [in the documentation](/doc/api-reference/widgets/geo-search/js).
Since the widget is built on top of Google Maps you might want to apply some options to your maps.
For example you might want to switch on a satellite image rather than a normal street map.
You can provide those extra options to the [`mapOptions`](/doc/api-reference/widgets/geo-search/js#param-map-options) attribute of the [`geoSearch`](/doc/api-reference/widgets/geo-search/js) widget and they will be forwarded to the Google Maps instance.
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "airports",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.geoSearch({
container: "#maps",
googleReference: window.google,
mapOptions: {
mapTypeId: window.google.maps.MapTypeId.SATELLITE,
},
}),
]);
search.start();
```
You can find a live example on [CodeSandbox](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-hits).
The source code is available on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-hits).
## Custom marker
The standard marker API of Google Maps let you update the image of the marker.
You can also use custom styling to add information to the marker with the [`customHTMLMarker`](/doc/api-reference/widgets/geo-search/js#param-custom-html-marker) option.
The widget accept a regular template like any other InstantSearch.js widgets.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "airports",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.geoSearch({
container: "#maps",
googleReference: window.google,
templates: {
HTMLMarker: (hit, { html }) => html`
${hit.city} - ${hit.airport_id}
`,
},
}),
]);
search.start();
```
You can find a live example on [CodeSandbox](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-custom-marker).
The source code is available on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-custom-marker).
## Control
A common pattern that comes with a geographical search experience is the ability to control how the refinement behaves from the map.
By default the widget will use [the map bounding box](/doc/api-reference/api-parameters/insideBoundingBox) to filter the results as soon the map has moved.
But sometimes it's better for the experience to let users decide when to apply the refinement.
This way, they can explore the map without having the results changing every time.
For this pattern, the [`enableRefineControl`](/doc/api-reference/widgets/geo-search/js#param-enable-refine-control) option lets users control how the refinement behaves directly from the widget.
You can also control the default value applied to the checkbox of the control component with [`enableRefineOnMapMove`](/doc/api-reference/widgets/geo-search/js#param-enable-refine-on-map-move).
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "airports",
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.geoSearch({
container: "#maps",
googleReference: window.google,
enableRefineControl: true,
enableRefineOnMapMove: false,
}),
]);
search.start();
```
You can find a live example on [CodeSandbox](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-control).
The source code is available on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/geo-search-control).
# Highlighting in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js
How to use highlighting and snippeting in InstantSearch.js to demonstrate to searchers why a result matches a query.
## Highlighting
Highlighting is a vital tool for showing searchers why a result matched their query by providing different styling to all matched query words.
By default, Highlighting is enabled on all [`searchableAttributes`](/doc/api-reference/api-parameters/searchableAttributes)
unless specified otherwise in [`attributesToHighlight`](/doc/api-reference/api-parameters/attributesToHighlight).
Use one of the API clients or the Algolia dashboard, not InstantSearch, to configure
`attributesToHighlight`.
Below is an example of how to configure which attributes to highlight:
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesToHighlight = new List { "author", "title", "content" },
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesToHighlight: [
"author",
"title",
"content",
],
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesToHighlight(
[]string{"author", "title", "content"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesToHighlight(Arrays.asList("author", "title", "content"))
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { attributesToHighlight: ['author', 'title', 'content'] },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesToHighlight = listOf("author", "title", "content")),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesToHighlight' => [
'author',
'title',
'content',
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesToHighlight": [
"author",
"title",
"content",
],
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_to_highlight: ["author", "title", "content"])
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesToHighlight = Some(Seq("author", "title", "content"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesToHighlight: ["author", "title", "content"])
)
```
**If `exactOnSingleWordQuery` is set to `word`, only exact matches will be highlighted: partial matches and prefixes won't be.**
### Response information
The response includes a [`_highlightResult`](/doc/rest-api/search/search-single-index#response-hits) for each attribute, containing:
* The attribute's value with highlighting
* The match level (how many query words matched)
* A boolean indicating if the whole attribute is highlighted
* The matched words for each attribute.
For example:
```json JSON icon=braces theme={"system"}
{
"value": "Actiontec - 4-Port Ethernet Broadband Router with Wireless-N - Black",
"matchLevel": "full",
"fullyHighlighted": false,
"matchedWords": ["action"]
}
```
#### Performance
Algolia highlights the first 50,000 characters of the whole result set (5,000 logograms for CJK languages).
This limit prevents impacting the user experience by ensuring a fast response time even with large results.
#### Sanitize the results
Since Algolia returns highlighted results as they're stored in the engine, make sure you sanitize your HTML.
**Unsanitized or invalid HTML content, particularly with user-provided content, can be a security risk** and opens up the possibility of [cross-site-scripting attacks](https://en.wikipedia.org/wiki/Cross-site_scripting) on your site.
### Pre- and post-tags
The primary configurations that you can set for highlighting are pre-tags and post-tags.
This configuration lets you use any tag (HTML or otherwise) to highlight results in the UI.
You can set the pre-tags and post-tags (that is, the strings before and after the matched query words) to any string value.
They're set to `em` and `em` by default.
You can configure this setting using the [`highlightPreTag`](/doc/api-reference/api-parameters/highlightPreTag) and [`highlightPostTag`](/doc/api-reference/api-parameters/highlightPostTag) parameters at either query or indexing time.
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesToHighlight = new List { "author", "title", "content" },
HighlightPreTag = "",
HighlightPostTag = "",
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesToHighlight: [
"author",
"title",
"content",
],
highlightPreTag: "",
highlightPostTag: "",
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesToHighlight(
[]string{"author", "title", "content"}).SetHighlightPreTag("").SetHighlightPostTag("")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings()
.setAttributesToHighlight(Arrays.asList("author", "title", "content"))
.setHighlightPreTag("")
.setHighlightPostTag("")
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: {
attributesToHighlight: ['author', 'title', 'content'],
highlightPreTag: '',
highlightPostTag: '',
},
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(
attributesToHighlight = listOf("author", "title", "content"),
highlightPreTag = "",
highlightPostTag = "",
),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesToHighlight' => [
'author',
'title',
'content',
],
'highlightPreTag' => '',
'highlightPostTag' => '',
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesToHighlight": [
"author",
"title",
"content",
],
"highlightPreTag": '',
"highlightPostTag": "",
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(
attributes_to_highlight: ["author", "title", "content"],
highlight_pre_tag: "",
highlight_post_tag: ""
)
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesToHighlight = Some(Seq("author", "title", "content")),
highlightPreTag = Some(""),
highlightPostTag = Some("")
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesToHighlight: ["author", "title", "content"],
highlightPreTag: "",
highlightPostTag: ""
)
)
```
**Pre-tag and post-tag settings are typically used together.**
### Highlighting using InstantSearch
Algolia provides a `Highlight` widget in the InstantSearch libraries to highlight matches on the frontend.
Refer to the [widget docs](/doc/api-reference/widgets/highlight/js) for usage notes and code examples.
## Snippeting
Snippeting returns parts of the matched attribute, namely, the matched words and some words around them.
Unlike highlighting, you must enable snippeting for each attribute you wish to snippet, although you can set the value `*` to snippet all attributes.
The snippeted result wraps the matched words in the pre-highlighting and post-highlighting tags.
Use one of the API clients or the Algolia dashboard, not InstantSearch, to configure
`attributesToSnippet`.
An example configuration of the [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet):
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesToSnippet = new List { "content:80", "description" },
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesToSnippet: [
"content:80",
"description",
],
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesToSnippet(
[]string{"content:80", "description"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesToSnippet(Arrays.asList("content:80", "description"))
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { attributesToSnippet: ['content:80', 'description'] },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesToSnippet = listOf("content:80", "description")),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesToSnippet' => [
'content:80',
'description',
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesToSnippet": [
"content:80",
"description",
],
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_to_snippet: ["content:80", "description"])
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesToSnippet = Some(Seq("content:80", "description"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesToSnippet: ["content:80", "description"])
)
```
For each attribute, you can configure how snippet results are displayed by defining:
* The number of words in the resulting snippet (`nbWords`)
* The text used to replace removed words in the snippet (`snippetEllipsisText`).
### Number of words
You can set the number of words to return when defining your [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet) with the syntax `attribute:nbWords`.
For example, `body:20` returns twenty-word snippets for the attribute `body`.
When undefined, the value defaults to 10.
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesToSnippet = new List { "body:20", "title" },
}
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesToSnippet: [
"body:20",
"title",
],
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesToSnippet(
[]string{"body:20", "title"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesToSnippet(Arrays.asList("body:20", "title"))
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { attributesToSnippet: ['body:20', 'title'] },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesToSnippet = listOf("body:20", "title")),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesToSnippet' => [
'body:20',
'title',
],
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesToSnippet": [
"body:20",
"title",
],
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_to_snippet: ["body:20", "title"])
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesToSnippet = Some(Seq("body:20", "title"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesToSnippet: ["body:20", "title"])
)
```
With `nbWords` set to 6:
```txt theme={"system"}
"As Gregor Samsa awoke one morning …"
```
And `nbWords` set to 10:
```txt theme={"system"}
"As Gregor Samsa awoke one morning from uneasy dreams he …"
```
### Ellipsis text
The engine inserts text to show where words have been removed from the snippeted text.
The default replacement text is `…` (Unicode `U+20216`): an ellipsis character. However, you can change this with the [`snippetEllipsisText`](/doc/api-reference/api-parameters/snippetEllipsisText) setting:
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { SnippetEllipsisText = "[…]" }
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
snippetEllipsisText: "[…]",
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetSnippetEllipsisText("[…]")))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setSnippetEllipsisText("[…]"));
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { snippetEllipsisText: '[…]' },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(snippetEllipsisText = "[…]"),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['snippetEllipsisText' => '[…]',
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"snippetEllipsisText": "[…]",
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(snippet_ellipsis_text: "[…]")
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
snippetEllipsisText = Some("[…]")
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(snippetEllipsisText: "[…]")
)
```
The engine adds an extra space (Unicode `U+0020`) before and after `snippetEllipsisText`. For example:
```txt theme={"system"}
"… awoke one morning … "
```
### Sanitize the results
Algolia removes all HTML tags from the snippeted results to ensure the text displays correctly within pre-highlighting and post-highlighting tags.
#### An alternative to snippets
If you want an attribute to keep its HTML formatting,
instead of asking Algolia to shorten (snippet) it for you,
create short-form in your data based on a subset of the original,
longer-form records.
Ensure that these short-form records are restricted to an appropriate length and contain valid HTML (all opening tags have matching closing tags).
You can do this programmatically,
for example, with:
* RegExp. If you have a limited, known set of HTML, you can use regular expressions to split the content. Be aware of the [limitations of the RegExp approach](https://blog.codinghorror.com/parsing-html-the-cthulhu-way/).
* HTML splitter code
### Snippeting using InstantSearch
There's a `Snippet` widget in the InstantSearch libraries to snippet text attributes on the frontend.
Refer to the [widget docs](/doc/api-reference/widgets/snippet/js) for usage notes and code examples.
# Infinite scroll with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/infinite-scroll/js
Implement an infinite scroll experience with InstantSearch.js.
An "infinite list" is a common way of displaying results. It's especially well-suited to mobile devices and has two variants:
* **Infinite hits** with a "See more" button at the end of a batch of results. Implement this with InstantSearch's [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js) widget.
* **Infinite scroll** uses a listener on the scroll event (called when users have scrolled to the end of the first batch of results). The following guidance implements such an infinite scroll. Find the complete example on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/instantsearch.js/infinite-scroll).
If there aren't any results, [display a message to users and clear filters](/doc/guides/building-search-ui/going-further/conditional-display/js#handle-searches-without-results) so they can start over.
## Display a list of hits
Render the results with the [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js) connector.
Read more about connectors in the [customizing widgets guide](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js).
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript icon=code theme={"system"}
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
(renderArgs, isFirstRender) => {
const { items, showMore, widgetParams } = renderArgs;
const { container } = widgetParams;
if (isFirstRender) {
container.appendChild(document.createElement('ul'));
return;
}
container.querySelector('ul').innerHTML = items
.map(
item =>
`
`
)
.join('');
}
);
```
## Track the scroll position
Once you have your results, the next step is to track the scroll position to determine when the rest of the content needs to be loaded (using the Intersection Observer API).
Use the API to track when the bottom of the list (the "sentinel" element) enters the viewport.
You can reuse the same element across different renders.
The [Web Fundamentals](https://web.dev/articles/intersectionobserver/) website discusses the use of this API in more detail.
```js JavaScript icon=code theme={"system"}
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
(renderArgs, isFirstRender) => {
const { items, showMore, widgetParams } = renderArgs;
const { container } = widgetParams;
if (isFirstRender) {
const sentinel = document.createElement("div");
container.appendChild(document.createElement("ul"));
container.appendChild(sentinel);
return;
}
// ...
},
);
```
This implementation uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
To support [Internet Explorer 11](https://wikipedia.org/wiki/Internet_Explorer_11) you need a [polyfill](https://wikipedia.org/wiki/Polyfill_\(programming\)) for
`IntersectionObserver`.
A browser API is used in the example, but you can apply the concepts to any infinite scroll library.
Once you have the reference to the "sentinel" element, create the Intersection Observer instance to determine when this element enters the page.
Update the Intersection Observer's `options` object to adjust this code to your needs.
```js JavaScript icon=code theme={"system"}
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
(renderArgs, isFirstRender) => {
const { items, showMore, widgetParams } = renderArgs;
const { container } = widgetParams;
if (isFirstRender) {
const sentinel = document.createElement("div");
container.appendChild(document.createElement("ul"));
container.appendChild(sentinel);
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// In that case, you can refine
}
});
});
observer.observe(sentinel);
return;
}
// ...
},
);
```
## Retrieve more results
Now that you can track when you reach the end of the results, use the [showMore](/doc/api-reference/widgets/infinite-hits/js#param-show-more) function inside the callback function of the observer.
**Only trigger the function when there are still results to retrieve.**
For this use case, the connector provides a parameter [isLastPage](/doc/api-reference/widgets/infinite-hits/js#param-is-last-page) that indicates if you still have results.
```js JavaScript icon=code theme={"system"}
let lastRenderArgs;
const infiniteHits = instantsearch.connectors.connectInfiniteHits(
(renderArgs, isFirstRender) => {
const { items, showMore, widgetParams } = renderArgs;
const { container } = widgetParams;
lastRenderArgs = renderArgs;
if (isFirstRender) {
const sentinel = document.createElement("div");
container.appendChild(document.createElement("ul"));
container.appendChild(sentinel);
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !lastRenderArgs.isLastPage) {
showMore();
}
});
});
observer.observe(sentinel);
return;
}
// ...
},
);
```
## Show more than 1,000 hits
To ensure excellent performance, the default limit for the number of hits you can retrieve for a query is
1,000.
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { PaginationLimitedTo = 1000 }
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
paginationLimitedTo: 1000,
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetPaginationLimitedTo(1000)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setPaginationLimitedTo(1000));
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { paginationLimitedTo: 1000 },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(paginationLimitedTo = 1000),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['paginationLimitedTo' => 1000,
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"paginationLimitedTo": 1000,
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(pagination_limited_to: 1000))
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
paginationLimitedTo = Some(1000)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(paginationLimitedTo: 1000)
)
```
If you need to show more than 1,000 hits, you can set a different limit using the [`paginationLimitedTo`](/doc/api-reference/api-parameters/paginationLimitedTo) parameter.
The higher you set this limit, the slower your search performance can become.
Increasing the limit doesn't mean you can go until the end of the hits,
but just that Algolia will go as far as possible in the to retrieve results in a reasonable time.
# Loading indicator with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/loading-indicator/js
Learn how to display a loading indicator with InstantSearch.js.
You can display a loading indicator to inform users on a slow connection that their search is still in progress.
Use InstantSearch's `status` indicator to decide when to show a loading indicator. The `status` can be one of the following:
* `loading`: The search is in progress.
* `stalled`: The search is in progress, but the response is taking longer than expected.
* `error`: The search failed.
* `idle`: The search succeeded.
Best practice is to display a loading indicator only when `status` is `stalled`, not during a standard (fast) search.
Tweak the timing of the `stalled` status by setting the [`stalledSearchDelay`](/doc/api-reference/widgets/instantsearch/js) option.
You can then use this condition to display a loading indicator:
```js JavaScript theme={"system"}
search.on('render', () => {
const container = document.querySelector('#loading-indicator');
if (search.status === 'stalled') {
container.innerText = 'Loading search results';
} else {
container.innerText = '';
}
})
```
If you want the loading indicator to display as soon as the search starts, use the `loading` status in combination with `stalled`:
```js JavaScript theme={"system"}
search.on('render', () => {
const container = document.querySelector('#loading-indicator');
if (search.status === 'loading' || search.status === 'stalled') {
container.innerText = 'Loading search results';
} else {
container.innerText = '';
}
})
```
# Multi-index search
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/js
Searching multiple indices in your InstantSearch.js app.
You are currently reading the documentation for InstantSearch.js v4.
Read the migration guide to learn [how to upgrade from v3 to v4](/doc/guides/building-search-ui/upgrade-guides/js#upgrade-from-v3-to-v4).
You can still access the [V3 documentation for this page](https://algolia.com/old-docs/deprecated/instantsearch/js/v3/guides/multi-index-search/).
Multi-index search (federated search) is a method for searching multiple data sources simultaneously.
This means that when users enter a search term, Algolia will look for and display results from all these data sources.
This doesn't necessarily mean that the results from Algolia indices are combined since their contents could be quite different.
[Your approach](https://www.algolia.com/blog/product/federated-search-types/) may be to display the results from each separately.
You could display the top-rated items from a movie index alongside the list of results from a book index.
Or you could display category matches alongside the list of results from a product index
## Search multiple indices with InstantSearch
The following demo uses an InstantSearch search box to display hits from two different indices:
Run and edit the Multi-index search example in CodeSandbox.
Browse the source for the Multi-index search example on GitHub.
This example uses a single [`searchBox`](/doc/api-reference/widgets/search-box/js) to search multiple indices.
It adds the first [`hits`](/doc/api-reference/widgets/hits/js) widget at the top level, while the second one is scoped under an [`index`](/doc/api-reference/widgets/index-widget/js) widget.
The second `hits` widget just displays results from the Algolia index referenced by the `index` widget that precedes it.
The first `hits` widget displays results from the top level `instant_search` index.
All examples in this guide assume you've included InstantSearch.js in your web page from a CDN.
If, instead, you're using it with a package manager, adjust how you [import InstantSearch.js and its widgets](/doc/guides/building-search-ui/installation/js) for more information.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: 'instant_search',
searchClient,
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
}),
instantsearch.widgets.hits({
container: '#hits-instant-search',
templates: {
item: (hit, { html, components }) =>
html`
`,
},
}),
]),
]);
search.start();
```
You can scope widgets under an [`index`](/doc/api-reference/widgets/index-widget/js).
The following example displays a different number of hits for the two sets of results.
The `instant_search` index displays 8 results and `instant_search_price_desc` 16 results.
To restrict the number of results per page, use the [`configure`](/doc/api-reference/widgets/configure/js) widget.
Each widget is scoped under the targeted level.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: 'instant_search',
searchClient,
});
search.addWidgets([
instantsearch.widgets.configure({
hitsPerPage: 8,
}),
instantsearch.widgets.searchBox({
container: '#searchbox',
}),
instantsearch.widgets.hits({
container: '#hits-instant-search',
templates: {
item: (hit, { html, components }) =>
html`
`,
},
}),
]),
]);
search.start();
```
## Search multiple indices with Autocomplete
You can use the [Autocomplete library](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete) in your InstantSearch app to build a dynamic multi-source search experience.
For example, you may want to display Query Suggestions together with recent searches, create a multi-column layout that mixes and item previews, or even dynamically change sources based on the query.
Autocomplete isn't limited to Algolia indices: you can use static sources or fetch data from other APIs.
For more information, see [Include multiple result types](/doc/ui-libraries/autocomplete/guides/including-multiple-result-types).
For more information, see:
* [Include multiple result types](/doc/ui-libraries/autocomplete/guides/including-multiple-result-types)
* [Reshape sources](/doc/ui-libraries/autocomplete/guides/reshaping-sources)
* [Create a multi-column layout](/doc/ui-libraries/autocomplete/guides/creating-a-multi-column-layout)
## Category display
Algolia can help you display both category matches and results if you:
* **Add categories to your Query Suggestions** either inline or [listed below a result](/doc/ui-libraries/autocomplete/introduction/sandboxes#search-suggestions-with-categories). For example, you might see the following in your Query Suggestions list "game of thrones in Books"
* **Use multi-index search to display categories from a separate category index.** This is useful if you want to display categories and Query Suggestions at the same time. Clicking such a result typically [redirects](/doc/guides/building-search-ui/ui-and-ux-patterns/redirects/js) to a category page. The following is a sample dataset for a product index and a category index.
### Example product index
```json JSON icon=braces theme={"system"}
[
{
"name": "Fashion Krisp",
"description": "A pair of red shoes with a comfortable fit.",
"image": "/fashion-krisp.jpg",
"price": 18.98,
"likes": 284,
"category": "Fashion > Women > Shoes > Court shoes"
},
{
"name": "Jiver",
"description": "A blue shirt made of cotton.",
"image": "/jiver.jpg",
"price": 17.70,
"likes": 338,
"category": "Fashion > Men > Shirts > Dress shirt"
}
]
```
### Example category index
```json JSON icon=braces theme={"system"}
[
{
"name": "Court shoes",
"category": "Fashion > Women > Shoes > Court shoes",
"description": "A dress shoe with a low-cut front and a closed heel.",
"url": "/women/shoes/court/"
},
{
"name": "Dress shirt",
"category": "Fashion > Men > Shirts > Dress shirt",
"description": "A long-sleeved, button-up formal shirt that is typically worn with a suit or tie.",
"url": "/men/shirts/dress/"
}
]
```
# Implement the OpenSearch protocol with InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/opensearch/js
Learn how to integrate the OpenSearch protocol into your website with InstantSearch.js.
This project is unrelated to AWS OpenSearch.
This page describes how to implement the [OpenSearch protocol](https://github.com/dewitt/opensearch) with InstantSearch.
The OpenSearch protocol lets you describe a search engine for your website,
so that browsers or other search clients can use that search engine.
## How OpenSearch works
In your website or web application, you include a link to an OpenSearch description document.
In this document, you describe how browsers can interact with an underlying search engine.
This interaction is based on URL templates, for example, `https://mysite.com?q={searchTerms}`,
which tell browsers how to make a search request.
Since the interaction between the browser and search engine is based on a URL,
you need to enable [URL synchronization](/doc/guides/building-search-ui/going-further/routing-urls/js).
## Create an OpenSearch description document
The OpenSearch description document is an XML file following the [OpenSearch specification](https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md).
For example:
```xml XML theme={"system"}
Example Web SearchSearch example.com content.UTF-8linkToImage
```
To adapt this example to your website, update these parts:
* `ShortName` is the name of your app or website search. It must be 16 or fewer characters.
* `Description` is a human readable description of your search engine. It must be 1024 or fewer characters.
* `Url` has the `template` attribute, that browsers use to create the URL to your search engine.
* `Image` has a URL (linkToImage) to a PNG image that represents your website, for example, the favicon.
The XML document must be served with `Content-Type: application/opensearchdescription+xml` media type.
For more information, see [OpenSearch description elements](https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#opensearch-description-elements).
## Include the OpenSearch description document in your website
You can reference the XML document in the `head` of your web pages:
```html HTML theme={"system"}
```
For more information,
see [Autodiscovery in HTML](https://github.com/dewitt/opensearch/blob/master/opensearch-1-1-draft-6.md#autodiscovery-in-htmlxhtml)
# Pagination
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/js
Organize your search results with pagination in InstantSearch.js.
Pagination gives you complete control over how you retrieve query results. You can:
* Implement standard paging
* Retrieve specific record subsets that ignore page boundaries.
* Set pagination defaults at indexing time
* Override pagination default settings at query time.
## Pagination at indexing time
At indexing time, you can set up two defaults:
* [`hitsPerPage`](/doc/api-reference/api-parameters/hitsPerPage): the number of hits returned for every page
* [`paginationLimitedTo`](/doc/api-reference/api-parameters/paginationLimitedTo): controls the number of hits retrieved for a given query
## Pagination at query time
You can:
* Retrieve a specific page with the [`page`](/doc/api-reference/api-parameters/page) parameter
* Specify specific record subsets by using the [`offset`](/doc/api-reference/api-parameters/offset) and [`length`](/doc/api-reference/api-parameters/length) parameters
* Override the default for `hitsPerPage`
## Response details
Algolia includes information about pagination in the API response.
```json JSON icon=braces theme={"system"}
{
"page": 0,
"nbHits": 40,
"nbPages": 2,
"hitsPerPage": 20,
"exhaustiveNbHits": true
}
```
* `page` returns the current page (this value **starts at 0**)
* `hitsPerPage` returns the maximum number of hits returned for each page
* `nbPages` returns the number of pages available for the current query
* `nbHits` returns the number of hits that the search query matched
* `exhaustiveNbHits` returns a boolean indicating if the nbHits count was exhaustive or approximate
* `exhaustiveTypo` returns a boolean indicating if the typo-tolerance search was exhaustive or approximate (only included when typo-tolerance is enabled)
### No hits or is the search still in progress?
Handle situations when there aren't any results (`nbHits` = 0) by [displaying a message to users and clearing filters](/doc/guides/building-search-ui/going-further/conditional-display/js#handle-searches-without-results) so they can start over.
Sometimes, though, no hits can be reported if the user's device [can't access the network or the network connection is slow](/doc/guides/building-search-ui/going-further/improve-performance/js).
To determine if there are no hits for a query or if the search is still in progress, examine the value of InstantSearch's [`status`](/doc/api-reference/widgets/instantsearch/js#param-status) property.
Consider adding a [loading indicator](/doc/guides/building-search-ui/ui-and-ux-patterns/loading-indicator/js) to inform users on a slow connection that their search is still in progress.
### Exhaustive hits
Usually, Algolia returns an accurate number of total hits.
However, if a query returns many hits, Algolia approximates the count to avoid scanning the whole .
This approximation protects other search and indexing operations.
If your API response includes the [`exhaustive.nbHits`](/doc/rest-api/search/search-single-index#response-exhaustive) parameter,
[the number of hits is approximate](https://support.algolia.com/hc/en-us/articles/4406975248145-Why-are-my-facet-and-hit-counts-not-accurate-).
## Retrieve specific pages
At query time, you can pass in the [`page`](/doc/api-reference/api-parameters/page) parameter to access a specific results page.
The [`hitsPerPage`](/doc/api-reference/api-parameters/hitsPerPage) parameter provides a way to set the default number of hits returned for each page.
By default, its value is **20**.
However, this can be overridden as an index setting or as a query parameter.
```js javaScript icon=code theme={"system"}
index
.search("query", {
page: 2,
hitsPerPage: 5,
})
.then(({ hits }) => {
console.log(hits);
});
```
## Pagination limitations
### Default number of hits per query
By default, Algolia includes up to 1,000 hits per query.
### Overriding the default limits
You can adjust the number of hits in a paginated query with the [`paginationLimitedTo`](/doc/api-reference/api-parameters/paginationLimitedTo) parameter:
```cs C# theme={"system"} theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { PaginationLimitedTo = 1000 }
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
paginationLimitedTo: 1000,
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetPaginationLimitedTo(1000)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setPaginationLimitedTo(1000));
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.setSettings({
indexName: 'theIndexName',
indexSettings: { paginationLimitedTo: 1000 },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(paginationLimitedTo = 1000),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['paginationLimitedTo' => 1000,
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"paginationLimitedTo": 1000,
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(pagination_limited_to: 1000))
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
paginationLimitedTo = Some(1000)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(paginationLimitedTo: 1000)
)
```
The maximum value is 20,000 hits per query.
Increasing the pagination limit might decrease performance.
Only set this number as large as necessary.
### Paging beyond the limit
If you request a page that is out-of-range (`page >= nbPages`),
Algolia doesn't return an error but 0 results.
## Retrieve a subset of records (with offset and length)
In most cases, you only need `page` and `hitsPerPage` to add pagination.
However, if you want to insert items, such as ads, into the results without affecting pagination, use the [`offset`](/doc/api-reference/api-parameters/offset) and [`length`](/doc/api-reference/api-parameters/length) parameters.
They let you specify the to retrieve, not the page.
The `offset` parameter lets you specify the starting record, and `length` sets the number of records returned. For example, if you have 100 records in your result set, broken down into 10 pages (`hitsPerPage=10`), retrieve these subsets as follows (`offset` is zero-based):
* Records 50 to 80 (`offset=49` and `length = 30`).
* Records 21 to 25 (`offset=20` and `length = 5`).
With InstantSearch, you can only use `page` and `hitsPerPage` to implement pagination.
# Query Suggestions
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js
Query Suggestions suggest popular searches to guide users and help them search faster.
Query Suggestions help users search by showing a list of popular searches as search suggestions as they type.
By selecting suggestions, users type less and avoid searches that don't return any results.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
Users often interact with Query Suggestions in an autocomplete menu.
When typing, a list of suggestions appears below the search box.
Choosing a suggestion initiates a search using that suggestion as the query.
Query Suggestions can also help in these cases:
* To help users **before** they type anything.
This is often combined with featured categories or featured search results.
* To help users continue if a search [didn't return any results](/doc/guides/building-search-ui/going-further/conditional-display/js#handle-searches-without-results).
## Explore Query Suggestions user interfaces
For examples of Query Suggestions in action, see:
* [Query Suggestions demo with InstantSearch.js](https://codesandbox.io/s/github/algolia/instantsearch/tree/master/examples/js/query-suggestions)
* [Autocomplete examples with Query Suggestions](/doc/ui-libraries/autocomplete/introduction/sandboxes)
For more information about building a Query Suggestions UI,
see:
* [Build a Query Suggestions UI with InstantSearch](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/query-suggestions/js)
## How Query Suggestions work
Algolia stores Query Suggestions as in an .
This index is rebuilt every day, or when you change its settings.
Each update runs through these steps:
1. **Find the most frequent searches** from the [search analytics](/doc/guides/search-analytics/overview) of your source indices and their replicas.
During the early stages of your implementation, when you haven't gathered enough analytics, you can also generate suggestions using external analytics or your data.
2. **Normalize and complete suggestions** by removing duplicated, similar, or prefixed suggestions, depending on your [settings](#remove-duplicate-or-unhelpful-suggestions). Suggestions must match the following criteria:
* Only contain alphanumeric characters
* Return a minimum number of search results
* Longer than a minimum number of letters
* Not be a banned expression.
Query Suggestions also deduplicate similar searches, keeping only the most frequently searched terms as suggestions.
3. **Rank the suggestions** by popularity (number of searches) in the last 30 days.
For all similar searches, the individual counts are summed.
Counts from external analytics are also added to the suggestion's popularity.
Each suggestion in the Query Suggestions index has at least these attributes:
* `objectID`. Unique record identifier, usually the suggestion itself
* `query`. The suggestion
* `popularity`. Number of searches in the last 30 days for the suggestion
If you add suggestions from facets, more attributes are added.
## Set up Query Suggestions
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index.
4. Open the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page and click **New Query Suggestions Index**.
5. In the **Source index** field, enter the name of the index with the search results and provide a name for the Query Suggestions index.
You can add more source indices later.
6. Click **Accept and Continue** to start building the Query Suggestions index.
### Augment Query Suggestions with external analytics
If you don't have enough searches in your search analytics yet, or you want to add specific suggestions,
you can upload external analytics from a JSON file.
External analytics give Query Suggestions another data source.
They don't replace your source index, which must contain your searchable records.
1. Prepare your suggestions in a JSON file.
Each suggestion must have `query` and `count` attributes.
Algolia adds the value of the `count` attribute to the value of the `popularity` attribute in the Query Suggestions index for that suggestion.
To increase the popularity of existing suggestions, add them to this JSON file with a high value for the `count` attribute.
For example:
```json JSON icon="braces" theme={"system"} theme={"system"}
[
{
"query": "shoes",
"count": 10031
},
{
"query": "dress shirt",
"count": 731
},
{
"query": "yellow jacket",
"count": 122
}
]
```
2. [Import](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api) this JSON file into Algolia as a new index.
This index is your external analytics index.
3. Create or open the Query Suggestions index you want to enrich.
If you create a Query Suggestions index, set **Source index** to the index that contains your searchable records, not the external analytics index you imported in step 2.
For example, if your searchable records are in `products`, use `products` as the source index.
4. In the **Data sources** section, click **Enable** next to **External Analytics**.
5. In the **External analytics index** field, enter the name of the index you imported in step 2.
For example, if you imported your JSON file into `external_query_analytics`, use `external_query_analytics` as the external analytics index.
### Augment Query Suggestions with facet data
If you don't have enough data from search analytics or external analytics,
Query Suggestions can create suggestions from the combination of facet values.
For example, for the facets `color` and `brand`, these Query Suggestions are created:
* red nike
* blue nike
* red adidas
* blue adidas
Suggestions created from facets are less relevant than those created by search analytics since they aren't based on popularity.
But they can still enrich the suggestions, especially for rarer queries.
That's why it's best to keep suggestions from facets, even after you collect sufficient data from analytics.
To augment your Query Suggestions with facet data:
1. Set appropriate [searchable attributes](/doc/guides/managing-results/must-do/searchable-attributes)
**and** [attributes for faceting](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard).
2. On the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page in the Algolia dashboard,
select your Query Suggestions index.
3. In the **Data sources** section, click **Enable** for **Queries by facets**.
4. Select two facet attributes and click **Add attributes**.
Repeat for any combination of two facets for which you want to generate suggestions.
To show categories from facets in your suggestions,
see [Query Suggestions with categories](#query-suggestions-with-categories).
## Optimize Query Suggestions
After the initial setup, you can optimize your experience with one of the following strategies.
### Improve relevance
A Query Suggestions index is a regular Algolia index,
so you can use the same features and settings to tune the relevance of your suggestions.
* Use **Rules** to adjust the suggestions based on conditions.
For example, you can [promote](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits) or
[hide](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-hide-hits) specific suggestions,
[change the user's query](/doc/guides/managing-results/rules/detecting-intent),
or [display a banner](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
* Use [**synonyms**](/doc/guides/managing-results/optimize-search-results/adding-synonyms) to suggest similar search terms.
This helps if users search for terms that aren't in your records (but those similar terms are).
* Adjust **index settings** such as [typo tolerance](/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance),
or [tokenization](/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/tokenization).
### Query Suggestions with categories
You can make your suggestions more targeted by adding categories:
1. Make sure you configure your category attribute as a [facet](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard).
2. On the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page, select your Query Suggestions index.
3. On the **Overview & Settings** tab, in the **Categories** section, click **Edit categories**.
4. In the **Categories attribute** field, choose a category to display.
In the **Show top** field, select how many top categories to display.
For example, if you chose `2`, the top two categories are added to every suggestion.
#### How to determine top categories
Query Suggestions determines the top categories with the following algorithm:
1. Perform a "strict" search with the suggestion in the source index—without features such as prefixing or typo-tolerance.
2. Add the most frequently found category values to the [`facets.exact_matches`](#query-suggestions-index-schema) attribute.
3. Get the top filters (most often applied) for this search term from analytics and add them to the [`facets.analytics`](#query-suggestions-index-schema) attribute.
The category information is added to each record in your Query Suggestions index.
For your user interface, it's better to rely on the `facets.exact_matches` attribute
because it represents data from your index.
For example, if users often by a specific category,
but you don't have many items in that category,
they won't see many results when accepting this suggestion.
#### Display categories in your UI
Once you have categorized your Query Suggestions, decide how to display them in your frontend.
One option is to redirect users to an InstantSearch page on category selection.
This page uses the chosen category as a filter with the user's initial query pre-filled in the search box.
For an example of this,
see [Build a Query Suggestions UI with InstantSearch](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/vue).
### Filter suggestions on category pages
To show only suggestions that have matching source-index hits in the current category page, configure category page filtering:
1. Configure the source attribute as a [facet](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard). The attribute can't use the `filterOnly` modifier because Query Suggestions must retrieve its values when building suggestions.
2. On the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page, select your Query Suggestions index.
1) On the **Overview & Settings** tab, select the source index.
1. In the **Category page filtering** section, select the source facet.
After the Query Suggestions index rebuilds, Algolia adds the matching facet values to each suggestion record and configures the required Query Suggestions index setting. For example, if you select `category` on the `products` source index, a suggestion record can include:
```json JSON icon="braces" theme={"system"} theme={"system"}
{
"products": {
"browsing_facet_values": ["Accessories", "Winter"]
}
}
```
Algolia also adds `filterOnly(products.browsing_facet_values)` to the Query Suggestions index's `attributesForFaceting` setting. You can then filter suggestions with `products.browsing_facet_values:`.
If your Query Suggestions index has multiple source indices, configure a source facet for each applicable source index. Algolia creates a separate `browsing_facet_values` attribute for each source index.
For an example of filtering Query Suggestions in your UI, see [Filter Query Suggestions with InstantSearch](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/query-suggestions/js#filter-suggestions-on-category-pages).
### Remove duplicate or unhelpful suggestions
On the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page,
select your Query Suggestions index,
and adjust the following settings to improve the quality of the suggestions:
1. In the **Languages** field, add the language of your index.
This lets Query Suggestions automatically remove duplicate suggestions such as singulars and plurals.
2. Adjust the **Minimum letters** setting to prevent short, incomplete suggestions.
A suggestion must have at least this many letters to be included.
3. Adjust the **Minimum hits** setting to ensure enough search results are returned.
A suggestion must return at least this many search results to be included.
4. On the **Banned expressions** tab, add expressions or patterns for excluding suggestions.
### Personalize Query Suggestions
To add [personalized](/doc/guides/personalization/classic-personalization/what-is-personalization) Query Suggestions,
follow these steps:
1. On the [**Query Suggestions**](https://dashboard.algolia.com/query-suggestions) page, select your Query Suggestions index.
2. On the **Overview & Settings** tab, in the **Settings** section, enable **Personalized Query Suggestions**.
Algolia then retrieves the top 20 facet values (based on your [personalization strategy](/doc/guides/personalization/classic-personalization/personalizing-results#configure-your-personalization-strategy)) from your source index
and adds new attributes to the suggestions in your Query Suggestions index for each facet defined in your personalization strategy.
### Generate different suggestions for different environments
You can use [analytics tags](/doc/guides/search-analytics/guides/segments) to create different suggestions for different environments,
such as mobile or desktop users, or for different geographical regions.
1. [Create a new Query Suggestions index](#set-up-query-suggestions) with a recognizable name.
For example, include the analytics tag in the name, such as `products_query_suggestions_mobile`.
2. After creating the index, on the **Overview & Settings** tab, in the **Data sources** section,
click **Manage** next to **Algolia Analytics API**.
3. Enter the appropriate analytics tags. For example, `products_query_suggestions_mobile`.
This only includes suggestions with specific analytics tags.
In your UI, you can target the different Query Suggestions indices depending on the user's device or geographical region.
## Troubleshoot missing Query Suggestions
If suggestions are missing, the Query Suggestions API can help identify the issue.
For example, run the following command to retrieve the most recent log entries.
```sh icon="square-terminal" theme={"system"} theme={"system"}
curl -H "x-algolia-api-key: $ALGOLIA_ADMIN_API_KEY" \
-H "x-algolia-application-id: $ALGOLIA_APPLICATION_ID" \
"https://query-suggestions.us.algolia.com/1/logs/$ALGOLIA_QUERY_SUGGESTIONS_INDEX"
```
The log entries provide the following details:
* Source indices
* Why suggestions were skipped
* The number of [popular searches](#how-query-suggestions-work)
* The number of suggestion candidates from [facets](#augment-query-suggestions-with-facet-data)
* The [normalization](#how-query-suggestions-work) process
For more information, see [Get a log file](/doc/rest-api/query-suggestions/get-log-file).
## Query Suggestions index schema
The following example shows a record from a Query Suggestions index with categories from facets.
```jsonc JSON icon="braces" theme={"system"} theme={"system"}
{
"query": "phone charger", // The suggestion
"objectID": "phone charger", // Unique identifier
"popularity": 2457, // Number of searches for this suggestion in the last 30 days
"nb_words": 2, // Number of words in the suggestion
"products": {
// Name of the source index
"exact_nb_hits": 42, // Number of search results for this suggestion
"browsing_facet_values": ["Accessories", "Winter"], // Matching values of the configured category page facet
"facets": {
"exact_matches": {
// Most frequent facet values found for this suggestion
"brand": [
{ "value": "Anker", "count": 12 }, // This means 12 of the search results have "Anker" as "brand"
{ "value": "Yootech", "count": 11 },
{ "value": "TOZO", "count": 9 },
{ "value": "Aukey", "count": 4 },
{ "value": "Apple", "count": 2 }
]
},
"analytics": {
// Most frequently applied filters for this suggestion
"brand": [
{
"attribute": "brand",
"count": 347,
"operator": ":",
"value": "Anker"
},
{
"attribute": "brand",
"count": 203,
"operator": ":",
"value": "Apple"
},
{
"attribute": "brand",
"count": 152,
"operator": ":",
"value": "TOZO"
},
{
"attribute": "brand",
"count": 128,
"operator": ":",
"value": "Yootech"
},
{
"attribute": "brand",
"count": 80,
"operator": ":",
"value": "Aukey"
}
]
}
}
}
}
```
## See also
For more information, see these blog articles:
* [How Query Suggestions increase conversions](https://www.algolia.com/blog/ux/autocomplete-how-search-suggestions-increase-conversions/)
* [Build a Query Suggestion UX like Google's](https://www.algolia.com/blog/algolia/query-suggestion-ux-like-googles/)
* [Three best practices for search autocomplete on mobile](https://www.algolia.com/blog/ecommerce/search-autocomplete-on-mobile/)
# Recommendations in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/recommend/js
Show recommended items from Algolia Recommend in your InstantSearch.js app.
InstantSearch.js provides widgets for displaying recommendations on your site with [Algolia Recommend](/doc/guides/algolia-recommend/overview).
**Recommendations encourage users to discover more of your catalog based on what they're already interested in.**
* [Frequently bought together](#frequently-bought-together)
* [Related items](#related-items)
* [Trending items](#trending-items)
* [Looking similar](#looking-similar)
**Recommendations are an ideal complement to search experiences.**
If users land on an item that isn't exactly what they wanted, they can explore similar items.
Conversely, when they find what they want, they can discover items that would complement their current selection.
### Frequently bought together
Frequently bought together recommends items that users often buy together (based on [conversion events](/doc/guides/sending-events)).
The component lets you pass a reference item.
It then displays a list of recommended items, based on the [Frequently bought together](/doc/guides/algolia-recommend/overview#frequently-bought-together) model from Algolia Recommend.
See the [`frequentlyBoughtTogether`](/doc/api-reference/widgets/frequently-bought-together/js) widget.
### Related items
These models recommend items that are related to each other (based on [click and conversion events](/doc/guides/sending-events)).
The component lets you pass a reference item.
It then displays a list of recommended items, based on the [Related items](/doc/guides/algolia-recommend/overview#related-items) model from Algolia Recommend.
See the [`relatedProduct`](/doc/api-reference/widgets/related-products/js) widget.
### Trending items
The **Trending items** model looks for popular items in your product catalog.
Trends can be global, for example, recommend the most popular items in your entire product catalog.
Or it can also be within a specific or category, for example, recommend the most popular winter sweaters.
The [`trendingItems`](/doc/api-reference/widgets/trending-items/js) widget can fetch global trends or trends within a facet.
It displays a list of trending items, based on the [Trending items](/doc/guides/algolia-recommend/overview#trending-items-and-trending-facets-value) model from Algolia Recommend.
### Looking similar
This model recommends items that are visually related to each other,
based on the images linked in your .
The component lets you pass a source item.
It then displays a list of recommended items, based on the [Looking similar](/doc/guides/algolia-recommend/overview#looking-similar) model from Algolia Recommend.
See the [`lookingSimilar`](/doc/api-reference/widgets/looking-similar/js) widget.
# Redirects in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/redirects/js
Learn how to add redirects on specific searches with InstantSearch.js.
Redirects can enhance the search experience. For example:
* Searching for "help" redirects users to a support page
* Searching for a category, such as, "TV" redirects users to a category page
* Searching for a specific product or brand redirects users to a seasonal promotional page for it
* Searching for specific keywords related to an ongoing event, such as, "masks", redirects users to a more specific landing page
This guide explains how to add redirects to InstantSearch through [Rules](/doc/guides/managing-results/rules/detecting-intent) and using custom data.
Redirects are handled by Autocomplete's [redirect plugin](/doc/ui-libraries/autocomplete/api-reference/autocomplete-plugin-redirect-url).
However, if you don't use [Autocomplete](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete), you must handle redirects in InstantSearch:
* If a redirect was created in the Manual Editor, [handle it with InstantSearch's `queryRuleCustomData` widget](#handle-a-manual-editor-rule-redirect).
* If a redirect was created in the Visual Editor, you must [create a custom InstantSearch widget to handle redirects](#handle-a-visual-editor-rule-redirect).
## Handle a Visual Editor rule redirect
### Configure the redirect with a custom widget
If a redirect was created in the Visual Editor, the API response contains the `renderingContent.redirect` property which, in turn, contains the redirect `URL`.
To access this property from InstantSearch, you must create a custom widget.
Custom widgets have [lifecycle hooks](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js#set-up-the-lifecycle) for each of the widget stages: `init`, `render`, and `dispose`.
The `render` stage runs when Algolia sends results: in other words, when a user types something.
Use it as follows:
```js JavaScript theme={"system"}
search.addWidgets([
{
render({ results }) {
if (
results &&
results.renderingContent &&
results.renderingContent.redirect
) {
window.location.href = results.renderingContent.redirect.url;
}
},
}
]);
```
## Handle a Manual Editor rule redirect
If a redirect was created in the Manual Editor, handle it with InstantSearch's [`queryRuleCustomData`](/doc/api-reference/widgets/query-rule-custom-data/js) widget.
### Configure a rule
To set up a rule that returns custom data whenever the matches specific keywords,
see [Create a rule for returning custom data](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects#create-a-rule-for-returning-custom-data).
### Configure the `queryRuleCustomData` widget
After setting up a rule that returns the custom redirect data, you can use the [`queryRuleCustomData`](/doc/api-reference/widgets/query-rule-custom-data/js) widget to update the page location when `userData` contains a redirect.
```js JavaScript theme={"system"}
search.addWidgets([
instantsearch.widgets.queryRuleCustomData({
container: '#queryRuleCustomData',
templates: {
default: '',
},
transformItems(items) {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
},
})
]);
```
### Offer a suggested redirect rather than a hard redirect
The preceding is an example of a "hard redirect", which forces users to a specific URL.
This works in most situations, but sometimes you might want to suggest a redirect URL rather than force it onto them.
To do this, create the rule similarly but distinguish between "hard" and "suggested" redirects by ensuring the returned data is a JSON object. The object's properties are:
* **name** (used to display the link)
* **URL**
* **force** (`true` for a hard redirect, `false` for a suggested redirect).
```json JSON icon=braces theme={"system"}
{
"redirect": {
"name": "help",
"url": "https://www.algolia.com/support",
"force": true
}
}
```
With this change applied, the [`queryRuleCustomData`](/doc/api-reference/widgets/query-rule-custom-data/js) widget that had been previously defined needs updating to decide what happens to a user based upon the returned data.
To do this, the redirect check (within `transformItems`) needs updating to decide whether to force the redirect onto users or not.
The default template also needs setting to display a user message with the specific redirect URL.
```diff JavaScript theme={"system"}
search.addWidgets([
instantsearch.widgets.queryRuleCustomData({
container: '#queryRuleCustomData',
templates: {
- default: '',
+ default({ items }, { html }) {
+ const customData = items.find(data => Boolean(data.redirect));
+
+ if (!customData) {
+ return null;
+ }
+
+ return html`
+
+ `;
+ },
},
transformItems(items) {
const match = items.find(data => Boolean(data.redirect));
- if (match && match.redirect) {
+ if (match && match.redirect && match.redirect.force) {
window.location.href = match.redirect;
+ return [];
}
- return [];
+ return items;
},
})
]);
```
# Structured results in InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/structured-results/js
Discover structured results, a feature to showcase compelling, targeted content in InstantSearch.js.
One of the most interesting additions to search result pages are what's commonly called "structured results."
* Google calls them
[Knowledge Graph Cards](https://blog.google/products/search/about-knowledge-graph-and-knowledge-panels/),
* Bing calls them
[Visually Rich Snippets](https://www.bing.com/webmasters/help/marking-up-your-site-with-structured-data-3a93e731),
* [DuckDuckGo](https://duckduckgo.com/?q=new+york+red+bulls\&t=h_\&ia=web)
and [Baidu](http://www.baidu.com/s?ie=utf-8\&f=8\&rsv_bp=0\&rsv_idx=1\&tn=baidu\&wd=shanghai\&rsv_pq=c60de6e300062bc4\&rsv_t=5f92QVPi1SZAv5LTbiv8%2BknY2koOuywEhKyxU76njHxjQdFem7QhV7roOx0\&rqlang=cn\&rsv_enter=1\&rsv_sug3=3\&rsv_sug2=0\&inputT=743\&rsv_sug4=743) all have their own flavors as well.
Traditional search, which consists of displaying long lists of search results once users press `Enter` on their keyboard,
usually falls short of piquing their curiosity and triggering engagement.
This is when new ways of searching emerge.
Instead, a successful search should attempt to communicate clearly and cleanly with a user
on multiple levels when applicable, where different types of results help guide users to the information they want.
Sometimes, it's desirable to showcase a particular piece of relevant content against the rest of a user's
standard query search results. This is why many believe in the value of leveraging "structured results".
How about your search?
### Uses for structured results
You'll want to use structured results whenever there is a singular piece of information you want to
stand out from the rest for a given query.
Here are a few examples:
* Movie titles on an entertainment website
* User details and avatar on a customer relationship management (CRM) system
* Sponsored brands on an ecommerce search ([view code on GitHub](https://github.com/algolia/demo-structured-result)).
You can see a common thread among these examples - a need to search two different types of structured data -
and Algolia happens to handle these cases out of the box.
## An example
First, take a look at the ecommerce example mentioned in the preceding section.
You can view the code and follow along [on GitHub](https://github.com/algolia/demo-structured-result).
Imagine a scenario where certain brands within your ecommerce site have requested that their brand will be
displayed if it happens to match a user's query. In your search, if you determine that someone is searching
for a particular brand name, you can embed a section displaying the brand and its name within the main search interface.
### How will the data look?
One will be the main index to host traditional results - in your case, tech products -
regardless of a match on the brand.
Another index will be used to store your brands as , like this:
```json JSON icon="braces" theme={"system"}
{
"name": "Bosch",
"url": "https://logo.clearbit.com/bosch.com",
"objectID": "3414165441"
}
```
### Tweaking the relevance
Now that you added a new index for structured results to use in conjunction with your main index, outline what you'd like to emphasize in your structured results:
* Only display a single result that matches a user's search
* Continue to handle typos, but be more stringent
* Ensure that you have all words matching in order
(so "The Bad News" and "The Bad News Bears" would match but "Bears Bad News" would not)
Next, determine the corresponding settings that would be configured in Algolia.
In this case, you only need to care about results with a minimal number of typos, especially if you only render one result.
Set typo tolerance to `min` in the [**Typo-Tolerance** section](https://dashboard.algolia.com/explorer/configuration/typo-tolerance/) of the Algolia dashboard, or with the API:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { TypoTolerance = new TypoTolerance(Enum.Parse("Min")) }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
typoTolerance: TypoToleranceEnum.fromJson("min"),
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetTypoTolerance(search.TypoToleranceEnumAsTypoTolerance(search.TypoToleranceEnum("min")))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setTypoTolerance(TypoToleranceEnum.MIN));
```
```js JavaScript theme={"system"}
const response = await client.setSettings({ indexName: 'theIndexName', indexSettings: { typoTolerance: 'min' } });
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(typoTolerance = TypoToleranceEnum.entries.first { it.value == "min" }),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['typoTolerance' => 'min',
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"typoTolerance": "min",
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(typo_tolerance: "min"))
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
typoTolerance = Some(TypoToleranceEnum.withName("min"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(typoTolerance: SearchTypoTolerance
.searchTypoToleranceEnum(SearchTypoToleranceEnum.min))
)
```
Next, decrease your typo tolerance by increasing the minimum characters from 4 to 5 for a single typo.
Change this with the **Min chars to accept 1 typo** setting in the same section, or with the API:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { MinWordSizefor1Typo = 5 }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
minWordSizefor1Typo: 5,
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetMinWordSizefor1Typo(5)))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings("INDEX_NAME", new IndexSettings().setMinWordSizefor1Typo(5));
```
```js JavaScript theme={"system"}
const response = await client.setSettings({ indexName: 'theIndexName', indexSettings: { minWordSizefor1Typo: 5 } });
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(minWordSizefor1Typo = 5),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['minWordSizefor1Typo' => 5,
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"minWordSizefor1Typo": 5,
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings("INDEX_NAME", Algolia::Search::IndexSettings.new(min_word_sizefor1_typo: 5))
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
minWordSizefor1Typo = Some(5)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(minWordSizefor1Typo: 5)
)
```
These configuration changes make the typo tolerance more stringent.
The next section addresses the last point: ensuring that all words match in order.
### Display the data
Federated search lets you perform a single query against several datasets.
You can use it with your structured brand result:
```js JavaScript theme={"system"}
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(
appID,
apiKey
);
const search = instantsearch({
searchClient,
indexName: 'mainIndex'
});
search.addWidgets([
// Add your widgets here
index({ indexName: 'structuredIndex' }).addWidgets([
configure({
page: 0,
hitsPerPage: 1,
getRankingInfo: true
})
// We'll add the structured results widget here
])
]);
search.start();
```
In this snippet, you're taking advantage of the [`index`](/doc/api-reference/widgets/index-widget/js) widget to nest multiple indices that share the same [`uiState`](/doc/api-reference/widgets/ui-state/js) (or "search state").
When a user searches in the app, it searches in both the main and the structured indices.
You can use the [`configure`](/doc/api-reference/widgets/configure/js) widget to:
* Stay on the first page (`0` since [`page`](/doc/api-reference/api-parameters/page) is zero-based) because the main index can be paginated, and the structured index would then "inherit" from its pagination state.
* Set [`hitsPerPage`](/doc/api-reference/api-parameters/hitsPerPage) to `1` because you only show the first result.
* Set [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) to `true` so that Algolia returns metadata about the hit's ranking.
When rendering results, take the first result with the [`connectHits`](/doc/api-reference/widgets/hits/js#customize-the-ui-with-connecthits) connector and do a conditional check:
```js JavaScript theme={"system"}
const structuredResults = connectHits(({ items, widgetParams }) => {
const result = items[0];
const { container } = widgetParams;
if (
result &&
result._rankingInfo.words - 1 === result._rankingInfo.proximityDistance
) {
// Render the result
container.innerHTML = `
${result.name}
`;
return;
}
// Render nothing
container.innerHTML = '';
});
search.addWidgets([
// Add your widgets here
index({ indexName: 'structuredIndex' }).addWidgets([
configure({
page: 0,
hitsPerPage: 1,
getRankingInfo: true
}),
structuredResults({
container: document.querySelector('#structured-results')
})
])
]);
// ...
```
By comparing the number of exact word matches to 1 + the total distance between all the words in the match, you can make sure that all matched words are sorted. In other terms, when all matching words against a user's query are sequential and adjacent, the proximity (or distance between search terms) is always equal to `exact word matches - 1`
There's no requirement as to when to display your content, this part is up to you.
Another constraint you may impose on the rendering of the structured result could be to check that the count of exact word matches is equal to the number of words in the value of the attribute being queried.
For example, you may only want to show a brand when all words of that brand have been typed.
Check the [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) documentation to learn about other useful metrics you can access in the results.
## Another example, a different use case
Consider an entertainment website that displays structured results on movies.
If you can determine that someone is searching for a specific movie, you will embed a section displaying the movie
details within the main search interface.
That is, you'd like to communicate to users that there might be a salient piece of content worth showcasing.
### How will the data look?
Consider how to structure your data.
First, you need a main index, as with the ecommerce example.
This is where people will always see traditional results even if there's no structured data match.
Think about a major search engine when you search a new movie: you might get structured
results with the cast, showtimes, and video clips at the same time.
In Algolia, that
would require three extra indices—one for each result type.
This example is a bit simpler, as it only has a single data type to show structured results: movies.
Yet, you might also want to match actors, directors, and other entities.
Here's how a sample movie record could look like:
```json JSON icon=braces theme={"system"}
{
"title": "The Shawshank Redemption",
"year": 1994,
"image": "https://image.tmdb.org/t/p/w154/9O7gLzmreU0nGkIB6K3BsJbzvNv.jpg",
"color": "#8C634B",
"score": 9.97764206054169,
"rating": 5,
"actors": ["Tim Robbins", "Morgan Freeman", "Bob Gunton"],
"objectID": "439817390"
}
```
Records with this structure would be stored in another index.
Keep in mind though that if your new index doesn't contain any new data, it could be a [replica](/doc/api-reference/api-parameters/replicas) of your primary index.
(for example, searchable attributes or custom ranking order).
You can display the data and render the result exactly the same way as in the .
## Conclusion
The movie and ecommerce examples you went over are just two scenarios among many that may call for structured data,
all following a similar pattern of giving users the ability to search through multiple indices
and showcase results accordingly.
Search paradigms continue to evolve, and the rise of "structured results" is part of an ongoing itch to enrich a user's
process of discovery while browsing content.
Algolia enables users the flexibility to roll out their own flavor of
"structured results" by making simple the ability to search against multiple types of content
and customizing how the search is performed.
# Upgrade InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/upgrade-guides/js
Upgrade your InstantSearch.js version
## Upgrade templates
Starting from InstantSearch.js v4.46.0, **string-based and Hogan.js templates are deprecated.**
If you're using Hogan.js templates or HTML strings using string-based templates,
you can replace them either with safe HTML strings using the provided `html` tagged template, or JSX templates.
These new templating options are safer against cross-site scripting (XSS) attacks,
perform better, and are more accessible,
because they reuse and patch the existing DOM instead of replacing it entirely at each render.
For more information, see:
* [Using HTML strings](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#using-html-strings)
* [Using JSX](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#using-jsx)
### Interpolation
You can interpolate dynamic values using [template literals placeholders](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#string_interpolation).
All available dynamic values are exposed within the scope of the template function.
```js Tagged templates theme={"system"}
toggleRefinement({
// ...
templates: {
labelText({ count }, { html }) {
return html`Free shipping (${count})`;
},
},
});
```
```js Hogan.js theme={"system"}
// Don't do this, use `html` instead
toggleRefinement({
// …
templates: {
labelText: 'Free shipping ({{ count }})',
},
});
```
```js Classic HTML strings theme={"system"}
// Don't do this, use `html` instead
toggleRefinement({
// ...
templates: {
labelText({ count }) {
return `Free shipping (${count})`;
},
},
});
```
#### Highlighting and snippeting
In both [`hits`](/doc/api-reference/widgets/hits/js) and [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js) widgets,
templates expose a set of built-in components to handle highlighting and snippeting.
```js Tagged templates theme={"system"}
hits({
// ...
templates: {
item(hit, { html, components }) {
return html`
`,
},
});
```
```js Classic HTML strings theme={"system"}
// Don't do this, use `html` instead
hits({
// ...
templates: {
item(hit) {
return `
${instantsearch.highlight({ attribute: 'name', hit })}
${instantsearch.snippet({ attribute: 'description', hit })}
`;
},
},
});
```
### Loops
You can use plain JavaScript to build dynamic templates.
For example, you can use [`Array.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map)to loop over an array and display a list.
```js Tagged templates theme={"system"}
ratingMenu({
// …
templates: {
item({ url, stars }, { html }) {
return html`
${stars.map((_, index) => html``)}
`;
},
},
});
```
```js Hogan.js theme={"system"}
// Don't do this, use `html` instead
ratingMenu({
// …
templates: {
item: `
{{#stars}}
{{/stars}}
`,
},
});
```
```js Classic HTML strings theme={"system"}
// Don't do this, use `html` instead
ratingMenu({
// …
templates: {
item({ url, stars }) {
return `
${stars.map((_, index) => ``)}
`;
},
},
});
```
Passing a unique `key` attribute is helpful when mapping over items.
It helps the virtual DOM keep track of each element when they change, and update the UI efficiently.
### Conditional rendering
To conditionally render a part of your UI,
you can use a [short-circuit operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND)
or a [ternary](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator).
Since templates are functions, you can also use conditional `return` statements to provide alternative templates.
```js Tagged templates theme={"system"}
stats({
// …
templates: {
text({ nbHits }, { html }) {
if (nbHits === 0) {
return html`
{{/hasNoResults}}
`,
},
});
```
```js Classic HTML strings theme={"system"}
// Don't do this, use `html` instead
stats({
// …
templates: {
text({ nbHits }) {
if (nbHits === 0) {
return `
No results.
`;
}
return `
${nbHits} result${nbHits > 1 ? 's' : ''} found.
`;
},
},
});
```
## Upgrade event tracking
**Starting from v4.55.0, InstantSearch simplifies the event tracking process with the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option.**
You no longer need to install the [`search-insights`](/doc/libraries/search-insights) library or set up the [`insights`](/doc/api-reference/widgets/insights/js) middleware yourself.
Here are some benefits when using the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option:
* It better handles the . Once you set it, all search and event tracking calls include the token.
* It automatically sends default events from built-in widgets such as [`refinementList`](/doc/api-reference/widgets/refinement-list/js), [`menu`](/doc/api-reference/widgets/menu/js), etc.
You can also change the event payloads, or remove them altogether.
* It lets you send custom events from your custom widgets.
* It simplifies forwarding events to third-party trackers.
If you've been tracking events directly with [`search-insights`](/doc/libraries/search-insights) or with the [`insights`](/doc/api-reference/widgets/insights/js) middleware, follow these steps:
1. Upgrade InstantSearch.js to v4.55.0 or greater
2. [Migrate](#use-the-insights-option) from using the [`insights`](/doc/api-reference/widgets/insights/js) middleware to the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option
3. Either [update](#update-search-insights) or [remove](#remove-search-insights) the [`search-insights`](/doc/libraries/search-insights) library
### Use the Insights option
**Starting from v4.55.0, InstantSearch lets you enable event tracking with the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option.** You no longer need to set up the [`insights`](/doc/api-reference/widgets/insights/js) middleware yourself.
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
// …
insights: true,
});
```
If you had already set up the [`insights`](/doc/api-reference/widgets/insights/js) middleware in your code, you can now remove it and move its configuration to the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option.
```diff JavaScript icon=code theme={"system"}
- import { createInsightsMiddleware } from 'instantsearch.js/es/middlewares';
const search = instantsearch({
insights: {
+ insightsInitParams: {
+ useCookie: false,
+ },
},
});
- search.use(
- createInsightsMiddleware({
- insightsInitParams: {
- useCookie: false,
- },
- })
- );
```
### Update search-insights
**Starting from v4.55.0, InstantSearch can load [`search-insights`](/doc/libraries/search-insights) for you so the [`insightsClient`](/doc/api-reference/widgets/insights/js#param-insights-client) option is no longer required.**
If you prefer loading it yourself, make sure to update [`search-insights`](/doc/libraries/search-insights) to at least v2.4.0 and forward the reference to [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights).
If you're using the UMD bundle with a `script` tag, make sure to update the full code snippet (not just the version):
If you're using a package manager, you can upgrade it to the latest version.
```sh Command line icon=square-terminal theme={"system"}
npm install search-insights
```
Now you can pass the reference to the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights) option.
```js UMD theme={"system"}
const search = instantsearch({
insights: {
insightsClient: window.aa,
},
});
```
```js Module theme={"system"}
import aa from 'search-insights';
const search = instantsearch({
insights: {
insightsClient: aa,
},
});
```
Otherwise, you can [remove it and let InstantSearch handle it for you](#remove-search-insights).
### Remove search-insights
**Starting from v4.55.0, InstantSearch loads [`search-insights`](/doc/libraries/search-insights) for you from jsDelivr if not detected in the page.** If you've installed [`search-insights`](/doc/libraries/search-insights), you can now remove it.
If you're using the UMD bundle with a `script` tag, you can remove the snippet in any page that uses InstantSearch:
```diff HTML icon=code-xml theme={"system"}
-
```
If you're using a package manager, you can remove it from your dependencies.
```sh Command line icon=square-terminal theme={"system"}
npm uninstall search-insights
```
Then you can remove the reference to [`search-insights`](/doc/libraries/search-insights) from your code:
```diff UMD theme={"system"}
const search = instantsearch({
insights: {
- insightsClient: window.aa,
},
});
```
```diff Module theme={"system"}
- const aa = require('search-insights');
// or
- import aa from 'search-insights';
const search = instantsearch({
insights: {
- insightsClient: aa,
},
});
```
InstantSearch loads [`search-insights`](/doc/libraries/search-insights) from the jsDelivr CDN, which requires that your site or app allows script execution from foreign resources.
Check the [security best practices](/doc/guides/security/security-best-practices) for recommendations.
### No longer needed configuration
#### `clickAnalytics: true`
```js JavaScript icon=code theme={"system"}
instantsearch.widgets.configure({
clickAnalytics: true,
}),
```
This isn't needed anymore because it's automatically added by the middleware.
#### `getInsightsAnonymousUserToken`
```js JavaScript icon=code theme={"system"}
instantsearch.widgets.configure({
userToken: instantsearch.getInsightsAnonymousUserToken()
});
```
You no longer need this.
Once you integrate the `insights` middleware, it initially sets the anonymous `userToken`.
As soon as you set another `userToken`, it automatically syncs it between search and analytics calls.
#### `insightsClient` at InstantSearch
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
searchClient,
indexName: 'INDEX_NAME',
insightsClient: window.aa,
});
```
Now that you are using the `Insights` middleware, you don't need to pass `aa` to `instantsearch()` anymore.
### `hits` and `infiniteHits` template
You might have templates in your [`hits`](/doc/api-reference/widgets/hits/js) or [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js) widgets that look like the following:
```html HTML icon=code-xml theme={"system"}
```
These are now deprecated.
Instead, you can use the `sendEvent` function.
```js JavaScript icon=code theme={"system"}
instantsearch.widgets.hits({
container: '#hits',
templates: {
item(hit, { html, sendEvent, components }) {
return html`
${components.Highlight({ attribute: 'name', hit })}
`;
}
}
})
```
### `analytics` widget
The [`analytics`](/doc/api-reference/widgets/analytics/js) widget has been deprecated in favor of the [`insights`](/doc/api-reference/widgets/insights/js) middleware.
When the built-in widgets trigger [default events](/doc/guides/building-search-ui/events/js#default-events), you can intercept them using the [`insights`](/doc/api-reference/widgets/insights/js) middleware's [`onEvent`](/doc/api-reference/widgets/insights/js#param-on-event) hook and send them to third-party trackers.
```js JavaScript icon=code theme={"system"}
import { createInsightsMiddleware } from 'instantsearch.js/es/middlewares';
// Or, use `instantsearch.middlewares.createInsightsMiddleware()`
const indexName = '';
const search = instantsearch({
indexName,
// ...
});
const insightsMiddleware = createInsightsMiddleware({
insightsClient: window.aa,
onEvent: (event, aa) => {
const { insightsMethod, payload, widgetType, eventType } = event;
// Send the event to Algolia
aa(insightsMethod, payload);
// Send click events on Hits to a third-party tracker
if (widgetType === 'ais.hits' && eventType === 'click') {
thirdPartyTracker.send('Product Clicked', payload);
}
}
});
search.use(insightsMiddleware);
```
If you want to send events every time the or refinements change,
you can add a custom middleware to listen to [`uiState`](/doc/api-reference/widgets/ui-state/js) changes with the `onStateChange` function.
```js JavaScript icon=code theme={"system"}
const indexName = '';
const search = instantsearch({
indexName,
// ...
});
const analyticsMiddleware = () => {
return {
onStateChange({ uiState }) {
// Google Analytics (Universal Analytics)
window.ga('set', 'page', window.location.pathname + window.location.search);
window.ga('send', 'pageView');
// Google Analytics V4
gtag('event', 'page_view', {
page_location: window.location.pathname + window.location.search,
});
// Google Tag Manager (GTM)
// You can use `uiState` to make payloads for third-party trackers.
dataLayer.push({
'event': 'search',
'Search Query': uiState[indexName].query,
'Brand': uiState[indexName].refinementList['brand'].join(','),
});
},
subscribe() {},
unsubscribe() {},
}
}
search.use(analyticsMiddleware);
```
You can also use external libraries like `lodash.debounce` to debounce the events you send.
```js JavaScript icon=code theme={"system"}
import debounce from 'lodash.debounce';
const indexName = '';
const search = instantsearch({
indexName,
// ...
});
const sendEventDebounced = debounce((uiState) => {
// Google Analytics
window.ga('set', 'page', window.location.pathname + window.location.search);
window.ga('send', 'pageView');
// Google Analytics V4
gtag('event', 'page_view', {
page_location: window.location.pathname + window.location.search,
});
// Google Tag Manager (GTM)
dataLayer.push({
'event': 'search',
'Search Query': uiState[indexName].query,
'Brand': uiState[indexName].refinementList['brand'].join(','),
});
}, 3000);
const analyticsMiddleware = () => {
return {
onStateChange({ uiState }) {
sendEventDebounced(uiState);
},
subscribe() {},
unsubscribe() {},
}
}
search.use(analyticsMiddleware);
```
## Upgrade to Recommend
The [`relatedProducts`](/doc/api-reference/widgets/related-products/js) widget is now available instead of the previous experimental widget `EXPERIMENTAL_relatedItems`. To migrate, you change the widget name and update the props:
```diff JavaScript icon=code theme={"system"}
- import { EXPERIMENTAL_relatedItems } from 'instantsearch.js/es/widgets';
+ import { relatedProducts } from 'instantsearch.js/es/widgets';
search.addWidgets([
- EXPERIMENTAL_relatedItems({
- hit: { objectID: '123' },
- }),
- hits({ container: '#related-products' }),
+ relatedProducts({
+ container: '#related-products',
+ objectIDs: ['123'],
+ }),
]);
```
## Upgrade from v3 to v4
The latest version of InstantSearch focuses on federated search and bundle size.
This required some changes that might impact your app.
### Federated search (multi-index)
If you were already using federated search (for example by synchronizing two [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) instances with the [`searchFunction`](/doc/api-reference/widgets/instantsearch/js#param-search-function)),
the implementation is now simpler. Instead, try the new [`index`](/doc/api-reference/widgets/index-widget/js) widget,
to which you can attach more widgets.
For example:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({ indexName: 'primary', /* ... */ });
search.addWidgets([
searchBox(),
hits(),
index({ indexName: 'secondary' }).addWidgets([
searchBox(),
hits(),
])
]);
```
To learn more, see [Multi-index search with InstantSearch.js](/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/js)
### Routing
Even if you aren't using multi-index search, the way in which [`uiState`](/doc/api-reference/widgets/ui-state/js) is stored has changed.
It used to look like this:
```json JSON icon=braces theme={"system"}
{
"query": "value",
"page": 5
}
```
It now looks like this:
```json JSON icon=braces theme={"system"}
{
"indexName": {
"query": "value",
"page": 5
}
}
```
If you are using the default state mapping ([`simpleStateMapping`](/doc/api-reference/widgets/simple-state-mapping/js)) with the current version, you can replace it with [`singleIndexStateMapping('yourIndexName')`](/doc/api-reference/widgets/single-index-state-mapping/js). You have to change the code as followed:
```diff JavaScript icon=code theme={"system"}
instantsearch({
indexName: 'myIndex',
routing: {
- stateMapping: instantsearch.stateMappings.simple(),
+ stateMapping: instantsearch.stateMappings.singleIndex('myIndex'),
}
// ...
});
```
If you are using a custom state mapping, you have to loop over the outer level of the `index` widget and add this extra level to the `routeToState`.
You can check the [source](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.js/src/lib/stateMappings/singleIndex.ts) for a reference on how to implement this.
For example, a `stateMapping` that maps a subset of properties would change like this:
```js JavaScript icon=code theme={"system"}
// Before
const stateMapping = {
stateToRoute(uiState) {
return {
query: uiState.query,
page: uiState.page,
// ...
};
},
routeToState(routeState) {
return {
query: routeState.query,
page: routeState.page,
// ...
};
},
};
// After
const stateMapping = {
stateToRoute(uiState) {
const indexUiState = uiState[indexName];
return {
query: indexUiState.query,
page: indexUiState.page,
// ...
};
},
routeToState(routeState) {
return {
[indexName]: {
query: routeState.query,
page: routeState.page,
// ...
},
};
},
};
```
#### Configure
The [`configure`](/doc/api-reference/widgets/configure/js) widget is now included in [`uiState`](/doc/api-reference/widgets/ui-state/js).
If you want to exclude it from the URL you can use the default `stateMapping`s or exclude it in your custom state mapping.
A good reason to exclude the configure widget from the UI state is to prevent users from adding any search parameters.
You must exclude this widget in both the `stateToRoute`, to keep it from appearing in the URL and `routeToState`, so that the URL doesn't apply to the state.
Check the [`stateMapping` source code](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.js/src/lib/stateMappings/simple.ts) for implementation details.
### Algolia search helper
This release includes version 3 of the [`algoliasearch-helper` package](https://www.npmjs.com/package/algoliasearch-helper/).
**If you're using the built-in widgets or connectors, nothing changes for you.**
This version of `algoliasearch-helper` no longer includes [Lodash](https://lodash.com/), which reduces its bundle size (from 27.5 KB to 9.1 KB Gzipped).
If you're using any methods from the `helper`, `searchParameters` or `searchResults`, refer to the package's [change log](https://instantsearchjs.netlify.app/algoliasearch-helper-js/upgrade.html).
### Search parameters
The option `searchParameters` was removed from the [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) widget.
You can replace it with the [`configure`](/doc/api-reference/widgets/configure/js) widget:
```diff JavaScript icon=code theme={"system"}
const search = instantsearch({
// ...
- searchParameters: {
- hitsPerPage: 5,
- },
});
+ search.addWidgets([
+ instantsearch.widgets.configure({
+ hitsPerPage: 5,
+ }),
+ ]);
```
You can now add [`initialUiState`](/doc/api-reference/widgets/instantsearch/js#param-initial-ui-state) to your `InstantSearch` widget.
This overwrites specific `searchParameters` that are otherwise set during widget instantiation.
`initialUiState` is only taken into account if a widget owning that "state" is mounted.
A warning appears in development mode explaining which widget needs to be added for the UI state to have an effect.
An example is a [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget:
```diff JavaScript icon=code theme={"system"}
const search = instantsearch({
// ...
- searchParameters: {
- disjunctiveFacets: ['brand'],
- disjunctiveFacetsRefinements: {
- brand: ['Apple'],
- },
- },
+ initialUiState: {
+ refinementList: {
+ brand: ['Apple'],
+ },
+ },
});
search.addWidgets([
refinementList({ attribute: 'brand' }),
]);
// or when there should be no display for this widget:
const virtualRefinementList = connectRefinementList(() => null);
search.addWidgets([
virtualRefinementList({ attribute: 'brand' }),
]);
```
### `addWidget` and `removeWidget`
The `search.addWidget` function is deprecated in favor of `search.addWidgets`, which accepts an array of widgets.
Using arrays makes it clearer to see the structure of nested widgets.
`search.removeWidget` is deprecated in favor of `search.removeWidgets`, which also accepts an array of widgets.
### Custom widgets
Because version 3 of the `algoliasearch-helper` package is used,
you have to replace the `getConfiguration` lifecycle with `getWidgetSearchParameters` and `getWidgetState`.
This also means that your custom widget takes part in the routing.
You can exclude it from the URL with `stateMapping`.
```js JavaScript icon=code theme={"system"}
const widget = {
getConfiguration(searchParams) {
return {
disjunctiveFacets: ['myAttribute'],
};
},
getWidgetSearchParameters(searchParameters, { uiState }) {
return searchParameters
.addDisjunctiveFacetRefinement(
'myAttribute',
uiState.myWidgetName.myAttribute
);
},
getWidgetState(uiState, { searchParameters }) {
return {
...uiState,
myWidgetName: {
myAttribute: searchParameters.getDisjunctiveRefinements('myAttribute')
}
};
}
};
```
Becomes:
```js JavaScript icon=code theme={"system"}
const widget = {
getWidgetSearchParameters(searchParameters, { uiState }) {
return searchParameters
.addDisjunctiveFacet('myAttribute')
.addDisjunctiveFacetRefinement(
'myAttribute',
uiState.myWidgetName.myAttribute
);
},
getWidgetState(uiState, { searchParameters }) {
return {
...uiState,
myWidgetName: {
myAttribute: searchParameters.getDisjunctiveRefinements('myAttribute')
}
};
}
};
```
### `connectAutocomplete`
The `indices` option was removed in favor of `index` widgets (see the [federated search section](#federated-search-multi-index)).
For example, the following code:
```js JavaScript icon=code theme={"system"}
const autocomplete = connectAutocomplete(() => {/* ... */});
search.addWidget(
autocomplete({
indices: [{
name: 'additional'
}]
})
);
```
Should be replaced with this:
```js JavaScript icon=code theme={"system"}
const autocomplete = connectAutocomplete(() => {/* ... */});
search.addWidgets([
index({ indexName: 'additional' }),
autocomplete()
]);
```
### `onHistoryChange`
The `onHistoryChange` function is removed.
To subscribe to changes in the URL, create a custom `router` and attach to the `write` hook.
For example:
```js JavaScript icon=code theme={"system"}
const router = historyRouter()
const originalWrite = router.write.bind(router)
router.write = state => {
console.log('listen to route state here');
originalWrite(state)
}
```
### `router`
The `dispose` function on the `router` interface is now required and its signature is updated (see the example below).
This change only impacts you if you use a custom router.
```ts TypeScript icon=code theme={"system"}
interface Router {
// ...
// Before
dispose?({ helper, state }: { helper: AlgoliaSearchHelper, state: SearchParameters }): SearchParameters | void
// After
dispose(): void
}
```
### `pagination`
The `noRefinementRoot` CSS class gets added once there are no more possible refinements.
It no longer gets applied on the first page, which was the wrong behavior.
## Upgrade from v2 to v3
InstantSearch v3 introduces some breaking changes in the widgets' naming, options, and markup.
### What's new
#### Search from your own search client
Since InstantSearch.js 2.8.0,
it's possible to [search from your own backend](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/js) using the [`searchClient`](/doc/api-reference/widgets/instantsearch/js#param-search-client) option.
This option is now the way to plug InstantSearch to either Algolia's [official JavaScript client](https://github.com/algolia/algoliasearch-client-javascript) or to yours.
This means that the official search client isn't bundled in InstantSearch, allowing for more flexibility.
This results in a smaller bundle size when you're not searching directly from the frontend, or that you use multiple instances of the search client.
#### Predictable DOM structure
InstantSearch.js v3 becomes more predictable by generating a DOM that follows Algolia's InstantSearch.css specification.
All flavors are now aligned, which makes it easier to migrate from one library flavor to another.
The options and the DOM output are similar whether you use React InstantSearch, Vue InstantSearch, or InstantSearch.js.
#### Optimize InstantSearch based on your environment
The new bundle comes in two forms:
* **Development bundle.** This is a heavier bundle that helps developers better debug in the development phase of an InstantSearch app. It's more verbose and gives clues and warnings about the library usage.
* **Production bundle.** This is a lighter bundle that doesn't include any development warnings. Use this when deploying your app in a production environment.
### Imports
#### UMD (Universal Module Definition)
New naming has been introduced for the UMD imports.
This makes it clearer which InstantSearch.js bundle to use either in development or in production.
The production bundle will get lighter over time as it won't include the runtime warnings and documentation.
##### Before
```txt theme={"system"}
dist
├── instantsearch.js
├── instantsearch.js.map
├── instantsearch.min.js
└── instantsearch.min.js.map
```
##### After
```txt theme={"system"}
dist
├── instantsearch.development.js
├── instantsearch.development.js.map
├── instantsearch.production.min.js
└── instantsearch.production.min.js.map
```
#### CommonJS (CJS)
##### Before
```js JavaScript icon=code theme={"system"}
const instantsearch = require('instantsearch.js');
```
##### After
```js JavaScript icon=code theme={"system"}
const instantsearch = require('instantsearch.js').default;
```
#### ES (ECMAScript)
No changes.
### InstantSearch
The variables `appId` and `apiKey` are replaced by `searchClient`.
#### Earlier usage
1. [Import `InstantSearch.js`](/doc/guides/building-search-ui/installation/js)
2. Initialize InstantSearch
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: 'indexName',
appId: 'appId',
apiKey: 'apiKey',
});
search.start();
```
##### New usage
1. [Import `algoliasearch`](/doc/libraries/sdk/install) (prefer the [lite version](https://github.com/algolia/algoliasearch-client-javascript#search-onlylite-client) for search only)
2. [Import `InstantSearch.js`](/doc/guides/building-search-ui/installation/js)
3. Initialize InstantSearch with the `searchClient` option
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: 'indexName',
searchClient: algoliasearch('appId', 'apiKey'),
});
search.start();
```
#### `transformData` is replaced by `transformItems`
Since InstantSearch.js first public release, you can customize the values used in the widgets.
This method was letting you map 1-1 the values with other values.
With React InstantSearch,
a slightly different API was implemented that lets you map over the list of values and to change their content.
##### Earlier usage
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widget.refinementList({
container: '#facet',
attributeName: 'facet',
transformData: {
item: data => {
data.count = 0;
return data;
},
},
})]
);
```
##### New usage
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widget.refinementList({
container: '#facet',
attribute: 'facet',
transformItems: items =>
items.map(item => ({
...item,
count: 0,
})),
})
]);
```
#### Highlights and snippets
Highlighting is a powerful tool for showing users why results match their queries.
Earlier versions of InstantSearch used internal mechanisms or Hogan.js to implement highlighting within widget templates.
Algolia now provides `html` tagged templates.
For more information, see:
* [Highlights and snippets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#highlighting-and-snippeting)
* [`html` tagged templates](#upgrade-templates)
##### Earlier usage
```js string theme={"system"}
search.addWidgets([
instantsearch.widget.hits({
container: '#hits',
templates: {
item: '{{{ _highlightResult.name.value }}}',
},
})
]);
```
```js Hogan.js theme={"system"}
search.addWidgets([
instantsearch.widget.hits({
container: '#hits',
templates: {
item: '{{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}}',
},
})
]);
```
##### New usage
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widget.hits({
container: '#hits',
templates: {
item: (hit, { components }) => components.Highlight({ attribute: 'name', hit }),
},
})
]);
```
#### `urlSync` is dropped
If you were using the `urlSync` option,
migrate to the new [`routing`](/doc/api-reference/widgets/instantsearch/js#param-routing) feature.
Here are the elements you need to migrate:
* `urlSync: true` becomes `routing: true`
* `threshold` becomes `routing: { router: instantsearch.routers.history({ writeDelay: 400 }) }`
* `mapping` and `trackedParameters` are replaced with `stateMapping`. Read "[User friendly URLs](/doc/guides/building-search-ui/going-further/routing-urls/js#seo-friendly-urls)" to know how to configure it
* `useHash` is removed but can be achieved using an advanced configuration of the [history router](/doc/guides/building-search-ui/going-further/routing-urls/js)
* `getHistoryState` is removed but can be achieved using an advanced configuration of the [history router](/doc/guides/building-search-ui/going-further/routing-urls/js)
#### `collapsible` is dropped
`collapsible` is replaced by the [`collapsed`](/doc/api-reference/widgets/panel/js#param-collapsed) option in the [`panel`](/doc/api-reference/widgets/panel/js) widget.
#### `autoHideContainer` is dropped
`autoHideContainer` is replaced by the [`hidden`](/doc/api-reference/widgets/panel/js#param-hidden) option in the [`panel`](/doc/api-reference/widgets/panel/js) widget.
#### `createAlgoliaClient` is dropped
`createAlgoliaClient` is replaced by `searchClient`.
#### `createQueryString` is dropped
URL synchronization is done with Routing alone.
### Widgets
#### Breadcrumb
##### CSS classes
| Before | After |
| - | - |
| `ais-breadcrumb` | `ais-Breadcrumb` |
| | `ais-Breadcrumb--noRefinement` |
| `ais-breadcrumb` | `ais-Breadcrumb-list` |
| `ais-breadcrumb--separator` | `ais-Breadcrumb-separator` |
| `ais-breadcrumb--label` | `ais-Breadcrumb-link` |
| `ais-breadcrumb--disabledLabel` | |
| | `ais-Breadcrumb-item` |
| | `ais-Breadcrumb-item--selected` |
##### Markup
```html HTML icon=code-xml theme={"system"}
```
#### `RefinementList`
##### Options
| Before | After |
| - | - |
| `attributeName` | `attribute` |
| `searchForFacetValues` | `searchable` |
| `searchForFacetValues.placeholder` | `searchablePlaceholder` |
| `searchForFacetValues.isAlwaysActive` | `searchableIsAlwaysActive` |
| `searchForFacetValues.escapeFacetValues` | `searchableEscapeFacetValues` |
| `searchForFacetValues.templates.noResults` | `templates.searchableNoResults` |
| `showMore.templates.active` | `templates.showMoreText` |
| `showMore.templates.inactive` | `templates.showMoreText` |
* `searchablePlaceholder` defaults to `"Search..."`
* `searchableEscapeFacetValues` defaults to `true`
* `searchableIsAlwaysActive` defaults to `true`
* `showMore` is now a boolean option (`searchForFacetValues.templates` and `showMore.templates` are now in `templates`)
* An object containing `isShowingMore` is passed to `showMoreText` template to toggle between the two states:
```js JavaScript icon=code theme={"system"}
{
showMoreText: `
{{#isShowingMore}}
Show less
{{/isShowingMore}}
{{^isShowingMore}}
Show more
{{/isShowingMore}}
`
}
```
##### CSS classes
| Before | After |
| - | - |
| `ais-refinement-list` | `ais-RefinementList` |
| | `ais-RefinementList--noRefinement` |
| | `ais-RefinementList-noResults` |
| `ais-refinement-list--header` | |
| `ais-refinement-list--body` | |
| `ais-refinement-list--footer` | |
| `ais-refinement-list--list` | `ais-RefinementList-list` |
| `ais-refinement-list--item` | `ais-RefinementList-item` |
| `ais-refinement-list--item__active` | `ais-RefinementList-item--selected` |
| `ais-refinement-list--label` | `ais-RefinementList-label` |
| `ais-refinement-list--checkbox` | `ais-RefinementList-checkbox` |
| | `ais-RefinementList-labelText` |
| `ais-refinement-list--count` | `ais-RefinementList-count` |
| | `ais-RefinementList-showMore` |
| | `ais-RefinementList-showMore--disabled` |
##### Markup
###### Default
```html HTML icon=code-xml theme={"system"}
```
###### Show more disabled
```html HTML icon=code-xml theme={"system"}
```
###### With search and no results
```html HTML icon=code-xml theme={"system"}
No results.
```
#### `SearchBox`
##### Options
| Before | After |
| - | - |
| `poweredBy` | use the `poweredBy` widget |
| `wrapInput` | use the `connectSearchBox` connector |
| `searchOnEnterKeyPressOnly` | `searchAsYouType` (default: `true`) |
| `reset` | `showReset` |
| `magnifier` | `showSubmit` |
| `loadingIndicator` | `showLoadingIndicator` (default: `true`) |
With the drop of `wrapInput`, it was decided not to accept `input`s as containers anymore.
If you want complete control over the rendering, use the [`connectSearchBox`](/doc/api-reference/widgets/search-box/js#customize-the-ui-with-connectsearchbox) connector.
The search box doesn't support [`poweredBy`](/doc/api-reference/widgets/powered-by/js).
[Algolia requires](https://www.algolia.com/policies/free-services-terms/) that you use this widget if you're on a community plan (open source, not-for-profit, or [DocSearch](https://docsearch.algolia.com/)).
Configuration options for `reset`, `submit` and `loadingIndicator` have been moved to `templates` and `cssClasses`. For example, for `reset`:
* `reset.template` => `templates.reset`
* `reset.cssClasses.root` => `cssClasses.reset`
`autofocus` is now set to `false` by default and doesn't support the `"auto"` value anymore.
##### CSS classes
| Before | After |
| - | - |
| `ais-search-box` | `ais-SearchBox` |
| | `ais-SearchBox-form` |
| `ais-search-box--input` | `ais-SearchBox-input` |
| `ais-search-box--magnifier-wrapper` | |
| `ais-search-box--magnifier` | `ais-SearchBox-submit` |
| | `ais-SearchBox-submitIcon` |
| `ais-search-box--reset-wrapper` | |
| `ais-search-box--reset` | `ais-SearchBox-reset` |
| | `ais-SearchBox-resetIcon` |
| `ais-search-box--loading-indicator-wrapper` | |
| `ais-search-box--loading-indicator` | `ais-SearchBox-loadingIndicator` |
| | `ais-SearchBox-loadingIcon` |
##### Markup
```html HTML icon=code-xml theme={"system"}
```
#### `SortBy`
##### Options
| Before | After |
| - | - |
| `indices` | `items` |
* A `sortBy` item value is now `value` instead of `name`:
```js JavaScript icon=code theme={"system"}
const sortByItem = {
value: string,
label: string,
};
```
##### CSS classes
| Before | After |
| - | - |
| | `ais-SortBy` |
| `ais-sort-by-selector` | `ais-SortBy-select` |
| `ais-sort-by--item` | `ais-SortBy-option` |
##### Markup
```html HTML icon=code-xml theme={"system"}
```
#### `ToggleRefinement` (formerly `Toggle`)
##### Options
| Before | After |
| - | - |
| `attributeName` | `attribute` |
| `collapsible` | |
| `autoHideContainer` | |
| `label` | `templates.labelText` |
| `templates.item` | |
| `values.on` | `on` |
| `values.off` | `off` |
`collapsible` and `autoHideContainer` options have been removed.
These options are now implemented as part of the Panel widget wrapper.
The `label` options has been moved into the `templates.labelText` template to make it consistent with the templates parameters of other widgets and the `item` template was removed.
Data that was provided to `templates.item` is now provided to `templates.labelText`.
If your attribute is called `free_shipping`, the default template displays "free\_shipping".
To rename it, change `templates.labelText` to "Free shipping".
##### CSS classes
| Before | After |
| - | - |
| `ais-toggle` | `ais-ToggleRefinement` |
| `ais-toggle--list` | |
| `ais-toggle--item` | |
| | `ais-ToggleRefinement-label` |
| `ais-toggle--checkbox` | `ais-ToggleRefinement-checkbox` |
| `ais-toggle--label` | `ais-ToggleRefinement-labelText` |
##### Markup
```html HTML icon=code-xml theme={"system"}
```
### Connectors
#### `connectAutocomplete`
##### Options
| Before | After |
| - | - |
| `escapeHits` | `escapeHTML` |
* `escapeHTML` becomes `true` by default.
#### `connectBreadcrumb`
* The BreadcrumbItem `name` property is renamed to `label`.
#### `connectGeoSearch`
##### Options
| Before | After |
| - | - |
| `paddingBoundingBox` | Removed |
| `enableGeolocationWithIP` | Removed - use the Configure widget instead (see below) |
| `position` | Removed - use the Configure widget instead (see below) |
| `radius` | Removed - use the Configure widget instead (see below) |
| `precision` | Removed - use the Configure widget instead (see below) |
Since `paddingBoundingBox` conflicted with the `routing` option it was removed to support URLSync for the GeoSearch widget.
##### `enableGeolocationWithIP`
**Before:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
customGeoSearch({
enableGeolocationWithIP: true,
});
```
**After:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
instantsearch.widgets.configure({
aroundLatLngViaIP: true,
});
customGeoSearch();
```
##### `position`
**Before:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
customGeoSearch({
position: { lat: 40.71, lng: -74.01 },
});
```
**After:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
instantsearch.widgets.configure({
aroundLatLng: '40.71, -74.01',
});
customGeoSearch();
```
##### `radius`
**Before:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
customGeoSearch({
radius: 1000,
});
```
**After:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
instantsearch.widgets.configure({
aroundRadius: 1000,
});
customGeoSearch();
```
##### `precision`
**Before:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
customGeoSearch({
precision: 1000,
});
```
**After:**
```js JavaScript icon=code theme={"system"}
const customGeoSearch = instantsearch.connectors.connectGeoSearch(() => {
// Render implementation
});
instantsearch.widgets.configure({
aroundPrecision: 1000,
});
customGeoSearch();
```
#### `connectRange`
##### Options
| Before | After |
| - | - |
| `attributeName` | `attribute` |
#### `connectRangeSlider`
Connector removed (use `connectRange` instead).
#### `connectClearRefinements` (formerly `connectClearAll`)
##### Options
| Before | After |
| - | - |
| `excludeAttributes` | `excludedAttributes` |
#### `connectNumericMenu` (formerly `connectNumericRefinementList`)
##### Options
| Before | After |
| - | - |
| `attributeName` | `attribute` |
| `options` | `items` |
* `name` becomes `label`
#### `connectRefinementList`
##### Options
| Before | After |
| - | - |
| `attributeName` | `attribute` |
* `escapeFacetValues` defaults to `true`
#### `connectCurrentRefinements` (used to be `connectCurrentRefinedValues`)
##### Options
| Before | After |
| - | - |
| `clearsQuery` | `excludedAttributes: []` |
##### `items` (used to be refinements )
`refinements` used to be a flat list, now it's called `items` (one per attribute), which each has its own list of `refinements`
## Upgrade from v1 to v2
This guide has all the major changes that introduced in v2 with the description on how to
migrate from v1.
### The `searchBox` widget has new default options
To identify the input as a search box, a magnifying glass icon was placed at the start, and a reset button displayed as a cross at the end.
You can customize the result with these two options:
* **reset** `boolean|{template?: string|Function, cssClasses?: {root: string}}`
Display a reset button in the input when there is a query.
* **magnifier** `boolean|{template?: string|Function, cssClasses?: {root: string}}`
Display a magnifier should at beginning of the input.
To make the search box like in v1, you can do the following:
```js JavaScript icon=code theme={"system"}
const search = instantsearch(/* Your parameters here */);
search.addWidgets([
instantsearch.widgets.searchbox({
container: '#your-search-input',
reset: false,
magnifier: false,
})
]);
```
You can read more about these options on the [`searchBox`](/doc/api-reference/widgets/search-box/js) API reference.
### No more `hitsPerPage` in `hits` and `infiniteHits`
This option was removed from those two widgets. To configure
this option of the engine, there are still three ways:
* Use the [dashboard](https://dashboard.algolia.com/explorer/configuration/pagination) or
the [client](/doc/libraries/sdk/v1/methods/set-settings),
to change the setting at the index level.
* Use the [`hitsPerPage`](/doc/api-reference/widgets/hits-per-page/js) widget.
* Use the configuration option of `instantsearch`:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
// ... do not forget the credentials
searchParameters: {
hitsPerPage: 42,
}
});
```
### The items aren't sorted like before in the refinementList / menu
The default sort order of those widgets has changed.
This might have affected your implementation
if you didn't specify them originally. To change back the order use the `sortBy` configuration
key.
Here are examples of usage of `sortBy` using the previous sorting scheme:
```js JavaScript icon=code theme={"system"}
const yourSearch = instantsearch(/* parameters */);
yourSearch.addWidgets([
instantsearch.widgets.refinementList({
container: '#brands',
attributeName: 'brand',
// now the default is ['isRefined', 'count:desc', 'name:asc']
sortBy: ['count:desc', 'name:asc'],
}),
instantsearch.widgets.menu({
container: '#categories',
attributeName: 'categories',
// now the default is ['name:asc']
sortBy: ['count:desc', 'name:asc']
})
]);
```
If you want to learn more about sorting the values, check out the widget API to see what are
the valid values for the `sortBy` option of [`menu`](/doc/api-reference/widgets/menu/js) or
[`refinementList`](/doc/api-reference/widgets/refinement-list/js)
### Some variables have been changed
Internally all the widgets are now using the connectors.
The aim was to ensure the API
was as close to the one offered by
[`react-instantsearch connectors`](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#customize-the-complete-ui-of-the-widgets).
This then affected the name of some variables in the templates and the API.
Changes in templates:
* In the item template of the hierarchicalMenu and menu widgets, `name` becomes `label`
* In the item template of the refinementList widget, `name` becomes `value`.
Changes in the API:
* In `hitsPerPageSelector`, the `options` was renamed to `items`.
### React components can't be used as templates
When InstantSearch.js was created it was built using React and it wasn't known that react-instantsearch would be built.
[`react-instantsearch`](/doc/guides/building-search-ui/what-is-instantsearch/react) is now the recommended solution if your app uses React.
That's why support for the React based templates was dropped in this release.
Algolia considers the engine used to build the widgets in InstantSearch.js to be an implementation detail.
Since it isn't exposed anymore, Algolia can change it and use the best solution for each release.
### `rangeSlider` widget is using Rheostat as slider component
Slider components are hard to implement and that's why Algolia relies on an external
component for that.
Algolia is taking the opportunity of this new version
to switch to the current 'state of the art' of sliders: [Rheostat](https://github.com/airbnb/rheostat).
If you want to customize the style, some CSS classes have changed.
The look is still similar as the one in the V1.
### `searchFunction` can be used to change parameters
Introduced in 1.3.0, `searchFunction` was originally meant as a way to change
the timing of the search.
However it was realized that it was a good way to
alter the search state before making the actual state.
This is what's required to force the query string:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
/* other parameters */
searchFunction(helper) {
search.helper.setQuery('fixed query');
helper.search();
}
});
```
And now, it's more straightforward:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
/* other parameters */
searchFunction(helper) {
helper.setQuery('fixed query').search();
}
});
```
The helper [still resets the page to 0](https://community.algolia.com/algoliasearch-helper-js/concepts.html#smart-page-behavior)
when the parameters change.
To keep the page you set, do the following:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
/* other parameters */
searchFunction(helper) {
const p = helper.getPage();
helper.setQuery('fixed query')
.setPage(p)
.search();
}
});
```
# What is InstantSearch.js?
Source: https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js
What is the InstantSearch.js UI library and what are the available customization APIs.
InstantSearch.js is an open source UI library for vanilla JS that lets you build a search interface in your frontend app.
InstantSearch focuses on enhancing your frontend with widgets that you can combine to create unique search interfaces.
Run and edit the What is InstantSearch.js? example in CodeSandbox.
InstantSearch offers three levels of increasing control over your UI:
1. Start with a **predefined widget** that you can configure and style with CSS.
2. To change its render output (DOM or Native), **customize a predefined widget** to render what you want.
3. To implement something that doesn't exist, create a **custom widget**.
## Predefined widgets
The recommended way to use InstantSearch is with its predefined widgets such as [`searchBox`](/doc/api-reference/widgets/search-box/js).
InstantSearch includes a [set of widgets](/doc/guides/building-search-ui/widgets/showcase/js) that are most often used in search experiences.
Widgets provide features and a rendered output.
You can place them anywhere on your UI, configure them, and style them with CSS.
For example, add the [`refinementList`](/doc/api-reference/widgets/refinement-list/js)\` widget and ask it to show a list of brands,
so your users can refine their search using those brands.
```js JavaScript icon=code theme={"system"}
instantsearch.widgets.refinementList({
container: document.querySelector("#brand"),
attribute: "brand",
});
```
### The InstantSearch wrapper
The [`instantsearch`](/doc/api-reference/widgets/instantsearch/js) wrapper communicates between your app and Algolia.
This is where you add all the widgets.
It accepts a search client and an name.
```js JavaScript icon=code theme={"system"}
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
const search = instantsearch({
indexName: "INDEX_NAME",
searchClient,
});
search.addWidgets([
instantsearch.widgets.refinementList({
container: document.querySelector("#brand"),
attribute: "brand",
}),
]);
```
Once you've added all the desired widgets to the wrapper, call the `start` method to begin searching:
```js JavaScript icon=code theme={"system"}
search.start();
```
### CSS theme
The predefined widgets in InstantSearch.js are compatible with the default CSS theme:
For more information, see [Style your widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#style-your-widgets).
## Customize widgets
Algolia's predefined widgets, with their fixed behavior and output, may not fully meet your requirements.
For example, you might want to customize the rendering of the [`menu`](/doc/api-reference/widgets/menu/js) widget to display as a [`select`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) element instead of a list of links.
You may also want to render it as a keyboard-controlled slideshow of images.
To address these limitations, InstantSearch.js uses *connectors* to redefine widget behavior and DOM output.
For more information, see [Customize an InstantSearch.js widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js).
## Custom widgets
If none of the previous solutions work for you and you want to create a new widget from scratch,
InstantSearch provides a third API layer of API for this: **creating custom widgets**.
This requires two APIs: the first one lets you create a new connector, and the second one lets you create a new widget.
Both solutions give you full control of the render and behavior.
When building a new widget, be prepared to dive deep into the Algolia semantics to achieve what you want.
For more information, see [Create your own InstantSearch.js widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js).
## Resources
Use the following links and resources to learn more.
### See also
* [Install InstantSearch.js](/doc/guides/building-search-ui/installation/js)
* [Get started](/doc/guides/building-search-ui/getting-started/js)
* [API reference](/doc/api-reference/widgets/js)
* [Code Exchange](https://www.algolia.com/developers/code-exchange/?category=frontend-tools)
* [Support knowledge base](https://support.algolia.com/hc/en-us/sections/4406469041041-InstantSearch-InstantSearch-js)
### Join the community
Ask questions and find answers on the following platforms:
* [Algolia Community Discord](https://alg.li/discord)
* [Stack Overflow](https://stackoverflow.com/questions/tagged/instantsearch)
* [GitHub issues](https://github.com/algolia/instantsearch/issues)
* [Changelog](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.js/CHANGELOG.md)
# Create your own widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/js
Learn how you can create a custom widget for your InstantSearch.js app.
InstantSearch.js comes with [multiple widgets](/doc/api-reference/widgets/js) that you can [extensively customize](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js).
You can also use connectors to completely change your rendering.
If no existing widget or connector suits your needs, you can create your own.
Creating custom widgets is the most advanced way to tailor your search experience.
It requires in-depth knowledge of InstantSearch and Algolia.
This guide covers how to build a custom widget:
* [Build a custom connector](#build-a-custom-connector)
* [Build a UI with a connector](#render-a-custom-user-interface)
If you're creating a custom InstantSearch widget because you didn't find a built-in option for your use case,
consider [opening a feature request](https://github.com/algolia/instantsearch/discussions/new?category=ideas\&labels=triage\&title=Feature%20request%3A%20) to describe what you're trying to build.
## When to create custom widgets
You can create a new widget **when none of the [existing widgets](/doc/api-reference/widgets/js) fit your functional needs.**
However, if you're trying to redefine the UI or DOM output of a widget,
[extend it instead by using its connector counterpart](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#customize-the-complete-ui-of-the-widgets).
**Existing widgets and connectors should fit most of your use cases.** Look into them before creating custom connectors.
For example, to create buttons that set predefined queries, you could use `useSearchBox`.
Although you're not rendering a search box, the connector provides the necessary APIs for this, so there's no need to re-develop it.
**For help, [explain your situation and ask questions on GitHub](https://github.com/algolia/instantsearch/discussions/new?category=q-a\&labels=triage).**
If you're using TypeScript,
install the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper) as a development dependency to access the necessary types.
## Build a custom connector
When creating a custom widget, start by **writing a connector that encapsulates all the logic of your widget**,
yet keeps the rendering separate.
**This guide uses the example of a negative refinement list widget.**
It's similar to a [`RefinementList`](/doc/api-reference/widgets/refinement-list/js),
but instead of filtering on the selected items, it excludes them from the search.
For example, selecting the brand "Apple" would results to all matching that aren't Apple products.
### Write the connector function
Create a `connectNegativeRefinementList` function that takes a render and an unmount function.
It should return a `negativeRefinementList` function (the widget factory) that takes widget parameters and returns an object (the widget).
For the sake of simplicity, the only parameter that the widget accepts is `attribute`.
This lets users specify which record attribute to filter on.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
};
};
}
const noop = () => {};
```
```ts TypeScript theme={"system"}
import type { SearchResults } from "algoliasearch-helper";
import type { Connector } from "instantsearch.js";
export type NegativeRefinementListConnectorParams = {
attribute: string;
};
type NegativeRefinementListRenderState = {
items: SearchResults.FacetValue[];
refine(value: string): void;
};
type NegativeRefinementListWidgetDescription = {
$$type: "myOrganization.negativeRefinementList";
renderState: NegativeRefinementListRenderState;
indexRenderState: {
negativeRefinementList: {
[attribute: string]: NegativeRefinementListRenderState;
};
};
indexUiState: {
negativeRefinementList: {
[attribute: string]: string[];
};
};
};
type NegativeRefinementListConnector = Connector<
NegativeRefinementListWidgetDescription,
NegativeRefinementListConnectorParams
>;
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
};
};
};
const noop = () => {};
```
Your custom connector needs an identifier.
The naming convention is `"myOrganization.myWidget"` (for example, `"microsoft.negativeRefinementList"`).
If you don't have an organization, you can use your name.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
$$type: "myOrganization.negativeRefinementList",
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
$$type: "myOrganization.negativeRefinementList",
};
};
};
// …
```
## Compute the render state
At this point, the widget doesn't perform any custom logic.
To make it functional, you need to hook into the InstantSearch lifecycle to update search parameters, extract response data, and expose values for rendering.
#### Widget render state
Implement the `getWidgetRenderState` method.
This is where you consume data from the API response.
It should return an object with the data and APIs you want to expose to the render function.
For the negative refinement list, you need to expose:
* The `items` to display in the list.
* A `refine` function to trigger a new search from the UI with new items to exclude.
The widget parameters are also passed under the `widgetParams` key. This is necessary for internal purposes.
In the returned object from the `negativeRefinementList` function, add a function `getWidgetRenderState`.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
getWidgetRenderState({ results, helper }) {
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
getWidgetRenderState({ results, helper }) {
// …
},
};
};
};
// …
```
You need to expose a `refine` function to toggle exclusions for the given attribute.
**The `refine` function must keep the same reference whenever `getWidgetRenderState` is called** so that UI frameworks can consider it stable between renders.
To do so, create a `connectorState` object outside the widget and attach the `refine` function to it.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// An empty `connectorState` object is used to store information
// that needs to be shared across multiple method calls.
const connectorState = {};
return {
getWidgetRenderState({ results, helper }) {
// To ensure `refine` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.refine) {
connectorState.refine = (value) =>
helper.toggleFacetExclusion(attribute, value).search();
}
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
type ConnectorState = {
refine?(value: string): void;
};
// An empty `connectorState` object is used to store information
// that needs to be shared across multiple method calls.
const connectorState: ConnectorState = {};
return {
getWidgetRenderState({ results, helper }) {
// To ensure `refine` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.refine) {
connectorState.refine = (value) =>
helper.toggleFacetExclusion(attribute, value).search();
}
// …
},
};
};
};
// …
```
You need to be able to consume the facet values from the API response so you can display them on the UI,
let users click them, and so on.
Find them on the exposed `results` and store them in an `items` variable.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// Retrieve the facet values for the given attribute and sort them by
// ascending name. Store the facet values in the `items` variable.
const items =
results.getFacetValues(attribute, {
sortBy: ["name:asc"],
}) || [];
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// Retrieve facet values from the results for the given attribute
// and sort them by ascending name.
// Later on, you could let users pass a `sortBy` parameter.
const items =
(results.getFacetValues(attribute, {
sortBy: ["name:asc"],
}) as SearchResults.FacetValue[]) || [];
// …
},
};
};
};
// …
```
You need to handle what happens when there are no search results.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// When there are no results, return the API with default values.
// This shows a fallback UI while waiting for results.
if (!results) {
return { items: [], refine: connectorState.refine, widgetParams };
}
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// When there are no results, return the API with default values.
// It's helpful to render a default UI until results are available.
if (!results) {
return { items: [], refine: connectorState.refine, widgetParams };
}
// …
},
};
};
};
// …
```
Return the `items` containing the facet values and the `refine` function.
You should also return the parameters passed to the widget under the `widgetParams` key.
You can use all this data and APIs in the render function later.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
return {
items,
// Toggles exclusion of a facet value when selected.
// If the value is already excluded, the exclusion is unset.
// Otherwise, it's added to the exclusion list.
// Then, a search is triggered.
refine: connectorState.refine,
widgetParams,
};
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
return {
items,
// A function to toggle a value when selected.
// If the value is already excluded, the exclusion is unset.
// Otherwise, it's added to the exclusion list.
// Then, a search is triggered.
refine: connectorState.refine,
widgetParams,
};
},
};
};
};
// …
```
#### Global render state
In InstantSearch, each widget you add registers its render state in one global object.
You need to specify how to store your widget render state in this global tree by implementing the `getRenderState` method.
You might use multiple negative refinement lists in your application but with different attributes.
For example,
you might want to exclude by brand and by categories.
Here, you want to store each widget's render state individually so they don't override each other.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getRenderState(renderState, renderOptions) {
// The global render state is merged with a new one to store the render
// state of the current widget.
return {
...renderState,
negativeRefinementList: {
...renderState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's render state is stored
// by the `attribute` it affects.
[attribute]: this.getWidgetRenderState(renderOptions),
},
};
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getRenderState(renderState, renderOptions) {
// The global render state is merged with a new one to store the render
// state of the current widget.
return {
...renderState,
negativeRefinementList: {
...renderState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's render state is stored
// by the `attribute` it affects.
[attribute]: this.getWidgetRenderState(renderOptions),
},
};
},
};
};
};
// …
```
### Set up the lifecycle
When you add InstantSearch widgets to your app, they go through several steps in response to internal events.
**These steps are the InstantSearch lifecycle.**
You must register lifecycle hooks on your widget to run code at the `init`, `render`, and `dispose` stages.
Use these functions to call the user-provided render and unmount functions with the correct information.
The lifecycle code shouldn't change, so you can copy the code from this step without modifying it.
Most of the custom logic happens in `getWidgetRenderState`.
The `init` step runs when the app starts (before the initial search is performed).
**Don't use this function to add Algolia-related logic**.
Instead, use `getWidgetSearchParameters`, `getWidgetUiState`, or `getWidgetRenderState`.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=true` lets you perform
// conditional logic in the render function.
true,
);
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=true` lets you perform
// conditional logic in the render function.
true,
);
},
// …
};
};
};
// …
```
The `render` step runs whenever new results come back from Algolia.
It's usually triggered by search state changes,
such as when a user submits a new query or clicks on a filter.
During this step, the widget can react to the updated search results by re-rendering with the new information.
This lets the widget remain synchronized with the current state of the search experience.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=false` lets you perform
// conditional logic in the render function.
false,
);
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=false` lets you perform
// conditional logic in the render function.
false,
);
},
// …
};
};
};
// …
```
The `dispose` step runs when removing the widget.
Use it to clean up anything the widget created during its "lifetime" such as search parameters, UI, and event listeners.
This helps prevent memory leaks and ensures the widget doesn't continue affecting the search experience once it's no longer in use.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
dispose(disposeOptions) {
unmountFn();
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
dispose(disposeOptions) {
unmountFn();
},
// …
};
};
};
// …
```
### Interact with routing
An important aspect of building an InstantSearch widget is how to make it work with [routing](/doc/guides/building-search-ui/going-further/routing-urls/js).
**Your custom widget should be able to synchronize its state with the browser URL** so you can share a link to your search experience in any given state.
#### Set the widget UI state
In InstantSearch, routing uses an internal [`uiState`](/doc/api-reference/widgets/ui-state/js) object to derive the route.
As with the [render state](#compute-the-render-state),
you need to specify how to store your widget UI state in the global UI state by implementing the `getWidgetUiState` method.
As with `getRenderState`, since you might use the widget multiple times with different attributes,
you need to store each widget's UI state individually so they don't override each other.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetUiState(uiState, { searchParameters }) {
// The global UI state is merged with a new one to store the UI
// state of the current widget.
return {
...uiState,
negativeRefinementList: {
...uiState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's UI state is stored by
// the `attribute` it affects.
[attribute]: searchParameters.getExcludeRefinements(attribute),
},
};
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetUiState(uiState, { searchParameters }) {
// The global UI state is merged with a new one to store the UI
// state of the current widget.
return {
...uiState,
negativeRefinementList: {
...uiState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's UI state is stored by
// the `attribute` it affects.
[attribute]: searchParameters.getExcludeRefinements(attribute),
},
};
},
};
};
};
// …
```
#### Set the widget search parameters
When initializing InstantSearch state from a URL, you need to convert the URL into [search parameters](/doc/api-reference/search-api-parameters) to trigger the first search.
To define how to derive search parameters from the UI state, use the `getWidgetSearchParameters` method.
This method lets you modify the existing search parameters based on the widget's configuration and the current UI state.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetSearchParameters(searchParameters, { uiState }) {
const state = searchParameters.addFacet(attribute);
const values = uiState.negativeRefinementList?.[attribute];
if (Array.isArray(values)) {
return values.reduce(
(acc, curr) => acc.addExcludeRefinement(attribute, curr),
state,
);
}
return state;
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
import type { SearchResults, SearchParameters } from "algoliasearch-helper";
// …
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetSearchParameters(searchParameters, { uiState }) {
const state = searchParameters.addFacet(attribute);
const values = uiState.negativeRefinementList?.[attribute];
if (Array.isArray(values)) {
return values.reduce(
(acc, curr) => acc.addExcludeRefinement(attribute, curr),
state,
);
}
return state;
},
};
};
};
// …
```
## Send events to the Insights API
To better understand your users,
you could capture when they use the widget to exclude refinements.
The Insights API lets you collect such events from the frontend so that you can, later on,
unlock features such as [Algolia Recommend](/doc/guides/algolia-recommend/overview),
[analytics](/doc/guides/search-analytics/overview), and [more](/doc/guides/sending-events).
You can set up your widget so it automatically sends the right events to Algolia Insights when using the [Insights middleware](/doc/api-reference/widgets/insights/js).
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// To ensure `sendEvent` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.sendEvent) {
connectorState.sendEvent = (
eventType,
facetValue,
eventName = "Negative Filter Applied",
) => {
if (helper.state.isExcludeRefined(attribute, facetValue)) {
instantSearchInstance.sendEventToInsights({
insightsMethod: "clickedFilters",
widgetType: this.$$type,
eventType,
payload: {
eventName,
index: helper.getIndex(),
filters: [`${attribute}:-${facetValue}`],
},
attribute,
});
}
};
}
if (!connectorState.refine) {
connectorState.refine = (value) => {
helper.toggleFacetExclusion(attribute, value);
// Send `click` event once the facet is toggled.
connectorState.sendEvent("click", value);
return helper.search();
};
}
// …
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
type ConnectorState = {
// …
sendEvent?(
eventType: "view" | "click" | "conversion",
facetValue: string,
eventName?: string,
): void;
};
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// To ensure `sendEvent` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.sendEvent) {
connectorState.sendEvent = (
eventType,
facetValue,
eventName = "Negative Filter Applied",
) => {
if (helper.state.isExcludeRefined(attribute, facetValue)) {
instantSearchInstance.sendEventToInsights({
insightsMethod: "clickedFilters",
widgetType: this.$$type,
eventType,
payload: {
eventName,
index: helper.getIndex(),
filters: [`${attribute}:-${facetValue}`],
},
attribute,
});
}
};
}
if (!connectorState.refine) {
connectorState.refine = (value) => {
helper.toggleFacetExclusion(attribute, value);
// Send `click` event once the facet is toggled.
connectorState.sendEvent!("click", value);
return helper.search();
};
}
// …
},
};
};
};
// …
```
Now when clicking on a refinement, it automatically sends an event to Algolia Insights.
This only works when [providing an Insights client with the Insights middleware](/doc/api-reference/widgets/insights/js).
You can make the connector even more flexible by providing the `sendEvent` function to the render function.
This lets you customize events depending on the use case.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// …
return {
// …
sendEvent: connectorState.sendEvent,
};
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// …
return {
// …
sendEvent: connectorState.sendEvent,
};
},
};
};
};
// …
```
## Render a custom user interface
An InstantSearch widget is a custom connector with a render function.
In this example, the `NegativeRefinementList` component uses the `useNegativeRefinementList` connector to build a reactive, stateful UI.
Rendering consists of two parts:
* A `render` function, which outputs the updated UI and injects it in the DOM
* A `dispose` unmounting function, which cleans up the DOM when the widget unmounts
Both functions require access to the DOM element (for injection and clean up).You can provide it with a factory function.
```js JavaScript (Preact) theme={"system"}
import { render as preactRender } from 'preact';
import { html } from 'htm/preact';
function createNegativeRefinementListRenderer({ container }) {
const containerNode =
typeof container === 'string'
? document.querySelector(container)
: container;
return {
render({ items, refine, canRefine }) {
preactRender(
html`
`;
[...containerNode.querySelectorAll(input)].forEach((element) => {
element.addEventListener('change', (event) => {
event.preventDefault();
refine(event.currentTarget.value);
});
});
},
dispose() {
containerNode.innerHTML = '';
},
};
}
function cx(
...classNames: Array
) {
return classNames.filter(Boolean).join(' ');
}
```
You can now create the widget (consisting of a connector and associated UI rendering).
```js JavaScript theme={"system"}
function negativeRefinementList(params) {
const { container, attribute, ...connectorParams } = params;
const { render, dispose } = createNegativeRefinementListRenderer({
container,
});
const createWidget = connectNegativeRefinementList(render, dispose);
return {
...createWidget(connectorParams),
// This is helpful for debugging purposes and allows to differentiate
// between the connector and the widget.
$$widgetType: 'myOrganization.negativeRefinementList',
};
}
```
```ts TypeScript theme={"system"}
import { WidgetFactory, WidgetRenderState } from 'instantsearch.js';
type NegativeRefinementListWidgetDescription = {
$$type: 'myOrganization.negativeRefinementList',
renderState: NegativeRefinementListRenderState,
indexRenderState: {
negativeRefinementList: {
[attribute: string]: WidgetRenderState<
NegativeRefinementListRenderState,
NegativeRefinementListConnectorParams
>,
},
},
indexUiState: {
negativeRefinementList: {
[attribute: string]: string[],
},
},
};
type NegativeRefinementListWidget = WidgetFactory<
NegativeRefinementListWidgetDescription & {
$$widgetType: 'myOrganization.negativeRefinementList',
},
NegativeRefinementListConnectorParams,
NegativeRefinementListWidgetParams
>;
function negativeRefinementList(params: NegativeRefinementListParams) {
const { container, attribute, ...connectorParams } = params;
const { render, dispose } = createNegativeRefinementListRenderer({
container,
});
const createWidget = connectNegativeRefinementList(render, dispose);
return {
...createWidget(connectorParams),
// This is helpful for debugging purposes and allows to differentiate
// between the connector and the widget.
$$widgetType: 'myOrganization.negativeRefinementList',
};
}
```
InstantSearch widgets use a [standardized class naming convention](#expose-standard-options).
The widget is now [usable in an InstantSearch app](#use-the-custom-widget).
Still, if you want to reuse or distribute the widget, you can [further tweak the API](#make-the-widget-reusable) to use the same standards as the built-in InstantSearch widgets.
## Make the widget reusable
You might want to reuse your widget within your app, share it across multiple projects, or even publish it on [npm](https://www.npmjs.com/) for others to enjoy.
To do so, **you can provide APIs to allow customization** while abstracting the complexity away.
InstantSearch exposes consistent APIs.
You can follow the same guidelines and conventions in your own widgets and connectors.
### Expose standard classes
Widgets expose a class on each DOM element to help you style them.
Built-in InstantSearch widgets use the [SUITCSS](https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md#components) component syntax:
```txt theme={"system"}
[-][-descendantName][--modifierName]
```
* **Every class starts with the `ais-` namespace** (for Algolia InstantSearch). This helps target all InstantSearch elements with selectors like `[class^="ais-"]`.
* **Every class has a component name mapped to the widget name.** In the example on this page, the widget uses the `NegativeRefinementList` component name. Component names are always in [Pascal case](https://en.wiktionary.org/wiki/Pascal_case).
* **Each element is identified with a descendant name.** In the example on this page, each item of the widget uses the `-item` descendant name. Descendant names are always in camel case.
* If an element has multiple states, **identify each state with a "modifier".** In the example on this page, the selected item of the widget uses the `--selected` modifier. Modifiers are always in camel case and prefixed with two hyphens. You should include the modified class on the element in addition to the base component class (for example, `ais-NegativeRefinementList-item` and `ais-NegativeRefinementList-item--selected`).
See these conventions [in action](#render-a-custom-user-interface).
If you're using a CSS linter like [Stylelint](https://stylelint.io/),
you can validate your InstantSearch classes with the following regular expression:
```txt theme={"system"}
^ais-(?:[A-Z][a-z]+)+(?:-[a-z]+(?:[A-Z][a-z]+)*)?(?:--[a-z]+(?:[A-Z][a-z]+)*)?$
```
### Pass custom classes
Widgets expose [standardized class names](#expose-standard-options) to let users write custom CSS but you could open the styling API further to allow passing classes directly on each element.
This lets users of class-based CSS frameworks like [Bootstrap](https://getbootstrap.com/) or [Tailwind CSS](https://tailwindcss.com/) use them without friction or workarounds.
In built-in widgets, the convention is to provide a prop that takes an object of named classes.
```js JavaScript (Preact) theme={"system"}
function createNegativeRefinementListRenderer({ container, cssClasses = {} }) {
// …
return {
render({ items, refine, canRefine }) {
preactRender(
html`
`,
containerNode
);
// …
},
// …
};
};
```
### Customize the UI
**UI customizability is an important aspect of building reusable InstantSearch widgets.**
If you need to internationalize your app, control the markup, or change icons, you shouldn't have to opt out of using widgets and resort to using connectors just to tweak the UI.
#### Templates
If users need to control the markup, they shouldn't have to immediately reach out to connectors.
Instead, you can let them template parts of the UI.
For example, users may want to change the rendering of each item to display a custom checkbox that requires additional markup.
In InstantSearch.js, widgets expose a `templates` option.
Templates are dictionaries to customize the UI elements, text, and support internationalization.
For more information, see [Templating your UI](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#templating-your-ui).
Behind the scenes, InstantSearch.js uses Preact to render widgets and lets widget users [return templates as virtual DOM elements](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#templating-your-ui)).
Consider using Preact and exposing the `html` function in your templates for a consistent experience with built-in widgets.
```js JavaScript (Preact) theme={"system"}
import { render as preactRender } from 'preact';
import { html } from 'htm/preact';
function createNegativeRefinementListRenderer({
container,
cssClasses = {},
templates: providedTemplates = {},
}) {
// …
const templates = {
listItem({ item, cssClasses, html }) {
return html`
${items
.map((item) => templates.listItem({ item, cssClasses, html }))
.join('')}
`,
containerNode
);
},
// …
};
};
```
### Expose standard options
Each widget and connector exposes options specific to its use case.
Some options are available across multiple widgets and connectors to address common issues and offer a consistent experience.
In addition to root props, classes, translations, and templates,
you can expose the following options on widgets and connectors when relevant.
#### Providing a container
In InstantSearch.js, widgets are rendered within a container:
a DOM element to inject the widget into.
Your [render function](#render-a-custom-user-interface) should accept a container CSS selector as a string (such as `#negative-refinement-list`) or a direct reference to the element, as an `HTMLElement`.
#### Target a record attribute
When a widget needs to target a specific record attribute, expose an `attribute` option in the connector.
This is the case for most refinement widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/js) so you can select the specific attribute to refine.
The `attribute` option typically accepts a string.
For deeply nested objects,
you can accept dot-separated values like `brand.name` or an array of strings like `["brand", "name"]`.
#### Include or exclude attributes
When a widget manipulates selected refinements, you may want to let users select what attributes to include or exclude.
This is the case with widgets like [`CurrentRefinements`](/doc/api-reference/widgets/current-refinements/js) or [`ClearRefinements`](/doc/api-reference/widgets/clear-refinements/js) so you can hand-pick exactly what attributes to manipulate.
#### Limit items
When a widget manipulates items, notably facet refinements, you may want to let users limit how many of them to retrieve.
Widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/js) or [`Menu`](/doc/api-reference/widgets/menu/js) expose a `limit` option,
which is directly forwarded to the Algolia search engine with the [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet) search parameter.
Such widgets usually expose two extra options to let users toggle more facets: `showMore`, a boolean option to enable the feature, and `showMoreLimit`, to define the maximum number of items to display if the widget is showing more items.
#### Transform items
When a widget manipulates items like hits or facets,
users may want to change them before rendering.
To do so, expose a `transformItems` option for transforming,
removing, or reordering items on the connector.
The `transformItems` option is a function that receives the items and should return a new array of the same shape.
The default value is an identity function.
## Use the custom widget
You can use a custom widget like any widget provided by the library.
It takes the parameters to forward to the connectors, HTML props for the root element, and an object for classes.
```js JavaScript theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';
import { negativeRefinementList } from './negativeRefinementList';
const searchClient = algoliasearch('undefined', 'undefined');
const search = instantsearch({
indexName: 'instant_search',
searchClient,
});
search.addWidgets([
negativeRefinementList({
// Connector parameters
attribute: 'brand',
// Container to inject into
container: '#negative-refinement-list',
// Custom CSS classes
cssClasses: {
root: 'MyCustomNegativeRefinementList',
item: 'MyCustomNegativeRefinementListItem',
},
}),
]);
search.start();
```
This is what the widget looks like in an app:
## Next steps
You now have a good starting point to take full control of your InstantSearch experience.
You can further improve it by:
* Improving the widget's API with useful parameters like [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#instance-options).
* Open sourcing your custom widget on [GitHub](https://github.com/) and publish it on [npm](https://www.npmjs.com/) (if you do, [let Algolia know.](https://github.com/algolia/instantsearch/discussions/new?category=show-and-tell)).
# Customize existing widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js
Learn how you can customize an existing InstantSearch.js widget.
## Highlight and snippet your search results
Search helps users understand what they're seeing in the results.
When users run a ,
they should be able to tell why a result matches.
Algolia highlights the matched text in each result.
Algolia can also generate snippets:
short excerpts from longer text, truncated to a fixed length around the match.
This feature is already packaged for you in InstantSearch.js through two functions
[`Highlight`](/doc/api-reference/widgets/highlight/js) and [`Snippet`](/doc/api-reference/widgets/snippet/js).
### Usage with templates
You have a direct access to the highlight and snippet functions through the [template system](#templating-your-ui).
```js HTML tagged templates theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
instantsearch.widgets.hits({
container: "#hits",
templates: {
item(hit, { html, components }) {
return html`
`,
},
}),
]);
```
## Style your widgets
All widgets in InstantSearch.js namespace are shipped with CSS class names that can be overridden.
The format for those class names is `ais-NameOfWidget-element--modifier` (following the naming convention defined by [SUIT CSS](https://suitcss.github.io/)).
The different class names used by each widget are described on their respective documentation pages.
You can also inspect the underlying DOM and style accordingly.
### Load the InstantSearch theme
InstantSearch.js doesn't inject any styles by default but comes with two optional themes: **Algolia** and **Satellite**.
#### From a CDN
The themes are available on jsDelivr.
Unminified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite.css`
Minified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite-min.css`
The `reset` style sheet is included by default in the Satellite theme,
so you don't need to import it separately.
Either copy the files into your own app or use a direct link to jsDelivr.
```html HTML icon=code-xml theme={"system"}
```
#### Using npm
If your project imports CSS into JavaScript files using build tools,
you can install the theme with npm.
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
```sh pnpm theme={"system"}
pnpm add instantsearch.css
```
Then, you can import the theme directly into your app.
```js JavaScript icon=code theme={"system"}
import "instantsearch.css/themes/satellite.css";
```
### Customize with CSS variables
The InstantSearch.css themes provide CSS variables that you can override to customize colors, spacing, typography, and more without writing custom CSS classes.
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 62, 52, 211;
/* Larger spacing */
--ais-spacing-factor: 1.5;
}
```
For a complete list of available CSS variables and customization options, see [Styling and theming](/doc/guides/building-search-ui/styling/js).
### CSS class override
You can override the class names of every widgets with the `cssClasses` option.
The different key provided by each widget to override the class are described on their respective documentation pages. Here is an example with the [`hits`](/doc/api-reference/widgets/hits/js) widget:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.hits({
container: "#hits",
cssClasses: {
item: "item-custom-css-class",
},
}),
]);
```
### Styling icons
You can style the icon colors using the widget class names:
```css CSS icon=paintbrush theme={"system"}
.ais-SearchBox-submitIcon path
.ais-SearchBox-resetIcon path {
fill: red,
}
```
## Translate your widgets
Most elements in InstantSearch.js widgets can be customized by means of templates.
Those templates can be text labels or complete piece of HTML.
Underneath, InstantSearch.js is using Hogan, which is an implementation of mustache, the logic-less templating.
### Example: translating the "Show more" label
Here is an example of a menu widget with a show more label translated in french:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
// Provide the search client here
});
search.addWidgets([
instantsearch.widgets.menu({
attribute: "...",
templates: {
showMoreText: ({ isShowingMore }, { html }) => {
return isShowingMore ? html`Voir moins…` : html`Voir plus…`;
},
},
}),
]);
```
## Templating your UI
**InstantSearch.js widgets provide templates to customize parts of the UI.**
This lets you customize icons, provide a hit component, define an empty [`uiState`](/doc/api-reference/widgets/ui-state/js), and change the default labels.
Templates can be either functions or strings.
You can provide templates in several ways:
* [HTML strings with `html`](#using-html-strings) (with function-based templates). This is the recommended way, especially if you're using InstantSearch.js without a build step.
* [JSX templates](#using-jsx) (with function-based templates). This is useful if you're already using a JSX implementation (like Preact) in your application.
* [Hogan.js templates](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#using-hogan-js) (with string-based templates). **This method is deprecated and shouldn't be used for new templates.**
* "Classic" HTML strings (with string-based templates). **This method is deprecated and shouldn't be used for new templates.**
```js JavaScript icon=code theme={"system"}
searchBox({
// ...
templates: {
submit() {
return "Submit";
},
},
});
```
You might want to customize a part of the widget that the template system doesn't cover.
For these use cases take a look at the section [extending widgets](#customize-the-complete-ui-of-the-widgets).
### Using HTML strings
Every InstantSearch.js template provides an `html` function that you can use as a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
Using `html` lets you safely provide templates as an HTML string.
It works directly in the browser, no need for a transpiler or a build step.
The `html` function is only available starting from v4.46.0.
```js JavaScript icon=code theme={"system"}
hits({
// ...
templates: {
item(hit, { html, components, sendEvent }) {
return html`
`;
},
},
});
```
**Internet Explorer 11 doesn't support tagged template literals.**
If you need to support Internet Explorer 11,
check out the [suggested solutions](#internet-explorer-11-support).
#### Highlighting and snippeting
In both [`hits`](/doc/api-reference/widgets/hits/js) and [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/js) widgets, templates expose a set of built-in components to handle highlighting and snippeting.
You can use either their function form, or interpolate them.
```js Function theme={"system"}
hits({
// ...
templates: {
item(hit, { html, components }) {
return html`
`;
},
},
});
```
Four components are available:
* `Highlight` to highlight matching parts in Algolia results.
* `Snippet` to snippet matching parts in Algolia results.
* `ReverseHighlight` to highlight non-matching parts in Algolia results.
* `ReverseSnippet` to highlight and snippet non-matching parts in Algolia results.
#### Loops and conditional rendering
You can use plain JavaScript to build dynamic templates.
For example, you can use [`Array.map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) to loop over an array and display a list.
```js JavaScript icon=code theme={"system"}
ratingMenu({
// …
templates: {
item({ url, stars }, { html }) {
return html`
${stars.map((_, index) => html``)}
`;
},
},
});
```
Passing a unique `key` attribute is helpful when mapping over items.
It helps the virtual DOM keep track of each element when they change, and update the UI efficiently.
To conditionally render a part of your UI,
you can use a [short-circuit operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND) or a [ternary](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator).
```js JavaScript icon=code theme={"system"}
ratingMenu({
// …
templates: {
item({ url, stars }, { html }) {
return html`
${stars.map(
(isFilled, index) =>
html``,
)}
`;
},
},
});
```
#### Internet Explorer 11 support
**You can't use `html` as a tagged template in Internet Explorer 11.**
Depending on your project setup,
you can work around this problem to still use `html` while providing compatible code to your Internet Explorer 11 users.
##### With Babel
If you're already using [Babel](https://babeljs.io/) to compile your code for legacy browsers, you can transform all `html` expressions into regular function calls.
The recommended setup is to use [`@babel/preset-env`](https://babeljs.io/docs/babel-preset-env).
```js .babelrc icon=braces theme={"system"}
{
"presets": [["@babel/preset-env"]]
}
```
##### With a shim
If you don't have a build step in your project,
you can write a [shim](https://developer.mozilla.org/en-US/docs/Glossary/Shim).
Tagged templates are regular functions with a specific signature,
so you can wrap their calls with a friendlier API to avoid using tagged template notation.
This function takes templates either as static strings,
or as an array of interspersed chunks, splits them, and passes them to the `html` function.
```js Shim theme={"system"}
function htmlShim(template, html) {
if (typeof template === "string") {
return html([template]);
}
const parts = template.reduce(
(acc, part, index) => {
const isEven = index % 2 === 0;
acc[Math.abs(Number(!isEven))].push(part);
return acc;
},
[[], []],
);
return html(parts[0], ...parts[1]);
}
```
```js Usage theme={"system"}
hits({
// ...
templates: {
item(hit, { html, components }) {
return htmlShim(
"
",
html,
);
},
},
});
```
The documented shim assumes every even array entry is a template string,
and every odd entry is a dynamic value.
You can adapt it if you need a different behavior.
You can also get this transform as a [standalone Babel plugin](https://babeljs.io/docs/babel-plugin-transform-template-literals).
#### Further optimizations
The provided `html` function works in the browser without any build step, with a negligible impact on memory and bundle size (\< 600 bytes).
For optimal performance, you can use the [`babel-plugin-htm`](https://github.com/developit/htm/tree/master/packages/babel-plugin-htm) Babel plugin to compile `html` into `preact.createElement` calls.
To use this plugin,
**you need to adapt your code so that the pragma to replace `html` calls with is always accessible.**
For example, instead of destructuring in the signature,
you need to name the parameter and destructure it in the function body—or not destructure it at all.
**The parameter (here, `params`) must have the same name in every template.**
```js JavaScript icon=code theme={"system"}
hits({
// ...
templates: {
item(hit, { html, components }) { // [!code --]
item(hit, params) { // [!code ++]
const { html, components } = params; // [!code ++]
return html`
`;
},
},
});
```
Then, you can set up the Babel plugin.
```json .babelrc icon=braces theme={"system"}
{
"plugins": [
[
"htm",
{
"pragma": "params.createElement"
}
]
]
}
```
If you're destructuring objects,
make sure to also transpile it using [`@babel/preset-env`](https://babeljs.io/docs/en/babel-preset-env).
### Using JSX
If you're already using JSX in your project,
**you can directly return JSX templates.**
```jsx JSX icon=code theme={"system"}
/** @jsx h */
import { h } from "preact";
searchBox({
// ...
templates: {
submit({ cssClasses }) {
return (
);
},
},
});
```
### Using Hogan.js
You can provide templates using [Hogan.js](https://twitter.github.io/hogan.js/), a [Mustache](https://mustache.github.io/mustache.5.html) implementation.
For a comprehensive reference, check the respective documentations.
**Hogan.js and string-based templates are deprecated and won't be supported in InstantSearch.js 5.**
You can replace them with function-form templates and use either the provided [`html` function](#using-html-strings)
or [JSX templates](#using-jsx).
For more information, see [Upgrade templates](/doc/guides/building-search-ui/upgrade-guides/js#upgrade-templates).
```js JavaScript icon=code theme={"system"}
searchBox({
// ...
templates: {
submit: ``,
},
});
```
### Using classic HTML strings
You can provide plain HTML strings using string-based templates.
They're injected as HTML.
**HTML strings and string-based templates are deprecated and won't be supported in InstantSearch.js 5.**
You can replace them with function-form templates and use either the provided [`html` function](#using-html-strings)
or [JSX templates](#using-jsx).
For more information, see [Upgrade templates](/doc/guides/building-search-ui/upgrade-guides/js#upgrade-templates).
```js JavaScript icon=code theme={"system"}
searchBox({
// ...
templates: {
submit: ``,
},
});
```
## Change the list of items in widgets
Every widget and connector that handles a list of items exposes a `transformItems` option.
This option is a function that takes the items as a parameter and expects to return the items back.
This option can be used to sort, filter, and add manual values.
### Sorting
#### Using `transformItems`
In this example, the `transformItems` option to order the items by `label` in a ascending mode:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#brands",
attribute: "brand",
transformItems(items) {
// Assume that LoDash is available
return _.orderBy(items, "label", "asc");
},
}),
]);
```
#### Using `sortBy`
Some widgets and connectors provide a `sortBy` option.
It accepts either an array of strings:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#brands",
attribute: "brand",
sortBy: ["isRefined", "count:desc", "name:asc"],
}),
]);
```
Or use a comparison function:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.refinementList({
container: '#brands',
attribute: 'brand',
sortBy: (a, b) {
// if a should be before b return -1
// if b should be before a return 1
// otherwise return 0
},
})
]);
```
### Filtering
This example uses the `transformItems` option to filter out items when the count is lower than 150.
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#brands",
attribute: "brand",
transformItems(items) {
return items.filter((item) => item.count >= 150);
},
}),
]);
```
### Add manual values
By default, the values in a `RefinementList` or a `Menu` are dynamic.
This means that the values are updated with the context of the search.
Most of the time this is the expected behavior,
but sometimes you may want to have a static list of values that never change.
To achieve this, you can use the connectors.
This example uses the [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#customize-the-ui-with-connectrefinementlist)
connector to display a static list of values.
This `RefinementList` will always display and only display the items "Apple" and "Microsoft".
```js JavaScript icon=code theme={"system"}
const staticRefinementList = instantsearch.connectors.connectRefinementList(
({ items, refine, widgetParams }, isFirstRender) => {
const container = document.getElementById("brand-list");
if (isFirstRender) {
container.addEventListener("click", ({ target }) => {
const input = target.closest("input");
if (input) {
refine(input.value);
}
});
return;
}
const list = widgetParams.items.map(({ label: staticLabel, value }) => {
const { isRefined } = items.find(
({ label }) => label === staticLabel,
) || {
isRefined: false,
};
return `
`;
});
container.innerHTML = `
${list.join("")}
`;
},
);
search.addWidgets([
staticRefinementList({
attribute: "brand",
items: [
{ label: "Apple", value: "Apple" },
{ label: "Microsoft", value: "Microsoft" },
],
}),
]);
```
### Display facets with no matches
Showing facets that don't match the current query helps users explore and refine results.
Algolia returns only facet values that match the current query.
To show non-matching values, add frontend logic.
For example, you could cache facet values the first time you receive them.
Then, if Algolia returns fewer values than your facet limit,
append the cached values.
This approach has limitations:
* It doesn't work with [search for facet values](/doc/guides/managing-results/refine-results/faceting#search-for-facet-values) because Algolia doesn't return non-matching values. As a result, highlighting doesn't work for cached values.
* You may need to re-apply sorting in the [`transformItems`](#using-transformitems) function because InstantSearch applies its internal sorting before calling the function.
```js JavaScript icon=code theme={"system"}
function uniqBy(items, property) {
const seen = {};
return items.filter((item) => {
const val = item[property];
if (seen[val]) {
return false;
} else {
seen[val] = true;
return true;
}
});
}
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
const indexName = "instant_search";
const brandAttribute = "brand";
const brandLimit = 10;
const search = instantsearch({
indexName,
searchClient,
});
const initialFacets = [];
// On the first load, all facets returned by Algolia are stored
// so we can append them to the returned facets when Algolia return enough.
// We set their `count` to 0 because they'll show up when nothing
// else matches.
// Note that this triggers another query.
searchClient
.searchForFacetValues([
{
indexName,
params: {
facetName: brandAttribute,
facetQuery: "",
maxFacetHits: brandLimit,
},
},
])
.then(([{ facetHits }]) => {
initialFacets.push(
...facetHits.map((facet) => ({
...facet,
label: facet.value,
value: facet.value,
isRefined: false,
count: 0,
})),
);
});
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#brand-list",
attribute: brandAttribute,
limit: brandLimit,
transformItems: (items) => {
// If Algolia doesn't return enough results, we lose track of a
// potentially refined facet.
// For example, if you refine on "Apple", then search for "chromecast",
// "Apple" is no longer returned, and we don't know that it was selected
// based on the initial facets.
// We need to keep track of the last state to reflect the fact that it
// was refined in the UI.
initialFacets.forEach((facet, index) => {
const updatedItem = items.find((item) => item.value === facet.value);
if (updatedItem) {
initialFacets[index] = {
...updatedItem,
count: 0,
};
}
});
// If a cached facet is already returned by Algolia, we want it to be
// displayed rather than to display its cached value.
// You might need to sort the items again here because the internal
// sorting happens before `transformItems` is called.
return uniqBy([...items, ...initialFacets], "value").slice(0, brandLimit);
},
}),
]);
search.start();
```
### Searching long lists
For some cases, you want to be able to directly search into a list of facet values.
This can be achieved using the `searchable` prop on widgets like [`refinementList`](/doc/api-reference/widgets/refinement-list/js) or [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#customize-the-ui-with-connectrefinementlist) connector.
To enable this feature, you'll need to make the attribute searchable [using the API](/doc/api-reference/api-parameters/attributesForFaceting) or the [dashboard](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard).
#### With widgets
Use the `searchable` prop to add a search box to supported widgets:
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#brand-list",
attribute: "brand",
searchable: true,
}),
]);
```
#### With connectors
You can implement your own search box for searching for items in lists when using supported connectors by using those provided parameters:
* `searchForItems(query)`: call this function with a search query to trigger a new search for items
* `isFromSearch`: `true` when you are in search mode and the provided `items` are search items results
```js JavaScript icon=code theme={"system"}
const customRefinementListWithSearchBox =
instantsearch.connectors.connectRefinementList(
({ items, refine, searchForItems, isFromSearch }, isFirstRender) => {
const container = document.getElementById("brand-list");
if (isFirstRender) {
container.innerHTML = `
`,
)
.join("");
},
);
search.addWidgets([
customRefinementListWithSearchBox({
attribute: "brand",
}),
]);
```
## Apply default value to widgets
A question that comes up frequently is "How to instantiate a [`refinementList`](/doc/api-reference/widgets/refinement-list/js) widget with a pre-selected item?".
For this use case, you can use the [`configure`](/doc/api-reference/widgets/configure/js) widget.
The following example instantiates a search page with a default query of "apple" and will show a category menu where the item "Cell Phones" is already selected:
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
instantsearch.widgets.refinementList({
container: "#category",
attribute: "categories",
}),
// This needs to be after the refinement list has been declared
instantsearch.widgets.configure({
query: "apple",
disjunctiveFacetsRefinements: {
categories: ["Cell Phones"],
},
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
```
## How to provide search parameters
Algolia has a [wide range of parameters](/doc/api-reference/search-api-parameters).
If one of the parameters you want to use isn't covered by any widget or connector,
then you can use the `configure` widget.
Here's an example configuring the [`distinct` parameter](/doc/api-reference/api-parameters/distinct):
```js JavaScript icon=code theme={"system"}
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
search.addWidgets([
instantsearch.widgets.configure({
distinct: 1,
}),
instantsearch.widgets.hits({
container: "#hits",
}),
]);
```
### Dynamic update of search parameters
To dynamically update the search parameters, create [a new custom widget](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js).
### Filter your results without widgets
Widgets already provide a lot of different ways to filter your results but sometimes you might have more complicated needs that require usage of the [`filters`](/doc/api-reference/api-parameters/filters) search parameter.
To avoid conflicts, don't use filters on attributes already used with a widget.
```js JavaScript icon=code theme={"system"}
search.addWidgets([
instantsearch.widgets.configure({
filters: 'NOT categories:"Cell Phones"',
}),
]);
```
To learn more, see [Filters and facet filters](/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters).
## Customize the complete UI of the widgets
InstantSearch.js comes with widgets that have a standardized rendering.
If you feel limited by the options provided, you can go further using connectors.
Connectors are the render-less counterparts of the widgets.
They encapsulate
all the logic needed for making search widgets. Each one of them is specialized
to make a certain type of widget.
If you want to create a type of widget that isn't available,
create a [custom widget](/doc/guides/building-search-ui/widgets/create-your-own-widgets/js).
### Introduction to connectors
#### Anatomy of a connector
A connector is a function that will create a widget factory, which is a function that can
create widget instances.
They follow the pattern:
```js JavaScript icon=code theme={"system"}
(rendering, unmount?) => (widgetParameters) => Widget
```
In practice, creating a new custom widget based on a connector would look like that:
```js JavaScript icon=code theme={"system"}
const makeHits = instantsearch.connectors.connectHits(function renderHits(
{ hits },
isFirstRendering,
) {
hits.forEach((hit) => {
console.log(hit);
});
});
const search = instantsearch(/* options */);
search.addWidgets([makeHits()]);
```
#### Reuse connectors
Connectors are meant to be reusable, it's important to be able to pass options to
the rendering of each single widget instance when instantiating them.
That's why
all the options passed to the newly created widget factory will be forwarded to the
rendering.
In the following example you want to configure the DOM element that will
host the widget:
```js JavaScript icon=code theme={"system"}
const makeHits = instantsearch.connectors.connectHits(function renderHits(
{ items, widgetParams },
isFirstRendering,
) {
// widgetParams contains all the option used to call the widget factory
const container = widgetParams.container;
$(container).html(items.map((item) => JSON.stringify(item)));
});
const search = instantsearch(/* options */);
search.addWidgets([
makeHits({ container: $("#hits-1") }),
makeHits({ container: $("#hits-2") }),
]);
```
#### When is the rendering function called?
The rendering function is called before the first search (init lifecycle step)
and each time results come back from Algolia (render lifecycle step).
Depending on the method you are relying on to render your widget, you might
want to use the first call to create the basic DOM structure (like when using
vanilla JS or jQuery).
To be able to identify at which point of the lifecycle the rendering function is
called, a second argument `isFirstRendering` is provided to the rendering function.
This parameter is there to be able to only do some operations once, like creating
the basic structure of the new widget once.
The latter calls can then be used to
only update the DOM.
```js JavaScript icon=code theme={"system"}
const makeHits = instantsearch.connectors.connectHits(function renderHits(
{ hits },
isFirstRendering,
) {
if (isFirstRendering) {
// Do some initial rendering
}
// Do the normal rendering
});
const search = instantsearch(/* options */);
search.addWidgets([makeHits()]);
```
#### When is the unmount function called?
The unmount function is called when you remove a widget.
When `search.removeWidgets([widget])` is called, InstantSearch.js cleans up the internal
data of the widget, and calls the `unmount` function to clean up the DOM.
```js JavaScript icon=code theme={"system"}
const makeHits = instantsearch.connectors.connectHits(
function renderHits({ items, widgetParams }, isFirstRendering) {
const container = widgetParams.container;
$(container).html(items.map((item) => JSON.stringify(item)));
},
function unmount() {
$("#hits").remove();
},
);
```
### Customize widgets hands-on
InstantSearch.js comes bundled with a set of 15+ UI components.
Each of them
has options to manipulate CSS classes or even modifying part of the HTML
output (templates).
To go a step further in terms of customization, InstantSearch.js offers connectors
that contain the logic of the widgets without their rendering.
### A custom menu with jQuery
This example shows you how to create a new custom widget using [`connectMenu`](/doc/api-reference/widgets/menu/js#customize-the-ui-with-connectmenu) connector.
It covers,
step by step, how to write a render function used by the connector.
For simplicity, custom widgets use [jQuery](https://jquery.com/) to manipulate the DOM.
#### Set up the DOM
Since these examples use jQuery,
you only want to update the changing parts of the markup at every render.
To help you to do that, the connectors API provides the `isFirstRendering` boolean as second argument of the render function.
Use this to insert the initial markup of your custom widget.
```js JavaScript icon=code theme={"system"}
const customMenuRenderFn = (renderParams, isFirstRendering) => {
if (isFirstRendering) {
// insert needed markup in the DOM
// here we want a `` element and the title
$(document.body).append(`
My first custom menu widget
`);
}
};
```
If you use a rendering library such as React, you can omit this part because React will compute this for you.
#### Display the available drop-down menu options
Then, on every render, you want to update and insert the available menu items as `option` DOM nodes:
```js JavaScript icon=code theme={"system"}
const customMenuRenderFn = (renderParams, isFirstRendering) => {
if (isFirstRendering) {
$(document.body).append(`
My first custom menu widget
`);
}
// `renderParams` is an object containing all the information
// you need to create a custom widget.
const items = renderParams.items;
// Transform `items[]` to HTML markup:
// Each item comes with a `value` and `label`, it will also have a boolean to true
// called `isRefined` when the current menu item is selected by users.
const optionsHTML = items.map(
({ value, isRefined, label, count }) => `
`,
);
// Then replace the content of `` node with the new menu items markup.
$(document.body).find("select").html(optionsHTML);
};
```
Now all the menu options are displayed on the page but nothing is updated when the user selects a new option.
To fix that, connect the drop-down menu to the search.
#### Make the drop-down menu interact with the search results
The menu connector comes with a `refine()` function in the first argument `renderParams` object.
You need to call this `refine()` function every time a user select another option to refine the search results:
```js JavaScript icon=code theme={"system"}
const customMenuRenderFn = (renderParams, isFirstRendering) => {
if (isFirstRendering) {
$(document.body).append(`
My first custom menu widget
`);
// We will bind the `select` change event on first render
// because we don't want to create new listeners on every render
// for potential performance issues:
const refine = renderParams.refine;
// we will use `event.target.value` to identify
// which option is selected and then refine it:
$(document.body)
.find("select")
.on("change", ({ target }) => {
refine(target.value);
});
}
const items = renderParams.items;
const optionsHTML = items.map(
({ value, isRefined, label, count }) => `
`,
);
$(document.body).find("select").html(optionsHTML);
};
```
Now every time a user selects a new option in the drop-down menu,
it triggers a new search to refine the search results.
#### Mount the custom drop-down menu widget on your page
You've just written the render function and can now use it with the menu connector.
This will create a new widget factory for the custom drop-down menu widget.
Use this factory in your search:
```js JavaScript icon=code theme={"system"}
const customMenuRenderFn = (renderParams, isFirstRendering) => {
if (isFirstRendering) {
$(document.body).append(`
My first custom menu widget
`);
const refine = renderParams.refine;
$(document.body)
.find("select")
.on("change", ({ target }) => {
refine(target.value);
});
}
const items = renderParams.items;
const optionsHTML = items.map(
({ value, isRefined, label, count }) => `
`,
);
$(document.body).find("select").html(optionsHTML);
};
// Create a new factory of the custom menu select widget:
const dropdownMenu = instantsearch.connectors.connectMenu(customMenuRenderFn);
// Instantiate custom widget and display it on the page.
// Custom widgets that are created with connectors accepts
// the same options as a built-in widget, for instance
// the menu widget takes a mandatory `attribute` option
// so we have to do the same:
search.addWidgets([
dropdownMenu({
attribute: "categories",
}),
]);
```
This example works on a single DOM element, which means that you won't be able to re-use it for another attribute.
#### Make connectors reusable
Connectors are meant to be reusable, it's important to be able to pass options to
the rendering of each single widget instance when instantiating them.
That's why all the options passed to the newly created widget factory will be forwarded to the
rendering.
Now update your custom render function to configure the DOM element where the widget is mounted and also the title:
```js JavaScript icon=code theme={"system"}
const customMenuRenderFn = (renderParams, isFirstRendering) => {
// widgetParams contains all the original options used to instantiate the widget on the page.
const container = renderParams.widgetParams.containerNode;
const title =
renderParams.widgetParams.title || "My first custom menu widget";
if (isFirstRendering) {
// Replace `document.body` with the container provided by users
// and also the new title
$(container).append(`
${title}
`);
const refine = renderParams.refine;
$(container)
.find("select")
.on("change", ({ target }) => {
refine(target.value);
});
}
const items = renderParams.items;
const optionsHTML = items.map(
({ value, isRefined, label, count }) => `
`,
);
$(container).find("select").html(optionsHTML);
};
const dropdownMenu = instantsearch.connectors.connectMenu(customMenuRenderFn);
// Now you can use the dropdownMenu at two different places in the DOM:
// (since they use the same `attribute` they will display the same options)
search.addWidgets([
dropdownMenu({
attribute: "categories",
containerNode: "#first-dropdown",
}),
dropdownMenu({
attribute: "categories",
containerNode: "#second-dropdown",
}),
]);
```
These steps introduced a way to provide custom parameters:
* a DOM container
* a title
# Change browser defaults for InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/disabling-default-browser-behavior/js
Change the default properties for the HTML input element when using custom search boxes.
**If you don't use the built-in InstantSearch or Autocomplete components to render a search box, you need to change the default properties of the HTML input element** to turn off the default autocomplete behavior of the browser.
A classic search HTML input looks like:
```html HTML theme={"system"}
```
When a user starts typing into this input, the browser may use its default search-as-you-type behavior, which interferes with the custom search box.
This guide describes how to avoid this behavior.
## Updating input attributes
Set these attributes on the HTML input element to turn off the browser's default behavior:
```html HTML theme={"system"}
```
In Chrome, `autocomplete="off"` doesn't work.
In Chrome, you can [turn off autocomplete](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion#Disabling_autocompletion) by setting the attribute to an invalid value like `autocomplete="nope"`.
The same solution doesn't work in Firefox (it disregards the invalid value and reverts to the default autocompletion behavior).
## More information about input attributes
For more information, see these MDN references:
* [How to turn autocomplete off](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion)
* [Autocomplete](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocomplete)
* [Autocorrect](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocorrect)
* [Autocapitalize](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocapitalize)
* [Spellcheck](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-spellcheck)
# Showcase for InstantSearch.js widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/js
All InstantSearch.js widgets in action.
For a list of available widgets, see [Widgets](/doc/api-reference/widgets/js).
# Compositions
Source: https://www.algolia.com/doc/guides/compositions
Combine results from multiple indices, content types, and data sources into a single response.
The Composition API lets you combine results from multiple indices, content types and data sources into a single response.
Use it to build federated search, category pages, and dynamic experiences.
These experiences can show curated or sponsored content next to .
Manage compositions in the Algolia dashboard and render them with InstantSearch.
Update experiences without changing frontend code.
## What you can do with compositions
* **Compose results from multiple sources:** combine multiple indices, content types, and data sources in a single request. No frontend orchestration needed.
* **Inject [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups):** place curated product groups at specific positions within organic results. Inject sponsored content, trending items, brand partnerships and more.
* **Scalable orchestration:** the composition holds all the complex logic. Frontend teams can focus on the best rendering and experience for their customers.
## When to use compositions
Use the Composition API when you need to return multiple result sets in one request.
It also works when you want to inject dynamic sponsored results at set spots.
Use it to build collections, category pages, or federated search too.
## Key concepts
A composition combines results from multiple sources into one response.
Your frontend queries a single endpoint instead of searching each source on its own.
Sets when and where groups appear, based on a query, catalog, or collection.
A set of results that match specific attributes, placed at a chosen spot in an organic results feed.
A product landing page built from a search request with applied filters.
Curated product listings.
You can create a collection by manually selecting records or defining them using conditions.
[Search-based groups](https://algolia.com/doc/guides/compositions/smart-groups/search-based-groups) use Algolia filters to automatically select items that match your criteria.
You define what to promote (like `brand:Adidas AND in_stock:true`), and Algolia finds which items match.
Algolia's AI-powered ranking finds which matching items best fit the user's query.
This keeps promoted items feeling natural in the search results.
[External source groups](https://algolia.com/doc/guides/compositions/smart-groups/external-source-groups) let you pick items to promote by giving a list of object IDs.
These object IDs come from an external system like:
* Retail media platforms
* Custom bidding or campaign management systems
Your backend calls the external source, receives objectIDs, then passes them to Algolia's Composition API.
Algolia handles injection, deduplication, and optional re-ranking by relevance.
[Recommend-based groups](https://algolia.com/doc/guides/compositions/smart-groups/recommend-based-groups) let you populate a group with items returned by an [Algolia Recommend](https://algolia.com/doc/guides/algolia-recommend/overview) model.
You configure them through the Composition API by setting the group source to `recommend`.
## Before you begin using the Compositions API
Before using the Composition API, make sure your setup meets these requirements:
* Your Algolia application must use Algolia's [dynamic scaling infrastructure](/doc/guides/scaling/infrastructure/dynamic/dynamic). If your app uses [classic infrastructure](/doc/guides/scaling/infrastructure/classic/servers-clusters), migrate it first. Contact your customer success manager or the [support team](https://support.algolia.com/hc/en-us/requests/new).
* You must have access to [Merchandising Studio](/doc/guides/managing-results/rules/merchandising-and-promoting#merchandising-studio) to curate results.
* Your application must have the Composition API and Smart Groups enabled. If your plan should include these features but you can't access them, contact your account manager, your customer success manager, or the [support team](https://support.algolia.com/hc/en-us/requests/new).
* [Multifeed compositions](/doc/guides/compositions/multifeed-compositions) require additional access. To request it, contact your Algolia customer success manager.
# Return multiple result sets with multifeed compositions
Source: https://www.algolia.com/doc/guides/compositions/multifeed-compositions
How to provide a federated search experience with a single composition.
Multifeed compositions are available to selected users.
To request access, contact your Algolia customer success manager.
Use multifeed compositions to return several independent result sets from a single API request.
Each result set, called a , can have its own search parameters, filters, and [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
Use multifeed compositions to build federated search experiences.
These experiences blend results from different categories, brands, or content types in a single response.
## Use cases
Use multifeed compositions when you need to return several independently ranked result sets for the same query in a single request.
### Federated search across categories
Show results from different product categories in parallel.
For example, when users search for "running", return separate feeds for running shoes, running apparel, and running accessories.
Each feed maintains its own ranking strategy, filters, and promoted items.
This way, category-specific merchandising doesn't interfere with other results.
### Multi-brand or multi-tenant results
Return results grouped by brand, seller, or tenant in a single response.
For example, a marketplace can display products from different sellers.
Each seller controls their own promoted items and positioning.
### Content type segmentation
Separate results by content type, such as products, articles, or videos, while using the same user query.
Each feed applies ranking and filtering tailored to its content type.
For example, products may focus on conversion signals, while articles focus on recency or engagement.
## How multifeed works
A multifeed composition defines several feeds in the `behavior.multifeed` object. Each feed:
* Has a unique identifier (`feedID`)
* Contains its own behavior, typically an `injection` with search parameters and Smart Groups
* Returns an independent result set in the response
All feeds run in parallel, sharing the same base query but applying their own filters and injection logic.
Each feed is standalone, with its own Smart Groups injected into its .
## Define a multifeed composition
Use the [Composition API](/doc/rest-api/composition/put-composition) to create a multifeed composition.
The following example creates a composition that returns three separate feeds for different content types: products, articles, and videos.
```cs C# theme={"system"}
var response = await client.PutCompositionAsync(
"my-compo",
new Composition
{
ObjectID = "my-compo",
Name = "my composition",
Behavior = new CompositionBehavior(
new CompositionMultifeedBehavior
{
Multifeed = new Multifeed
{
Feeds = new Dictionary
{
{
"products",
new FeedInjection
{
Injection = new Injection
{
Main = new InjectionMain
{
Source = new InjectionMainSource(
new InjectionMainSearchSource
{
Search = new MainSearch
{
Index = "products",
Params = new MainInjectionQueryParameters { HitsPerPage = 12 },
},
}
),
},
InjectedItems = new List
{
new InjectionInjectedItem
{
Key = "featured-products",
Source = new InjectedItemSource(
new InjectedItemSearchSource
{
Search = new InjectedItemSearch
{
Index = "products",
Params = new BaseInjectionQueryParameters
{
Filters = "featured:true",
},
},
}
),
Position = 0,
Length = 2,
},
},
},
}
},
{
"articles",
new FeedInjection
{
Injection = new Injection
{
Main = new InjectionMain
{
Source = new InjectionMainSource(
new InjectionMainSearchSource
{
Search = new MainSearch
{
Index = "articles",
Params = new MainInjectionQueryParameters
{
HitsPerPage = 5,
AttributesToRetrieve = new List
{
"title",
"excerpt",
"publishedAt",
},
},
},
}
),
},
InjectedItems = new List
{
new InjectionInjectedItem
{
Key = "editorial-picks",
Source = new InjectedItemSource(
new InjectedItemSearchSource
{
Search = new InjectedItemSearch
{
Index = "articles",
Params = new BaseInjectionQueryParameters
{
Filters = "editorial_pick:true",
},
},
}
),
Position = 0,
Length = 1,
},
},
},
}
},
{
"videos",
new FeedInjection
{
Injection = new Injection
{
Main = new InjectionMain
{
Source = new InjectionMainSource(
new InjectionMainSearchSource
{
Search = new MainSearch
{
Index = "videos",
Params = new MainInjectionQueryParameters
{
HitsPerPage = 3,
AttributesToRetrieve = new List
{
"title",
"thumbnail",
"duration",
},
},
},
}
),
},
},
}
},
},
FeedsOrder = new List { "products", "articles", "videos" },
},
}
),
}
);
```
```dart Dart theme={"system"}
final response = await client.putComposition(
compositionID: "my-compo",
composition: Composition(
objectID: "my-compo",
name: "my composition",
behavior: CompositionMultifeedBehavior(
multifeed: Multifeed(
feeds: {
'products': FeedInjection(
injection: Injection(
main: InjectionMain(
source: InjectionMainSearchSource(
search: MainSearch(
index: "products",
params: MainInjectionQueryParameters(
hitsPerPage: 12,
),
),
),
),
injectedItems: [
InjectionInjectedItem(
key: "featured-products",
source: InjectedItemSearchSource(
search: InjectedItemSearch(
index: "products",
params: BaseInjectionQueryParameters(
filters: "featured:true",
),
),
),
position: 0,
length: 2,
),
],
),
),
'articles': FeedInjection(
injection: Injection(
main: InjectionMain(
source: InjectionMainSearchSource(
search: MainSearch(
index: "articles",
params: MainInjectionQueryParameters(
hitsPerPage: 5,
attributesToRetrieve: [
"title",
"excerpt",
"publishedAt",
],
),
),
),
),
injectedItems: [
InjectionInjectedItem(
key: "editorial-picks",
source: InjectedItemSearchSource(
search: InjectedItemSearch(
index: "articles",
params: BaseInjectionQueryParameters(
filters: "editorial_pick:true",
),
),
),
position: 0,
length: 1,
),
],
),
),
'videos': FeedInjection(
injection: Injection(
main: InjectionMain(
source: InjectionMainSearchSource(
search: MainSearch(
index: "videos",
params: MainInjectionQueryParameters(
hitsPerPage: 3,
attributesToRetrieve: [
"title",
"thumbnail",
"duration",
],
),
),
),
),
),
),
},
feedsOrder: [
"products",
"articles",
"videos",
],
),
),
),
);
```
```go Go theme={"system"}
response, err := client.PutComposition(client.NewApiPutCompositionRequest(
"my-compo",
composition.NewEmptyComposition().
SetObjectID("my-compo").
SetName("my composition").
SetBehavior(composition.CompositionMultifeedBehaviorAsCompositionBehavior(
composition.NewEmptyCompositionMultifeedBehavior().SetMultifeed(
composition.NewEmptyMultifeed().
SetFeeds(map[string]composition.FeedInjection{"products": *composition.NewEmptyFeedInjection().SetInjection(
composition.NewEmptyInjection().SetMain(
composition.NewEmptyInjectionMain().SetSource(composition.InjectionMainSearchSourceAsInjectionMainSource(
composition.NewEmptyInjectionMainSearchSource().SetSearch(
composition.NewEmptyMainSearch().SetIndex("products").SetParams(
composition.NewEmptyMainInjectionQueryParameters().SetHitsPerPage(12)))))).SetInjectedItems(
[]composition.InjectionInjectedItem{*composition.NewEmptyInjectionInjectedItem().SetKey("featured-products").SetSource(composition.InjectedItemSearchSourceAsInjectedItemSource(
composition.NewEmptyInjectedItemSearchSource().SetSearch(
composition.NewEmptyInjectedItemSearch().SetIndex("products").SetParams(
composition.NewEmptyBaseInjectionQueryParameters().SetFilters("featured:true"))))).SetPosition(0).SetLength(2)})), "articles": *composition.NewEmptyFeedInjection().SetInjection(
composition.NewEmptyInjection().SetMain(
composition.NewEmptyInjectionMain().SetSource(composition.InjectionMainSearchSourceAsInjectionMainSource(
composition.NewEmptyInjectionMainSearchSource().SetSearch(
composition.NewEmptyMainSearch().SetIndex("articles").SetParams(
composition.NewEmptyMainInjectionQueryParameters().SetHitsPerPage(5).SetAttributesToRetrieve(
[]string{"title", "excerpt", "publishedAt"})))))).SetInjectedItems(
[]composition.InjectionInjectedItem{*composition.NewEmptyInjectionInjectedItem().SetKey("editorial-picks").SetSource(composition.InjectedItemSearchSourceAsInjectedItemSource(
composition.NewEmptyInjectedItemSearchSource().SetSearch(
composition.NewEmptyInjectedItemSearch().SetIndex("articles").SetParams(
composition.NewEmptyBaseInjectionQueryParameters().SetFilters("editorial_pick:true"))))).SetPosition(0).SetLength(1)})), "videos": *composition.NewEmptyFeedInjection().SetInjection(
composition.NewEmptyInjection().SetMain(
composition.NewEmptyInjectionMain().SetSource(composition.InjectionMainSearchSourceAsInjectionMainSource(
composition.NewEmptyInjectionMainSearchSource().SetSearch(
composition.NewEmptyMainSearch().SetIndex("videos").SetParams(
composition.NewEmptyMainInjectionQueryParameters().SetHitsPerPage(3).SetAttributesToRetrieve(
[]string{"title", "thumbnail", "duration"})))))))}).
SetFeedsOrder(
[]string{"products", "articles", "videos"}),
),
)),
))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
TaskIDResponse response = client.putComposition(
"my-compo",
new Composition()
.setObjectID("my-compo")
.setName("my composition")
.setBehavior(
new CompositionMultifeedBehavior().setMultifeed(
new Multifeed()
.setFeeds(
new HashMap() {
{
put(
"products",
new FeedInjection().setInjection(
new Injection()
.setMain(
new InjectionMain().setSource(
new InjectionMainSearchSource().setSearch(
new MainSearch().setIndex("products").setParams(new MainInjectionQueryParameters().setHitsPerPage(12))
)
)
)
.setInjectedItems(
Arrays.asList(
new InjectionInjectedItem()
.setKey("featured-products")
.setSource(
new InjectedItemSearchSource().setSearch(
new InjectedItemSearch()
.setIndex("products")
.setParams(new BaseInjectionQueryParameters().setFilters("featured:true"))
)
)
.setPosition(0)
.setLength(2)
)
)
)
);
put(
"articles",
new FeedInjection().setInjection(
new Injection()
.setMain(
new InjectionMain().setSource(
new InjectionMainSearchSource().setSearch(
new MainSearch()
.setIndex("articles")
.setParams(
new MainInjectionQueryParameters()
.setHitsPerPage(5)
.setAttributesToRetrieve(Arrays.asList("title", "excerpt", "publishedAt"))
)
)
)
)
.setInjectedItems(
Arrays.asList(
new InjectionInjectedItem()
.setKey("editorial-picks")
.setSource(
new InjectedItemSearchSource().setSearch(
new InjectedItemSearch()
.setIndex("articles")
.setParams(new BaseInjectionQueryParameters().setFilters("editorial_pick:true"))
)
)
.setPosition(0)
.setLength(1)
)
)
)
);
put(
"videos",
new FeedInjection().setInjection(
new Injection().setMain(
new InjectionMain().setSource(
new InjectionMainSearchSource().setSearch(
new MainSearch()
.setIndex("videos")
.setParams(
new MainInjectionQueryParameters()
.setHitsPerPage(3)
.setAttributesToRetrieve(Arrays.asList("title", "thumbnail", "duration"))
)
)
)
)
)
);
}
}
)
.setFeedsOrder(Arrays.asList("products", "articles", "videos"))
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.putComposition({
compositionID: 'my-compo',
composition: {
objectID: 'my-compo',
name: 'my composition',
behavior: {
multifeed: {
feeds: {
products: {
injection: {
main: { source: { search: { index: 'products', params: { hitsPerPage: 12 } } } },
injectedItems: [
{
key: 'featured-products',
source: { search: { index: 'products', params: { filters: 'featured:true' } } },
position: 0,
length: 2,
},
],
},
},
articles: {
injection: {
main: {
source: {
search: {
index: 'articles',
params: { hitsPerPage: 5, attributesToRetrieve: ['title', 'excerpt', 'publishedAt'] },
},
},
},
injectedItems: [
{
key: 'editorial-picks',
source: { search: { index: 'articles', params: { filters: 'editorial_pick:true' } } },
position: 0,
length: 1,
},
],
},
},
videos: {
injection: {
main: {
source: {
search: {
index: 'videos',
params: { hitsPerPage: 3, attributesToRetrieve: ['title', 'thumbnail', 'duration'] },
},
},
},
},
},
},
feedsOrder: ['products', 'articles', 'videos'],
},
},
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.putComposition(
compositionID = "my-compo",
composition =
Composition(
objectID = "my-compo",
name = "my composition",
behavior =
CompositionMultifeedBehavior(
multifeed =
Multifeed(
feeds =
mapOf(
"products" to
FeedInjection(
injection =
Injection(
main =
InjectionMain(
source =
InjectionMainSearchSource(
search =
MainSearch(
index = "products",
params = MainInjectionQueryParameters(hitsPerPage = 12),
)
)
),
injectedItems =
listOf(
InjectionInjectedItem(
key = "featured-products",
source =
InjectedItemSearchSource(
search =
InjectedItemSearch(
index = "products",
params =
BaseInjectionQueryParameters(
filters = "featured:true"
),
)
),
position = 0,
length = 2,
)
),
)
),
"articles" to
FeedInjection(
injection =
Injection(
main =
InjectionMain(
source =
InjectionMainSearchSource(
search =
MainSearch(
index = "articles",
params =
MainInjectionQueryParameters(
hitsPerPage = 5,
attributesToRetrieve =
listOf("title", "excerpt", "publishedAt"),
),
)
)
),
injectedItems =
listOf(
InjectionInjectedItem(
key = "editorial-picks",
source =
InjectedItemSearchSource(
search =
InjectedItemSearch(
index = "articles",
params =
BaseInjectionQueryParameters(
filters = "editorial_pick:true"
),
)
),
position = 0,
length = 1,
)
),
)
),
"videos" to
FeedInjection(
injection =
Injection(
main =
InjectionMain(
source =
InjectionMainSearchSource(
search =
MainSearch(
index = "videos",
params =
MainInjectionQueryParameters(
hitsPerPage = 3,
attributesToRetrieve =
listOf("title", "thumbnail", "duration"),
),
)
)
)
)
),
),
feedsOrder = listOf("products", "articles", "videos"),
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->putComposition(
'my-compo',
['objectID' => 'my-compo',
'name' => 'my composition',
'behavior' => ['multifeed' => ['feeds' => ['products' => ['injection' => ['main' => ['source' => ['search' => ['index' => 'products',
'params' => ['hitsPerPage' => 12,
],
],
],
],
'injectedItems' => [
['key' => 'featured-products',
'source' => ['search' => ['index' => 'products',
'params' => ['filters' => 'featured:true',
],
],
],
'position' => 0,
'length' => 2,
],
],
],
],
'articles' => ['injection' => ['main' => ['source' => ['search' => ['index' => 'articles',
'params' => ['hitsPerPage' => 5,
'attributesToRetrieve' => [
'title',
'excerpt',
'publishedAt',
],
],
],
],
],
'injectedItems' => [
['key' => 'editorial-picks',
'source' => ['search' => ['index' => 'articles',
'params' => ['filters' => 'editorial_pick:true',
],
],
],
'position' => 0,
'length' => 1,
],
],
],
],
'videos' => ['injection' => ['main' => ['source' => ['search' => ['index' => 'videos',
'params' => ['hitsPerPage' => 3,
'attributesToRetrieve' => [
'title',
'thumbnail',
'duration',
],
],
],
],
],
],
],
],
'feedsOrder' => [
'products',
'articles',
'videos',
],
],
],
],
);
```
```python Python theme={"system"}
response = client.put_composition(
composition_id="my-compo",
composition={
"objectID": "my-compo",
"name": "my composition",
"behavior": {
"multifeed": {
"feeds": {
"products": {
"injection": {
"main": {
"source": {
"search": {
"index": "products",
"params": {
"hitsPerPage": 12,
},
},
},
},
"injectedItems": [
{
"key": "featured-products",
"source": {
"search": {
"index": "products",
"params": {
"filters": "featured:true",
},
},
},
"position": 0,
"length": 2,
},
],
},
},
"articles": {
"injection": {
"main": {
"source": {
"search": {
"index": "articles",
"params": {
"hitsPerPage": 5,
"attributesToRetrieve": [
"title",
"excerpt",
"publishedAt",
],
},
},
},
},
"injectedItems": [
{
"key": "editorial-picks",
"source": {
"search": {
"index": "articles",
"params": {
"filters": "editorial_pick:true",
},
},
},
"position": 0,
"length": 1,
},
],
},
},
"videos": {
"injection": {
"main": {
"source": {
"search": {
"index": "videos",
"params": {
"hitsPerPage": 3,
"attributesToRetrieve": [
"title",
"thumbnail",
"duration",
],
},
},
},
},
},
},
},
"feedsOrder": [
"products",
"articles",
"videos",
],
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.put_composition(
"my-compo",
Algolia::Composition::Composition.new(
algolia_object_id: "my-compo",
name: "my composition",
behavior: Algolia::Composition::CompositionMultifeedBehavior.new(
multifeed: Algolia::Composition::Multifeed.new(
feeds: {
products: Algolia::Composition::FeedInjection.new(
injection: Algolia::Composition::Injection.new(
main: Algolia::Composition::InjectionMain.new(
source: Algolia::Composition::InjectionMainSearchSource.new(
search: Algolia::Composition::MainSearch.new(
index: "products",
params: Algolia::Composition::MainInjectionQueryParameters.new(hits_per_page: 12)
)
)
),
injected_items: [
Algolia::Composition::InjectionInjectedItem.new(
key: "featured-products",
source: Algolia::Composition::InjectedItemSearchSource.new(
search: Algolia::Composition::InjectedItemSearch.new(
index: "products",
params: Algolia::Composition::BaseInjectionQueryParameters.new(filters: "featured:true")
)
),
position: 0,
length: 2
)
]
)
),
articles: Algolia::Composition::FeedInjection.new(
injection: Algolia::Composition::Injection.new(
main: Algolia::Composition::InjectionMain.new(
source: Algolia::Composition::InjectionMainSearchSource.new(
search: Algolia::Composition::MainSearch.new(
index: "articles",
params: Algolia::Composition::MainInjectionQueryParameters.new(
hits_per_page: 5,
attributes_to_retrieve: ["title", "excerpt", "publishedAt"]
)
)
)
),
injected_items: [
Algolia::Composition::InjectionInjectedItem.new(
key: "editorial-picks",
source: Algolia::Composition::InjectedItemSearchSource.new(
search: Algolia::Composition::InjectedItemSearch.new(
index: "articles",
params: Algolia::Composition::BaseInjectionQueryParameters.new(filters: "editorial_pick:true")
)
),
position: 0,
length: 1
)
]
)
),
videos: Algolia::Composition::FeedInjection.new(
injection: Algolia::Composition::Injection.new(
main: Algolia::Composition::InjectionMain.new(
source: Algolia::Composition::InjectionMainSearchSource.new(
search: Algolia::Composition::MainSearch.new(
index: "videos",
params: Algolia::Composition::MainInjectionQueryParameters.new(
hits_per_page: 3,
attributes_to_retrieve: ["title", "thumbnail", "duration"]
)
)
)
)
)
)
},
feeds_order: ["products", "articles", "videos"]
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.putComposition(
compositionID = "my-compo",
composition = Composition(
objectID = "my-compo",
name = "my composition",
behavior = CompositionMultifeedBehavior(
multifeed = Multifeed(
feeds = Map(
"products" -> FeedInjection(
injection = Injection(
main = InjectionMain(
source = Some(
InjectionMainSearchSource(
search = MainSearch(
index = "products",
params = Some(
MainInjectionQueryParameters(
hitsPerPage = Some(12)
)
)
)
)
)
),
injectedItems = Some(
Seq(
InjectionInjectedItem(
key = "featured-products",
source = InjectedItemSearchSource(
search = InjectedItemSearch(
index = "products",
params = Some(
BaseInjectionQueryParameters(
filters = Some("featured:true")
)
)
)
),
position = 0,
length = 2
)
)
)
)
),
"articles" -> FeedInjection(
injection = Injection(
main = InjectionMain(
source = Some(
InjectionMainSearchSource(
search = MainSearch(
index = "articles",
params = Some(
MainInjectionQueryParameters(
hitsPerPage = Some(5),
attributesToRetrieve = Some(Seq("title", "excerpt", "publishedAt"))
)
)
)
)
)
),
injectedItems = Some(
Seq(
InjectionInjectedItem(
key = "editorial-picks",
source = InjectedItemSearchSource(
search = InjectedItemSearch(
index = "articles",
params = Some(
BaseInjectionQueryParameters(
filters = Some("editorial_pick:true")
)
)
)
),
position = 0,
length = 1
)
)
)
)
),
"videos" -> FeedInjection(
injection = Injection(
main = InjectionMain(
source = Some(
InjectionMainSearchSource(
search = MainSearch(
index = "videos",
params = Some(
MainInjectionQueryParameters(
hitsPerPage = Some(3),
attributesToRetrieve = Some(Seq("title", "thumbnail", "duration"))
)
)
)
)
)
)
)
)
),
feedsOrder = Some(Seq("products", "articles", "videos"))
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.putComposition(
compositionID: "my-compo",
composition: Composition(
objectID: "my-compo",
name: "my composition",
behavior: CompositionBehavior
.compositionMultifeedBehavior(CompositionMultifeedBehavior(multifeed: Multifeed(
feeds: [
"products": FeedInjection(injection: Injection(
main: InjectionMain(source: InjectionMainSource
.injectionMainSearchSource(InjectionMainSearchSource(search: MainSearch(
index: "products",
params: MainInjectionQueryParameters(hitsPerPage: 12)
)))),
injectedItems: [InjectionInjectedItem(
key: "featured-products",
source: InjectedItemSource
.injectedItemSearchSource(InjectedItemSearchSource(search: InjectedItemSearch(
index: "products",
params: BaseInjectionQueryParameters(filters: "featured:true")
))),
position: 0,
length: 2
)]
)),
"articles": FeedInjection(injection: Injection(
main: InjectionMain(source: InjectionMainSource
.injectionMainSearchSource(InjectionMainSearchSource(search: MainSearch(
index: "articles",
params: MainInjectionQueryParameters(
attributesToRetrieve: ["title", "excerpt", "publishedAt"],
hitsPerPage: 5
)
)))),
injectedItems: [InjectionInjectedItem(
key: "editorial-picks",
source: InjectedItemSource
.injectedItemSearchSource(InjectedItemSearchSource(search: InjectedItemSearch(
index: "articles",
params: BaseInjectionQueryParameters(filters: "editorial_pick:true")
))),
position: 0,
length: 1
)]
)),
"videos": FeedInjection(injection: Injection(main: InjectionMain(source: InjectionMainSource
.injectionMainSearchSource(InjectionMainSearchSource(search: MainSearch(
index: "videos",
params: MainInjectionQueryParameters(
attributesToRetrieve: ["title", "thumbnail", "duration"],
hitsPerPage: 3
)
)))))),
],
feedsOrder: ["products", "articles", "videos"]
)))
)
)
```
### Key parameters
* `feeds`: A map of feed definitions, where each key is the feed ID and each value is a behavior, typically an `injection`
* `feedsOrder` (optional): An array specifying the order feeds appear in the response. If omitted, feeds return in alphabetical order by feed ID
## Query a multifeed composition
Query a multifeed composition the same way as a standard composition using the [Run a Composition endpoint](/doc/rest-api/composition/search).
Use the optional `feedsOrder` parameter to reorder, enable, or turn feeds off at run time.
This overrides the value defined in the composition itself.
```cs C# theme={"system"}
var response = await client.SearchAsync(
"foo",
new RequestBody
{
Params = new Params { Query = "batman" },
FeedsOrder = new List { "feed-movies", "feed-comics" },
}
);
```
```dart Dart theme={"system"}
final response = await client.search(
compositionID: "foo",
requestBody: RequestBody(
params: Params(
query: "batman",
),
feedsOrder: [
"feed-movies",
"feed-comics",
],
),
);
```
```go Go theme={"system"}
response, err := client.Search(client.NewApiSearchRequest(
"foo",
composition.NewEmptyRequestBody().SetParams(
composition.NewEmptyParams().SetQuery("batman")).SetFeedsOrder(
[]string{"feed-movies", "feed-comics"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.search(
"foo",
new RequestBody().setParams(new Params().setQuery("batman")).setFeedsOrder(Arrays.asList("feed-movies", "feed-comics")),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.search({
compositionID: 'foo',
requestBody: { params: { query: 'batman' }, feedsOrder: ['feed-movies', 'feed-comics'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.search(
compositionID = "foo",
requestBody =
RequestBody(
params = Params(query = "batman"),
feedsOrder = listOf("feed-movies", "feed-comics"),
),
)
```
```php PHP theme={"system"}
$response = $client->search(
'foo',
['params' => ['query' => 'batman',
],
'feedsOrder' => [
'feed-movies',
'feed-comics',
],
],
);
```
```python Python theme={"system"}
response = client.search(
composition_id="foo",
request_body={
"params": {
"query": "batman",
},
"feedsOrder": [
"feed-movies",
"feed-comics",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search(
"foo",
Algolia::Composition::RequestBody.new(
params: Algolia::Composition::Params.new(query: "batman"),
feeds_order: ["feed-movies", "feed-comics"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.search(
compositionID = "foo",
requestBody = RequestBody(
params = Some(
Params(
query = Some("batman")
)
),
feedsOrder = Some(Seq("feed-movies", "feed-comics"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: CompositionSearchResponse = try await client.search(
compositionID: "foo",
requestBody: RequestBody(
params: CompositionParams(query: "batman"),
feedsOrder: ["feed-movies", "feed-comics"]
)
)
```
The response includes one result set per feed.
Algolia orders the feeds based on the `feedsOrder` value.
Each feed's result set includes the corresponding `feedID` and Smart Groups injected into its .
Deduplication applies within that feed.
```json JSON expandable icon=braces theme={"system"}
{
"results": [
{
"feedID": "products",
"hits": [
{
"objectID": "prod-123",
"title": "Professional Camera",
"featured": true
},
{
"objectID": "prod-456",
"title": "Camera Lens Kit",
"featured": true
},
{ "objectID": "prod-789", "title": "Camera Tripod" },
{ "objectID": "prod-012", "title": "Memory Card" }
],
"nbHits": 87,
"page": 0,
"nbPages": 8,
"hitsPerPage": 12,
"query": "camera"
},
{
"feedID": "articles",
"hits": [
{
"objectID": "article-111",
"title": "How to Choose Your First Camera",
"excerpt": "A beginner's guide...",
"editorial_pick": true
},
{
"objectID": "article-222",
"title": "Camera Settings Guide",
"excerpt": "Master exposure..."
},
{
"objectID": "article-333",
"title": "Camera Maintenance Tips",
"excerpt": "Keep your equipment..."
}
],
"nbHits": 24,
"page": 0,
"nbPages": 5,
"hitsPerPage": 5,
"query": "camera"
},
{
"feedID": "videos",
"hits": [
{
"objectID": "video-555",
"title": "Camera Setup Tutorial",
"duration": 420
},
{
"objectID": "video-666",
"title": "Product Review: Latest Camera",
"duration": 615
}
],
"nbHits": 15,
"page": 0,
"nbPages": 5,
"hitsPerPage": 3,
"query": "camera"
}
]
}
```
## Deduplication and facet management
Algolia processes each feed in a multifeed composition independently, as a complete result set. This means:
* **Deduplication** applies within each feed according to your [deduplication strategy](/doc/guides/compositions/smart-groups/deduplication-strategy). This prevents duplicate items between Smart Groups and in that feed
* **Facet values** are computed based on each feed's configuration and results
* **Pagination** works independently for each feed
Items can appear in more than one feed if they match the criteria for different feeds.
For example, the same product could appear in both a "featured products" feed and a "new arrivals" feed.
## Integrate with InstantSearch
Before integrating multifeed compositions with InstantSearch, make sure you've completed the [base composition setup](/doc/guides/compositions/smart-groups/search-based-groups#integrate-compositions-with-instantsearch).
This includes installing the Composition API client and configuring your `compositionID`.
Multifeed support requires these versions of the InstantSearch libraries:
* React InstantSearch: version 7.32.0 or later
* InstantSearch.js: version 4.96.0 or later
* Vue InstantSearch: version 4.26.0 or later
Use the `Feeds` widget to render each feed from a multifeed composition as an independent result set.
Each feed gets its own widget scope, so widgets like `Hits` or `RefinementList` inside a feed only see that feed's results.
```jsx React expandable theme={"system"}
import { Feeds, Hits, Highlight } from "react-instantsearch";
function Search() {
return (
(
```
### `useFeeds` Hook (React)
The `useFeeds` Hook gives you direct access to the list of feed IDs without rendering feeds.
This is useful for building custom UI around feeds, such as tab navigation or feed counts.
```jsx React icon=code theme={"system"}
import { useFeeds, Feeds } from "react-instantsearch";
function TabbedFeeds() {
const { feedIDs } = useFeeds({ searchScope: "global" });
const [activeTab, setActiveTab] = useState(feedIDs[0]);
return (
<>
(
```
```ios iOS theme={"system"}
struct Banner: Decodable {
let title: String
}
class BannerViewController: UIViewController, ItemController {
let titleLabel: UILabel = .init()
override func viewDidLoad() {
super.viewDidLoad()
// some layout code
}
func setItem(_ item: Banner?) {
titleLabel.text = item?.title
}
}
func setupWidget() {
let searcher = HitsSearcher(appID: "undefined",
apiKey: "undefined",
indexName: "YourIndexName")
let bannerViewController = BannerViewController()
let queryRuleCustomDataConnector = QueryRuleCustomDataConnector(searcher: searcher,
controller: bannerViewController)
searcher.search()
}
```
```kotlin Android theme={"system"}
@Serializable
data class Banner(val title: String)
class MyActivity: AppCompatActivity() {
val searcher = HitsSearcher(
applicationID = "YourApplicationID",
apiKey = "YourAPIKey",
indexName = "YourIndexName"
)
val searchBox = SearchBoxConnector(searcher, searchMode = SearchMode.AsYouType)
val queryRuleCustomData = QueryRuleCustomDataConnector(searcher)
val connection = ConnectionHandler(searchBox, queryRuleCustomData)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bannerView = TextView(this)
queryRuleCustomData.subscribe { banner ->
bannerView.text = banner?.title
}
searcher.searchAsync()
}
override fun onDestroy() {
super.onDestroy()
searcher.cancel()
connection.clear()
}
}
```
For more information, see the InstantSearch API reference:
* InstantSearch.js: [`queryRuleCustomData`](/doc/api-reference/widgets/query-rule-custom-data/js)
* React InstantSearch: [`QueryRulesCustomData`](/doc/api-reference/widgets/query-rule-custom-data/react)
* Vue InstantSearch: [`ais-query-rule-custom-data`](/doc/api-reference/widgets/query-rule-custom-data/vue)
### Apply rules with context conditions
If you want to apply rules based on context, see:
* InstantSearch.js: [`queryRuleContext`](/doc/api-reference/widgets/query-rule-context/js)
* React InstantSearch: [`QueryRuleContext`](/doc/api-reference/widgets/query-rule-context/react)
* Vue InstantSearch: [`ais-query-rule-context`](/doc/api-reference/widgets/query-rule-context/vue)
## Determine priority when you have multiple banners
You may have several [semi-permanent promotional banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-category-pages#example-display-banners-on-category-pages) for
specific [product categories](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages),
as well as some time-limited, temporary banners for certain products.
For instance, an online IT hardware store has banners for the following categories:
* Phones
* Tablet computers
* Laptop computers
* Desktop computers
They also have temporary offers and associated banners for the phrases and products:
* Infinix Note 40 Pro
* Xiaomi Redmi Note 14 Pro+
* Samsung Galaxy S25 Ultra
* Samsung Galaxy Tab S10 Ultra
* Samsung Galaxy Book3 360
To decide which banner to show, Algolia uses a [tie-breaking algorithm](/doc/guides/managing-results/rules/rules-overview/in-depth/rule-matching-algorithm) when multiple rules match.
Using the preceding list of products as an example:
1. User starts on the Phones category page and sees the Phones banner.
2. They search for `note`.
3. At this point, three rules can match: the Phones category banner and the two product banners for **Infinix Note 40 Pro** and **Xiaomi Redmi Note 14 Pro+**.
4. The two product banners are more specific than the category banner, so they take precedence over it.
5. When multiple rules match, Algolia compares their `objectID` values and shows the banner whose `objectID` comes first alphabetically.
In this context, `objectID` refers to the rule's ID, not to any product or record `objectID` in your index.
Rule objectIDs are unique identifiers for rules.
When using the API, you can set them manually.
In the dashboard, they're generated automatically.
To control this tie-breaker, change the rule's `objectID` with the [`saveRule`](/doc/libraries/sdk/methods/search/save-rule) method.
# Multiple consequences
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/combining-multiple-consequences
How to combine multiple Rules on the same query.
When creating a rule, **it's not uncommon to set more than one consequence**.
A good example is when you want to use a search term as a or a value but not for textual search.
Consider a website where users can search for movies by title, actor, or director.
Some actors, like
Clint Eastwood are also directors: people searching for "clint eastwood" might not be looking for the same thing. For example, people looking only for movies directed by Clint Eastwood might search for "clint eastwood director",
hoping to filter out every movie that Clint Eastwood plays in but didn't direct.
In this case, you wouldn't be interested in the word "director for textual search purposes, but as a cue to set a filter.
You would therefore want to remove the word "director" from the so that Algolia doesn't textually look for matching that word.
You can achieve this by **combining several consequences in your rule**.
## Dataset example
In the following dataset, some movies have Clint Eastwood as an actor, some as a director, and some have both.
```json JSON icon=braces theme={"system"}
[
{
"title": "The Good, the Bad and the Ugly",
"director": "Sergio Leone",
"actors": ["Clint Eastwood", "Lee Van Cleef", "Elli Wallach"]
},
{
"title": "Dirty Harry",
"director": "Don Siegel",
"actors": ["Clint Eastwood", "Andy Robinson", "Harry Guardino"]
},
{
"title": "Million Dollar Baby",
"director": "Clint Eastwood",
"actors": ["Clint Eastwood", "Hilary Swank", "Morgan Freeman"]
},
{
"title": "Invictus",
"director": "Clint Eastwood",
"actors": ["Morgan Freeman", "Matt Damon", "Tony Kgoroge"]
}
]
```
If someone searches for
"clint eastwood", they would retrieve all movies by or with Clint Eastwood, even if they were only interested in movies with Clint Eastwood as the director.
You could solve this problem with faceting, by setting `director` as an attribute for faceting and providing a refinement list with its own search input on your frontend.
Yet, an even better way of helping your users would be to make your search smart and look only for movies where `director` is equal to "Clint Eastwood" whenever they type "clint eastwood director".
To achieve this, you first need to set `director` in your list of attributes for faceting.
Then, you can create a new Rule that filters on matching attributes.
You need to create two consequences whenever a user types in "clint eastwood director":
* Filter all results with "director"
* Remove the word "director" from the search
## Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
1. Set `director` as [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting). This happens at indexing time.
2. Set a Rule that:
* Detects the term "director" in a query whenever it comes after a term that matches the name of a director
* Applies a filter on facet value `director:'Clint Eastwood'`.
To do this, use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method.
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { AttributesForFaceting = new List { "director" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"director",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"director"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("director"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['director'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("director")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'director',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"director",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["director"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("director"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["director"])
)
```
However, setting the Rule isn't enough.
Even when you set a Rule, the engine still tries to match the terms in the query.
If a user searches for
"clint eastwood director",
even if your Rule applies, the engine will look for the word "director" in the records. With the example dataset, this wouldn't return any results at all. This is where combining consequences comes in handy.
When this Rule applies, you can add another consequence to remove the word "director" from the query.
This way, the engine will only use it for filtering, not search.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"director-rule",
new Rule
{
ObjectID = "director-rule",
Conditions = new List
{
new Condition
{
Pattern = "{facet:director} director",
Anchoring = Enum.Parse("Contains"),
},
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
RestrictSearchableAttributes = new List { "title", "book_id" },
AutomaticFacetFilters = new AutomaticFacetFilters(
new List { new AutomaticFacetFilter { Facet = "director" } }
),
Query = new ConsequenceQuery(
new ConsequenceQueryObject
{
Edits = new List
{
new Edit { Type = Enum.Parse("Remove"), Delete = "director" },
},
}
),
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "director-rule",
rule: Rule(
objectID: "director-rule",
conditions: [
Condition(
pattern: "{facet:director} director",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
params: ConsequenceParams(
restrictSearchableAttributes: [
"title",
"book_id",
],
automaticFacetFilters: [
AutomaticFacetFilter(
facet: "director",
),
],
query: ConsequenceQueryObject(
edits: [
Edit(
type: EditType.fromJson("remove"),
delete: "director",
),
],
),
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "director-rule",
search.NewEmptyRule().SetObjectID("director-rule").SetConditions(
[]search.Condition{
*search.NewEmptyCondition().SetPattern("{facet:director} director").SetAnchoring(search.Anchoring("contains")),
}).
SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetRestrictSearchableAttributes(
[]string{"title", "book_id"}).SetAutomaticFacetFilters(search.ArrayOfAutomaticFacetFilterAsAutomaticFacetFilters(
[]search.AutomaticFacetFilter{
*search.NewEmptyAutomaticFacetFilter().SetFacet("director"),
},
)).SetQuery(search.ConsequenceQueryObjectAsConsequenceQuery(
search.NewEmptyConsequenceQueryObject().SetEdits(
[]search.Edit{*search.NewEmptyEdit().SetType(search.EditType("remove")).SetDelete("director")})))))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"director-rule",
new Rule()
.setObjectID("director-rule")
.setConditions(Arrays.asList(new Condition().setPattern("{facet:director} director").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setParams(
new ConsequenceParams()
.setRestrictSearchableAttributes(Arrays.asList("title", "book_id"))
.setAutomaticFacetFilters(
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(Arrays.asList(new AutomaticFacetFilter().setFacet("director")))
)
.setQuery(new ConsequenceQueryObject().setEdits(Arrays.asList(new Edit().setType(EditType.REMOVE).setDelete("director"))))
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'director-rule',
rule: {
objectID: 'director-rule',
conditions: [{ pattern: '{facet:director} director', anchoring: 'contains' }],
consequence: {
params: {
restrictSearchableAttributes: ['title', 'book_id'],
automaticFacetFilters: [{ facet: 'director' }],
query: { edits: [{ type: 'remove', delete: 'director' }] },
},
},
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "director-rule",
rule =
Rule(
objectID = "director-rule",
conditions =
listOf(
Condition(
pattern = "{facet:director} director",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
params =
ConsequenceParams(
restrictSearchableAttributes = listOf("title", "book_id"),
automaticFacetFilters =
AutomaticFacetFilters.ofListOfAutomaticFacetFilter(
listOf(AutomaticFacetFilter(facet = "director"))
),
query =
ConsequenceQueryObject(
edits =
listOf(
Edit(
type = EditType.entries.first { it.value == "remove" },
delete = "director",
)
)
),
)
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'director-rule',
['objectID' => 'director-rule',
'conditions' => [
['pattern' => '{facet:director} director',
'anchoring' => 'contains',
],
],
'consequence' => ['params' => ['restrictSearchableAttributes' => [
'title',
'book_id',
],
'automaticFacetFilters' => [
['facet' => 'director',
],
],
'query' => ['edits' => [
['type' => 'remove',
'delete' => 'director',
],
],
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="director-rule",
rule={
"objectID": "director-rule",
"conditions": [
{
"pattern": "{facet:director} director",
"anchoring": "contains",
},
],
"consequence": {
"params": {
"restrictSearchableAttributes": [
"title",
"book_id",
],
"automaticFacetFilters": [
{
"facet": "director",
},
],
"query": {
"edits": [
{
"type": "remove",
"delete": "director",
},
],
},
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"director-rule",
Algolia::Search::Rule.new(
algolia_object_id: "director-rule",
conditions: [Algolia::Search::Condition.new(pattern: "{facet:director} director", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(
restrict_searchable_attributes: ["title", "book_id"],
automatic_facet_filters: [Algolia::Search::AutomaticFacetFilter.new(facet: "director")],
query: Algolia::Search::ConsequenceQueryObject.new(
edits: [Algolia::Search::Edit.new(type: "remove", delete: "director")]
)
)
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "director-rule",
rule = Rule(
objectID = "director-rule",
conditions = Some(
Seq(
Condition(
pattern = Some("{facet:director} director"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
restrictSearchableAttributes = Some(Seq("title", "book_id")),
automaticFacetFilters = Some(
AutomaticFacetFilters(
Seq(
AutomaticFacetFilter(
facet = "director"
)
)
)
),
query = Some(
ConsequenceQueryObject(
edits = Some(
Seq(
Edit(
`type` = Some(EditType.withName("remove")),
delete = Some("director")
)
)
)
)
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "director-rule",
rule: Rule(
objectID: "director-rule",
conditions: [SearchCondition(
pattern: "{facet:director} director",
anchoring: SearchAnchoring.contains
)],
consequence: SearchConsequence(params: SearchConsequenceParams(
restrictSearchableAttributes: ["title", "book_id"],
query: SearchConsequenceQuery
.searchConsequenceQueryObject(SearchConsequenceQueryObject(edits: [SearchEdit(
type: SearchEditType.remove,
delete: "director"
)])),
automaticFacetFilters: SearchAutomaticFacetFilters
.arrayOfSearchAutomaticFacetFilter([SearchAutomaticFacetFilter(facet: "director")])
))
)
)
```
## Using the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
### Preparation
1. Select **Search** in the Algolia dashboard and select your .
2. Open **Configuration > Filtering and Faceting > Facets**.
3. Click **Add an attribute** and select `director`.
1) Open **Rules** and select **New rule > Visual Editor**.
2) Click **Set query condition(s)** and select **Contains**.
Enter `{` to select the `director` facet placeholder, then add the literal word `director` after it.
The pattern is `{facet:director} director`.
Click **Apply**.
3) In **Consequences**, expand **More consequences** and select **Filter / Boost matching attribute**.
Select `director` in **Facet**, choose **Filter**, and click **Apply**.
If your plan doesn't support boosting, select **Filter facets** instead.
4) Click **Add Consequence**, expand **More consequences**, and select **Remove word**.
In **Word(s)**, enter the literal word `director`, keeping the director's name in the search query.
Click **Apply**.
5) Preview `clint eastwood director`, click **Review and Publish**, and click **Publish**.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and select **Add Facet "director"**.
Add the literal word `director` after the facet placeholder so the pattern is `{facet:director} director`.
3. In **Consequence(s)**, click **Add consequence** and select **Filter/Boost Matching Attributes**.
Under **Filter**, select **Add Facet "director"**.
4. Click **Add consequence** again and select **Remove Word**.
Enter the literal word `director`.
5. Save your changes.
# Hide results with rules
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-hide-hits
Learn how to use rules to hide records based on the value of specific attributes.
Sometimes, you may want to use a rule to hide based on what users are searching for.
For example, consider a user searching for "cheap laptops" on an ecommerce site.
Without a rule, cheap and premium laptops will appear in the search results.
By applying a rule where:
* Condition: query contains "cheap"
* Consequence: hide items where the `product_range` attribute is `premium`
Users searching for cheap laptops will only see the more affordable options in the search results.
To hide one or more items, use one of the Algolia API clients or the Algolia dashboard's Visual Editor.
You can also hide individual items with the Algolia dashboard's Manual Editor.
You can hide up to 50 items per rule.
## Using an API client
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
This example uses the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method to hide a record if the query contains the word "cheap".
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"hide-12345",
new Rule
{
ObjectID = "hide-12345",
Conditions = new List
{
new Condition { Pattern = "cheap", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
Hide = new List { new ConsequenceHide { ObjectID = "to-hide-12345" } },
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "hide-12345",
rule: Rule(
objectID: "hide-12345",
conditions: [
Condition(
pattern: "cheap",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
hide: [
ConsequenceHide(
objectID: "to-hide-12345",
),
],
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "hide-12345",
search.NewEmptyRule().SetObjectID("hide-12345").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("cheap").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetHide(
[]search.ConsequenceHide{*search.NewEmptyConsequenceHide().SetObjectID("to-hide-12345")}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"hide-12345",
new Rule()
.setObjectID("hide-12345")
.setConditions(Arrays.asList(new Condition().setPattern("cheap").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(new Consequence().setHide(Arrays.asList(new ConsequenceHide().setObjectID("to-hide-12345"))))
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'hide-12345',
rule: {
objectID: 'hide-12345',
conditions: [{ pattern: 'cheap', anchoring: 'contains' }],
consequence: { hide: [{ objectID: 'to-hide-12345' }] },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "hide-12345",
rule =
Rule(
objectID = "hide-12345",
conditions =
listOf(
Condition(
pattern = "cheap",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence = Consequence(hide = listOf(ConsequenceHide(objectID = "to-hide-12345"))),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'hide-12345',
['objectID' => 'hide-12345',
'conditions' => [
['pattern' => 'cheap',
'anchoring' => 'contains',
],
],
'consequence' => ['hide' => [
['objectID' => 'to-hide-12345',
],
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="hide-12345",
rule={
"objectID": "hide-12345",
"conditions": [
{
"pattern": "cheap",
"anchoring": "contains",
},
],
"consequence": {
"hide": [
{
"objectID": "to-hide-12345",
},
],
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"hide-12345",
Algolia::Search::Rule.new(
algolia_object_id: "hide-12345",
conditions: [Algolia::Search::Condition.new(pattern: "cheap", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(
hide: [Algolia::Search::ConsequenceHide.new(algolia_object_id: "to-hide-12345")]
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "hide-12345",
rule = Rule(
objectID = "hide-12345",
conditions = Some(
Seq(
Condition(
pattern = Some("cheap"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
hide = Some(
Seq(
ConsequenceHide(
objectID = "to-hide-12345"
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "hide-12345",
rule: Rule(
objectID: "hide-12345",
conditions: [SearchCondition(pattern: "cheap", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(hide: [SearchConsequenceHide(objectID: "to-hide-12345")])
)
)
```
## Using the dashboard
Use the Visual Editor to hide items by attribute.
To hide individual items, use either the Visual Editor or the Manual Editor.
### Hide items by attribute
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
On the left sidebar, select **Search**, select your index, and open [**Rules**](https://dashboard.algolia.com/rules).
Select **Create your first rule** or **New rule**, then select **Visual Editor**.
2. Click **Set query condition(s)**, enter `cheap`, and click **Apply**.
3. In **Consequences**, click **Hide items** and select **Hide by attribute**.
Enter `product_range` as the attribute and `premium` as the value, then click **Apply**.
4. Review the results, click **Review and Publish**, and click **Publish**.
### Hide individual items
You can hide individual items using either editor.
1. Open **Rules** and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **contains**, enter `cheap`, and click **Apply**.
3. In **Consequences**, click **Hide items** and select **Hide by item**.
In **Search items to hide**, search for the record you want to hide and select it.
Click **Hide multiple items** to select more records, then click **Apply**.
4. Review the results, click **Review and Publish**, and click **Publish**.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected, select **Contains**, and enter `cheap`.
3. In **Consequence(s)**, click **Add consequence** and select **Hide an item**.
Select the item you want to hide.
4. Save your changes.
## See also
* [Add filters based on the query](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-custom-filter-for-a-specific-query)
* [Dynamic filtering with rules](/doc/guides/managing-results/rules/detecting-intent/how-to/applying-a-filter-if-the-query-match-a-facet-value)
# Promote records with optional filters
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-promote-with-optional-filters
Learn how to use optional filters to modify your custom ranking.
Optional filters are a powerful tool for influencing the ranking of search results.
Use them to boost or bury records based on specific criteria, but without hiding those records.
For example, on a website that handles fast food deliveries, if a user types "hungry" and clicks the "deliver quickly" option, the website will show the restaurants that can deliver the fastest at the top of the list but still show other restaurants.
You can also use negative optional filters to demote records.
For example, to lower the ranking of restaurants with poor reviews.
This feature isn't available on every plan.
Refer to your [pricing plan](https://www.algolia.com/pricing) to see if it's included.
## Update your records
Since optional filters only work with exact matches,
you might need to update your records.
Using the fast food delivery website as an example,
flag restaurants that can deliver within 20 minutes by adding a boolean attribute called `can_deliver_quickly`
and set it to `true` or `false` for each record.
```json JSON icon=braces theme={"system"}
[
{
"name": "Pasta Bolognese",
"restaurant": "Millbrook Deli",
"delivery_waiting_time": 20,
"can_deliver_quickly": true,
"popularity": 80
},
{
"name": "Pasta Bolognese",
"restaurant": "The Hive",
"delivery_waiting_time": 15,
"can_deliver_quickly": true,
"popularity": 80
},
{
"name": "Pasta Bolognese",
"restaurant": "Bert's Inn",
"delivery_waiting_time": 40,
"can_deliver_quickly": false,
"popularity": 90
}
]
```
## How to apply optional filters
To use optional filters, you need to:
* Set the attribute in `attributesForFaceting` at indexing time.
You can do this either through the API or the Algolia dashboard.
* Add the optional filter through a rule or send it with your query using the API.
### Using the dashboard
Set the attribute to use for faceting in your Algolia dashboard:
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia index and go to the **Configuration** tab.
4. Under **Filtering and Faceting > Facets**, click **Add an attribute** and select the optional filter attributes (for example, `can_deliver_quickly`).
5. Save your changes.
You can create a rule to boost records where `can_deliver_quickly` is `true` using either the Visual Editor or the Manual Editor.
1. Open **Rules** and select **New rule > Visual Editor**.
2. In **Conditions**, select **Create conditionless rule** and click **Apply** to apply the boost to every search.
To limit the boost to specific queries, select **Set query condition(s)** instead.
3. In **Consequences**, select **Boost categories**.
Select `can_deliver_quickly` as the attribute and `true` as its value, then click **Apply**.
4. Preview the results with an empty query, click **Review and Publish**, and click **Publish**.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, remove the default condition to make the rule apply to every search.
To limit the boost to specific queries, configure a query condition instead.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "optionalFilters": ["can_deliver_quickly:true"] }`.
4. Save your changes.
Using the [example dataset](#update-your-records), the rule boosts records where `can_deliver_quickly` is `true`, while the other record remains in the results.
To demote records instead, use **Bury categories** in the Visual Editor.
For example, select `can_deliver_quickly` as the attribute and `false` as its value.
In the Manual Editor, use **Add Query Parameter** with `{ "optionalFilters": ["can_deliver_quickly:-false"] }` for the same effect.
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
Set the attribute to use for faceting with [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting).
For example:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings
{
AttributesForFaceting = new List { "can_deliver_quickly", "restaurant" },
}
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"can_deliver_quickly",
"restaurant",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"can_deliver_quickly", "restaurant"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("can_deliver_quickly", "restaurant"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['can_deliver_quickly', 'restaurant'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings =
IndexSettings(attributesForFaceting = listOf("can_deliver_quickly", "restaurant")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'can_deliver_quickly',
'restaurant',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"can_deliver_quickly",
"restaurant",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["can_deliver_quickly", "restaurant"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("can_deliver_quickly", "restaurant"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["can_deliver_quickly", "restaurant"])
)
```
Send the optional filters with your query.
For example:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
OptionalFilters = new OptionalFilters(
new List { new OptionalFilters("can_deliver_quickly:true") }
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
optionalFilters: [
"can_deliver_quickly:true",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetOptionalFilters(search.ArrayOfOptionalFiltersAsOptionalFilters(
[]search.OptionalFilters{*search.StringAsOptionalFilters("can_deliver_quickly:true")})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setOptionalFilters(OptionalFilters.of(Arrays.asList(OptionalFilters.of("can_deliver_quickly:true")))),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { optionalFilters: ['can_deliver_quickly:true'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
optionalFilters =
OptionalFilters.of(listOf(OptionalFilters.of("can_deliver_quickly:true")))
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['optionalFilters' => [
'can_deliver_quickly:true',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"optionalFilters": [
"can_deliver_quickly:true",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(optional_filters: ["can_deliver_quickly:true"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
optionalFilters = Some(OptionalFilters(Seq(OptionalFilters("can_deliver_quickly:true"))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(optionalFilters: SearchOptionalFilters
.arrayOfSearchOptionalFilters([SearchOptionalFilters.string("can_deliver_quickly:true")])))
)
```
You can also add negative optional filters.
For example, to demote a specific restaurant:
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
Query = "query",
OptionalFilters = new OptionalFilters(
new List { new OptionalFilters("restaurant:-Bert's Inn") }
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "query",
optionalFilters: [
"restaurant:-Bert's Inn",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("query").SetOptionalFilters(search.ArrayOfOptionalFiltersAsOptionalFilters(
[]search.OptionalFilters{*search.StringAsOptionalFilters("restaurant:-Bert's Inn")})))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject()
.setQuery("query")
.setOptionalFilters(OptionalFilters.of(Arrays.asList(OptionalFilters.of("restaurant:-Bert's Inn")))),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'query', optionalFilters: ["restaurant:-Bert's Inn"] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
query = "query",
optionalFilters =
OptionalFilters.of(listOf(OptionalFilters.of("restaurant:-Bert's Inn"))),
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'query',
'optionalFilters' => [
"restaurant:-Bert's Inn",
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "query",
"optionalFilters": [
"restaurant:-Bert's Inn",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(query: "query", optional_filters: ["restaurant:-Bert's Inn"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("query"),
optionalFilters = Some(OptionalFilters(Seq(OptionalFilters("restaurant:-Bert's Inn"))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "query",
optionalFilters: SearchOptionalFilters
.arrayOfSearchOptionalFilters([SearchOptionalFilters.string("restaurant:-Bert's Inn")])
))
)
```
## Optional or facet filters
Whether to use optional filters or [facet filters](/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters)
depends on your goals:
* Use optional filters to boost records.
* Use facet filters if you need to filter out records.
## See also
* [Filter scoring](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring)
* [Fuzzy search: optional filters](https://www.algolia.com/blog/engineering/discover-what-fuzzy-search-is-with-fuzzy-matching)
# Merchandise on empty queries
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandise-on-empty-query
Use an API client or the dashboard to promote or hide items or categories without a particular query.
You can create rules that promote or hide items, boost, or bury categories, or activate filters even when users haven't typed anything yet.
This guide shows you how to create a rule that boosts the category `clearance:true`, whenever there is an empty query and the context is set to `landing`.
You can use other consequences, such as promoting or hiding items, and boosting or burying categories, with the same condition.
To trigger this rule, you must send `landing` as a [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) on the frontend.
As soon as user begins typing, the rule is turned off.
If you want a rule that's always active for a particular context regardless of the query string, create a [context-only rule](/doc/guides/managing-results/rules/rules-overview#context-only-rules).
For more information, see [Using rules to customize search results by platform](/doc/guides/managing-results/rules/rules-overview/how-to/customize-search-results-by-platform).
To boost, bury, or on values of a certain attribute, you must declare that attribute as an [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting).
Rules on empty queries are useful for landing or category pages.
Use either [filters](/doc/guides/managing-results/rules/rules-overview#rules-responding-to-applied-filters) or [context](/doc/guides/managing-results/rules/rules-overview#context-only-rules) to determine which category pages should trigger these rules.
For more information, see:
* [Category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages)
* [Merchandise category pages](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-category-pages)
## Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To create a rule with one of the API clients,
use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method on a rule object with an empty search as the condition.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"clearance-category-filter",
new Rule
{
ObjectID = "clearance-category-filter",
Conditions = new List
{
new Condition
{
Pattern = "",
Anchoring = Enum.Parse("Is"),
Context = "landing",
},
},
Consequence = new Consequence
{
Params = new ConsequenceParams
{
OptionalFilters = new OptionalFilters("clearance:true"),
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "clearance-category-filter",
rule: Rule(
objectID: "clearance-category-filter",
conditions: [
Condition(
pattern: "",
anchoring: Anchoring.fromJson("is"),
context: "landing",
),
],
consequence: Consequence(
params: ConsequenceParams(
optionalFilters: "clearance:true",
),
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "clearance-category-filter",
search.NewEmptyRule().SetObjectID("clearance-category-filter").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("").SetAnchoring(search.Anchoring("is")).SetContext("landing")}).SetConsequence(
search.NewEmptyConsequence().SetParams(
search.NewEmptyConsequenceParams().SetOptionalFilters(search.StringAsOptionalFilters("clearance:true"))))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"clearance-category-filter",
new Rule()
.setObjectID("clearance-category-filter")
.setConditions(Arrays.asList(new Condition().setPattern("").setAnchoring(Anchoring.IS).setContext("landing")))
.setConsequence(new Consequence().setParams(new ConsequenceParams().setOptionalFilters(OptionalFilters.of("clearance:true"))))
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'clearance-category-filter',
rule: {
objectID: 'clearance-category-filter',
conditions: [{ pattern: '', anchoring: 'is', context: 'landing' }],
consequence: { params: { optionalFilters: 'clearance:true' } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "clearance-category-filter",
rule =
Rule(
objectID = "clearance-category-filter",
conditions =
listOf(
Condition(
pattern = "",
anchoring = Anchoring.entries.first { it.value == "is" },
context = "landing",
)
),
consequence =
Consequence(
params = ConsequenceParams(optionalFilters = OptionalFilters.of("clearance:true"))
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'clearance-category-filter',
['objectID' => 'clearance-category-filter',
'conditions' => [
['pattern' => '',
'anchoring' => 'is',
'context' => 'landing',
],
],
'consequence' => ['params' => ['optionalFilters' => 'clearance:true',
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="clearance-category-filter",
rule={
"objectID": "clearance-category-filter",
"conditions": [
{
"pattern": "",
"anchoring": "is",
"context": "landing",
},
],
"consequence": {
"params": {
"optionalFilters": "clearance:true",
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"clearance-category-filter",
Algolia::Search::Rule.new(
algolia_object_id: "clearance-category-filter",
conditions: [Algolia::Search::Condition.new(pattern: "", anchoring: "is", context: "landing")],
consequence: Algolia::Search::Consequence.new(
params: Algolia::Search::ConsequenceParams.new(optional_filters: "clearance:true")
)
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "clearance-category-filter",
rule = Rule(
objectID = "clearance-category-filter",
conditions = Some(
Seq(
Condition(
pattern = Some(""),
anchoring = Some(Anchoring.withName("is")),
context = Some("landing")
)
)
),
consequence = Consequence(
params = Some(
ConsequenceParams(
optionalFilters = Some(OptionalFilters("clearance:true"))
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "clearance-category-filter",
rule: Rule(
objectID: "clearance-category-filter",
conditions: [SearchCondition(pattern: "", anchoring: SearchAnchoring.`is`, context: "landing")],
consequence: SearchConsequence(params: SearchConsequenceParams(optionalFilters: SearchOptionalFilters
.string("clearance:true")))
)
)
```
## Using the dashboard
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
To match empty queries regardless of context, omit the context in either editor.
1. In the Algolia dashboard, select your application and index, open [**Rules**](https://dashboard.algolia.com/rules), and select **New rule > Visual Editor**.
2. In **Conditions**, click **Set query condition(s)**.
Keep **Query** toggled on, select **is**, and leave the search input empty.
Toggle **Context** on, enter `landing`, and click **Apply**.
3. In **Consequences**, select **Boost categories**.
Select `clearance` as the attribute and `true` as its value, then click **Apply**.
4. Review the results and click **Review and Publish**.
To forward the rule to replicas or other indices, select them under **Copy this rule to other indices and/or replicas**.
Click **Publish**, or use **Save as Draft** to keep the rule inactive.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, keep **Query** toggled on, select **Is**, and leave the search input empty.
Toggle **Context** on and enter `landing`.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "optionalFilters": ["clearance:true"] }`.
4. To forward the rule to replicas or other indices, toggle **Copy this rule to other indices and/or replicas** and select the indices.
Save your changes.
In JSON, you must write string values inside quotation marks, and number values and booleans without quotations.
# Merchandise category pages
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-category-pages
Create rules and apply them to merchandise category pages.
With Algolia, you can [merchandise products](/doc/guides/managing-results/rules/merchandising-and-promoting) by creating category pages.
For example, you could organize products into categories such as on-sale items, new arrivals, specific brands, product types (like smartphones), or colors.
If you want to merchandise product groups on your category pages dynamically,
see [Smart Groups](/doc/guides/compositions/smart-groups/smart-groups).
To use an attribute in a rule condition with filters, declare it in [`attributesForFaceting`](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
## Before you begin
1. [Choose an attribute as category page identifier](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages).
2. [Make that attribute searchable](/doc/guides/managing-results/must-do/searchable-attributes).
3. [Declare that attribute as a searchable facet](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages#implementation-steps).
## Merchandise category pages
Merchandise category pages by configuring an identifier, setting a trigger (a condition), and selecting a strategy (a consequence).
### Configure a category page identifier
1. Go to the [**Rules**](https://dashboard.algolia.com/rules/) section in the Algolia dashboard. Select the index to which you want to add a rule.
2. Select **Create your first Rule** or **New rule**, then select **Visual Editor**.
3. In **Conditions**, select **Choose category page**.
4. Click **Configure**, then **I'm good, let's go**.
5. Select a [searchable facet](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting) as your **Category page identifier**.
6. Optional: add a **Custom subcategory separator** if you're using [hierarchical categories](/doc/guides/managing-results/refine-results/faceting#hierarchical-facets). For example, for the hierarchical category: `Women / Coat / Raincoat`, enter `/` as the custom separator.
7. **Save** your changes.
To update these settings later, click the gear icon in the top-right corner of the Visual Editor and select **Category page identifiers**.
### Create a trigger
1. Go to the [**Rules**](https://dashboard.algolia.com/rules/) section in the Algolia dashboard. Select the index to which you want to add a rule.
2. Select **Create your first Rule** or **New rule**, then select **Visual Editor**.
3. In **Conditions**, select **Choose category page**.
4. Choose a **Category name**: this is a value from your category page identifier.
5. Click **Apply**.
### Determine a consequence
In **Consequences**, select [how you want to merchandise](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#consequence—adapt-the-results) the category page.
* Click **Review and Publish**, review the rule, and click **Publish** to enable the category page rule.
* Click **Save as Draft** if you don't want the rule to take effect yet.
## Example: promote items on category pages until users start searching
To promote items on category pages before users start searching, create a rule that's triggered if:
* A user visits the category page
* The is empty.
### Using the dashboard
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. In **Conditions**, click **Set query condition(s)**.
Toggle **Query** on, select **is**, and leave the search input empty.
Toggle **Filters** on, select `category` as the **Filter name** and `Smartphone` as the **Value**, then click **Apply**.
3. In **Consequences**, select **Pin items**.
Under **Pinned items**, search for an item and enter its **Position**.
To pin more items, click **Pin another item**.
Click **Apply**.
4. Review the results, click **Review and Publish**, and click **Publish**.
To keep the rule inactive, use **Save as Draft** instead.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, toggle **Query** on, select **Is**, and leave the search input empty.
Toggle **Filters** on, select `category` as the **Attribute name** and `Smartphone` as the **Attribute value**.
3. In **Consequence(s)**, click **Add consequence > Pin an item**.
Search for an **Item** and enter its **Position**.
To pin more items, click **Add consequence** again.
4. Optional: add a description and a validity period.
Save your changes.
## Example: display banners on category pages
Category page banners operate similarly to search results [banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners), but a filter rather than a search query triggers them.
### Display a banner with InstantSearch
Use the **Display banner** consequence to show a banner in your category page's InstantSearch `Hits` or `InfiniteHits` widget.
This requires InstantSearch.js version 4.73.0 or later, React InstantSearch version 7.12.0 or later, or Vue InstantSearch version 4.19.0 or later.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. In **Conditions**, click **Set query condition(s)**.
Toggle **Query** off and **Filters** on.
Select `category` as the **Filter name** and `Smartphone` as the **Value**, then click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Display banner**.
Enter the URL for your banner image and click **Apply**.
4. Review the banner preview, click **Review and Publish**, then click **Publish**.
When your category page sends a search with the matching category filter, the rule returns banner data in `renderingContent.widgets.banners`.
The supported InstantSearch widget displays the banner unless you configure it to hide banners.
For UI integration examples, see [Render banner with InstantSearch](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners#render-banner-with-instantsearch).
### Display a custom banner
This example returns custom banner data for your UI to display.
You can create this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. In **Conditions**, click **Set query condition(s)**.
Toggle **Query** off and **Filters** on.
Select `category` as the **Filter name** and `Smartphone` as the **Value**, then click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Return custom data**.
Enter `{ "banner": "iPhone" }` and click **Apply**.
4. Click **Review and Publish**, then click **Publish**.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, toggle **Query** off and **Filters** on.
Select `category` as the **Attribute name** and `Smartphone` as the **Attribute value**.
3. In **Consequence(s)**, select **Add consequence > Return Custom Data**.
Enter `{ "banner": "iPhone" }`.
4. Optional: add a description and a validity period.
Save your changes.
Now, every time a user visits the category page, the rule triggers and returns a JSON object as custom data.
You can [display this custom data](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners#retrieve-the-banner-data-in-your-ui) in your UI as a banner.
## See also
* [Category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages)
* [Add Dynamic Re-Ranking to category pages](/doc/guides/algolia-ai/re-ranking/tutorials/re-rank-category-pages)
* [Filters can trigger rules](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#filters-can-trigger-rules)
# Merchandise facets
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/merchandising-facets
Learn how to create Rules that tweak the display of facets in your UI.
Depending on the query or category, which facets to display in the UI and how to organize values can vary significantly.
For example, for an ecommerce platform selling both clothes and tech products, in the "clothing" category you might want to display the facets "brand", "size", and "color", while for TVs, you might want to show "brand", "screen type", and "screen size".
Algolia lets you control which facets to display and how to order their values for a given query or category.
This feature requires a [specific UI implementation](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js) that relies on the latest version of InstantSearch,
or with some custom logic built-in.
## Merchandise facets in the Visual Editor
Consider the earlier example, where an ecommerce platform sells clothes and tech products.
First, you need to [set a default facet display](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js) so that if a user lands on anything but tech products, they see the facets "brand", "size", and "color".
With the default order set, you need to set a rule to override this default order for the tech products category.
1. Select [Rules](https://dashboard.algolia.com/rules) in the Algolia dashboard.
2. Select **Create your first rule** or **New rule** and select **Visual Editor**.
3. To set the condition, check whether you have category pages configured on your Visual Editor.
You would follow separate steps, depending on whether it's configured or not:
1. Click **Choose category page**.
2. Select "Tech Products".
3. Click **Apply**.
1. Click **Set query condition**.
2. Toggle on **Filters** and toggle off **Query**,
3. In the **Filter name** field, enter `Brand`. In the **Filter value** field, enter `Tech Products`,
4. Click **Apply**.
4. In the **Consequences** section:
1. Click the **Order facets** button.
2. Import the default order defined for your facet display by selecting **Import default order**.
3. Click the trash icon next to the facets "size" and "color" so they're not displayed anymore in this category.
4. Click the **Add facets** button and select "screen type" and "screen size" to add them to the list of facets to display.
5. Use the `=` icon to reorder the facets by dragging and dropping them.
The order in the UI is the same as the order in the dashboard.
5. Review and publish your changes.
## Merchandise facet values in the Visual Editor
Selecting which facets to display and in which order is one aspect of facet merchandising.
The other aspect is about facets values.
By default, the values within each facet are ordered by count:
the values with the highest number of matching results appear at the top.
You can tweak this order in several ways:
* You can pin some values to the top of the list.
For example, you might have a partnership with a specific brand that you always want to display at the top of the list.
* The non-pinned values can then be ordered by count, alphabetically, or hidden so that only pinned values are displayed.
Going back to the earlier example, now on top of displaying the facets "brand", "screen type", and "screen size" in the category "tech products", you also want:
* To pin the brand "Sony" at the top of the list of brands
* Have the other brands ordered alphabetically
* Have the "screen type" values ordered by count
* Only display the three most common values for "screen size": 32, 55, and 65 inches.
1. Select [Rules](https://dashboard.algolia.com/rules) in the Algolia dashboard.
2. Under the **Rules** header, select the to which you're adding a rule.
3. Identify the rule created in the earlier example, click the three dots (ellipsis), and select the option "Edit this Rule".
4. In the strategy section, click the pen icon to edit the facet display strategy.
5. To set the strategy for the "brand" facet:
1. Click the pen icon to edit the "brand" facet.
2. Click "pin facet value" and select the brand "Sony" to pin it to the top of the list.
3. To order subsequent values, in the drop-down menu, click "alphabetically".
6. For the "screen type" facet, the order should already be by count, so there's nothing to change here.
7. To set the strategy for the "screen size" facet:
1. Click the pen icon to edit the "screen size" facet.
2. Click "pin facet value" and select the values 32in, 55in, 65in to pin them at the top of the list.
3. Use the `=` icon to drag the values into the correct order.
4. To order subsequent values, in the drop-down menu, select "do not display" to ensure that only the three selected values are displayed.
8. Review and publish your changes.
## Merchandise facets and their values in the Manual Editor
Merchandising facets and their values is also possible using the Manual Editor but requires an understanding of the setting `renderingContent`.
1. Select [Rules](https://dashboard.algolia.com/rules) in the Algolia dashboard.
2. Under the **Rules** header, select the index you are adding a Rule to.
3. Select **Create your first rule** or **New rule**. In the drop-down menu, select the Manual Editor option.
4. In the **Condition(s)** section, toggle **Filters** on.
Select "screen type" in the attribute name field, and "tech products" in the attribute value field.
5. Still in the Condition(s) section, toggle **Query** off.
6. In the **Consequence(s)** section:
1. Click the **Add consequence** button and select **Add query** parameter.
2. In the input field, enter the JSON to customize the `renderingContent` parameter. For example, to match the same example as described for the Visual Editor, the JSON would look like this:
```json JSON icon=braces theme={"system"}
{
"renderingContent": {
"facetOrdering": {
"facets": {
"order": ["brand", "screen type", "screen size"]
},
"values": {
"brand": {
"order": ["Sony"],
"sortRemainingBy": "alpha"
},
"screen type": {
"sortRemainingBy": "count"
},
"screen size": {
"order": ["32in", "55in", "65in"],
"sortRemainingBy": "hidden"
}
}
}
}
}
```
1. Save your changes.
# Keep pinned items together with pin blocks
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/pin-blocks
Use pin blocks so that pinned items shift up to close gaps left by unavailable pinned items.
A pinned item can become unavailable if an active filter excludes it—for example, because it's out of stock—or you remove it from the .
When that happens, Algolia fills its position with the next organic result, leaving gaps in your curation.
A *pin block* groups contiguous pinned items so they behave as a unit.
If one item in the block becomes unavailable, the remaining pinned items shift up.
Algolia fills positions with only after all available pinned items in the block have been placed.
## Pin block boundaries
A pin block is a set of adjacent pinned positions with no unpinned position between them.
An unpinned position splits the pins into separate blocks.
For example, pins at positions 1, 2, 3, 6, 7, and 8 form two pin blocks: one covering positions 1 to 3 and one covering positions 6 to 8.
* If you unpin an item inside a block, the remaining items stay together and the block's start position doesn't change.
* If you add a pin between two pins in a block, it joins that block.
* A rule can contain several pin blocks.
## Enable pin blocks
To enable pin blocks for a rule, turn on **Shift pins to close gaps** in the Visual Editor.
1. Go to the [Algolia dashboard](https://dashboard.algolia.com/explorer/browse) and select your Algolia .
2. On the left sidebar, select **Search**.
3. Select your Algolia .
4. Select [**Rules**](https://dashboard.algolia.com/rules) from the left sidebar.
5. Select **Create your first Rule** or **New rule**.
6. Click **Visual Editor**.
Under **Consequences**, click **Pin items**, then turn on **Shift pins to close gaps**.
Pin items at adjacent positions to create a pin block.
The Visual Editor labels each block so you can see which items stay together.
Click **Apply**, then **Review and Publish** your rule.
## Default pin block behavior
**Shift pins to close gaps** is on for new rules and off for rules that already exist.
It stays off on existing rules to avoid changing live curation, so turn it on manually where you want it.
For existing rules created with the API that contain groups of pinned items,
the Visual Editor shows the setting as on.
## Filter matching requirements
**Shift pins to close gaps** depends on **Must match active filters**.
If you turn off that setting, the Visual Editor turns off pin blocks and disables the setting.
For more information, see [Promote results matching active filters](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits#promote-results-matching-active-filters).
## Pin block limits
Pin blocks don't change the limit on pinned items: by default, you can pin up to per rule.
A single pin block can hold up to that limit, and there's no separate limit on the number of blocks per rule.
## See also
* [Promote results with rules](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/promote-hits)
* [`promote`](/doc/rest-api/search/save-rule#body-consequence)
# Redirect searches to a URL
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects
Learn how to redirect a query to a URL using Rules.
Search isn't only about retrieving results.
Sometimes, depending on what users search for,
you might want to redirect them to a specific page of your website or mobile app.
Redirecting searches to a URL helps users in these cases:
* **Improve user experience and navigation**.
For example, searching for "help" or "return policy" redirects users to a support page.
* **Display category pages for related keywords** to guarantee a consistent user experience and boost SEO by driving more traffic to category pages. For example, searching for a category keyword, such as, "TV" redirects users directly to the corresponding category page.
* **Promotional campaigns**.
For example, searching for a specific artist name or brand redirects users to a landing page created specifically to support a promotional operation.
* **Crisis management**.
Provide relevant answers to queries related to sudden events.
For example, if you detect that your users suddenly start searching for "masks",
redirect them to a specific landing page.
Algolia lets you create redirects to specific URLs with the
[Autocomplete redirect plugin](/doc/ui-libraries/autocomplete/guides/redirect) or a [custom implementation](/doc/guides/building-search-ui/ui-and-ux-patterns/redirects/js)
## Algolia out-of-the-box redirect solution
The easiest way to redirect search queries to a URL is by adding a rule with the [Visual Editor](/doc/guides/managing-results/rules/merchandising-and-promoting).
The rule adds data to the search response, which you can [handle in your frontend](#handle-redirects-in-your-user-interface).
The examples on this page add a redirect for the keyword "help" to a support page.
You can discover potential redirects by looking at your [search analytics](/doc/guides/search-analytics/overview).
For example, users searching for "help" and not getting any results.
### Using the Visual Editor
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **Contains**, enter `help`, and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Redirect**.
Enter the URL for your support page and click **Apply**.
4. Click **Review and Publish**, review the rule, and click **Publish**.
### Using the API
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
To add a rule with the API, use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method.
When adding a rule, you need to define a condition and a consequence.
In the consequence, [add a search parameters](/doc/guides/managing-results/rules/rules-overview/how-to/add-default-search-parameters-with-rules) and configure [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) using the following template:
```json JSON icon=braces theme={"system"}
{
"renderingContent": {
"redirect": {
"url": "https://www.algolia.com/support"
}
}
}
```
### Handle redirects in your user interface
After adding the rule, you can trigger the redirect in your UI when the `renderingContent.redirect` property is present in the API response. You have two options:
* Using the [redirect plugin](/doc/ui-libraries/autocomplete/guides/redirect) for the Autocomplete UI library
* Implement [your own logic](/doc/guides/building-search-ui/ui-and-ux-patterns/redirects/js)
## Extend the out-of-the-box approach or create custom redirection
If you want to define your own redirection format,
or if you need to add additional attributes to the [default redirect rule format](#algolia-out-of-the-box-redirect-solution),
you can create a rule with either editor that returns custom data.
### Create a rule for returning custom data
You can set a rule that contains the [custom data](/doc/guides/managing-results/rules/rules-overview/in-depth/implementing-rules#return-user-data) you need to build your custom redirect.
This example redirects users to your support page whenever the contains "help".
#### Using the dashboard
You can add this rule from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. Open **Rules** and select **New rule > Visual Editor**.
2. Click **Set query condition(s)**, select **Contains**, enter `help`, and click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Return custom data**.
Enter `{ "redirect": "https://www.algolia.com/support" }` in **Value** and click **Apply**.
4. Click **Review and Publish**, review the rule, and click **Publish**.
1. Open [**Rules**](https://dashboard.algolia.com/rules), select **Create your first rule** or **New rule**, and select **Manual Editor**.
2. In **Condition(s)**, keep **Query** selected and enter `help`.
3. In **Consequence(s)**, click **Add consequence** and select **Return Custom Data**.
Enter `{ "redirect": "https://www.algolia.com/support" }` in **Value**.
4. Optional: add a description that helps your team distinguish this rule from other rules.
Save your changes.
#### Using the API
To add a rule with the API, use the [`saveRule`](/doc/libraries/sdk/v1/methods/save-rule) method.
When setting a rule, you need to define a condition and a consequence.
```cs C# theme={"system"}
var response = await client.SaveRuleAsync(
"INDEX_NAME",
"redirect-help-rule",
new Rule
{
ObjectID = "redirect-help-rule",
Conditions = new List
{
new Condition { Pattern = "help", Anchoring = Enum.Parse("Contains") },
},
Consequence = new Consequence
{
UserData = new Dictionary
{
{ "redirect", "https://www.algolia.com/support" },
},
},
}
);
```
```dart Dart theme={"system"}
final response = await client.saveRule(
indexName: "INDEX_NAME",
objectID: "redirect-help-rule",
rule: Rule(
objectID: "redirect-help-rule",
conditions: [
Condition(
pattern: "help",
anchoring: Anchoring.fromJson("contains"),
),
],
consequence: Consequence(
userData: {
'redirect': "https://www.algolia.com/support",
},
),
),
);
```
```go Go theme={"system"}
response, err := client.SaveRule(client.NewApiSaveRuleRequest(
"INDEX_NAME", "redirect-help-rule",
search.NewEmptyRule().SetObjectID("redirect-help-rule").SetConditions(
[]search.Condition{*search.NewEmptyCondition().SetPattern("help").SetAnchoring(search.Anchoring("contains"))}).SetConsequence(
search.NewEmptyConsequence().SetUserData(map[string]any{"redirect": "https://www.algolia.com/support"}))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.saveRule(
"INDEX_NAME",
"redirect-help-rule",
new Rule()
.setObjectID("redirect-help-rule")
.setConditions(Arrays.asList(new Condition().setPattern("help").setAnchoring(Anchoring.CONTAINS)))
.setConsequence(
new Consequence().setUserData(
new HashMap() {
{
put("redirect", "https://www.algolia.com/support");
}
}
)
)
);
```
```js JavaScript theme={"system"}
const response = await client.saveRule({
indexName: 'indexName',
objectID: 'redirect-help-rule',
rule: {
objectID: 'redirect-help-rule',
conditions: [{ pattern: 'help', anchoring: 'contains' }],
consequence: { userData: { redirect: 'https://www.algolia.com/support' } },
},
});
```
```kotlin Kotlin theme={"system"}
var response =
client.saveRule(
indexName = "INDEX_NAME",
objectID = "redirect-help-rule",
rule =
Rule(
objectID = "redirect-help-rule",
conditions =
listOf(
Condition(
pattern = "help",
anchoring = Anchoring.entries.first { it.value == "contains" },
)
),
consequence =
Consequence(
userData =
buildJsonObject {
put("redirect", JsonPrimitive("https://www.algolia.com/support"))
}
),
),
)
```
```php PHP theme={"system"}
$response = $client->saveRule(
'INDEX_NAME',
'redirect-help-rule',
['objectID' => 'redirect-help-rule',
'conditions' => [
['pattern' => 'help',
'anchoring' => 'contains',
],
],
'consequence' => ['userData' => ['redirect' => 'https://www.algolia.com/support',
],
],
],
);
```
```python Python theme={"system"}
response = client.save_rule(
index_name="INDEX_NAME",
object_id="redirect-help-rule",
rule={
"objectID": "redirect-help-rule",
"conditions": [
{
"pattern": "help",
"anchoring": "contains",
},
],
"consequence": {
"userData": {
"redirect": "https://www.algolia.com/support",
},
},
},
)
```
```ruby Ruby theme={"system"}
response = client.save_rule(
"INDEX_NAME",
"redirect-help-rule",
Algolia::Search::Rule.new(
algolia_object_id: "redirect-help-rule",
conditions: [Algolia::Search::Condition.new(pattern: "help", anchoring: "contains")],
consequence: Algolia::Search::Consequence.new(user_data: {redirect: "https://www.algolia.com/support"})
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.saveRule(
indexName = "INDEX_NAME",
objectID = "redirect-help-rule",
rule = Rule(
objectID = "redirect-help-rule",
conditions = Some(
Seq(
Condition(
pattern = Some("help"),
anchoring = Some(Anchoring.withName("contains"))
)
)
),
consequence = Consequence(
userData = Some(JObject(List(JField("redirect", JString("https://www.algolia.com/support")))))
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.saveRule(
indexName: "INDEX_NAME",
objectID: "redirect-help-rule",
rule: Rule(
objectID: "redirect-help-rule",
conditions: [SearchCondition(pattern: "help", anchoring: SearchAnchoring.contains)],
consequence: SearchConsequence(userData: ["redirect": "https://www.algolia.com/support"])
)
)
```
#### Handle redirects in your user interface
After adding the rule, you can trigger the redirect in your UI when the `userData` property is present in the API response.
If you're using one of the InstantSearch UI libraries, see [Redirects in InstantSearch](/doc/guides/building-search-ui/ui-and-ux-patterns/redirects/js).
## Add redirects using a dedicated index
You can set up redirects by creating a separate index with the list of redirects **and** the queries that trigger them.
Whenever a user performs a search, you have to look in two indices: the regular index to search for items, and the redirect index that determines whether users should be redirected.
This technique is useful if you can't use rules,
or if you want to use other Algolia Search features with your redirect queries,
such as typo tolerance, synonyms, or a .
### Dataset
```json JSON icon=braces theme={"system"}
[
{
"url": "https://www.google.com/#q=star+wars",
"query_terms": ["star wars"]
},
{
"url": "https://www.google.com/#q=star+wars+actors",
"query_terms": ["george lucas", "luke skywalker"]
}
]
```
Each record in this dataset has a `url` to redirect the queries, and a list of `query_terms`, that trigger the redirect.
For example, the query "star wars" should redirect to `https://www.google.com/#q=star+wars`.
* [Download the dataset](https://raw.githubusercontent.com/algolia/datasets/master/small/redirects.json)
* To learn how to import the dataset, see [Import from the dashboard](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-from-the-dashboard)
### Configure the redirect index
To trigger the redirect only if the query exactly matches one of the query terms,
add `query_terms` to the list of [`attributesForFaceting`](/doc/api-reference/api-parameters/attributesForFaceting),
so that you can filter on it:
```js JavaScript theme={"system"}
const algoliaClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY");
const customSearchClient = {
...algoliaClient,
async search(requests) {
if (requests[0].params.query) {
const { results } = await algoliaClient.search([
...requests,
{
indexName: "your_redirect_index_name",
params: {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
},
},
]);
const redirect = results.pop();
if (redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return { results };
} else {
return algoliaClient.search(requests);
}
},
};
const search = instantsearch({
indexName: "instant_search",
searchClient: customSearchClient,
});
```
```jsx React theme={"system"}
const algoliaClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_API_KEY");
const customSearchClient = {
...algoliaClient,
async search(requests) {
if (requests[0].params.query) {
const { results } = await algoliaClient.search([
...requests,
{
indexName: "your_redirect_index_name",
params: {
hitsPerPage: 1,
facetFilters: [`query_terms:${requests[0].params.query}`],
},
},
]);
const redirect = results.pop();
if (redirect.hits.length) {
window.location.href = redirect.hits[0].url;
}
return { results };
} else {
return algoliaClient.search(requests);
}
},
};
const App = () => ({
/* Widgets */
});
```
```vue Vue theme={"system"}
```
### Create a custom search client
You need to create a [custom search client](/doc/guides/building-search-ui/going-further/backend-search/js) that **simultaneously sends requests to the regular and redirect indices**.
The request to the redirect index is sent with:
* An empty query, so it doesn't filter with it
* A filter on `query_terms` with the content of the current query, to match only that contain the current query exactly
* `hitsPerPage=1`, as you only need the first record
Whenever a record matches in the redirect index, a redirect is triggered:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { AttributesForFaceting = new List { "query_terms" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"query_terms",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"query_terms"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("query_terms"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['query_terms'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("query_terms")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'query_terms',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"query_terms",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["query_terms"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("query_terms"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["query_terms"])
)
```
# Add search parameters with JSON
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/rules-query-parameters
Learn how to write JSON templates for adding query parameters to a search when applying rules.
You can use [Rules](/doc/guides/managing-results/rules/rules-overview) to apply search parameters when a condition matches.
Both the Visual Editor and Manual Editor accept JSON for search parameters.
The Visual Editor provides separate controls for filters, optional filters, and query edits.
## Using the dashboard
You can add search parameters from the Algolia dashboard using either the Visual Editor or the Manual Editor.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Visual Editor**.
2. Select **Set query condition(s)** and define when the rule applies.
Click **Apply**.
3. In **Consequences**, expand **More consequences** and select **Apply search parameters**.
In **Value**, enter `{ "aroundRadius": 1000 }` to limit the search radius to 1,000 meters when your search uses geolocation.
Click **Apply**.
4. Review the results, click **Review and Publish**, and click **Publish**.
1. In the Algolia dashboard, select your application and index, open **Rules**, and select **New rule > Manual Editor**.
2. In **Condition(s)**, define when the rule applies.
3. In **Consequence(s)**, click **Add consequence** and select **Add Query Parameter**.
Enter `{ "aroundRadius": 1000 }` to limit the search radius to 1,000 meters when your search uses geolocation.
4. Save your changes.
The editor validates the JSON and rejects unsupported parameters.
Use dedicated consequences for the following parameters:
`filters`, `optionalFilters`, `redirect`, `query`, `automaticFacetFilters`, `automaticOptionalFacetFilters`, and `renderingContent`.
Use **Filter results**, **Boost categories**, **Bury categories**, or the corresponding consequence instead of entering these parameters in **Apply search parameters**.
## Use JSON templates
You can use the following templates in the Manual Editor's **Add Query Parameter** consequence or in a rule's `consequence.params` with the API.
The filter templates below aren't accepted by the Visual Editor's **Apply search parameters** consequence.
In this guide, `FACET:VALUE` refers to a [facet-value pair](/doc/glossary#facet-value-pair), such as, `author:shakespeare`.
## Filters
Filters excludes that don't match the filter.
| Scenario | JSON template |
| - | - |
| **Select a category** | `{ "filters":"FACET:VALUE" }` |
| **Remove a category** | `{ "filters":"NOT FACET:VALUE" }` |
| **Remove several categories** | `{ "filters":"NOT FACET:VALUE AND NOT FACET:VALUE" }` |
If you have a multi-word `VALUE` (such as William Shakespeare), wrap it in single quotes.
For example:
```json JSON icon=braces theme={"system"}
{
"filters": "NOT author:'William Shakespeare' AND NOT author:Plato"
}
```
If your `VALUE` contains an apostrophe (such as Tony d'Arbon),
escape the apostrophe with double backslashes and wrap the whole string in single quotes. For example:
```json JSON icon=braces theme={"system"}
{
"filters": "NOT author:'William Shakespeare' AND NOT author:'Tony d\\'Arbon'"
}
```
## Optional filters
[Optional filters](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-promote-with-optional-filters) don't remove records from the results,
but they promote or demote matching records.
You can fine-tune the behavior of optional filters with [filter scores](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring).
Any attribute you want to use as an optional filter must be [declared as a facet](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting).
### Examples for optional filters
The following examples show different use cases for optional filters, their JSON representation, and the effect on filter scoring.
### Promote a category
**Filter scoring:** +1 for all matching records
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET:VALUE"]
}
```
### Promote several categories with equal weight
**Filter scoring:** +1 for all records matching one or more filter
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET1:VALUE1", "FACET2:VALUE2"]
}
```
### Promote several categories with different weights per category
**Filter scoring:** +X or +Y, whichever matches and has the highest value
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET1:VALUE1", "FACET2:VALUE2"]
}
```
### Promote several categories with a combined score
**Filter scoring:** +1 or +2, depending on the number of matching filters
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET1:VALUE1", "FACET2:VALUE2"],
"sumOrFiltersScores": true
}
```
### Promote several categories with combined weighting
**Filter scoring:** +X, +Y, or +X+Y, depending on the matching filters
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET1:VALUE1", "FACET2:VALUE2"],
"sumOrFiltersScores": true
}
```
### Demote a category
**Filter scoring:** +1 for all records that don't match the filter
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET:-VALUE"]
}
```
### Demote several categories equally
**Filter scoring:** +1 for all records that don't match any of the filters
```json JSON icon=braces theme={"system"}
{
"optionalFilters": ["FACET1:-VALUE1", "FACET2:-VALUE2"]
}
```
### Optional filters with multiple words
Unlike filters, you don't need to wrap a multi-word `VALUE` with single quotes if it contains spaces.
For example:
```json JSON icon=braces theme={"system"}
{
"optionalFilters": [
["author:William Shakespeare", "author:John Doe"]
],
"sumOrFiltersScores": true
}
```
## See also
* [Troubleshooting rules](/doc/guides/managing-results/rules/rules-overview/in-depth/debugging-rules)
* [How to combine optional filter scores](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring#sum-optional-filter-scores)
* [`optionalFilters`](/doc/api-reference/api-parameters/optionalFilters) API parameter
# Optional filters
Source: https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/in-depth/optional-filters
What optional filters are and how to use them to boost or bury records in your search results.
Unlike [filters](/doc/guides/managing-results/refine-results/filtering),
optional filters influence ranking without excluding that don't match them.
Records must still match the query and any required filters.
Combine optional filters to give results different priorities.
Use this to [boost results](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/how-to-promote-with-optional-filters)
you want to show before others or *bury* results you want to show last.
## Before you begin
Declare each attribute used in optional filters in the `attributesForFaceting` index setting.
Your ranking formula must include the [Filters ranking criterion](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria#filters).
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
## Optional filters, replicas, and sorting
On standard replicas, a [sort-by attribute](/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute)
takes precedence over optional filters.
Optional filters can't override the order determined by that attribute.
## Filter scoring
You can add extra nuance to your ranking by specifying scores for different optional filters.
For example, you want to display your matching products in the following order:
* Apple products first
* Samsung products second
* Huawei products last
In this scenario, products from any other brand appear after Apple and Samsung products and before Huawei products.
You can achieve this by using scored optional filters to boost Apple and Samsung products and negative filters to bury Huawei products.
A negative filter negates the filter value.
It doesn't assign a negative numeric score.
```cs C# theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(
new SearchParamsObject
{
OptionalFilters = new OptionalFilters(
new List
{
new OptionalFilters("brand:Apple"),
new OptionalFilters("brand:Samsung"),
new OptionalFilters("brand:-Huawei"),
}
),
}
)
);
```
```dart Dart theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
optionalFilters: [
"brand:Apple",
"brand:Samsung",
"brand:-Huawei",
],
),
);
```
```go Go theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetOptionalFilters(search.ArrayOfOptionalFiltersAsOptionalFilters(
[]search.OptionalFilters{
*search.StringAsOptionalFilters("brand:Apple"),
*search.StringAsOptionalFilters("brand:Samsung"),
*search.StringAsOptionalFilters("brand:-Huawei"),
},
)))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setOptionalFilters(
OptionalFilters.of(
Arrays.asList(
OptionalFilters.of("brand:Apple"),
OptionalFilters.of("brand:Samsung"),
OptionalFilters.of("brand:-Huawei")
)
)
),
Hit.class
);
```
```js JavaScript theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { optionalFilters: ['brand:Apple', 'brand:Samsung', 'brand:-Huawei'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams =
SearchParamsObject(
optionalFilters =
OptionalFilters.of(
listOf(
OptionalFilters.of("brand:Apple"),
OptionalFilters.of("brand:Samsung"),
OptionalFilters.of("brand:-Huawei"),
)
)
),
)
```
```php PHP theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['optionalFilters' => [
'brand:Apple',
'brand:Samsung',
'brand:-Huawei',
],
],
);
```
```python Python theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"optionalFilters": [
"brand:Apple",
"brand:Samsung",
"brand:-Huawei",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(
optional_filters: ["brand:Apple", "brand:Samsung", "brand:-Huawei"]
)
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
optionalFilters = Some(
OptionalFilters(
Seq(
OptionalFilters("brand:Apple"),
OptionalFilters("brand:Samsung"),
OptionalFilters("brand:-Huawei")
)
)
)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams
.searchSearchParamsObject(SearchSearchParamsObject(optionalFilters: SearchOptionalFilters
.arrayOfSearchOptionalFilters([
SearchOptionalFilters.string("brand:Apple"),
SearchOptionalFilters.string("brand:Samsung"),
SearchOptionalFilters.string("brand:-Huawei"),
])))
)
```
For information about how Algolia turns these scores into a ranking,
see [Optional filter scores](/doc/guides/managing-results/refine-results/filtering/in-depth/filter-scoring#optional-filter-scores).
## Performance
Optional filters are powerful but have some significant performance limitations.
* For attributes that you need for filtering but not faceting, use the [`filterOnly`](/doc/api-reference/api-parameters/attributesForFaceting#param-filter-only) modifier in the `attributesForFaceting` index setting. For example:
```cs C# theme={"system"}
var response = await client.SetSettingsAsync(
"INDEX_NAME",
new IndexSettings { AttributesForFaceting = new List { "filterOnly(brand)" } }
);
```
```dart Dart theme={"system"}
final response = await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(
attributesForFaceting: [
"filterOnly(brand)",
],
),
);
```
```go Go theme={"system"}
response, err := client.SetSettings(client.NewApiSetSettingsRequest(
"INDEX_NAME",
search.NewEmptyIndexSettings().SetAttributesForFaceting(
[]string{"filterOnly(brand)"})))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"}
UpdatedAtResponse response = client.setSettings(
"INDEX_NAME",
new IndexSettings().setAttributesForFaceting(Arrays.asList("filterOnly(brand)"))
);
```
```js JavaScript theme={"system"}
const response = await client.setSettings({
indexName: 'INDEX_NAME',
indexSettings: { attributesForFaceting: ['filterOnly(brand)'] },
});
```
```kotlin Kotlin theme={"system"}
var response =
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(attributesForFaceting = listOf("filterOnly(brand)")),
)
```
```php PHP theme={"system"}
$response = $client->setSettings(
'INDEX_NAME',
['attributesForFaceting' => [
'filterOnly(brand)',
],
],
);
```
```python Python theme={"system"}
response = client.set_settings(
index_name="INDEX_NAME",
index_settings={
"attributesForFaceting": [
"filterOnly(brand)",
],
},
)
```
```ruby Ruby theme={"system"}
response = client.set_settings(
"INDEX_NAME",
Algolia::Search::IndexSettings.new(attributes_for_faceting: ["filterOnly(brand)"])
)
```
```scala Scala theme={"system"}
val response = Await.result(
client.setSettings(
indexName = "INDEX_NAME",
indexSettings = IndexSettings(
attributesForFaceting = Some(Seq("filterOnly(brand)"))
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"}
let response = try await client.setSettings(
indexName: "INDEX_NAME",
indexSettings: IndexSettings(attributesForFaceting: ["filterOnly(brand)"])
)
```
* Prefer attributes with fewer unique values for [optional filters](/doc/api-reference/api-parameters/optionalFilters). For example, if `brand` has fewer unique values than `product_name`, use `brand`.
* Use as few optional filters as possible. Aim for queries to return a smaller result set.
* Depending on your plan, it may be possible to apply [sharding and other custom settings](/doc/guides/scaling/scaling-to-larger-datasets) to optimize your implementation. For more information, contact the [support team](https://www.algolia.com/contactus/).
For performance reasons,
don't use filter scoring on searches that may return more than 100,000 results.
## See also
To learn more, see these blog articles:
* [Optional filters](https://www.algolia.com/blog/engineering/feature-spotlight-optional-filters/)
* [Optional filters and fuzzy matching](https://www.algolia.com/blog/engineering/discover-what-fuzzy-search-is-with-fuzzy-matching/)
# Troubleshooting relevance
Source: https://www.algolia.com/doc/guides/managing-results/troubleshooting/troubleshooting-relevance
Troubleshooting relevance - debugging, getting ranking info, using the dashboard.
Try these strategies to troubleshoot any relevance issue:
* **Get ranking information with the `getRankingInfo` parameter**.
This parameter lets you look at and analyze the criteria Algolia uses to rank results.
For a given query it returns the value for each ranking criteria.
By understanding those values you can understand why a result appears before or after another.
* **Reproducing a problem on a small dataset and solving it by process of elimination**.
This strategy can be used alongside the `getRankingInfo` parameter.
It helps you improve the results of specific queries.
The idea is to create a small index that mirrors your main index,
and to test different configurations one-by-one until you've singled out the problem or inefficiency.
* **Analytics and Insights**.
This lets you follow your users' behavior with [Analytics](/doc/guides/search-analytics/overview).
## The engine's ranking decisions
You can find out why a record is ranked the way it is,
and use this information to troubleshoot your data and relevance settings.
You can do this in the Algolia dashboard or with the API.
### Troubleshooting in the dashboard
If you go to your dashboard and search, you have a "Ranking Info" section that details how Algolia ranked this record.
If you look at the second hit, you'll have the difference between this object and the one preceding it.
### Troubleshooting with the API
Ranking information can be retrieved with the API.
For that, you need to use the parameter [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) and set it to true.
To run the code examples on this page, [install the latest API client](/doc/libraries/sdk/install).
#### Initialize the client
[Set up an API client and send your data to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data/how-to/importing-with-the-api).
#### Search with `getRankingInfo`
```cs C# theme={"system"} theme={"system"}
var response = await client.SearchSingleIndexAsync(
"INDEX_NAME",
new SearchParams(new SearchParamsObject { Query = "query", GetRankingInfo = true })
);
```
```dart Dart theme={"system"} theme={"system"}
final response = await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchParamsObject(
query: "query",
getRankingInfo: true,
),
);
```
```go Go theme={"system"} theme={"system"}
response, err := client.SearchSingleIndex(client.NewApiSearchSingleIndexRequest(
"INDEX_NAME").WithSearchParams(search.SearchParamsObjectAsSearchParams(
search.NewEmptySearchParamsObject().SetQuery("query").SetGetRankingInfo(true))))
if err != nil {
// handle the eventual error
panic(err)
}
```
```java Java theme={"system"} theme={"system"}
SearchResponse response = client.searchSingleIndex(
"INDEX_NAME",
new SearchParamsObject().setQuery("query").setGetRankingInfo(true),
Hit.class
);
```
```js JavaScript theme={"system"} theme={"system"}
const response = await client.searchSingleIndex({
indexName: 'indexName',
searchParams: { query: 'query', getRankingInfo: true },
});
```
```kotlin Kotlin theme={"system"} theme={"system"}
var response =
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = SearchParamsObject(query = "query", getRankingInfo = true),
)
```
```php PHP theme={"system"} theme={"system"}
$response = $client->searchSingleIndex(
'INDEX_NAME',
['query' => 'query',
'getRankingInfo' => true,
],
);
```
```python Python theme={"system"} theme={"system"}
response = client.search_single_index(
index_name="INDEX_NAME",
search_params={
"query": "query",
"getRankingInfo": True,
},
)
```
```ruby Ruby theme={"system"} theme={"system"}
response = client.search_single_index(
"INDEX_NAME",
Algolia::Search::SearchParamsObject.new(query: "query", get_ranking_info: true)
)
```
```scala Scala theme={"system"} theme={"system"}
val response = Await.result(
client.searchSingleIndex(
indexName = "INDEX_NAME",
searchParams = Some(
SearchParamsObject(
query = Some("query"),
getRankingInfo = Some(true)
)
)
),
Duration(100, "sec")
)
```
```swift Swift theme={"system"} theme={"system"}
let response: SearchResponse = try await client.searchSingleIndex(
indexName: "INDEX_NAME",
searchParams: SearchSearchParams.searchSearchParamsObject(SearchSearchParamsObject(
query: "query",
getRankingInfo: true
))
)
```
This gives you ranking info.
Take a look at the following results in the `_rankingInfo` attribute:
```json JSON icon=braces theme={"system"}
{
"_rankingInfo": {
"nbTypos": 0,
"firstMatchedWord": 0,
"proximityDistance": 0,
"userScore": 7,
"geoDistance": 1600,
"geoPrecision": 1,
"nbExactWords": 0,
"words": 0,
"filters": 0,
"matchedGeoLocation": {
"lat": 37.3688,
"lng": -122.036,
"distance": 1600
}
}
}
```
For more information, see [`_rankingInfo` in the search response](/doc/rest-api/search/search-single-index#response-hits).
# Troubleshooting search
Source: https://www.algolia.com/doc/guides/managing-results/troubleshooting/troubleshooting-search
Troubleshoot your search experience by determining search parameters and reproducing them on the Algolia dashboard.
If search results aren't what you expect them to be, you can investigate what's happening with a two-step approach:
1. Find out [which search parameters are in use](#retrieve-search-parameters).
2. [Reproduce those search parameters](#reproduce-the-search-in-algolia’s-dashboard) in the dashboard.
This approach helps you understand why your search behaves a certain way and lets you test improvements and fixes.
## Retrieve search parameters
The first step in troubleshooting is to clarify precisely the search parameters you sent to Algolia, to see what's returned and ensure it matches your intent.
Use your browser's developer tools to check what data you sent in your search requests.
Open your browser's [developer tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools) and click **Network**.
The next steps differ depending on your browser:
1. Set `algolia` as a filter.
2. Enter your search request. Network requests to Algolia appear.
3. Click the latest request.
4. Click **Payload**
5. Scroll to **Form Data**
1. Change the network request type to **XHR/Fetch** (the default is **All**)
2. Set `algolia` as a filter.
3. Enter your search request. Network requests to Algolia appear.
4. Click the latest **queries** request.
5. Scroll to **Request Data**
1. Change the network request type to **Fetch/XHR** (the default is **All**)
2. Set `algolia` as a filter.
3. Enter your search request. Network requests to Algolia appear.
4. Click the latest **queries…** request.
5. Click **Payload**
6. Scroll to **Form Data**
1. Change the network request type to **XHR** (the default is **All**)
2. Set `algolia` as a filter.
3. Enter your search request. Network requests to Algolia appear.
4. Click the latest **POST** request.
5. Click **Request**
### Check parameters in the browser developer tools
* **Check the name.** Is it correct?
* **Check the .** Has your app incorrectly changed it?
* **Investigate parameters that can alter search results**, for example, [`facetFilters`](/doc/api-reference/api-parameters/facetFilters). Check [`ruleContexts`](/doc/api-reference/api-parameters/ruleContexts) and [`filters`](/doc/api-reference/api-parameters/filters) which are other commonly used parameters that can alter search results. See the API documentation for [the complete list of parameters](/doc/api-reference/api-parameters).
With backend search, check the [parameters sent with your request](/doc/libraries/sdk/v1/methods/search#param-search-parameters) and
the [URL-encoded search parameters](/doc/libraries/sdk/v1/methods/search#param-search-parameters) in the response.
## Reproduce the search in Algolia's dashboard
Use the dashboard to reproduce the same search queries and parameters that you tested in the browser. This process:
* Helps determine if your code is changing search in an undesirable way by comparing results between your app and the dashboard.
* Lets you test potential improvements on the dashboard.
### How to reproduce search queries and parameters
Go to your index in the Algolia dashboard and click **Add Query Parameter** in the **Browse** tab.
* Add tag, facet, and numeric filters in the **Filters** tab.
* Add other search parameters as JSON in the **Custom** tab.
## See also
* [Troubleshooting relevance](/doc/guides/managing-results/troubleshooting/troubleshooting-relevance)
* [Filters and arrays of objects](/doc/guides/managing-results/refine-results/filtering/in-depth/arrays-of-objects)
* [How do I configure display preferences in the dashboard?](https://support.algolia.com/hc/en-us/articles/4406981890833-How-do-I-configure-display-preferences-in-the-dashboard-#troubleshooting-problems-with-your-images)
* [How can I troubleshoot with Search API Logs in the dashboard?](https://support.algolia.com/hc/en-us/articles/4406981928081)
* [Support policies (including troubleshooting services)](https://www.algolia.com/policies/support/)
# Send click and conversion events with React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/events/react
Unlock Algolia's most powerful features by sending click, conversion, and view events from your React InstantSearch app.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Run and edit the Send click and conversion events with React InstantSearch example in CodeSandbox.
Browse the source for the Send click and conversion events with React InstantSearch example on GitHub.
## What are events?
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
## How to send events
InstantSearch widgets can automatically send certain events if you enable the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights)
option when setting up your app.
For a full guide to implementing events, see [Send events with InstantSearch](/doc/guides/sending-events/instantsearch/send-events).
## Default events
With the `insights` middleware, your InstantSearch widgets send default events.
To check the default events, go to the [**Events Debugger**](https://dashboard.algolia.com/events/debugger).
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
### Default `click` events for refinement widgets
The following widgets send `click` events ("Filter Applied") when users select a refinement.
[Custom widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react) using the Hooks send the same events.
| Widget | Hook |
| - | - |
| [`HierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/react) | [`useHierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/react#hook) |
| [`Menu`](/doc/api-reference/widgets/menu/react) | [`useMenu`](/doc/api-reference/widgets/menu/react#hook) |
| [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) | [`useRefinementList`](/doc/api-reference/widgets/refinement-list/react#hook) |
| [`ToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react) | [`useToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react#hook) |
Numeric refinement widgets don't send `clicked` events as the `clickedFilters` event doesn't apply to numeric filters.
This is relevant for [`useNumericMenu`](/doc/api-reference/widgets/numeric-menu/react)-
and [`useRange`](/doc/api-reference/widgets/range-input/react#hook)-based widgets.
### Default view events for results widgets
The following widgets send `view` events ("Hits Viewed") for the visible items in the search results.
[Custom widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react) using the connectors send the same events.
| Widget | Hook |
| - | - |
| [`Hits`](/doc/api-reference/widgets/hits/react) | [`useHits`](/doc/api-reference/widgets/hits/react#hook) |
| [`InfiniteHits`](/doc/api-reference/widgets/infinite-hits/react) | [`useInfiniteHits`](/doc/api-reference/widgets/infinite-hits/react#hook) |
### Default click events for results widgets
The following widgets send `click` events ("Hit Clicked") when users click a search result.
| Widget | Hook |
| - | - |
| [`Hits`](/doc/api-reference/widgets/hits/react) | [`useHits`](/doc/api-reference/widgets/hits/react#hook) |
| [`InfiniteHits`](/doc/api-reference/widgets/infinite-hits/react) | [`useInfiniteHits`](/doc/api-reference/widgets/infinite-hits/react#hook) |
**Because of technical limitations, the default `click` events aren't sent when using [`useHits`](/doc/api-reference/widgets/hits/react#hook) or [`useInfiniteHits`](/doc/api-reference/widgets/infinite-hits/react#hook).** If you're using Hooks, make sure to set up `click` events on them.
```jsx React icon=code theme={"system"}
function CustomHits(props) {
const { items, sendEvent } = useHits(props); // or `useInfiniteHits`
return (
);
}
```
# Send click and conversion events with Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/events/vue
Unlock Algolia's most powerful features by sending click and conversion events from your Vue InstantSearch app.
This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
## What are events?
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
## How to send events
InstantSearch widgets can automatically send certain events if you enable the [`insights`](/doc/api-reference/widgets/instantsearch/js#param-insights)
option when setting up your app.
For a full guide to implementing events, see [Send events with InstantSearch](/doc/guides/sending-events/instantsearch/send-events).
## Default events
With the `insights` middleware, your InstantSearch widgets send default events.
To check the default events, go to the [**Events Debugger**](https://dashboard.algolia.com/events/debugger).
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
### Default click events for refinement widgets
The following widgets send `click` events ("Filter Applied") when users select a refinement.
[Custom widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue) use connectors to send the same events.
| Widget | Connector |
| - | - |
| [`ais-hierarchical-menu`](/doc/api-reference/widgets/hierarchical-menu/vue#param-default) | `connectHierarchicalMenu` |
| [`ais-menu`](/doc/api-reference/widgets/menu/vue#param-default) | `connectMenu` |
| [`ais-menu-select`](/doc/api-reference/widgets/menu-select/vue#param-default) | `connectMenu` |
| [`ais-rating-menu`](/doc/api-reference/widgets/rating-menu/vue#param-default) | `connectRatingMenu` |
| [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue#param-default) | `connectRefinementList` |
| [`ais-toggle-refinement`](/doc/api-reference/widgets/toggle-refinement/vue#param-default) | `connectToggleRefinement` |
Numeric refinement widgets don't send `clicked` events as the `clickedFilters` event doesn't apply to numeric filters.
This is relevant for `connectNumericMenu`- and `connectRange`-based widgets.
### Default view events for results widgets
The following widgets send `view` events ("Hits Viewed") for the visible items in the search results.
[Custom widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue) using the connectors send the same events.
| Widget | Connector |
| - | - |
| [`ais-autocomplete`](/doc/api-reference/widgets/autocomplete/vue#param-default) | `connectAutocomplete` |
| [`ais-hits`](/doc/api-reference/widgets/hits/vue#param-default) | `connectHits` |
| [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue#param-default) | `connectInfiniteHits` |
### Default click events for results widgets
The following widgets send `click` events ("Hit Clicked") when users click a search result.
| Widget | Connector |
| - | - |
| [`hits`](/doc/api-reference/widgets/hits/vue#param-default) | [`ais-hits`](/doc/api-reference/widgets/hits/vue#customize-the-ui) |
| [`infiniteHits`](/doc/api-reference/widgets/infinite-hits/vue#param-default) | [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue#customize-the-ui) |
**Because of technical limitations, the default `click` events aren't sent when using [`ais-hits`](/doc/api-reference/widgets/hits/vue#customize-the-ui) or [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue#customize-the-ui).** If you're using connectors, make sure to set up `click` events on them.
```html Vue theme={"system"}
```
# Get started with InstantSearch.js in Angular
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/angular
Integrate InstantSearch.js into an Angular app.
Use InstantSearch.js in your Angular projects to manage state and build custom search interfaces.
This approach lets you build a custom search solution without relying on Angular InstantSearch,
which isn't compatible with the latest Angular versions.
This simplifies development and gives you full control of your search interface.
Browse the source for the InstantSearch.js in Angular quickstart example on GitHub.
## Base use-case
Rather than adding InstantSearch to a component directly,
store the instance in a service and expose utility methods.
You can reuse a single instance across multiple components and start it from just one component.
```js JavaScript icon=code theme={"system"}
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import type { IndexWidget, Widget } from 'instantsearch.js';
const searchClient = algoliasearch(
'ALGOLIA_APPLICATION_ID',
'ALGOLIA_SEARCH_API_KEY'
);
@Injectable({
providedIn: 'root',
})
export class InstantSearchService {
public instantSearchInstance: InstantSearch;
constructor(router: Router) {
this.instantSearchInstance = new InstantSearch({
searchClient,
indexName: 'instant_search',
future: { preserveSharedStateOnUnmount: true },
});
}
start() {
this.instantSearchInstance.start();
}
addWidgets(widgets: Array) {
this.instantSearchInstance.addWidgets(widgets);
}
removeWidgets(widgets: Array) {
this.instantSearchInstance.removeWidgets(widgets);
}
}
```
Inject this service into any component to access the InstantSearch instance:
```js Javascript icon=code theme={"system"}
import { Component } from "@angular/core";
import { InstantSearchService } from "../instant-search.service";
@Component({
selector: "app-search",
standalone: true,
templateUrl: "./search.component.html",
})
export class SearchComponent {
ngAfterContentInit() {
this.InstantSearchService.start();
}
}
```
Add widgets in any component using the service,
whether in the constructor or `ngOnInit`.
Start the instance in a single component with `ngAfterContentInit`.
Use connectors to track state changes and bind data to your components.
```js JavaScript icon=code theme={"system"}
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
import { BaseHit } from 'instantsearch.js';
import { connectHits, connectSearchBox } from 'instantsearch.js/es/connectors';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
public hits: BaseHit[] = [];
public refine: (query: string) => void;
public query: string;
constructor(private InstantSearchService: InstantSearchService) {
this.InstantSearchService.addWidgets([
connectSearchBox(({ refine, query }) => {
this.refine = refine;
this.query = query;
})({
// ...widgetParameters
}),
connectHits(({ hits }) => {
this.hits = hits;
})({}),
]);
}
ngAfterContentInit() {
this.InstantSearchService.start();
}
public search(event: Event) {
this.refine!((event.target as HTMLInputElement).value);
}
}
```
This lets you have multiple widgets in a single component.
The template automatically re-renders when the state updates with new results:
```html HTML icon=code-xml theme={"system"}
@for (let hit of hits; track hit.objectID) {
{{ hit.name }}
}
```
## Server-side rendering and routing
Because this service reads the Angular Router's initial state, routing integration works out of the box.
Server-side rendering also works without extra code, as Angular renders the page with the initial state coming from the network request.
```js JavaScript icon=code theme={"system"}
import { Injectable } from '@angular/core';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import history from 'instantsearch.js/es/lib/routers/history';
import InstantSearch from 'instantsearch.js/es/lib/InstantSearch';
import type { Widget } from 'instantsearch.js';
import { Router } from '@angular/router';
const searchClient = algoliasearch(
'ALGOLIA_APPLICATION_ID',
'ALGOLIA_SEARCH_API_KEY'
);
@Injectable({
providedIn: 'root',
})
export class InstantSearchService {
public instantSearchInstance: InstantSearch;
constructor(router: Router) {
this.instantSearchInstance = new InstantSearch({
searchClient,
indexName: 'instant_search',
future: { preserveSharedStateOnUnmount: true },
routing: {
router: history({
getLocation: () => {
if (typeof window === 'undefined') {
// No other way to get this in constructor
return new URL(
router['location']._locationStrategy._platformLocation.href
) as unknown as Location;
}
return window.location;
},
}),
},
});
}
}
```
## Multi-index
The [`index`](/doc/api-reference/widgets/index-widget/js) widget supports multiple indices.
```js JavaScript icon=code theme={"system"}
import { Component } from '@angular/core';
import { InstantSearchService } from '../instant-search.service';
import { BaseHit } from 'instantsearch.js';
import { connectHits } from 'instantsearch.js/es/connectors';
import index from 'instantsearch.js/es/widgets/index/index';
@Component({
selector: 'app-search',
standalone: true,
templateUrl: './search.component.html',
})
export class SearchComponent {
public hits: BaseHit[] = [];
constructor(private InstantSearchService: InstantSearchService) {
this.InstantSearchService.addWidgets([
index({ indexName: 'instant_search' }).addWidgets([
connectHits(({ hits }) => {
this.hits = hits;
})({}),
]),
]);
}
ngAfterContentInit() {
this.InstantSearchService.start();
}
}
```
# Get started with React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/react
Use React InstantSearch to create a dynamic, filterable search UI.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
## Before you begin
This quickstart requires:
* Basic React knowledge.
* A React ≥ [16.8.0](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) app. If you don't have a React app, use this [CodeSandbox template](https://codesandbox.io/s/react-new).
* An Algolia account. If you don't have one, [create a new account](https://www.algolia.com/users/sign_up) for free.
* An Algolia with `image`, `categories`, `name`, `brand`, and `price` attributes. If you don't have one, use [a sample dataset](https://github.com/algolia/datasets/blob/master/ecommerce/records.json) and [send it to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data).
* [`algoliasearch` and `react-instantsearch`](/doc/guides/building-search-ui/installation/react) installed.
## Quickstart
In this quickstart, you'll add an Algolia search interface to some starter code.
The code:
* Displays and formats a search box and results
* Uses InstantSearch's pre-built UI components (widgets) to filter results
The main UI component of a search experience is a search box.
It's often how users discover content in your app.
React InstantSearch provides a [`SearchBox`](/doc/api-reference/widgets/search-box/react) widget to display an Algolia search box.
```jsx React icon=code theme={"system"}
// App.jsx
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
function App() {
return (
);
}
```
Replace `INDEX_NAME` with the name of your index.
If you type in the search box, you won't see any results until you add a `hits` widget to display them.
Display the results returned by Algolia using the [`Hits`](/doc/api-reference/widgets/hits/react) widget.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
-import { InstantSearch, SearchBox } from 'react-instantsearch';
+import { InstantSearch, SearchBox, Hits } from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
+function Hit({ hit }) {
+ return (
+
+
+
{hit.categories[0]}
+
{hit.name}
+
${hit.price}
+
+ );
+}
function App() {
return (
+
);
}
```
Replace `INDEX_NAME` with the name of your index.
The `Hits` widget updates whenever you type a character in the search box.
It's not always clear why one result ranks above another.
To help make things more evident to users, **you can highlight the parts of a result that match the .**
Algolia supports highlighting and returns the highlighted parts of a result in the response.
Use the [`Highlight`](/doc/api-reference/widgets/highlight/react) widget to highlight matches in each attribute.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
-import { InstantSearch, SearchBox, Hits } from 'react-instantsearch';
+import {
+ InstantSearch,
+ SearchBox,
+ Hits,
+ Highlight,
+} from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
function Hit({ hit }) {
return (
{hit.categories[0]}
-
{hit.name}
+
+
+
${hit.price}
);
}
```
When users type a query, the UI highlights matches in each search result.
To send [click and conversion events](/doc/guides/sending-events/getting-started) when users interact with your search UI,
set the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option to `true`.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
} from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
function Hit({ hit }) {
return (
{hit.categories[0]}
${hit.price}
);
}
function App() {
return (
-
+
);
}
```
A search box is a great way to refine a search experience,
but sometimes users need to narrow down the results to find what they're looking for in a specific category.
Use [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) to items based on attributes such as brand, size, or color.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
+ RefinementList,
} from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
function Hit({ hit }) {
return (
{hit.categories[0]}
${hit.price}
);
}
function App() {
return (
+
);
}
```
Replace `INDEX_NAME` with the name of your index.
The app only shows 20 hits but Algolia returns more results.
Use the [`Pagination`](/doc/api-reference/widgets/pagination/react) widget to navigate the pages.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
RefinementList,
+ Pagination,
} from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
// ...
function App() {
return (
+
);
}
```
You can override any [search parameters](/doc/api-reference/search-api-parameters) with [`Configure`](/doc/api-reference/widgets/configure/react).
This widget doesn't render anything but instead instructs InstantSearch to send custom search parameters to Algolia.
This update sets 40 hits per page.
```diff App.jsx icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
RefinementList,
Pagination,
+ Configure,
} from 'react-instantsearch';
// ...
function App() {
return (
+
);
}
```
## Next steps
With this quickstart, you've got a solid starting point for building dynamic search interfaces.
To enhance it:
* Use your own data.
* Load hits without pagination using [`InfiniteHits`](/doc/api-reference/widgets/infinite-hits/react).
* [Search more than one index](/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/react) with [`Index`](/doc/api-reference/widgets/index-widget/react).
* [Server-side render](/doc/guides/building-search-ui/going-further/server-side-rendering/react) your app for increased performance.
* Provide some more refinement widgets. For example:
* [`Menu`](/doc/api-reference/widgets/menu/react) to refine on a single value per attribute
* [`HierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/react) to filter a hierarchy of categories
* [`ToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react) to filter on and off on a given attribute value
If you have several refinement widgets,
use [`CurrentRefinements`](/doc/api-reference/widgets/current-refinements/react) to display the active filters and let users remove them individually.
## See also
* [What is InstantSearch?](/doc/guides/building-search-ui/what-is-instantsearch/react)
* [InstantSearch widgets](/doc/api-reference/widgets/react)
* [Send click and conversion events](/doc/guides/building-search-ui/events/react)
# Get started with Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/vue
Use Vue InstantSearch to create a dynamic, filterable search UI.
## Before you begin
This quickstart assumes basic Vue knowledge.
It includes all the code, data, and credentials you need.
## Quickstart
In this quickstart, you'll add an Algolia search interface to some starter code.
The code:
* Displays and formats a search box and results
* Uses InstantSearch's pre-built UI components (widgets) to filter results
Run and edit the Vue InstantSearch quickstart example in CodeSandbox.
Browse the source for the Vue InstantSearch quickstart example on GitHub.
To generate some starter code, use the `create-instantsearch-app` tool.
It provides the data, all necessary code, and predefined credentials ( and ).
In a terminal, paste:
```sh Command line icon=square-terminal theme={"system"}
npx create-instantsearch-app ais-ecommerce-demo-app --template "Vue InstantSearch"
```
This command creates a folder structure on your machine:
```txt theme={"system"}
ais-ecommerce-demo-app/
├── node_modules/
├── src/
├──── App.vue
├──── main.js
├── package.json
├── README.md
└── yarn.lock
```
Open `src/App.vue` and replace the whole file with the following:
```vue Vue icon=code theme={"system"}
{{ item.name }}
```
1. In your terminal, type:
```sh Command line icon=square-terminal theme={"system"}
cd ais-ecommerce-demo-app
npm start
```
2. Open your browser and go to [http://localhost:3000](https://localhost:3000).
The app displays the search box and results.
Open `src/App.vue` and update it:
```vue Vue icon=code theme={"system"}
Brands
{{ item.name }}
```
The widgets come with a [predefined style](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue#style-your-widgets) but you can customize it.
1. Update the content of the style tag with the following to apply a two-column layout:
```css CSS icon=paintbrush theme={"system"}
body {
font-family: sans-serif;
padding: 1em;
}
.ais-Hits-list {
margin-top: 0;
margin-bottom: 1em;
}
.ais-InstantSearch {
display: grid;
grid-template-columns: 1fr 4fr;
grid-gap: 1em;
}
```
1. In your browser, after a page refresh, the layout includes a **Brands** .
Open `src/App.vue` and replace the content of the `ais-hits` item slot with:
```vue Vue icon=code theme={"system"}
{{ item.name }}
{{ item.price }}
```
1. Update the content of the style tag with:
```css CSS icon=paintbrush theme={"system"}
body {
font-family: sans-serif;
padding: 1em;
}
.ais-Hits-list {
margin-top: 0;
margin-bottom: 1em;
}
.ais-InstantSearch {
display: grid;
grid-template-columns: 1fr 4fr;
grid-gap: 1em;
}
.ais-Hits-item img {
margin-right: 1em;
}
.hit-name {
margin-bottom: 0.5em;
}
.hit-description {
color: #888;
font-size: 0.8em;
margin-bottom: 0.5em;
}
```
1. In your browser, after a page refresh, you'll see product images.
## The widgets
This quickstart uses several InstantSearch widgets:
* [`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue) is the root Vue InstantSearch component. You must wrap all other widgets inside this component.
* [`ais-search-box`](/doc/api-reference/widgets/search-box/vue) displays a search box for users to type queries into.
* [`ais-hits`](/doc/api-reference/widgets/hits/vue) displays the results from Algolia, based on the .
* [`ais-clear-refinements`](/doc/api-reference/widgets/clear-refinements/vue) displays a button that clears the current refinements.
* [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) displays a list of brands that users can use to filter the search.
* [`ais-configure`](/doc/api-reference/widgets/configure/vue) passes [search parameters](/doc/api-reference/search-api-parameters). In this example, it sets [`hitsPerPage`](/doc/api-reference/api-parameters/hitsPerPage) to 8.
* [`ais-pagination`](/doc/api-reference/widgets/pagination/vue) adds navigation controls to browse through pages of results.
## Next steps
Instead of the credentials from `create-instantsearch-app`,
use your own Algolia [credentials](https://www.algolia.com/users/sign_up).
You can send your own data or [a sample dataset](https://github.com/algolia/datasets/blob/master/ecommerce/records.json) to Algolia using the JavaScript API client that comes preinstalled with `create-instantsearch-app`.
Then, configure the index with the JavaScript API client.
For example, to configure the sample dataset,
use the following code:
```js JavaScript icon="code" theme={"system"}
index
.setSettings({
searchableAttributes: [
"brand",
"name",
"categories",
"unordered(description)",
],
customRanking: ["desc(popularity)"],
attributesForFaceting: ["searchable(brand)", "type", "categories", "price"],
})
.then(() => {
// done
});
```
### Initializing your app
If you're using your app instead of `create-instantsearch-app`,
initialize it by changing the contents of `main.js` to include the Vue InstantSearch library:
```js Vue 3 theme={"system"}
import { createApp } from "vue";
import App from "./App.vue";
import InstantSearch from "vue-instantsearch/vue3/es";
const app = createApp(App);
app.use(InstantSearch);
app.mount("#app");
```
```js Vue 2 theme={"system"}
import Vue from "vue";
import App from "./App.vue";
import InstantSearch from "vue-instantsearch";
Vue.use(InstantSearch);
new Vue({
el: "#app",
render: (h) => h(App),
});
```
## See also
* [What is InstantSearch?](/doc/guides/building-search-ui/what-is-instantsearch/vue)
* [Send and update your data](/doc/guides/sending-and-managing-data/send-and-update-your-data)
* [Send click and conversion events](/doc/guides/building-search-ui/events/vue)
* [Customize an existing widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue)
# Secured API keys
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/api-keys-security/react
Use secured API keys in your React InstantSearch app to keep your data safe.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
## Search-only API keys in web apps
You can use the [search-only API key](/doc/guides/security/api-keys#search-only-api-key) in your frontend code.
This key is safe to expose because it only allows search operations.
If you need additional restrictions,
such as limiting the access to specific indices,
or applying rate limits,
generate a **secured API key** on your backend.
## Secured API keys
Secured API keys add restrictions and search parameters that your users can't change.
You must generate them on your server, or anyone can remove the restrictions again.
This guide shows how to use secured API keys in an InstantSearch app.
For mode details, see [User-restricted access to data](/doc/guides/security/api-keys/how-to/user-restricted-access-to-data).
The aim is to build an app that generates a secured API key with restrictions on the server,
and then uses it on the client.
See the full example on
[GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/secured-api-keys).
### Server
#### Generate a secured API key
Generate the secured API key on your server.
This example uses [Express](https://expressjs.com), but the same approach works with any server.
To generate a key, you need the Algolia JavaScript client and a parent API key with the
[search ACL](/doc/guides/security/api-keys#access-control-list-acl).
In this example, the generated key only allows searching the `demo_ecommerce` .
You can also apply
[additional restrictions](/doc/guides/security/api-keys/in-depth/api-key-restrictions),
including
[search parameters](/doc/guides/security/api-keys/in-depth/api-key-restrictions#query-parameter-restrictions).
```js JavaScript icon=code theme={"system"}
const algoliasearch = require("algoliasearch");
const client = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const securedApiKey = client.generateSecuredApiKey({
parentApiKey: "ALGOLIA_SEARCH_API_KEY",
restrictions: {
restrictIndices: "demo_ecommerce",
},
});
```
Send the secured API key to the client in one of these ways:
* Return it from an endpoint, then fetch it before you create your InstantSearch app.
* Inline it in the HTML, then read it from a global object in the client.
### Inline the API key
If your build tool generates the HTML, add a placeholder value and replace it on the server before sending the page.
If you control the HTML template, inline the value directly.
```html HTML icon=code-xml theme={"system"}
```
Replace the placeholder with the generated API key:
```js JavaScript icon=code theme={"system"}
const path = require("path");
const fs = require("fs");
const util = require("util");
const express = require("express");
const app = express();
const readFileAsync = util.promisify(fs.readFile);
app.get("/", async (_, res) => {
const index = await readFileAsync(
path.join(__dirname, "dist", "index.html"),
"utf-8",
);
const indexWithServerData = index.replace(
"__SERVER_DATA__",
JSON.stringify({
ALGOLIA_API_KEY: securedApiKey,
}),
);
res.send(indexWithServerData);
});
```
Start the server and read the key from `window.SERVER_DATA` in the client.
The next section explains how to use this key in an InstantSearch app.
### Client
#### Retrieve the API key
Now that you have the API key on the global object, you can retrieve it from the client code and inject it into [`searchClient`](/doc/api-reference/widgets/instantsearch/react).
Make sure you clean up the global object.
Otherwise, this value will stay in memory.
```jsx React icon=code theme={"system"}
import React from "react";
import { createRoot } from "react-dom/client";
import { InstantSearch } from "react-instantsearch";
import { liteClient as algoliasearch } from "algoliasearch/lite";
function App({ searchClient }) {
return (
{/* Widgets */}
);
}
const SERVER_DATA = window.SERVER_DATA;
// clean up the global object
delete window.SERVER_DATA;
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
SERVER_DATA.ALGOLIA_API_KEY,
);
createRoot(document.getElementById("root")).render(
,
);
```
That's it for the client.
Your app can now only target the `demo_ecommerce` index.
You can try to target a different one like `demo_media` but the API will return an error.
* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/react-instantsearch/secured-api-keys)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch/secured-api-keys)
## Prevent cross-site scripting vulnerabilities
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/react) helps users understand how their query matches the results.
It's a key visual cue.
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/vue) helps users understand how their query matches the results.
It's a key visual cue.
Algolia wraps matching words in HTML tags.
By default, it uses tags like [`em`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em), which the browser renders as HTML.
If your site includes user-generated content, this can expose your app to cross-site scripting (XSS) attacks.
The same risk applies to the snippeting feature.
To prevent this, InstantSearch restricts highlighting to a predefined set of HTML tags and escapes all others.
If you set a custom tag in the Algolia dashboard,
it's overridden and replaced by [`mark`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark).
To use a different tag, such as [`strong`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong),
set the `highlightedTagName` props in [`Highlight>`](/doc/api-reference/widgets/highlight/react) or [`Snippet>`](/doc/api-reference/widgets/snippet/react).
```jsx React icon=code theme={"system"}
import React from "react";
import { InstantSearch, Hits, Highlight, Snippet } from "react-instantsearch";
const Hit = ({ hit }) => (
);
const App = () => (
);
```
# Secured API keys in Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/api-keys-security/vue
Use secured API keys in your Vue InstantSearch app to keep your data safe.
## Search-only API keys in web apps
You can use the [search-only API key](/doc/guides/security/api-keys#search-only-api-key) in your frontend code.
This key is safe to expose because it only allows search operations.
If you need additional restrictions,
such as limiting the access to specific indices,
or applying rate limits,
generate a **secured API key** on your backend.
## Secured API keys
Secured API keys add restrictions and search parameters that your users can't change.
You must generate them on your server, or anyone can remove the restrictions again.
This guide shows how to use secured API keys in an InstantSearch app.
For mode details, see [User-restricted access to data](/doc/guides/security/api-keys/how-to/user-restricted-access-to-data).
The aim is to build an app that generates a secured API key with restrictions on the server,
and then uses it on the client.
See the full example on
[GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/secured-api-keys).
### Server
#### Generate a secured API key
Generate the secured API key on your server.
This example uses [Express](https://expressjs.com), but the same approach works with any server.
To generate a key, you need the Algolia JavaScript client and a parent API key with the
[search ACL](/doc/guides/security/api-keys#access-control-list-acl).
In this example, the generated key only allows searching the `demo_ecommerce` .
You can also apply
[additional restrictions](/doc/guides/security/api-keys/in-depth/api-key-restrictions),
including
[search parameters](/doc/guides/security/api-keys/in-depth/api-key-restrictions#query-parameter-restrictions).
```js JavaScript icon=code theme={"system"}
const algoliasearch = require("algoliasearch");
const client = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const securedApiKey = client.generateSecuredApiKey({
parentApiKey: "ALGOLIA_SEARCH_API_KEY",
restrictions: {
restrictIndices: "demo_ecommerce",
},
});
```
Send the secured API key to the client in one of these ways:
* Return it from an endpoint, then fetch it before you create your InstantSearch app.
* Inline it in the HTML, then read it from a global object in the client.
### Inline the API key
If your build tool generates the HTML, add a placeholder value and replace it on the server before sending the page.
If you control the HTML template, inline the value directly.
```html HTML icon=code-xml theme={"system"}
```
Replace the placeholder with the generated API key:
```js JavaScript icon=code theme={"system"}
const path = require("path");
const fs = require("fs");
const util = require("util");
const express = require("express");
const app = express();
const readFileAsync = util.promisify(fs.readFile);
app.get("/", async (_, res) => {
const index = await readFileAsync(
path.join(__dirname, "dist", "index.html"),
"utf-8",
);
const indexWithServerData = index.replace(
"__SERVER_DATA__",
JSON.stringify({
ALGOLIA_API_KEY: securedApiKey,
}),
);
res.send(indexWithServerData);
});
```
Start the server and read the key from `window.SERVER_DATA` in the client.
The next section explains how to use this key in an InstantSearch app.
### Client
#### Retrieve the API key
Now that you have the API key on the global object, you can retrieve it from the client code and inject it into [`searchClient`](/doc/api-reference/widgets/instantsearch/vue).
Make sure you clean up the global object.
Otherwise, this value will stay in memory.
```html Vue theme={"system"}
```
That's it for the client.
Your app can now only target the `demo_ecommerce` index.
You can try to target a different one like `demo_media` but the API will return an error.
* [Demo](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/vue-instantsearch/secured-api-keys)
* [Source code](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/secured-api-keys)
This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
## Prevent cross-site scripting vulnerabilities
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/vue) helps users understand how their query matches the results.
It's a key visual cue.
Algolia's [highlighting feature](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/vue) helps users understand how their query matches the results.
It's a key visual cue.
Algolia wraps matching words in HTML tags.
By default, it uses tags like [`em`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em), which the browser renders as HTML.
If your site includes user-generated content, this can expose your app to cross-site scripting (XSS) attacks.
The same risk applies to the snippeting feature.
To prevent this, InstantSearch restricts highlighting to a predefined set of HTML tags and escapes all others.
If you set a custom tag in the Algolia dashboard,
it's overridden and replaced by [`mark`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark).
To use a different tag, such as [`strong`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong),
set the `highlightedTagName` props in [`ais-highlight`](/doc/api-reference/widgets/highlight/vue) or [`ais-snippet`](/doc/api-reference/widgets/snippet/vue).
```html Vue theme={"system"}
```
To turn this safety feature off,
use `:escape-hits="false"` on the [`ais-hits`](/doc/api-reference/widgets/hits/vue) component, then the HTML will no longer be escaped.
# Turn off automatic filtering in React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/automatic-filtering/react
Build a widget to turn off automatic filtering and boosting with React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Automatic Filtering and Boosting applies a category filter when users enter a .
You can use [Query Categorization](/doc/guides/algolia-ai/query-categorization) to
predict categories for your most popular queries.
If you enable [Automatic Filtering and Boosting](/doc/guides/algolia-ai/query-categorization#automatic-filtering-and-boosting)
the categories are automatically applied as filters.
## Control Automatic Filtering and Boosting in your UI
If you want to have Automatic Filtering and Boosting on or off **for all searches**, you only need to enable the feature to filter the results automatically.
If you want to let users turn Automatic Filtering and Boosting on or off, you can build an InstantSearch widget. This widget should inform users that the results are filtered. The widget should also let users remove any applied filters.
## Build a widget for Automatic Filtering and Boosting
The search response includes the following properties if [Automatic Filtering and Boosting is enabled](/doc/guides/algolia-ai/query-categorization#detect-the-impact-of-automatic-filtering-and-boosting-at-query-time):
```json JSON icon="braces" theme={"system"}
{
"extensions": {
"queryCategorization": {
"autofiltering": {
"facetFilters": [],
"optionalFilters": []
}
}
}
}
```
If filters are applied to the query, they're listed in the `facetFilters` property.
```json JSON icon="braces" theme={"system"}
{
"facetFilters": [
"category.level0:Fresh products",
"category.level1:Fresh vegetables",
"category.level2:Tomatoes"
]
}
```
To remove filters applied with Automatic Filtering and Boosting, you need to [turn the feature off for the current query](/doc/guides/algolia-ai/query-categorization#override-automatic-filtering-and-boosting-at-query-time) using the `enableAutoFiltering` API parameter.
```json JSON icon="braces" theme={"system"}
{
"extensions": {
"queryCategorization": {
"enableAutoFiltering": false
}
}
}
```
To keep Automatic Filtering and Boosting for other queries,
check for a query change and then set `enableAutoFiltering` to `true`.
## Implementing the widget
The following custom connector implements all steps to let users turn off Automatic Filtering and Boosting for a search query.
The connector requires the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
```ts TypeScript icon=code expandable theme={"system"}
// connectAutoFiltering.ts
import type { Connector } from "instantsearch.js";
export type AutoFilteringConnectorParams = {};
export type AutoFilteringRenderState = {
appliedFilters: Array<{
name: string;
value: string;
}>;
cancelAutoFiltering(): void;
};
export type AutoFilteringWidgetDescription = {
$$type: "algolia.beta.autoFiltering";
renderState: AutoFilteringRenderState;
indexRenderState: {};
indexUiState: {};
};
type AutoFilteringConnector = Connector<
AutoFilteringWidgetDescription,
AutoFilteringConnectorParams
>;
type ConnectorState = {
disabledQuery?: string;
cancelAutoFiltering?: AutoFilteringRenderState["cancelAutoFiltering"];
};
// util function to set the query parameter (step 2)
function setAutoFiltering(value, helper) {
return helper.setQueryParameter("extensions", {
queryCategorization: {
enableAutoFiltering: value,
},
});
}
type WidgetRenderStateWithResultsExtensions = Parameters<
ReturnType>["getWidgetRenderState"]
>[0] & {
state: {
extensions?: {
queryCategorization?: {
enableAutoFiltering?: boolean;
};
};
};
results: {
extensions?: {
queryCategorization?: {
normalizedQuery?: string;
autofiltering?: {
facetFilters?: string[];
};
};
};
};
};
export const connectAutoFiltering: AutoFilteringConnector = (
renderFn,
unmountFn = () => {},
) => {
return function autoFiltering(widgetParams) {
const connectorState: ConnectorState = {};
return {
$$type: "algolia.beta.autoFiltering",
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
true,
);
},
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
false,
);
},
dispose() {
unmountFn();
},
getWidgetUiState(uiState) {
return uiState;
},
getWidgetSearchParameters(searchParameters) {
return searchParameters;
},
getRenderState(renderState, renderOptions) {
return {
...renderState,
autoFiltering: this.getWidgetRenderState(renderOptions),
};
},
// this is where the logic happens
getWidgetRenderState({
results,
helper,
state,
}: WidgetRenderStateWithResultsExtensions) {
if (!connectorState.cancelAutoFiltering) {
connectorState.cancelAutoFiltering = () => {
// Disable auto filtering for next search
setAutoFiltering(false, helper);
helper.search();
// storing in the state the disabled query
connectorState.disabledQuery = helper.getQuery().query;
};
}
// empty results case
if (!results) {
return {
appliedFilters: [],
cancelAutoFiltering: () => {},
widgetParams,
};
}
// enabling back auto filtering if the query has changed (step 3)
if (
// "state" stores the current query parameters
state.extensions?.queryCategorization?.enableAutoFiltering ===
false &&
connectorState.disabledQuery &&
results.query !== connectorState.disabledQuery
) {
setAutoFiltering(true, helper);
if (results.extensions.queryCategorization.normalizedQuery) {
// if the current query has predicted categories, we refine the search with autofiltering enabled
helper.search();
}
}
// Retrieving the applied filters (step 1)
const facetFilters =
results.extensions?.queryCategorization?.autofiltering
?.facetFilters || [];
return {
appliedFilters: facetFilters.map((facetFilter) => ({
name: facetFilter.split(":")[0],
value: facetFilter.split(":")[1],
})),
cancelAutoFiltering: connectorState.cancelAutoFiltering,
widgetParams,
};
},
};
};
};
```
With the [`useConnector`](/doc/api-reference/widgets/connector/react) Hook,
you can turn this connector into a `useAutoFiltering` Hook.
```ts TypeScript icon=code theme={"system"}
// useAutoFiltering.ts
import { useConnector } from "react-instantsearch";
import { connectAutoFiltering } from "./connectAutoFiltering";
import type { AdditionalWidgetProperties } from "react-instantsearch";
import type {
AutoFilteringConnectorParams,
AutoFilteringWidgetDescription,
} from "./connectAutoFiltering";
export type AutoFilteringParams = AutoFilteringConnectorParams;
export function useAutoFiltering(
props?: AutoFilteringParams,
additionalWidgetProperties?: AdditionalWidgetProperties,
) {
return useConnector<
AutoFilteringConnectorParams,
AutoFilteringWidgetDescription
>(connectAutoFiltering, props, additionalWidgetProperties);
}
```
And finally, you can build the UI around the Hook.
```tsx React icon=code theme={"system"}
// AutoFiltering.tsx
import React from "react";
import { useAutoFiltering } from "./useAutoFiltering";
export function AutoFiltering() {
const { appliedFilters, cancelAutoFiltering } = useAutoFiltering(
{},
{
$$widgetType: "algolia.beta.autoFiltering",
},
);
/**
* Here you choose to display the applied filters in a banner with a button to disable autofiltering.
* Only the last filter of the hierarchy is displayed here, but you can choose to display all of them if you want to.
* It is entirely up to you how you choose to render the filters.
*/
if (appliedFilters.length === 0) {
return null;
}
return (
);
}
```
If you have a standard React InstantSearch implementation, you can copy and paste both the preceding snippets into your app.
However, ensure you tweak the final `AutoFiltering` component to fit your design.
You can then place `AutoFiltering` in your app.
# Implement backend search with React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/react
Implement server-side search with React InstantSearch and Algolia's API clients.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
You may wish to query Algolia's servers from your backend instead of the frontend, while still being able to use InstantSearch widgets.
Possible motivations could be for security restrictions, for SEO purposes or to enrich the Algolia data with data from your servers.
Keep in mind though that [Algolia recommends frontend search](https://support.algolia.com/hc/en-us/articles/4406981933457-Searching-from-the-frontend-or-the-backend-What-do-you-recommend-) for performance and high availability reasons.
## How backend search works
InstantSearch is the UI on top of a search client.
Its state is managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
However, you can use a different search client.
### Algolia search client
Algolia's [search client](https://github.com/algolia/algoliasearch-client-javascript) queries Algolia's backend whenever users refine the search.
The response from Algolia's search client is a JSON object that includes:
* Results already formatted for display (full record content, embedded HTML, image URLs, and so on).
* Highlighting and snippeting
* Pagination
To learn more, see [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/react).
### Custom search client
You can implement your own search client to query your backend which, in turn, uses the Algolia search client to query Algolia's backend.
To create your own client, you must implement an interface that receives and returns formatted data that InstantSearch can understand.
## On the backend: create the necessary routes
This guide assumes that you have an existing server running on [`http://localhost:3000`](http://localhost:3000) with the route `POST /search` that takes the default Algolia parameters as JSON.
This backend could be using the [JavasScript API client](/doc/libraries/sdk/install) to query Algolia, on top of any other operations you want to perform.
The `algoliasearch` package lets you query Algolia from your backend. Here's an example using [Express](https://expressjs.com/):
```js JavaScript icon="code" theme={"system"}
// Instantiate an Algolia client
const algoliasearch = require("algoliasearch");
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
// Add the search endpoint
app.post("/search", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.search(requests);
res.status(200).send(results);
});
// Add the recommendations endpoint
app.post("/getRecommendations", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.getRecommendations(requests);
});
```
Once your new route is ready, get back to the frontend and create a search client that can communicate with this server.
## On the frontend: call your new backend routes
### Searching for results
The `search()` method runs whenever users search or refine results.
Because the server accepts requests in the InstantSearch format,
forward the from this method to your backend and return the response.
```js JavaScript icon="code" theme={"system"}
const requestsCache = new Map();
const resultsCache = new Map();
function makeCacheKey(requests, target) {
// You can go further here, like sorting individual values inside the requests.
// This makse sure the key is always the same for the same requests.
return target + JSON.stringify(requests);
}
const customSearchClient = {
search(requests) {
const key = makeCacheKey(requests, 'search');
if (resultsCache.has(key)) {
return Promise.resolve(resultsCache.get(key));
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/search', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
getRecommendations(requests) {
const key = makeCacheKey(requests, 'getRecommendations');
if (resultsCache.has(key)) {
return resultsCache.get(key);
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/getRecommendations', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
clearCache() {
requestsCache.clear();
resultsCache.clear();
},
};
```
This example uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to query the server.
Check the [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility) requirements before using it in production.
If you want to transform the data passed to your server,
you can learn more about the parameters that the `search` and `getRecommendations` methods support:
* [Search API parameters](/doc/api-reference/api-parameters)
* [Recommendations API parameters](/doc/rest-api/recommend/get-recommendations)
### Search for facet values
The same `search()` method is used by InstantSearch to fetch values from refinement lists.
You must set the option [`searchable`](/doc/api-reference/widgets/refinement-list/react#param-searchable) to `true` in your refinement list to make it searchable.
## Use the search client with InstantSearch
Now, you need to tell InstantSearch to use the search client you created.
This is possible with the `searchClient` option.
This parameter turns off all Algolia requests coming from the frontend and proxies them to your backend and displays the UI.
```jsx React icon=code theme={"system"}
import { InstantSearch } from "react-instantsearch";
const App = ({ children }) => (
{children}
);
```
## Enrich data from the backend
Now that InstantSearch is querying your backend before fetching results from Algolia, you could merge Algolia's data with yours (to offer your users more exhaustive results).
A recurring problem with ecommerce websites using Algolia managing the remaining stock for each product.
It's sometimes hard to keep track of the exact number of items.
An approach made possible with a custom backend is to only store the item's availability on each Algolia record (`none`, `low`, `medium`, `high`) and to fetch the exact stock on your database.
You need to make a few changes to your backend:
```js JavaScript icon=code theme={"system"}
app.post('/search', async ({body}, res) => {
const requests = body;
const algoliaResults = await algoliaClient.search(requests);
const results = {
...algoliaResults,
results: algoliaResults.results.map(result => ({
...result,
hits: result.hits.map(async hit => ({
...hit,
// `getStock()` retrieves a product's exact stock from your database
stock: await getStock(hit.productID),
})),
})),
};
res.status(200).send(results);
});
app.post('/getRecommendations', async ({body, res) => {
/* The same logic as above can be applied here */
});
```
You can now access the property `stock` on each hit with InstantSearch on the frontend.
# Implement backend search with Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/vue
Implement server-side search with Vue InstantSearch and Algolia's API clients.
You may wish to query Algolia's servers from your backend instead of the frontend, while still being able to use InstantSearch widgets.
Possible motivations could be for security restrictions, for SEO purposes or to enrich the Algolia data with data from your servers.
Keep in mind though that [Algolia recommends frontend search](https://support.algolia.com/hc/en-us/articles/4406981933457-Searching-from-the-frontend-or-the-backend-What-do-you-recommend-) for performance and high availability reasons.
## How backend search works
InstantSearch is the UI on top of a search client.
Its state is managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
However, you can use a different search client.
### Algolia search client
Algolia's [search client](https://github.com/algolia/algoliasearch-client-javascript) queries Algolia's backend whenever users refine the search.
The response from Algolia's search client is a JSON object that includes:
* Results already formatted for display (full record content, embedded HTML, image URLs, and so on).
* Highlighting and snippeting
* Pagination.
For more information, see [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/vue)
### Custom search client
You can implement your own search client to query your backend which, in turn, uses the Algolia search client to query Algolia's backend.
To create your own client, you must implement an interface that receives and returns formatted data that InstantSearch can understand.
## On the backend: create the necessary routes
This guide assumes that you have an existing server running on [`http://localhost:3000`](http://localhost:3000) with the route `POST /search` that takes the default Algolia parameters as JSON.
This backend could be using the [JavasScript API client](/doc/libraries/sdk/install) to query Algolia, on top of any other operations you want to perform.
The `algoliasearch` package lets you query Algolia from your backend. Here's an example using [Express](https://expressjs.com/):
```js JavaScript icon="code" theme={"system"}
// Instantiate an Algolia client
const algoliasearch = require("algoliasearch");
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
// Add the search endpoint
app.post("/search", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.search(requests);
res.status(200).send(results);
});
// Add the recommendations endpoint
app.post("/getRecommendations", async ({ body }, res) => {
const { requests } = body;
const results = await algoliaClient.getRecommendations(requests);
});
```
Once your new route is ready, get back to the frontend and create a search client that can communicate with this server.
## On the frontend: call your new backend routes
### Searching for results
The `search()` method runs whenever users search or refine results.
Because the server accepts requests in the InstantSearch format,
forward the from this method to your backend and return the response.
```js JavaScript icon="code" theme={"system"}
const requestsCache = new Map();
const resultsCache = new Map();
function makeCacheKey(requests, target) {
// You can go further here, like sorting individual values inside the requests.
// This makse sure the key is always the same for the same requests.
return target + JSON.stringify(requests);
}
const customSearchClient = {
search(requests) {
const key = makeCacheKey(requests, 'search');
if (resultsCache.has(key)) {
return Promise.resolve(resultsCache.get(key));
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/search', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
getRecommendations(requests) {
const key = makeCacheKey(requests, 'getRecommendations');
if (resultsCache.has(key)) {
return resultsCache.get(key);
}
let request;
if (requestsCache.has(key)) {
request = requestsCache.get(key)!;
} else {
request = fetch('http://localhost:3000/getRecommendations', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requests }),
}).then(res => res.json());
}
return request.then(results => {
resultsCache.set(key, results);
return results;
});
},
clearCache() {
requestsCache.clear();
resultsCache.clear();
},
};
```
This example uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to query the server.
Check the [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility) requirements before using it in production.
If you want to transform the data passed to your server,
you can learn more about the parameters that the `search` and `getRecommendations` methods support:
* [Search API parameters](/doc/api-reference/api-parameters)
* [Recommendations API parameters](/doc/rest-api/recommend/get-recommendations)
### Search for facet values
The same `search` method is used by InstantSearch to fetch values from refinement lists.
You must set the option [`searchable`](/doc/api-reference/widgets/refinement-list/vue#param-searchable) to `true` in your refinement list to make it searchable.
## Use the search client with InstantSearch
Now, you need to tell InstantSearch to use the search client you created.
This is possible with the `searchClient` option.
This parameter turns off all Algolia requests coming from the frontend and proxies them to your backend and displays the UI.
```vue Vue icon=code theme={"system"}
```
## Enrich data from the backend
Now that InstantSearch is querying your backend before fetching results from Algolia,
you could merge Algolia's data with yours (to offer your users more exhaustive results).
A recurring problem with ecommerce websites using Algolia managing the remaining stock for each product.
It's sometimes hard to keep track of the exact number of items.
An approach made possible with a custom backend is to only store the item's availability on each Algolia record (`none`, `low`, `medium`, `high`) and to fetch the exact stock on your database.
You need to make a few changes to your backend:
```js JavaScript icon=code theme={"system"}
app.post('/search', async ({body}, res) => {
const requests = body;
const algoliaResults = await algoliaClient.search(requests);
const results = {
...algoliaResults,
results: algoliaResults.results.map(result => ({
...result,
hits: result.hits.map(async hit => ({
...hit,
// `getStock()` retrieves a product's exact stock from your database
stock: await getStock(hit.productID),
})),
})),
};
res.status(200).send(results);
});
app.post('/getRecommendations', async ({body, res) => {
/* The same logic as above can be applied here */
});
```
You can now access the property `stock` on each hit with InstantSearch on the frontend.
# Backend search with an API client
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/react
Understand the search Algolia provides out of the box.
To query your Algolia , you need an Application ID and a valid API key.
You can find them in the Algolia dashboard's [API Keys](https://dashboard.algolia.com/account/api-keys/) section.
These credentials let you connect to Algolia and perform operations on your data.
Use one of Algolia's [API clients](/doc/libraries/sdk) to guarantee end-to-end reliability.
## Basic queries
To retrieve results from Algolia, use the [Search index](/doc/libraries/sdk/v1/methods/search) method. Querying Algolia involves:
* Choosing the index to query against
* Sending the query string
* Adding optional parameters
Along with the user's text, you can provide any number of [query parameters](/doc/api-reference/api-parameters) in your .
Some override an index's settings, such as disabling typo tolerance.
Others enrich the search, such as filtering.
Adding optional parameters can alter textual relevance behavior at query-time.
For example, it's possible to define stop words or change plurals behavior only for certain queries.
Algolia recommends querying directly from the user's browser, mobile device, or client to ensure optimal performance by reducing latency (and benefiting from offloading your servers).
Here's a basic query example:
```js JavaScript icon="code" theme={"system"}
index.search("query", {
ignorePlurals: true,
});
```
### Empty queries
Sometimes, you might not need to pass in a textual query.
It's possible to retrieve results from Algolia by specifying an index and other optional parameters (most likely, a ).
In this way, it's possible to use Algolia beyond a standard "search results" experience.
For example, one common use case is to use filtering capabilities to generate category landing pages.
### Browsing your index
Algolia lets you fetch all in an index using the [`browse`](/doc/libraries/sdk/v1/methods/browse) method.
If you need to retrieve the full contents of your index (for backup, SEO purposes or for running a script on it),
use the [`browse`](/doc/libraries/sdk/v1/methods/browse) method instead of [`search`](/doc/libraries/sdk/v1/methods/search), as [`search`](/doc/libraries/sdk/v1/methods/search) is optimized for speed.
Results are returned ranked by attributes and custom ranking. Additionally, [`browse`](/doc/libraries/sdk/v1/methods/browse) supports most search parameters, meaning you can easily apply filters to retrieve a subset of results.
### Multiple indices / multiple queries
[Multiple indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices) are used when you have different kinds of data (movies + actors), or you need to display results with different rankings. You can do this by using the same query / response logic, one for each index or query.
## Response format
If a query is successful, your will generate a JSON response in return.
This response contains both the matching results (*hits*) as well as other information useful for building a full-fledged search experience (for example, facets, highlighting, hits count, paging, and more).
For more information, see [Understand the API response](/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/vue)
### Highlighting and snippeting
Highlighting is an important tool to demonstrate to searchers *why* a result matched their query by providing different styling to all matched query words.
By default, highlighting is enabled on all searchable attributes.
Snippets only return a portion of the matched attribute: the matched words and some words around them. Unlike highlighting, snippeting must be proactively enabled for each attribute you wish to snippet, although you can set the value `*` to snippet all attributes.
### Pagination
Pagination gives you full control over how you retrieve query results. You can implement standard paging or retrieve specific record subsets that ignore page boundaries (infinite scrolling).
You can set pagination defaults at indexing time and override them at query time.
### Faceting
Facets are used to create categories on a select group of attributes.
For example, useful facets for an index of books might be author and genre.
Additionally, Algolia calculates the count of records for each .
You can display facets and facet counts in the UI to let users filter results (for example, by author or genre).
### Search for facet values
Closely tied to *faceting* is *searching* for facet values.
With this powerful feature,
you give your user the ability to navigate through maybe hundreds of different facet values.
And because they can search, you are no longer concerned about creating too many values—they will find what they need.
## See also
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Paginate results](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/react)
* [Highlighting](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/react)
* [Search for facet values](/doc/libraries/sdk/v1/methods/search-for-facet-values)
# Backend search with an API client
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/vue
Understand the search Algolia provides out of the box.
To query your Algolia , you need an Application ID and a valid API key.
You can find them in the Algolia dashboard's [API Keys](https://dashboard.algolia.com/account/api-keys/) section.
These credentials let you connect to Algolia and perform operations on your data.
Use one of Algolia's [API clients](/doc/libraries/sdk) to guarantee end-to-end reliability.
## Basic queries
To retrieve results from Algolia, use the [Search index](/doc/libraries/sdk/v1/methods/search) method. Querying Algolia involves:
* Choosing the index to query against
* Sending the query string
* Adding optional parameters
Along with the user's text, you can provide any number of [query parameters](/doc/api-reference/api-parameters) in your .
Some override an index's settings, such as disabling typo tolerance.
Others enrich the search, such as filtering.
Adding optional parameters can alter textual relevance behavior at query-time.
For example, it's possible to define stop words or change plurals behavior only for certain queries.
Algolia recommends querying directly from the user's browser, mobile device, or client to ensure optimal performance by reducing latency (and benefiting from offloading your servers).
Here's a basic query example:
```js JavaScript icon="code" theme={"system"}
index.search("query", {
ignorePlurals: true,
});
```
### Empty queries
Sometimes, you might not need to pass in a textual query.
It's possible to retrieve results from Algolia by specifying an index and other optional parameters (most likely, a ).
In this way, it's possible to use Algolia beyond a standard "search results" experience.
For example, one common use case is to use filtering capabilities to generate category landing pages.
### Browsing your index
Algolia lets you fetch all in an index using the [`browse`](/doc/libraries/sdk/v1/methods/browse) method.
If you need to retrieve the full contents of your index (for backup, SEO purposes or for running a script on it),
use the [`browse`](/doc/libraries/sdk/v1/methods/browse) method instead of [`search`](/doc/libraries/sdk/v1/methods/search), as [`search`](/doc/libraries/sdk/v1/methods/search) is optimized for speed.
Results are returned ranked by attributes and custom ranking. Additionally, [`browse`](/doc/libraries/sdk/v1/methods/browse) supports most search parameters, meaning you can easily apply filters to retrieve a subset of results.
### Multiple indices / multiple queries
[Multiple indices](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices) are used when you have different kinds of data (movies + actors), or you need to display results with different rankings. You can do this by using the same query / response logic, one for each index or query.
## Response format
If a query is successful, your will generate a JSON response in return.
This response contains both the matching results (*hits*) as well as other information useful for building a full-fledged search experience (for example, facets, highlighting, hits count, paging, and more).
For more information, see [Understand the API response](/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/vue)
### Highlighting and snippeting
Highlighting is an important tool to demonstrate to searchers *why* a result matched their query by providing different styling to all matched query words.
By default, highlighting is enabled on all searchable attributes.
Snippets only return a portion of the matched attribute: the matched words and some words around them. Unlike highlighting, snippeting must be proactively enabled for each attribute you wish to snippet, although you can set the value `*` to snippet all attributes.
### Pagination
Pagination gives you full control over how you retrieve query results. You can implement standard paging or retrieve specific record subsets that ignore page boundaries (infinite scrolling).
You can set pagination defaults at indexing time and override them at query time.
### Faceting
Facets are used to create categories on a select group of attributes.
For example, useful facets for an index of books might be author and genre.
Additionally, Algolia calculates the count of records for each .
You can display facets and facet counts in the UI to let users filter results (for example, by author or genre).
### Search for facet values
Closely tied to *faceting* is *searching* for facet values.
With this powerful feature,
you give your user the ability to navigate through maybe hundreds of different facet values.
And because they can search, you are no longer concerned about creating too many values—they will find what they need.
## See also
* [Faceting](/doc/guides/managing-results/refine-results/faceting)
* [Paginate results](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/vue)
* [Highlighting](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/vue)
* [Search for facet values](/doc/libraries/sdk/v1/methods/search-for-facet-values)
# Understand the API response
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/react
Learn more about the Algolia API response
Algolia search responses look similar to:
```json JSON icon="braces" theme={"system"}
{
"hits": [
{
"firstname": "Jimmie",
"lastname": "Barninger",
"objectID": "433",
"_highlightResult": {
"firstname": {
"value": "Jimmie",
"matchLevel": "partial"
},
"lastname": {
"value": "Barninger",
"matchLevel": "none"
},
"company": {
"value": "California Paint & Wlpaper Str",
"matchLevel": "partial"
}
}
}
],
"page": 0,
"nbHits": 1,
"nbPages": 1,
"hitsPerPage": 20,
"processingTimeMS": 1,
"query": "jimmie paint"
}
```
## Hits
The most important field in the search response is `hits`, an array of objects matching the active search state.
Each hit contains the object's **retrievable attributes**,
those attributes that have been configured to be returned in search responses.
Two additional fields may be present if **highlighting** and **snippeting** have been enabled:
* `_highlightResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a highlighted version of the original attribute, as well as information describing the quality of the match.
* `_snippetResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a snippeted version of the original attribute, as well as information describing the quality of the match.
If **highlighting** or **snippeting** is enabled, the highlighted and snippeted values of `firstname`,
for example, will be available as `_highlightResult.firstname.value` and `_snippetResult.firstname.value`, respectively.
These values can then be used, instead of their original values, to support highlighting and snippeting in a results interface.
## Pagination
**A single response doesn't return all hits**—the hits are paginated,
with the current pagination state reflected by a few response fields:
* `nbHits`: the total number of hits matching the search state
* `nbPages`: the total number of pages in the search response
* `hitsPerPage`: the configurable number of hits presented per page
* `page`: the current, zero-indexed page of responses
In a search interface, these fields can be used to render appropriate pagination controls:
* Increment `page` when a "Next" button is clicked, or a specific page when a number is clicked.
* Don't show a "Next" button if `page` == `nbPages`.
* Don't show a "Back" button if `page` == `0`.
InstantSearch.js handles these cases with minimal configuration.
## Ranking information
By default, the search response doesn't include ranking information to keep the response as small as possible.
To include the ranking information, use the [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) parameter with your .
For example:
```json JSON icon="braces" theme={"system"}
{
"_rankingInfo": {
"nbTypos": 0,
"firstMatchedWord": 0,
"proximityDistance": 0,
"userScore": 7,
"geoDistance": 1600,
"geoPrecision": 1,
"nbExactWords": 0,
"words": 0,
"filters": 0,
"matchedGeoLocation": {
"lat": 37.3688,
"lng": -122.036,
"distance": 1600
}
}
}
```
For more information, see [Search API response](/doc/rest-api/search/search-single-index#response-hits)
# Understand the API response
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/in-depth/understanding-the-api-response/vue
Learn more about the Algolia API response
Algolia search responses look similar to:
```json JSON icon="braces" theme={"system"}
{
"hits": [
{
"firstname": "Jimmie",
"lastname": "Barninger",
"objectID": "433",
"_highlightResult": {
"firstname": {
"value": "Jimmie",
"matchLevel": "partial"
},
"lastname": {
"value": "Barninger",
"matchLevel": "none"
},
"company": {
"value": "California Paint & Wlpaper Str",
"matchLevel": "partial"
}
}
}
],
"page": 0,
"nbHits": 1,
"nbPages": 1,
"hitsPerPage": 20,
"processingTimeMS": 1,
"query": "jimmie paint"
}
```
## Hits
The most important field in the search response is `hits`, an array of objects matching the active search state.
Each hit contains the object's **retrievable attributes**,
those attributes that have been configured to be returned in search responses.
Two additional fields may be present if **highlighting** and **snippeting** have been enabled:
* `_highlightResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a highlighted version of the original attribute, as well as information describing the quality of the match.
* `_snippetResult`: an object keyed on the hit's **retrievable attributes**.
Each key's value returns a snippeted version of the original attribute, as well as information describing the quality of the match.
If **highlighting** or **snippeting** is enabled, the highlighted and snippeted values of `firstname`,
for example, will be available as `_highlightResult.firstname.value` and `_snippetResult.firstname.value`, respectively.
These values can then be used, instead of their original values, to support highlighting and snippeting in a results interface.
## Pagination
**A single response doesn't return all hits**—the hits are paginated,
with the current pagination state reflected by a few response fields:
* `nbHits`: the total number of hits matching the search state
* `nbPages`: the total number of pages in the search response
* `hitsPerPage`: the configurable number of hits presented per page
* `page`: the current, zero-indexed page of responses
In a search interface, these fields can be used to render appropriate pagination controls:
* Increment `page` when a "Next" button is clicked, or a specific page when a number is clicked.
* Don't show a "Next" button if `page` == `nbPages`.
* Don't show a "Back" button if `page` == `0`.
InstantSearch.js handles these cases with minimal configuration.
## Ranking information
By default, the search response doesn't include ranking information to keep the response as small as possible.
To include the ranking information, use the [`getRankingInfo`](/doc/api-reference/api-parameters/getRankingInfo) parameter with your .
For example:
```json JSON icon="braces" theme={"system"}
{
"_rankingInfo": {
"nbTypos": 0,
"firstMatchedWord": 0,
"proximityDistance": 0,
"userScore": 7,
"geoDistance": 1600,
"geoPrecision": 1,
"nbExactWords": 0,
"words": 0,
"filters": 0,
"matchedGeoLocation": {
"lat": 37.3688,
"lng": -122.036,
"distance": 1600
}
}
}
```
For more information, see [Search API response](/doc/rest-api/search/search-single-index#response-hits)
# Backend search with React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/react
Set up search on your backend with React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
## Frontend versus backend search
Use frontend search because:
* **It's faster**.
Speed alone justifies the choice.
Backend search requires two more trips between a user's browser and Algolia's servers.
This decreases the effectiveness of Algolia's engine and infrastructure optimizations.
* **UI libraries aid development**.
Algolia's UI libraries ([InstantSearch](/doc/guides/building-search-ui/getting-started/react) and [Autocomplete](/doc/ui-libraries/autocomplete/introduction/getting-started)) help build a frontend solution so that you can focus on configuring relevance and creating the best possible search experience.
* **It frees up your resources**.
Algolia reduces the need for development and eliminates the requirement for additional server deployments and its associated infrastructure.
## Why backend search?
* Server-side pre-processing. Perhaps the most important reason for a backend solution is to make complex calculations or format your data before displaying the results.
* Real-time availability updates (like Airbnb)
* In some situations, SEO is easier to manage.
## How to implement backend search
You only need a few lines of code to send a query to Algolia and receive a response.
The response contains everything needed to build your UI.
You can perform backend search with either InstantSearch or one of the Algolia API clients.
### Backend InstantSearch
[Backend InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/react) offers a good compromise.
You perform the search on your servers, carry out any required pre-processing,
and then send your results to InstantSearch's widgets.
This enables you to control the query and response but saves you the work of building a custom frontend.
### Backend search with an Algolia API client
This option requires a server-side solution and custom frontend development.
You manage all queries and responses, and you build the frontend UI.
You need to parse the API response, which includes:
* Results formatted for display, for example, full record content, embedded HTML, or image URLs
* Highlighting and snippeting
* Pagination
To learn more, see [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/react).
# Backend search with Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/backend-search/vue
Set up search on your backend with Vue InstantSearch.
## Frontend versus backend search
Use frontend search because:
* **It offers a better response speed**. Speed alone justifies the choice. Backend search requires two more trips between a user's browser and Algolia's servers. This decreases the effectiveness of Algolia's engine and infrastructure optimizations.
* **UI libraries aid development**. Algolia's UI libraries ([InstantSearch](/doc/guides/building-search-ui/getting-started/vue) and [Autocomplete](/doc/ui-libraries/autocomplete/introduction/getting-started)) help build a frontend solution so that you can focus on configuring relevance and creating the best possible search experience.
* **It frees up your resources**. Algolia reduces the need for development and eliminates the requirement for additional server deployments and its associated infrastructure.
## Why backend search?
* Server-side pre-processing. Perhaps the most important reason for a backend solution is to make complex calculations or format your data before displaying the results.
* Real-time availability updates (like Airbnb)
* In some situations, SEO is easier to manage
## How to implement backend search
You only need a few lines of code to send a query to Algolia and receive a response.
The response contains everything needed to build your UI.
You can perform backend search with either InstantSearch or one of the Algolia API clients.
### Backend InstantSearch
[Backend InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/vue) offers a good compromise.
You perform the search on your servers, carry out any required pre-processing, and then send your results to InstantSearch's widgets.
This enables you to control the query and response but saves you the work of building a custom frontend.
### Backend search with an Algolia API client
This option requires a server-side solution and custom frontend development. You manage all queries and responses, and you build the frontend UI.
You need to parse the API response: a JSON object.
The response includes:
* Results already formatted for display (full record content, embedded HTML, or image URLs)
* Highlighting and snippeting
* Pagination
For more information, see:
* [Backend search with an API client](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-search-with-an-api-client/vue)
* [Pagination](/doc/api-reference/api-parameters/page)
* [Highlighting](/doc/api-reference/api-parameters/attributesToHighlight)
* [Snippeting](/doc/api-reference/api-parameters/attributesToSnippet)
# Build an AI-powered search experience
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/ai-search-experience/react
Combine the search box, chat, and autocomplete widgets to build an AI-powered search experience with React InstantSearch.
Pair classic InstantSearch widgets with an [Agent Studio](/doc/guides/algolia-ai/agent-studio) chat agent to give users a conversational entry point alongside their search results. This guide wires together:
* An "AI Mode" button on the search box that opens a chat panel loaded with the user's query.
* The `Chat` widget itself, with hidden context to share session metadata.
* AI-generated prompt suggestions in the autocomplete drop-down menu.
* Streaming controls so you can stop or resume an in-flight response.
## Before you begin
* An [Agent Studio agent ID](https://dashboard.algolia.com/generativeAi/agent-studio/agents).
* An `InstantSearch` root with an Algolia search client.
* `react-instantsearch` installed at a version that ships AI Mode (for example, the release from PR [`algolia/instantsearch#6982`](https://github.com/algolia/instantsearch/pull/6982) or later).
Add the [`Chat`](/doc/api-reference/widgets/chat/react) widget on the same `InstantSearch` instance as your search UI.
The other widgets in this guide rely on it being present.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Chat, InstantSearch } from "react-instantsearch";
const searchClient = algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey");
function App() {
return (
);
}
```
`Chat` renders the chat panel but doesn't open it on its own.
In this guide, the AI Mode button you add in the next step is the entry point.
If you don't enable AI Mode, add the [`ChatTrigger`](/doc/api-reference/widgets/chat-trigger/react) widget instead, or `Chat` logs a development warning.
Set [`aiMode`](/doc/api-reference/widgets/search-box/react#param-ai-mode) on `SearchBox` to render an AI Mode button next to the input.
Clicking it opens the `Chat` widget and forwards the current query.
AI Mode is a valid chat entry point, so you don't also need a [`ChatTrigger`](/doc/api-reference/widgets/chat-trigger/react) widget.
```jsx React icon=code theme={"system"}
import { Chat, InstantSearch, SearchBox } from "react-instantsearch";
function App() {
return (
);
}
```
If you use [`Autocomplete`](/doc/api-reference/widgets/autocomplete/react), surface AI-generated prompt suggestions alongside hits.
See [`showPromptSuggestions`](/doc/api-reference/widgets/autocomplete/react#param-show-prompt-suggestions) for the full configuration surface.
```jsx React icon=code theme={"system"}
{item.name}
,
},
},
]}
/>
```
Selecting a prompt suggestion opens the `Chat` widget on the same index and sends the suggestion as the first message.
Use the [`context`](/doc/api-reference/widgets/chat/react#param-context) prop to share metadata with the agent on every message.
The widget serializes the value as JSON and adds it to the user message as a hidden text part.
Users don't see it in the chat UI, but the agent does.
The widget sends `context` to the agent in plain text.
Don't put secrets, access tokens, or personally identifiable information you don't intend to share with the model in this field.
```jsx Static object theme={"system"}
```
```jsx Function theme={"system"}
({
locale: navigator.language,
currentPage: window.location.pathname,
})}
/>
```
Listen for response completion with [`onFinish`](/doc/api-reference/widgets/chat/react#param-on-finish), and use the `useChat` Hook to expose Stop and resume controls.
See [Streaming and resumption](/doc/api-reference/widgets/chat/react#streaming-and-resumption) on the `Chat` widget reference for the full status surface.
```jsx React icon=code theme={"system"}
import { Chat, useChat } from "react-instantsearch";
function StopButton({ agentId }) {
const { status, stop } = useChat({ agentId });
if (status !== "streaming") {
return null;
}
return ;
}
function App() {
return (
{
if (!isError) {
analytics.track("chat_message_completed", { id: message.id });
}
}}
/>
);
}
```
Set [`resume`](/doc/api-reference/widgets/chat/react#param-resume) to reconnect to an in-flight assistant response after a page reload.
## End-to-end example
```jsx React icon=code expandable theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
Chat,
Autocomplete,
Hits,
InstantSearch,
SearchBox,
useChat,
} from "react-instantsearch";
const searchClient = algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey");
const agentId = "YOUR_AGENT_ID";
function StopButton() {
const { status, stop } = useChat({ agentId });
if (status !== "streaming") return null;
return ;
}
function Hit({ hit }) {
return {hit.name};
}
export default function App() {
return (
{item.name}
},
},
]}
/>
({
locale: navigator.language,
currentPage: window.location.pathname,
})}
onFinish={({ message, isError }) => {
if (!isError) {
analytics.track("chat_message_completed", { id: message.id });
}
}}
/>
);
}
```
## Related
* [`SearchBox` widget reference](/doc/api-reference/widgets/search-box/react)
* [`Chat` widget reference](/doc/api-reference/widgets/chat/react)
* [`Autocomplete` widget reference](/doc/api-reference/widgets/autocomplete/react)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
* [Add a legal notice to the chat widget](/doc/guides/building-search-ui/going-further/chat-customization/legal-notice/react)
* [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/react)
* [Show starter prompts on the chat welcome screen](/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/react)
# Add a legal notice to the chat widget
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/legal-notice/react
Add or replace a legal notice in the React InstantSearch chat widget.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Use the [`Chat`](/doc/api-reference/widgets/chat/react) widget's prompt to add a legal notice, replace the default disclaimer, or show a warning above the prompt.
If you only need to change the text in the prompt footer, use `translations.prompt.disclaimer`.
If you need richer content such as a link, use `promptFooterComponent` instead.
## Change the prompt disclaimer text
Use `translations.prompt.disclaimer` to replace the default prompt disclaimer without changing the footer layout.
```jsx React icon=code theme={"system"}
```
## Replace the prompt footer
Use `promptFooterComponent` when you need more than plain text.
For example, you can add a link to your AI policy or legal terms.
```jsx React icon=code theme={"system"}
function LegalNotice() {
return (
);
}
```
`promptFooterComponent` replaces the default prompt footer content.
Use `promptHeaderComponent` to show content above the text area instead.
## Add message footers
Use `assistantMessageFooterComponent` or `userMessageFooterComponent` to display content below each message bubble.
```jsx React icon=code theme={"system"}
function AssistantNotice() {
return (
Verify important details before acting on this response.
);
}
```
Use `userMessageFooterComponent` to show a footer below user messages.
## Show a warning above the prompt
Use `promptHeaderComponent` to display a warning above the prompt input instead of below it.
```jsx React icon=code theme={"system"}
function WarningBanner() {
return (
Don't share personal or sensitive information in the chat.
);
}
```
For more information, see the [Chat API reference](/doc/api-reference/widgets/chat/react).
# Control when and where the chat loader shows
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/js
Position the loader, override when it shows, and tune its timing in the InstantSearch.js chat widget.
The [`chat`](/doc/api-reference/widgets/chat/js) widget shows a loader while a chat turn is in progress.
You can control:
* Where it appears, with `loaderPosition` or CSS.
* When it appears, with `shouldShowLoader`.
* Its timing, with `loaderShowDelay` and `loaderMinDuration`.
To change what the loader says or how it looks, see [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/js).
## Render the loader inside the streaming message
By default, the loader appears in its own row after the last message.
Set [`loaderPosition`](/doc/api-reference/widgets/chat/js#param-loader-position) to `message-inline` to show it inside the streaming assistant message.
Before the assistant sends its first part, the loader appears in its own row.
```js JavaScript icon=code theme={"system"}
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
loaderPosition: "message-inline",
});
```
## Move the loader with CSS
You can use CSS to reposition the default row loader (`loaderPosition: "messages-end"`) within the chat panel.
Show it above the conversation:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
order: -1;
}
```
Keep it in view while the conversation scrolls behind it:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
position: sticky;
bottom: 0;
}
```
Pin it to the panel instead of the scroll area:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
position: absolute;
bottom: var(--ais-spacing);
left: var(--ais-spacing);
right: var(--ais-spacing);
z-index: 1;
}
```
The pinned loader positions itself against `.ais-ChatMessages`, outside the scroll container, so it stays put while messages scroll.
It also overlaps the end of the conversation, so add matching `padding-bottom` to `.ais-ChatMessages-scroll`.
CSS can't move the loader outside the message list.
To show progress in your header or next to the prompt input, render your own indicator there.
## Change when the loader shows
Use [`shouldShowLoader`](/doc/api-reference/widgets/chat/js#param-should-show-loader) to customize when the loader appears.
Relevant context properties include:
* `status`. The chat status: `submitted`, `streaming`, `ready`, or `error`.
* `phase`. The current phase of the turn: `submitted`, `tool`, `reasoning`, or `thinking`.
* `message`. The assistant message associated with the loader, when available.
* `messages`. The full conversation.
* `tools`. The tools available to the agent.
* `defaultValue`. Whether the widget would show the loader by default.
Return `defaultValue` for the cases you don't handle, so you adjust the built-in behavior instead of rebuilding it.
While the [reasoning disclosure](/doc/api-reference/widgets/chat/js#param-show-reasoning) is active, it reports progress itself, so `defaultValue` is `false` and the loader stays hidden.
Return `true` for the `reasoning` phase to show both.
Keep the loader visible for the whole tool call, including while the tool streams its input:
```js JavaScript icon=code theme={"system"}
chat({
// ...
shouldShowLoader: ({ phase, defaultValue }) =>
phase === "tool" || defaultValue,
});
```
Hide it for a tool that shows its own progress:
```js JavaScript icon=code theme={"system"}
chat({
// ...
shouldShowLoader: ({ message, defaultValue }) => {
const lastPart = message?.parts.at(-1);
if (lastPart?.type === "tool-YOUR_TOOL_NAME") {
return false;
}
return defaultValue;
},
});
```
Returning `false` for every turn removes the loader.
Give users another way to tell that the agent is working, such as an indicator in your own layout.
## Tune the loader timing
During a turn, the loader can hide and reappear as the chat moves between different phases.
Two options control these transitions:
* [`loaderShowDelay`](/doc/api-reference/widgets/chat/js#param-loader-show-delay). How long a renewed loading state must continue before the loader reappears. Defaults to 250 ms. The first time the loader appears in a turn, it isn't delayed.
* [`loaderMinDuration`](/doc/api-reference/widgets/chat/js#param-loader-min-duration). The minimum time the loader remains visible while the turn is running. Defaults to 200 ms. This prevents the loader from briefly appearing and disappearing. When the turn ends, the loader hides immediately.
```js JavaScript icon=code theme={"system"}
chat({
// ...
loaderShowDelay: 400,
loaderMinDuration: 300,
});
```
Raise `loaderShowDelay` if your agent alternates between short tool calls and text.
Set it to `0` to show the loader immediately whenever a loading state starts.
## Support assistive technology and reduced motion
The message list has `aria-busy="true"` while the chat status is `submitted` or `streaming`,
so assistive technology can identify that the response is still being processed.
The default loader's entrance, spinner, and skeleton animations all stop under `prefers-reduced-motion: reduce`.
Honor the same media query in your own loader.
## Related
* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/js)
* [Style your widgets](/doc/guides/building-search-ui/styling/js)
# Control when and where the chat loader shows
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/react
Position the loader, override when it shows, and tune its timing in the React InstantSearch chat widget.
The [`Chat`](/doc/api-reference/widgets/chat/react) widget shows a loader while a chat turn is in progress.
You can control:
* Where it appears, with `loaderPosition` or CSS.
* When it appears, with `shouldShowLoader`.
* Its timing, with `loaderShowDelay` and `loaderMinDuration`.
To change what the loader says or how it looks, see [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/react).
## Render the loader inside the streaming message
By default, the loader appears in its own row after the last message.
Set [`loaderPosition`](/doc/api-reference/widgets/chat/react#param-loader-position) to `message-inline` to show it inside the streaming assistant message.
Before the assistant sends its first part, the loader appears in its own row.
```jsx React icon=code theme={"system"}
```
## Move the loader with CSS
You can use CSS to reposition the default row loader (`loaderPosition="messages-end"`) within the chat panel.
Show it above the conversation:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
order: -1;
}
```
Keep it in view while the conversation scrolls behind it:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
position: sticky;
bottom: 0;
}
```
Pin it to the panel instead of the scroll area:
```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
position: absolute;
bottom: var(--ais-spacing);
left: var(--ais-spacing);
right: var(--ais-spacing);
z-index: 1;
}
```
The pinned loader positions itself against `.ais-ChatMessages`, outside the scroll container, so it stays put while messages scroll.
It also overlaps the end of the conversation, so add matching `padding-bottom` to `.ais-ChatMessages-scroll`.
CSS can't move the loader outside the message list.
To show progress in your header or next to the prompt input, render your own indicator there.
## Change when the loader shows
Use [`shouldShowLoader`](/doc/api-reference/widgets/chat/react#param-should-show-loader) to customize when the loader appears.
Relevant context properties include:
* `status`. The chat status: `submitted`, `streaming`, `ready`, or `error`.
* `phase`. The current phase of the turn: `submitted`, `tool`, `reasoning`, or `thinking`.
* `message`. The assistant message associated with the loader, when available.
* `messages`. The full conversation.
* `tools`. The tools available to the agent.
* `defaultValue`. Whether the widget would show the loader by default.
Return `defaultValue` for the cases you don't handle, so you adjust the built-in behavior instead of rebuilding it.
While the [reasoning disclosure](/doc/api-reference/widgets/chat/react#param-show-reasoning) is active, it reports progress itself, so `defaultValue` is `false` and the loader stays hidden.
Return `true` for the `reasoning` phase to show both.
Keep the loader visible for the whole tool call, including while the tool streams its input:
```jsx React icon=code theme={"system"}
phase === "tool" || defaultValue}
/>
```
Hide it for a tool that shows its own progress:
```jsx React icon=code theme={"system"}
{
const lastPart = message?.parts.at(-1);
if (lastPart?.type === "tool-YOUR_TOOL_NAME") {
return false;
}
return defaultValue;
}}
/>
```
Returning `false` for every turn removes the loader.
Give users another way to tell that the agent is working, such as an indicator in your own layout.
## Tune the loader timing
During a turn, the loader can hide and reappear as the chat moves between different phases.
Two options control these transitions:
* [`loaderShowDelay`](/doc/api-reference/widgets/chat/react#param-loader-show-delay). How long a renewed loading state must continue before the loader reappears. Defaults to 250 ms. The first time the loader appears in a turn, it isn't delayed.
* [`loaderMinDuration`](/doc/api-reference/widgets/chat/react#param-loader-min-duration). The minimum time the loader remains visible while the turn is running. Defaults to 200 ms. This prevents the loader from briefly appearing and disappearing. When the turn ends, the loader hides immediately.
```jsx React icon=code theme={"system"}
```
Raise `loaderShowDelay` if your agent alternates between short tool calls and text.
Set it to `0` to show the loader immediately whenever a loading state starts.
## Support assistive technology and reduced motion
The message list has `aria-busy="true"` while the chat status is `submitted` or `streaming`,
so assistive technology can identify that the response is still being processed.
The default loader's entrance, spinner, and skeleton animations all stop under `prefers-reduced-motion: reduce`.
Honor the same media query in your own loader.
## Related
* [Chat widget reference](/doc/api-reference/widgets/chat/react)
* [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/react)
* [Style your widgets](/doc/guides/building-search-ui/styling/react)
# Customize the chat loader
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/loading-message/js
Change the loader text or replace the loader in the InstantSearch.js chat widget.
The [`chat`](/doc/api-reference/widgets/chat/js) widget shows a loader while a chat turn is in progress.
The default loader includes a spinner, optional text, and skeleton lines.
Use `templates.loaderText` to change the text or
`templates.loader` to replace the loader.
To change when and where the loader renders, see [Control when and where the chat loader shows](/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/js).
## Change the loader text
The default loader doesn't show any text. Add some with `templates.loaderText`:
```js JavaScript icon=code theme={"system"}
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
templates: {
loaderText: "Preparing an answer...",
},
});
```
## Change the text by turn phase
Pass a function to `loaderText` to change the text based on the turn context.
Use `phase` to identify the current phase:
| Phase | Meaning |
| - | - |
| `submitted` | The prompt was submitted but no assistant part has arrived yet. |
| `tool` | Running a tool call, such as a search. |
| `reasoning` | Reasoning before the answer starts, unless [`showReasoning`](/doc/api-reference/widgets/chat/js#param-show-reasoning) is `false`. |
| `thinking` | Any other pause while the turn is in progress. |
With reasoning shown, the loader stays hidden while a reasoning step streams, so the `reasoning` text only appears in the pause between a finished step and the start of the answer.
```js JavaScript icon=code theme={"system"}
chat({
// ...
templates: {
loaderText: ({ phase }) => {
switch (phase) {
case "submitted":
return "Reading your question...";
case "tool":
return "Working on your request...";
case "reasoning":
return "Working through the details...";
default:
return "Writing a response...";
}
},
},
});
```
The function receives the same turn context as [`shouldShowLoader`](/doc/api-reference/widgets/chat/js#param-should-show-loader), without `defaultValue`.
## Replace the loader
Use the `loader` template to render your own loading state.
It receives:
* `translations`. The resolved loader translations.
* `context`. The same context as `loaderText` receives.
* `inline`. Whether the loader renders inside an assistant message. It's `true` when `loaderPosition` is `message-inline` and an assistant message is available.
```js JavaScript icon=code theme={"system"}
chat({
// ...
templates: {
loader: ({ context }, { html }) =>
html`
${context.phase === "tool" ? "Searching..." : "Preparing an answer..."}
`,
},
});
```
Use `inline` if your custom loader needs different markup or spacing when it appears inside a message.
## Reuse the default loader
Instead of writing the markup yourself, render the built-in loader with the `chatMessageLoader` template helper and override what you need.
It keeps the default spinner, skeleton lines, and animations.
Pass the received properties to `chatMessageLoader`, then override `translations`, `className`, or any element attribute:
```js JavaScript icon=code theme={"system"}
import { chatMessageLoader } from "instantsearch.js/es/templates";
chat({
// ...
templates: {
loader: (props) =>
chatMessageLoader({
...props,
className: "my-Loader",
translations: { loaderText: "Preparing an answer..." },
}),
},
});
```
## Rotate loading messages while the agent is working
Prefer `phase`, since it tracks the current phase of the turn.
For long waits within one phase, cycle through a short set of messages with CSS.
Render them all in the template, then fade between them:
```js JavaScript icon=code theme={"system"}
chat({
// ...
templates: {
loader: (_, { html }) => html`
Working on your request...Reviewing the results...Writing a response...
`,
},
});
```
```css CSS icon=paintbrush expandable theme={"system"}
.my-Loader {
display: grid;
}
.my-Loader span {
grid-area: 1 / 1;
opacity: 0;
animation: my-Loader-cycle 7.5s infinite;
}
.my-Loader span:nth-child(2) {
animation-delay: 2.5s;
}
.my-Loader span:nth-child(3) {
animation-delay: 5s;
}
@keyframes my-Loader-cycle {
0%,
33% {
opacity: 1;
}
34%,
100% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.my-Loader span {
animation: none;
}
.my-Loader span:first-child {
opacity: 1;
}
}
```
Keep each message short, and don't describe work the agent isn't doing.
## Related
* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [Control when and where the chat loader shows](/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/js)
# Customize the chat loader
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/loading-message/react
Change the loader text or replace the loader in the React InstantSearch chat widget.
The [`Chat`](/doc/api-reference/widgets/chat/react) widget shows a loader while a chat turn is in progress.
The default loader includes a spinner, optional text, and skeleton lines.
Use `translations.messages.loaderText` to change the text or
`loaderComponent` to replace the loader.
To change when and where the loader renders, see [Control when and where the chat loader shows](/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/react).
## Change the loader text
The default loader doesn't show any text. Add some with `translations.messages.loaderText`:
```jsx React icon=code theme={"system"}
```
## Change the text by turn phase
Pass a function to `loaderText` to change the text based on the turn context.
Use `phase` to identify the current phase:
| Phase | Meaning |
| - | - |
| `submitted` | The prompt was submitted but no assistant part has arrived yet. |
| `tool` | Running a tool call, such as a search. |
| `reasoning` | Reasoning before the answer starts, unless [`showReasoning`](/doc/api-reference/widgets/chat/react#param-show-reasoning) is `false`. |
| `thinking` | Any other pause while the turn is in progress. |
With reasoning shown, the loader stays hidden while a reasoning step streams, so the `reasoning` text only appears in the pause between a finished step and the start of the answer.
```jsx React icon=code theme={"system"}
{
switch (phase) {
case "submitted":
return "Reading your question...";
case "tool":
return "Working on your request...";
case "reasoning":
return "Working through the details...";
default:
return "Writing a response...";
}
},
},
}}
/>
```
The function receives the same turn context as [`shouldShowLoader`](/doc/api-reference/widgets/chat/react#param-should-show-loader), without `defaultValue`.
## Replace the loader
Use the `loaderComponent` prop to render your own loading state.
It receives:
* `translations`. The resolved loader translations.
* `context`. The same context as `loaderText` receives.
* `inline`. Whether the loader renders inside an assistant message. It's `true` when `loaderPosition` is `message-inline` and an assistant message is available.
```jsx React icon=code theme={"system"}
function Loader({ context }) {
return (
{context.phase === "tool" ? "Searching..." : "Preparing an answer..."}
);
}
```
Use `inline` if your custom loader needs different markup or spacing when it appears inside a message.
If you use React InstantSearch 7.30.0 or earlier, use `messagesLoaderComponent` instead of `loaderComponent`.
## Reuse the default loader
Instead of writing the markup yourself, render the built-in `ChatMessageLoader` and override what you need.
It keeps the default spinner, skeleton lines, and animations.
Pass the received props to `ChatMessageLoader`, then override `translations`, `className`, or any element attribute:
```jsx React icon=code theme={"system"}
import { Chat, ChatMessageLoader } from "react-instantsearch";
function Loader(props) {
return (
);
}
```
## Rotate loading messages while the agent is working
Prefer `phase`, since it tracks the current phase of the turn. For long waits within one phase, rotate through a short set of messages:
```jsx React icon=code expandable theme={"system"}
import React from "react";
const LOADING_MESSAGES = [
"Working on your request...",
"Reviewing the results...",
"Writing a response...",
];
function RotatingLoader() {
const [messageIndex, setMessageIndex] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setMessageIndex((index) => (index + 1) % LOADING_MESSAGES.length);
}, 2500);
return () => clearInterval(timer);
}, []);
return
{LOADING_MESSAGES[messageIndex]}
;
}
```
Keep each message short, and don't describe work the agent isn't doing.
## Related
* [Chat widget reference](/doc/api-reference/widgets/chat/react)
* [Control when and where the chat loader shows](/doc/guides/building-search-ui/going-further/chat-customization/loader-behavior/react)
# Customize the chat results carousel
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/results-carousel/js
Customize how the chat widget renders Algolia search results and recommendations in a carousel with InstantSearch.js.
When the agent searches your index, the [`chat`](/doc/api-reference/widgets/chat/js) widget renders the results in a built-in carousel.
If your agent uses the [Grouped Results tool](/doc/guides/algolia-ai/agent-studio/how-to/tools/grouped-results),
the widget renders each group of curated results in its own built-in carousel.
You can customize the search results carousel at three levels,
from the least to the most involved:
1. Restyle the default carousel with CSS.
2. Change the markup of each result card with the [`item`](/doc/api-reference/widgets/chat/js#param-templates) template.
3. Replace the entire carousel layout by overriding the search tool.
## Restyle the default carousel with CSS
If the default structure works and you only want a different look, target the carousel's CSS classes.
The search results carousel uses classes prefixed with `ais-ChatToolSearchIndexCarousel` (for example, `ais-ChatToolSearchIndexCarouselHeader`).
See the [styling guide](/doc/guides/building-search-ui/styling/js) for the full approach.
## Customize each result card
Use the [`item`](/doc/api-reference/widgets/chat/js#param-templates) template to change what each card in the carousel looks like.
The same template renders cards in search results, recommendations,
and in carousels created by the Grouped Results tool.
It receives a single record:
```js JavaScript icon=code theme={"system"}
import algoliasearch from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { chat } from "instantsearch.js/es/widgets";
const search = instantsearch({
indexName: "instant_search",
searchClient: algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey"),
});
search.addWidgets([
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
templates: {
item(hit, { html, components }) {
return html`
${components.Highlight({ attribute: "name", hit })}
$${hit.price}
`;
},
},
}),
]);
search.start();
```
Set the card's image source to the appropriate image attribute in your records.
If you restrict the attributes returned by the Algolia Search tool,
include the image attribute in your [`attributesToRetrieve`](/doc/guides/algolia-ai/agent-studio/how-to/tools/algolia-search#control-what-the-ai-can-change-per-index).
For most stores, this is enough: you swap in your own product-card markup and the carousel behavior (header, scroll buttons, "View all") stays as-is.
## Replace the entire carousel layout
To change more than the cards—such as the header, the scroll controls, or the wrapper—override the tool that renders the carousel. Import the tool type and use it as a key in the [`tools`](/doc/api-reference/widgets/chat/js#param-tools) option:
* `SearchIndexToolType` renders the search results carousel.
* `RecommendToolType` renders the recommendations carousel.
* `GroupedResultsToolType` renders the grouped results carousels. This tool builds its carousels from the streaming tool input instead of `message.output`, so the following example doesn't apply to it. For more information, see [`tools`](/doc/api-reference/widgets/chat/js#param-tools).
All of them are exported from `instantsearch.js/es/widgets/chat/chat`.
Whatever you pass under a tool key fully replaces the built-in rendering for that tool.
```js JavaScript icon=code expandable theme={"system"}
import { chat, SearchIndexToolType } from "instantsearch.js/es/widgets/chat/chat";
search.addWidgets([
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
tools: {
[SearchIndexToolType]: {
templates: {
layout({ message, sendEvent }, { html }) {
const items = message.output?.hits || [];
if (items.length === 0) {
return html`
No results found.
`;
}
return html`
${items.length} of ${message.output?.nbHits} results
`;
},
},
},
},
}),
]);
```
The `layout` template receives:
* `message`. The tool call message. The result is on `message.output` (`hits`, `nbHits`, `queryID`). The agent's parameters are on `message.input`.
* `sendEvent`. Sends `click` or `conversion` events for the result, using the [`insights`](/doc/api-reference/widgets/insights/js) middleware.
* `applyFilters` and `onClose`. Apply filters to the InstantSearch UI state, or dismiss the tool's UI.
Overriding a tool changes only how the result is **displayed** in the chat.
The tool itself—its name, when the agent calls it, and its parameters—is configured on the agent in the [Agent Studio dashboard](/doc/guides/algolia-ai/agent-studio/how-to/dashboard#client-side-tools).
## Related
* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [Show starter prompts on the chat welcome screen](/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/js)
* [insights middleware](/doc/api-reference/widgets/insights/js)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
# Customize the chat results carousel
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/results-carousel/react
Customize how the chat widget renders Algolia search results and recommendations in a carousel with React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
When the agent searches your index, the [`Chat`](/doc/api-reference/widgets/chat/react) widget renders the results in a built-in carousel.
If your agent uses the [Grouped Results tool](/doc/guides/algolia-ai/agent-studio/how-to/tools/grouped-results),
the widget renders each group of curated results in its own built-in carousel.
You can customize the search results carousel at three levels,
from the least to the most involved:
1. Restyle the default carousel with CSS.
2. Change the markup of each result card with the [`itemComponent`](/doc/api-reference/widgets/chat/react) prop.
3. Replace the entire carousel layout by overriding the search tool.
## Restyle the default carousel with CSS
If the default structure works and you only want a different look, target the carousel's CSS classes.
The search results carousel uses classes prefixed with `ais-ChatToolSearchIndexCarousel` (for example, `ais-ChatToolSearchIndexCarouselHeader`).
See the [styling guide](/doc/guides/building-search-ui/styling/react) for the full approach.
## Customize each result card
Use the [`itemComponent`](/doc/api-reference/widgets/chat/react) prop to change what each card in the carousel looks like.
The same component renders cards in search results, recommendations,
and in carousels created by the Grouped Results tool.
It receives a single record:
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Chat, InstantSearch } from "react-instantsearch";
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_ONLY_API_KEY"
);
function ProductCard({ item }) {
return (
{item.name}
${item.price}
);
}
function App() {
return (
);
}
```
Set the card's image source to the appropriate image attribute in your records.
If you restrict the attributes returned by the Algolia Search tool,
include the image attribute in your [`attributesToRetrieve`](/doc/guides/algolia-ai/agent-studio/how-to/tools/algolia-search#control-what-the-ai-can-change-per-index).
For most stores, this is enough: you swap in your own product-card markup and the carousel behavior (header, scroll buttons, "View all") stays as-is.
## Replace the entire carousel layout
To change more than the cards—such as the header, the scroll controls, or the wrapper—override the tool that renders the carousel. Import the tool type and use it as a key in the [`tools`](/doc/api-reference/widgets/chat/react) prop:
* `SearchIndexToolType` renders the search results carousel.
* `RecommendToolType` renders the recommendations carousel.
* `GroupedResultsToolType` renders the grouped results carousels. This tool builds its carousels from the streaming tool input instead of `message.output`, so the following example doesn't apply to it. For more information, see [`tools`](/doc/api-reference/widgets/chat/react#param-tools).
All of them are exported from `react-instantsearch`.
Whatever you pass under a tool key fully replaces the built-in rendering for that tool.
```jsx React icon=code expandable theme={"system"}
import { Chat, SearchIndexToolType } from "react-instantsearch";
function ResultsCarousel({ message, sendEvent }) {
const items = message.output?.hits || [];
if (items.length === 0) {
return
No results found.
;
}
return (
{items.length} of {message.output?.nbHits} results
{items.map((item) => (
sendEvent("click", item, "Product clicked from chat")}
>
{item.name}
${item.price}
))}
);
}
function App() {
return (
);
}
```
The `layoutComponent` receives:
* `message`. The tool call message. The result is on `message.output` (`hits`, `nbHits`, `queryID`). The agent's parameters are on `message.input`.
* `sendEvent`. Sends `click` or `conversion` events for the result, using the [`insights`](/doc/api-reference/widgets/insights/react) middleware.
* `applyFilters` and `onClose`. Apply filters to the InstantSearch UI state, or dismiss the tool's UI.
Overriding a tool changes only how the result is **displayed** in the chat.
The tool itself—its name, when the agent calls it, and its parameters—is configured on the agent in the [Agent Studio dashboard](/doc/guides/algolia-ai/agent-studio/how-to/dashboard#client-side-tools).
## Related
* [Chat widget reference](/doc/api-reference/widgets/chat/react)
* [Show starter prompts on the chat welcome screen](/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/react)
* [insights middleware](/doc/api-reference/widgets/insights/react)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
# Show starter prompts on the chat welcome screen
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/js
Build a custom chat welcome screen with starter prompts using InstantSearch.js.
When the [`chat`](/doc/api-reference/widgets/chat/js) widget doesn't have any messages yet, it shows a welcome screen.
Use the [`empty`](/doc/api-reference/widgets/chat/js#param-templates) template to replace it with your own greeting and a set of starter prompts.
Each prompt sends a message to the agent when clicked, so users can start a conversation in one tap.
If you only need prompt suggestions *after* the agent responds, use [Agent Studio follow-up prompt suggestions](/doc/guides/algolia-ai/agent-studio/how-to/integration#follow-up-prompt-suggestions) instead.
## Render starter prompts with the `empty` template
The `empty` template receives a `sendMessage` function. Call it with `{ text }` to submit a starter prompt as the first message:
```js JavaScript icon=code theme={"system"}
import algoliasearch from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { chat, chatTrigger } from "instantsearch.js/es/widgets";
const search = instantsearch({
indexName: "instant_search",
searchClient: algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey"),
});
const starterPrompts = [
"What are your best-selling headphones?",
"Help me find a gift under $50",
"Compare espresso machines",
];
search.addWidgets([
chat({
container: "#chat",
agentId: "YOUR_AGENT_ID",
templates: {
empty({ sendMessage }, { html }) {
return html`
How can I help you today?
Ask me anything about our products, or start with a suggestion.
${starterPrompts.map(
(prompt) =>
html``,
)}
`;
},
},
}),
chatTrigger({
container: "#chat-trigger",
}),
]);
search.start();
```
The `chat` widget renders the panel but doesn't open it on its own.
Add an entry point on the same InstantSearch instance: the [`chatTrigger`](/doc/api-reference/widgets/chat-trigger/js) widget, [AI mode](/doc/guides/building-search-ui/going-further/chat-customization/ai-search-experience/js) on a `searchBox`, or an inline layout.
The `empty` template reuses the built-in classes (`ais-ChatGreeting` and `ais-ChatPromptSuggestions`) so it inherits the default styling.
Replace them with your own classes to fully restyle the welcome screen.
## Reuse the default greeting
Instead of writing the greeting markup yourself, render the built-in greeting with the exported `chatGreeting` template helper, then add your starter prompts below it. The helper renders the default heading and subheading, and accepts a `banner` image, `translations`, and `classNames`:
```js JavaScript icon=code theme={"system"}
import { chatGreeting } from "instantsearch.js/es/templates";
const greeting = chatGreeting({
banner: "https://example.com/welcome-banner.png",
translations: {
heading: "Welcome to our store",
subheading: "Ask me anything, or pick a suggestion to get started.",
},
});
// In the chat widget:
templates: {
empty(props, { html }) {
return html`
${greeting(props)}
${starterPrompts.map(
(prompt) =>
html``,
)}
`;
},
},
```
## Fetch starter prompts dynamically
The prompts don't have to be static. Fetch them once (for example, from a dedicated Agent Studio agent that returns starter prompts), store them, and render them from the `empty` template:
```js JavaScript icon=code expandable theme={"system"}
const appID = "YourApplicationID";
const apiKey = "YourSearchOnlyAPIKey";
const promptsAgentId = "YOUR_STARTER_PROMPTS_AGENT_ID";
async function getStarterPrompts() {
const response = await fetch(
`https://${appID}.algolia.net/agent-studio/1/agents/${promptsAgentId}/completions?stream=false&cache=true&compatibilityMode=ai-sdk-5`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Algolia-Application-Id": appID,
"X-Algolia-API-Key": apiKey,
},
body: JSON.stringify({
messages: [
{
role: "user",
parts: [{ text: "Return starter prompts for the welcome screen." }],
},
],
}),
}
);
const data = await response.json();
const text = data.parts?.find((part) => part.type === "text")?.text ?? "";
return text
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}
let starterPrompts = [];
getStarterPrompts()
.then((prompts) => {
starterPrompts = prompts;
// Re-render the chat so the empty template picks up the prompts.
search.refresh();
})
.catch(() => {
starterPrompts = [];
});
```
Reference `starterPrompts` from the `empty` template, exactly as in the static example above.
## Customize more than the empty state
The `empty` template only replaces the initial empty state.
To customize the surrounding chat layout (header, message list, prompt input), use the [`layout`](/doc/api-reference/widgets/chat/js#param-templates) template instead.
See [`instantsearch.templates.chatInlineLayout()`](/doc/api-reference/widgets/chat/js) for an inline (non-overlay) layout.
## Related
* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [`chatTrigger` widget reference](/doc/api-reference/widgets/chat-trigger/js)
* [Customize the chat results carousel](/doc/guides/building-search-ui/going-further/chat-customization/results-carousel/js)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
# Show starter prompts on the chat welcome screen
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/react
Build a custom chat welcome screen with starter prompts for React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
When the [`Chat`](/doc/api-reference/widgets/chat/react) widget doesn't have any messages yet, it shows a welcome screen.
Use the [`emptyComponent`](/doc/api-reference/widgets/chat/react) prop to replace it with your own greeting and a set of starter prompts.
Each prompt sends a message to the agent when clicked, so users can start a conversation in one tap.
If you only need prompt suggestions *after* the agent responds, use [Agent Studio follow-up prompt suggestions](/doc/guides/algolia-ai/agent-studio/how-to/integration#follow-up-prompt-suggestions) instead.
## Render starter prompts with `emptyComponent`
The `emptyComponent` receives a `sendMessage` function. Call it with `{ text }` to submit a starter prompt as the first message:
```jsx React icon=code theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Chat, ChatTrigger, InstantSearch } from "react-instantsearch";
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_ONLY_API_KEY"
);
const starterPrompts = [
"What are your best-selling headphones?",
"Help me find a gift under $50",
"Compare espresso machines",
];
function WelcomeScreen({ sendMessage }) {
return (
How can I help you today?
Ask me anything about our products, or start with a suggestion.
{starterPrompts.map((prompt) => (
))}
);
}
function App() {
return (
);
}
```
Pair `Chat` with the [`ChatTrigger`](/doc/api-reference/widgets/chat-trigger/react) widget (or [AI mode](/doc/guides/building-search-ui/going-further/chat-customization/ai-search-experience/react) on a `SearchBox`) to provide a way to open the chat.
The `emptyComponent` reuses the built-in classes (`ais-ChatGreeting` and `ais-ChatPromptSuggestions`) so it inherits the default styling.
Replace them with your own classes to fully restyle the welcome screen.
## Reuse the default greeting
Instead of writing the greeting markup yourself, render the built-in `ChatGreeting` component, then add your starter prompts below it. It renders the default heading and subheading, and accepts a `banner` image, `translations`, and `classNames`:
```jsx React icon=code theme={"system"}
import { Chat, ChatGreeting } from "react-instantsearch";
function WelcomeScreen({ sendMessage }) {
return (
<>
{starterPrompts.map((prompt) => (
))}
>
);
}
```
## Fetch starter prompts dynamically
The prompts don't have to be static. Fetch them once—for example, from a dedicated Agent Studio agent that returns starter prompts—and render them from `emptyComponent`:
```jsx React icon=code expandable theme={"system"}
import React, { useEffect, useState } from "react";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_ONLY_API_KEY";
const promptsAgentId = "YOUR_STARTER_PROMPTS_AGENT_ID";
async function getStarterPrompts() {
const response = await fetch(
`https://${appID}.algolia.net/agent-studio/1/agents/${promptsAgentId}/completions?stream=false&cache=true&compatibilityMode=ai-sdk-5`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Algolia-Application-Id": appID,
"X-Algolia-API-Key": apiKey,
},
body: JSON.stringify({
messages: [
{
role: "user",
parts: [{ text: "Return starter prompts for the welcome screen." }],
},
],
}),
}
);
const data = await response.json();
const text = data.parts?.find((part) => part.type === "text")?.text ?? "";
return text
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}
function WelcomeScreen({ sendMessage }) {
const [prompts, setPrompts] = useState([]);
useEffect(() => {
getStarterPrompts()
.then(setPrompts)
.catch(() => setPrompts([]));
}, []);
return (
How can I help you today?
{prompts.map((prompt) => (
))}
);
}
```
## Customize more than the empty state
The `emptyComponent` only replaces the initial empty state.
To customize the surrounding chat layout (header, message list, prompt input), use the [`layoutComponent`](/doc/api-reference/widgets/chat/react) prop instead.
Use `ChatInlineLayout` for an inline (non-overlay) layout, or `ChatOverlayLayout` for the default floating overlay, and reuse `headerComponent`, `messagesComponent`, and `promptComponent` to keep the built-in behavior.
## Related
* [Chat widget reference](/doc/api-reference/widgets/chat/react)
* [ChatTrigger widget reference](/doc/api-reference/widgets/chat-trigger/react)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
# Conditional display
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-display/react
Learn how to display content conditionally in React InstantSearch for empty queries, searches without results, and errors.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
This guide describes what to do when there are [no results](#handle-searches-without-results),
when there's [no query](#handle-empty-queries),
or when there are [errors](#handle-search-errors).
If results haven't loaded, the search may still be in progress or the device may be offline.
For more information,
see [No hits or is the search still in progress?](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/react#no-hits-or-is-the-search-still-in-progress)
If you want to feature content in your search results based on a set of conditions, you can use Algolia Rules:
* [Feature specific data from within your records](https://www.algolia.com/ecommerce-merchandising-playbook/merchandise-search-results-page/) to, for example, show promotional prices during a sales period
* [Display advertisements or promotional banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
To learn how to suppress InstantSearch's initial ,
see [Conditional requests](/doc/guides/building-search-ui/going-further/conditional-requests/react).
## Handle searches without results
When a query doesn't return results,
display a message and suggest changing the query or clearing filters.
### Display a message
To display a fallback message when a query doesn't return results,
use the [`useInstantSearch`](/doc/api-reference/widgets/use-instantsearch/react) Hook to create a wrapper component:
```jsx React icon=code expandable theme={"system"}
function App(props) {
return (
}>
);
}
function NoResultsBoundary({ children, fallback }) {
const { results } = useInstantSearch();
// Skip the message for placeholder results before the first response.
if (!results.__isArtificial && results.nbHits === 0) {
return (
<>
{fallback}
);
}
```
You can pass anything to display results, including [`InfiniteHits`](/doc/api-reference/widgets/infinite-hits/react) or a custom component that uses the [`useHits`](/doc/api-reference/widgets/hits/react#hook) Hook.
To prevent undesirable, additional search requests,
see [Show and hide widgets](/doc/guides/building-search-ui/widgets/show-and-hide-widgets/react).
### Let users reset filters and facets
If users apply an overly specific configuration, they may not find any results.
Account for this by letting them reset filters from the "no results" display so they can start another search.
Do this with the [`ClearRefinements`](/doc/api-reference/widgets/clear-refinements/react) widget.
```jsx React icon=code theme={"system"}
function NoResults() {
const { indexUiState } = useInstantSearch();
return (
No results for {indexUiState.query}.
);
}
```
## Handle empty queries
By default, InstantSearch sends search requests even when the query is empty.
To hide results until users enter a query,
use the following wrapper.
This hides the results without preventing search requests.
```jsx React icon=code expandable theme={"system"}
function App(props) {
return (
);
}
function EmptyQueryBoundary({ children, fallback }) {
const { indexUiState } = useInstantSearch();
if (!indexUiState.query) {
return (
<>
{fallback}
{children}
>
);
}
return children;
}
```
Follow best practices for [showing and hiding React InstantSearch widgets](/doc/guides/building-search-ui/widgets/show-and-hide-widgets/react) to prevent undesirable additional search requests.
## Handle search errors
When an error occurs, you can display a specific piece of content to help users return to the standard state.
```jsx React icon=code expandable theme={"system"}
import React, { useEffect, useState } from "react";
import * as Toast from "@radix-ui/react-toast";
import { useInstantSearch } from "react-instantsearch";
function SearchErrorToast() {
const { addMiddlewares } = useInstantSearch();
const [error, setError] = useState(null);
useEffect(() => {
const middleware = ({ instantSearchInstance }) => {
function handleError(searchError) {
setError(searchError);
}
return {
subscribe() {
instantSearchInstance.addListener("error", handleError);
},
unsubscribe() {
instantSearchInstance.removeListener("error", handleError);
},
};
};
return addMiddlewares(middleware);
}, [addMiddlewares]);
if (!error) {
return null;
}
return (
{
if (!isOpen) {
setError(null);
}
}}
>
{error.name}{error.message}
);
}
function App(props) {
return (
);
}
```
# Conditional display in Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-display/vue
Learn how to display content conditionally in Vue InstantSearch for empty queries and searches without results.
This guide describes what to do when there are [no results](#handle-searches-without-results) or when there's [no query](#handle-empty-queries).
If results haven't loaded, the search may still be in progress or the device may be offline.
For more information,
see [No hits or is the search still in progress?](/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/vue#no-hits-or-is-the-search-still-in-progress)
If you want to feature content in your search results based on a set of conditions, you can use Algolia Rules to:
* [Feature specific data from within your records](https://www.algolia.com/ecommerce-merchandising-playbook/merchandise-search-results-page/) to, for example, show promotional prices during a sales period
* [Display advertisements or promotional banners](/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/add-banners).
To learn how to suppress InstantSearch's initial ,
see [Conditional requests](/doc/guides/building-search-ui/going-further/conditional-requests/vue).
## Handle searches without results
When a query doesn't return results,
display a message and suggest changing the query or clearing filters.
### Display a message
To display a fallback message when a query doesn't return results,
use the [`ais-state-results`](/doc/api-reference/widgets/state-results/vue) widget.
```html Vue icon=code-xml theme={"system"}
No results have been found for {{ query }}.
```
This also works with [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue).
To prevent undesirable, additional search requests,
see [Show and hide widgets](/doc/guides/building-search-ui/widgets/show-and-hide-widgets/vue).
### Let users reset filters and facets
If users apply an overly specific configuration, they may not find any results.
Account for this by letting them reset filters from the "no results" display so they can start another search.
Do this with the [`ais-clear-refinements`](/doc/api-reference/widgets/clear-refinements/vue) widget.
To clear the query, add `:excluded-attributes="[]".`
```html Vue icon=code-xml theme={"system"}
No results have been found for {{ query }}.
Clear all refinements
```
You can also use [`ais-current-refinements`](/doc/api-reference/widgets/current-refinements/vue) to show currently applied filters.
```html Vue icon=code-xml theme={"system"}
No results have been found for {{ query }}.
```
## Handle empty queries
By default, InstantSearch sends search requests even when the query is empty.
To hide results until users enter a query,
use the following code.
This hides the results without preventing search requests.
```html Vue icon=code-xml theme={"system"}
```
Follow best practices for [showing and hiding Vue InstantSearch widgets](/doc/guides/building-search-ui/widgets/show-and-hide-widgets/vue) to prevent undesirable additional search requests.
# Conditional requests
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/react
Learn how to make conditional requests with React InstantSearch
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
By default, InstantSearch sends an initial to Algolia's servers with an empty .
This connection helps speed up later requests.
However, sometimes you don't want to perform more network calls than are necessary.
For example, you may want to limit the number of search requests and reduce your overall Algolia usage.
This guide helps you build a UI that prevents this initial request.
## How a search client works
InstantSearch is the UI layer that sits on top of [Algolia's search client layer](https://github.com/algolia/algoliasearch-client-javascript),
with a state managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper) layer.
These three layers are interchangeable so that you can use the InstantSearch widgets with a different search client.
The search client queries Algolia's servers whenever users refines a search.
You can build a custom search client to add custom behavior.
For example,
to perform [backend searches with InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/react).
## Implement a proxy
To prevent the initial empty query,
you must wrap a custom search client around Algolia's,
and pass it to [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react).
The custom search client acts as a proxy for search requests:
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
const searchClient = {
...algoliaClient,
search(requests) {
return algoliaClient.search(requests);
},
};
const App = () => (
);
```
This proxy lets you add some logic before calling the `search` method.
In this case, you only call it when performing a query.
You can also use a proxy to only begin searching after a certain number of characters have been typed by adding a condition,
for example, `query.length > 3`.
## Detect empty search requests
If you don't want to perform a search request when the query is empty (`""`),
you need to detect it:
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
// Here we have to do something else
}
return algoliaClient.search(requests);
},
};
```
Sometimes, one user action results in multiple queries,
for example, clicking on an [`RefinementList`](/doc/api-reference/widgets/refinement-list/react)
or using multiple [`Index`](/doc/api-reference/widgets/index-widget/react) widgets.
Make sure that every query is empty before intercepting the function call.
When an empty search is detected, you must return a [formatted response](/doc/libraries/sdk/v1/methods/search#response) as an array of objects of the same length as the `requests` array.
Each object requires at least these properties: `hits`, `nbHits`, and `processingTimeMS`.
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
```
Now you can use the proxy with the [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) widget:
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
const algoliaClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
);
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
const App = () => (
);
```
# Conditional requests with Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/vue
Learn how to make conditional requests with Vue InstantSearch
By default, InstantSearch sends an initial to Algolia's servers with an empty .
This connection helps speed up later requests.
However, sometimes you don't want to perform more network calls than are necessary.
For example, you may want to limit the number of search requests and reduce your overall Algolia usage.
This guide helps you build a UI that prevents this initial request.
## How a search client works
InstantSearch is the UI layer that sits on top of [Algolia's search client layer](https://github.com/algolia/algoliasearch-client-javascript),
with a state managed by the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper) layer.
These three layers are interchangeable so that you can use the InstantSearch widgets with a different search client.
The search client queries Algolia's servers whenever users refines a search.
You can build a custom search client to add custom behavior.
For example,
to perform [backend searches with InstantSearch](/doc/guides/building-search-ui/going-further/backend-search/in-depth/backend-instantsearch/vue).
## Implementing a proxy
To prevent the initial empty query, you must wrap a custom search client around Algolia's,
and pass it to [`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue).
The custom search client acts as a proxy for search requests:
```vue Vue icon=code theme={"system"}
```
This proxy lets you add some logic before calling the `search` method.
In this case, you only call it when performing a query.
You can also use a proxy to only begin searching after a certain number of characters have been typed by adding a condition, for example, `query.length > 3`.
## Detecting empty search requests
If you don't want to perform a search request when the query is empty (`""`),
you need to detect it:
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
// Here we have to do something else
}
return algoliaClient.search(requests);
},
};
```
Sometimes, one user action results in multiple queries, for example, clicking on an [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) or using multiple [`ais-index`](/doc/api-reference/widgets/index-widget/vue) widgets.
Make sure that every query is empty before intercepting the function call.
When an empty search is detected, you must return a [formatted response](/doc/libraries/sdk/v1/methods/search#response) as an array of objects of the same length as the `requests` array.
Each object requires at least these properties: `hits`, `nbHits`, and `processingTimeMS`.
```js JavaScript icon=code theme={"system"}
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
```
Now you can use the proxy with the [`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue) widget, like this:
```vue Vue icon=code theme={"system"}
The request is only triggered once the query is not empty.
```
# Improve performance
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/react
Learn how to optimize your React InstantSearch app.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Algolia is fast by default.
But network speed and bandwidth can vary.
This page lists a few best practices you can implement to adapt to your users' network conditions.
## Prepare the connection to Algolia
When sending the first network request to a domain, a security handshake must happen, consisting of several round trips between the client and the Algolia server.
If the handshake first happened when users typed their first keystroke, the speed of that first request would be significantly slower.
Use a [preconnect link](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preconnect) to carry out the handshake immediately after loading the page but before any user interaction.
To do this, add a link tag with your Algolia domain in the `head` of your page.
```html HTML icon=code-xml theme={"system"}
```
## Customize the loading indicator
By default, the [`SearchBox`](/doc/api-reference/widgets/search-box/react) displays a loading indicator when the search is stalled. When the network is slow, this visual cue tells users that something is happening
You can change the loading icon with the
[`loadingIconComponent`](/doc/api-reference/widgets/search-box/react#param-loading-icon-component) prop.
```jsx React icon=code theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Hits, InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
function App() {
return (
"Loading…"} />
);
}
```
## Turn off search-as-you-type
Algolia is designed to deliver a search-as-you-type experience.
Yet, it can also lead to lag in slow network conditions because browsers can only run a limited number of parallel requests to the same domain. **Reducing requests can help prevent further lag.**
**Debouncing** helps you limit requests and avoid processing non-necessary ones by only sending requests once users have stopped typing.
Implement debouncing at the [`SearchBox`](/doc/api-reference/widgets/search-box/react) level with the [`queryHook`](/doc/api-reference/widgets/search-box/react#param-query-hook) prop.
For example:
```jsx React icon=code theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Hits, InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
let timerId = undefined;
let timeout = 200;
function App() {
return (
);
}
function queryHook(query, search) {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => search(query), timeout);
}
```
When network speeds improve, restore search-as-you-type to offer users the best possible experience.
You can use the [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) to detect connection changes.
The [Network Information API](https://developer.mozilla.org/en-US/docs/Web/API/Network_Information_API) is unavailable on some browsers.
```jsx React icon=code theme={"system"}
import React, { useEffect } from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { Hits, InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const connection = navigator.connection;
let timerId = undefined;
let timeout = 0;
updateTimeout();
function App() {
useEffect(() => {
connection.addEventListener("change", updateTimeout);
return () => connection.removeEventListener("change", updateTimeout);
});
return (
);
}
function queryHook(query, search) {
if (timerId) {
clearTimeout(timerId);
}
timerId = setTimeout(() => search(query), timeout);
}
function updateTimeout() {
timeout = ["slow-2g", "2g"].includes(connection?.effectiveType) ? 200 : 0;
}
```
Run and edit the Improve performance example in CodeSandbox.
Browse the source for the Improve performance example on GitHub.
### Select a debounce delay
The optimal debouncing delay should match your audience's typing speed
(typically 30 words per minute (WPM) on mobile devices and 40 WPM on desktop devices).
If the delay is too short, users still see flashes of content.
If the delay is too long, the time between keystrokes and results becomes too long.
200 ms is the preferred debounce delay.
Delays of over 300 ms will start degrading the user experience.
## Server-side rendering
[Server-side rendering (SSR)](/doc/guides/building-search-ui/going-further/server-side-rendering/react) lets you generate HTML from InstantSearch components on the server.
Before loading the page for the first time, a backend server makes an initial request to Algolia, renders it as HTML, and sends it to the browser.
SSR is beneficial in slow network conditions because **the browser directly loads an HTML document containing search results.** There's no need to wait for all the JavaScript assets to load before seeing the search results.
## Optimize build size
InstantSearch supports [dead code elimination through tree shaking](https://webpack.js.org/guides/tree-shaking/), but you must follow a few rules for it to work:
* Bundle your code using a module bundler that supports tree shaking with the `sideEffects` property in `package.json`, such as [Rollup](https://rollupjs.org/) or [webpack 4+](https://webpack.js.org).
* Ensure you pick the ES module build of InstantSearch by targeting the `module` field in `package.json` ([`resolve.mainFields` option in webpack](https://webpack.js.org/configuration/resolve/#resolvemainfields), [`mainFields` option in `@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve#mainfields)). This is the default configuration in most popular bundlers: you only need to change something if you have a custom configuration.
* Keep [Babel](https://babeljs.io) or other transpilers from transpiling ES6 modules to CommonJS modules.
Tree shaking is much less optimal on CommonJS modules, so it's better to let your bundler handle modules itself.
If you're using Babel, you can configure [`babel-preset-env`](https://babeljs.io/docs/en/babel-preset-env) not to process ES6 modules:
```js babel.config.js theme={"system"}
module.exports = {
presets: [
[
"env",
{
modules: false,
},
],
],
};
```
If you're using the [TypeScript compiler (`tsc`)](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#tsc-the-typescript-compiler):
```json tsconfig.json theme={"system"}
{
"compilerOptions": {
"module": "esnext"
}
}
```
### Troubleshooting
To check if tree shaking works, try to import InstantSearch into your project without using it.
```js JavaScript icon=code theme={"system"}
import { InstantSearch } from "react-instantsearch"; // Unused import
```
Build your app, then look for the unused code in your final bundle (for example, "InstantSearch").
If tree shaking works, you shouldn't find anything.
## Queries per second (QPS)
Search operations aren't limited by a fixed "search quota".
Instead, they're limited by your plan's [maximum QPS](https://support.algolia.com/hc/en-us/articles/4406975224721) and
[operations limit](https://support.algolia.com/hc/en-us/articles/18138875086865).
Every keystroke in InstantSearch using the `SearchBox` counts as one operation.
Then, depending on the widgets you add to your search interface,
you may have more operations being counted on each keystroke.
For example, you can expect additional network requests with `DynamicWidgets`.
If you experience QPS limitations, consider implementing [a debounced `SearchBox`](#turn-off-search-as-you-type).
For more information, see:
* [SearchBox](/doc/api-reference/widgets/search-box/react) widget
* [DynamicWidgets](/doc/api-reference/widgets/dynamic-widgets/react) widget
* [All React InstantSearch widgets](/doc/api-reference/widgets/react)
# Improve performance for Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/improve-performance/vue
Learn how to optimize your Vue InstantSearch app.
Algolia is fast by default.
But network speed and bandwidth can vary.
This page lists best practices you can implement to adapt to your users' network conditions.
## Prepare the connection to Algolia
When sending the first network request to a domain, a security handshake must happen, consisting of several round trips between the client and the Algolia server.
If the handshake first happened when users typed their first keystroke, the speed of that first request would be slower.
Use a [preconnect link](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preconnect) to carry out the handshake after loading the page but before any user interaction.
To do this, add a link tag with your Algolia domain in the `head` of your page.
```html HTML icon=code-xml theme={"system"}
```
## Add a loading indicator
Consider a user accessing your app in a subway:
1. They type some characters
2. Nothing happens
3. They wait, but still, nothing happens
However, you can enhance the user experience by displaying a loading indicator to indicate something is happening.
To display a loading indicator in the `ais-search-box`, use the [`show-loading-indicator`](/doc/api-reference/widgets/search-box/vue#param-show-loading-indicator) option.
The indicator will display slightly after the last query has been sent to Algolia.
Change the duration of the delay with [`stalled-search-delay`](/doc/api-reference/widgets/instantsearch/vue#param-stalled-search-delay) (on the `ais-instant-search` widget).
For example:
```vue Vue icon=code theme={"system"}
```
### Make your own loading indicator
You can also use the loading indicator in [custom widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue).
The following example shows how to make a custom component that writes `Loading...` when search stalls.
If network conditions are optimal, users won't see this message.
```vue Vue 3 theme={"system"}
Loading…
```
```vue Vue 2 theme={"system"}
Loading…
```
```vue Vue icon=code theme={"system"}
```
## Debouncing
Another way of improving the perception of performance is by preventing lag.
InstantSearch generates one query per keystroke by default.
This can lead to a lag in the worst network conditions because browsers can only make a limited number of parallel requests.
By reducing the number of requests, you can prevent this lag.
Debouncing prevents unnecessary requests by delaying them until a timeout period has passed.
Implement debouncing at the `ais-search-box` level with the [`connectSearchBox`](/doc/api-reference/widgets/search-box/js#customize-the-ui-with-connectsearchbox) connector. For example:
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
```vue Vue icon=code theme={"system"}
```
Find the complete source code on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/debounced-search-box).
This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
### Select a debounce delay
The optimal debouncing delay should match your audience's typing speed
(typically 30 words per minute (WPM) on mobile devices and 40 WPM on desktop devices).
If the delay is too short, users still see flashes of content.
If the delay is too long, the time between key presses and results becomes too long.
200 ms is the preferred debounce delay.
Delays of over 300 ms will start degrading the user experience.
## Optimize build size
InstantSearch supports [dead code elimination through tree shaking](https://webpack.js.org/guides/tree-shaking/), but you must follow these rules for it to work:
* Bundle your code using a module bundler that supports tree shaking with the `sideEffects` property in `package.json`, such as [Rollup](https://rollupjs.org/) or [webpack 4+](https://webpack.js.org).
* Ensure you pick the ES module build of InstantSearch by targeting the `module` field in `package.json` ([`resolve.mainFields` option in webpack](https://webpack.js.org/configuration/resolve/#resolvemainfields), [`mainFields` option in `@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve#mainfields)). This is the default configuration in most popular bundlers: you only need to change something if you have a custom configuration.
* Keep [Babel](https://babeljs.io) or other transpilers from transpiling ES6 modules to CommonJS modules. Tree shaking is much less optimal on CommonJS modules, so it's better to let your bundler handle modules itself.
If you're using Babel, you can configure [`babel-preset-env`](https://babeljs.io/docs/en/babel-preset-env) not to process ES6 modules:
```js JavaScript icon=code theme={"system"}
// babel.config.js
module.exports = {
presets: [
[
"env",
{
modules: false,
},
],
],
};
```
If you're using the [TypeScript compiler (`tsc`)](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#tsc-the-typescript-compiler):
```jsonc JSON icon=braces theme={"system"}
// tsconfig.json
{
"compilerOptions": {
"module": "esnext",
}
}
```
Import only what you need, and avoid using the plugin (`Vue.use(VueInstantSearch)`) plugin. Doing so imports all the widgets, even the ones you don't use.
Instead, individually import and register each InstantSearch widget within components:
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
With this approach, only the manually imported widgets end up in the production build,
and tree shaking removes the rest.
You can also register InstantSearch widgets at the app level:
```vue Vue 3 theme={"system"}
import Vue from 'vue';
import { AisInstantSearch, AisSearchBox } from 'vue-instantsearch/vue3/es';
import App from './App.vue';
Vue.component(AisInstantSearch.name, AisInstantSearch);
Vue.component(AisSearchBox.name, AisSearchBox);
new Vue({
el: '#app',
render: (h) => h(App),
});
```
```vue Vue 2 theme={"system"}
import Vue from 'vue';
import { AisInstantSearch, AisSearchBox } from 'vue-instantsearch';
import App from './App.vue';
Vue.component(AisInstantSearch.name, AisInstantSearch);
Vue.component(AisSearchBox.name, AisSearchBox);
new Vue({
el: '#app',
render: (h) => h(App),
});
```
### Troubleshooting
To check if tree shaking works, try to import InstantSearch into your project without using it.
```js JavaScript icon=code theme={"system"}
import "vue-instantsearch"; // Unused import
```
Build your app, then look for the unused code in your final bundle (for example, "InstantSearch").
If tree shaking works, you shouldn't find anything.
## Caching
### Caching by default (and how to turn it off)
By default, Algolia caches the search results of the queries, storing them locally in the cache. This cache only persists during the current page session, and as soon as the page reloads, the cache clears.
If users type a search (or part of it) that's already been entered, the results will be retrieved from the cache instead of requesting them from Algolia, making the app much faster.
While it's a convenient feature, sometimes you may want to clear the cache and make a new request to Algolia.
For instance, when changes are made to some in your , you should update your app's frontend to reflect that change (and avoid displaying stale results retrieved from the cache).
The `refresh` function, available for custom connectors, lets you clear the cache and trigger a new search.
The `refresh` function, available for custom connectors, lets you clear the cache and trigger a new search.
### When to discard the cache
Consider discarding the cache when your app's data is updated by:
* Your users (for example, in a dashboard). In this case, refresh the cache based on an app state, such as the last user modification.
* Another process you don't manage (for example, a cron job that updates users inside Algolia). In this case, refresh your app's cache periodically.
### Refresh the cache triggered by a user action
The following code triggers a refresh based on a user action (such as adding a new product or clicking a button).
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
Use it within your app as follows:
```vue Vue icon=code theme={"system"}
```
Find the complete source code on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/refresh).
This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
### Refresh the cache periodically
You can set an interval to determine how often the app clears the cache.
Use this approach if you can't trigger cache clearance based on user actions.
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
If you need to wait for an action from Algolia, use [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) to avoid refreshing the cache before the task completes.
### Turn off the cache
If you need the most current data and the performance impact of this isn't an issue, [turn off caching](/doc/libraries/sdk/caching#requests-and-responses).
```js JavaScript icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { createNullCache } from "@algolia/cache-common";
const searchClient = algoliasearch(
"ALGOLIA_APPLICATION_ID",
"ALGOLIA_SEARCH_API_KEY",
{
// Disable caching for completed requests
responsesCache: createNullCache(),
// Disable caching for in-flight requests
requestsCache: createNullCache(),
},
);
```
## Queries per second (QPS)
Search operations aren't limited by a fixed "search quota".
Instead, they're limited by your plan's [maximum QPS](https://support.algolia.com/hc/en-us/articles/4406975224721) and
[operations limit](https://support.algolia.com/hc/en-us/articles/18138875086865).
Every keystroke in InstantSearch using the `ais-search-box` counts as one operation.
Then, depending on the widgets you add to your search interface,
you may have more operations being counted on each keystroke.
For example, if you have a search interface with an `ais-search-box`, an `ais-menu`, and an `ais-refinement-list`,
then every keystroke triggers one operation.
Upon each user change to an `ais-menu` or `ais-refinement-list`, a new operation is executed.
If you experience QPS limitations, consider implementing [a debounced `ais-search-box`](#debouncing).
For more information, see:
* [`ais-search-box`](/doc/api-reference/widgets/search-box/vue)
* [`ais-menu`](/doc/api-reference/widgets/menu/vue)
* [`ais-refinement-list widget`](/doc/api-reference/widgets/refinement-list/vue)
# Integrate Google Analytics
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/integrate-google-analytics/react
Get insights from user searches in your React InstantSearch app.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Although Algolia provides analytics tailored to your search implementation,
you might want to send your search data to your existing analytics tools.
To do this, implement some custom middleware:
See [Set up the Google tag with gtag.js](https://developers.google.com/tag-platform/gtagjs).
In the event listener, send events to Google Analytics.
This example debounces the event for 3 seconds but adjust this to your needs.
```js JavaScript icon="code" theme={"system"}
function googleAnalyticsMiddleware() {
let timer;
const sendEventDebounced = () => {
clearTimeout(timer);
timer = setTimeout(() => {
gtag('event', 'page_view', {
page_location: window.location.pathname + window.location.search,
});
}, 3000);
});
return {
onStateChange() {
sendEventDebounced();
},
subscribe() {},
unsubscribe() {},
};
}
```
```jsx React icon=code theme={"system"}
import { useInstantSearch } from "react-instantsearch";
function Middleware() {
const { addMiddlewares } = useInstantsearch();
useLayoutEffect(() => {
return addMiddlewares(googleAnalyticsMiddleware);
}, []);
return null;
}
function Search() {
return (
);
}
```
# Integrate Google Analytics
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/integrate-google-analytics/vue
Get insights from user searches in your Vue InstantSearch app.
Although Algolia provides analytics tailored to your search implementation,
you might want to send your search data to your existing analytics tools.
To do this, implement some custom middleware:
See [Set up the Google tag with gtag.js](https://developers.google.com/tag-platform/gtagjs).
In the event listener, send events to Google Analytics.
This example debounces the event for 3 seconds but adjust this to your needs.
```js JavaScript icon="code" theme={"system"}
function googleAnalyticsMiddleware() {
let timer;
const sendEventDebounced = () => {
clearTimeout(timer);
timer = setTimeout(() => {
gtag('event', 'page_view', {
page_location: window.location.pathname + window.location.search,
});
}, 3000);
});
return {
onStateChange() {
sendEventDebounced();
},
subscribe() {},
unsubscribe() {},
};
}
```
```html Vue theme={"system"}
```
# React Native
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/native/react
How to create a React InstantSearch app using React Native.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
**React InstantSearch is compatible with [React Native](https://reactnative.dev/)** but,
since its UI components are designed for the web, they don't work directly in React Native apps.
You can use [Hooks](/doc/guides/building-search-ui/what-is-instantsearch/react#use-hooks)
with the React Native [core components](https://reactnative.dev/docs/intro-react-native-components)
or any third-party React Native component library to incorporate InstantSearch features into your React Native app.
This guide covers how to integrate InstantSearch in your React Native app:
* Adding a search box to send queries
* Displaying infinite hits and highlighting matches
* Filtering in a modal to narrow down the results set
* [View source code](https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch-native/getting-started)
## Before you begin
This tutorial assumes you have React and React Native knowledge and an existing React Native app with React ≥ [16.8.0](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) app.
If you don't have a React Native app, [create one](https://reactnative.dev/docs/environment-setup).
The tutorial also requires an [installation of `algoliasearch` and `react-instantsearch-core`](/doc/guides/building-search-ui/installation/react).
## Add InstantSearch to your app
The [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) wrapper connects your InstantSearch app to your Algolia .
```js App.js icon=code theme={"system"}
import React from "react";
import { SafeAreaView, StatusBar, StyleSheet, Text, View } from "react-native";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-core";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
export default function App() {
return (
{/* ... */}
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: "#252b33",
},
container: {
flex: 1,
backgroundColor: "#ffffff",
flexDirection: "column",
},
});
```
Replace `INDEX_NAME` with the name of your .
Add styles using the [`StyleSheet`](https://reactnative.dev/docs/stylesheet) API from React Native.
## Add a search box
The main UI component of a search experience is a search box.
It's often how users discover content in your app.
React InstantSearch provides a [`useSearchBox`](/doc/api-reference/widgets/search-box/react#hook) Hook to display an Algolia search box.
Use it with the [`TextInput`](https://reactnative.dev/docs/textinput) React Native component.
Then, add the custom `SearchBox` component to your app.
```jsx SearchBox.js theme={"system"}
import React, { useRef, useState } from "react";
import { StyleSheet, View, TextInput } from "react-native";
import { useSearchBox } from "react-instantsearch-core";
export function SearchBox(props) {
const { query, refine } = useSearchBox(props);
const [inputValue, setInputValue] = useState(query);
const inputRef = useRef(null);
function setQuery(newQuery) {
setInputValue(newQuery);
refine(newQuery);
}
// Track when the InstantSearch query changes to synchronize it with
// the React state.
// Bypass the state update if the input is focused to avoid concurrent
// updates when typing.
if (query !== inputValue && !inputRef.current?.isFocused()) {
setInputValue(query);
}
return (
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: "#252b33",
padding: 18,
},
input: {
height: 48,
padding: 12,
fontSize: 16,
backgroundColor: "#fff",
borderRadius: 4,
borderWidth: 1,
borderColor: "#ddd",
},
});
```
```jsx App.js theme={"system"}
// ...
import { SearchBox } from "./SearchBox";
// ...
export default function App() {
return (
);
}
// ...
```
Replace `INDEX_NAME` with the name of your index.
## Display infinite hits
When Algolia returns results, you want to list them in the UI.
A common way of dealing with lists on mobile devices is to display an "infinite list":
a list that loads more results when users scroll to the bottom of the screen.
To do this, use the [`useInfiniteHits`](/doc/api-reference/widgets/infinite-hits/react#hook) Hook with the [`FlatList`](https://reactnative.dev/docs/flatlist) React Native component.
Then, add the custom `InfiniteHits` component in your app and pass it a custom `Hit` component to display each Algolia result.
```jsx InfiniteHits.js theme={"system"}
import React from "react";
import { StyleSheet, View, FlatList } from "react-native";
import { useInfiniteHits } from "react-instantsearch-core";
export function InfiniteHits({ hitComponent: Hit, ...props }) {
const { items, isLastPage, showMore } = useInfiniteHits({
...props,
escapeHTML: false,
});
return (
item.objectID}
ItemSeparatorComponent={() => }
onEndReached={() => {
if (!isLastPage) {
showMore();
}
}}
renderItem={({ item }) => (
)}
/>
);
}
const styles = StyleSheet.create({
separator: {
borderBottomWidth: 1,
borderColor: "#ddd",
},
item: {
padding: 18,
},
});
```
```jsx App.js theme={"system"}
// ...
import { InfiniteHits } from "./InfiniteHits";
// ...
export default function App() {
return (
);
}
function Hit({ hit }) {
return {hit.name};
}
// ...
```
Replace `INDEX_NAME` with the name of your index.
The search box remains visible, so users can update their without manually scrolling back up.
When users do this, reset the scroll position of the list so they can see the top results first.
To do this:
1. Assign a reference (`ref`) to the list for later use in your app.
2. Wrap the `InfiniteHits` component in [`forwardRef`](https://reactjs.org/docs/forwarding-refs.html). This lets you pass `ref` from the app to the list.
3. Listen for the query to change by waiting for an `onChange` callback on the custom `SearchBox`.
4. When the query changes, call [`FlatList.scrollToOffset`](https://reactnative.dev/docs/flatlist#scrolltooffset) to scroll the list to the correct position.
```jsx InfiniteHits.js theme={"system"}
import React, { forwardRef } from "react";
// ...
export const InfiniteHits = forwardRef(
({ hitComponent: Hit, ...props }, ref) => {
// ...
return (
);
},
);
// ...
```
```jsx SearchBox.js theme={"system"}
// ...
export function SearchBox({ onChange, ...props }) {
// ...
return (
{
setQuery(newValue);
onChange(newValue);
}}
/>
);
}
// ...
```
```jsx App.js theme={"system"}
import React, { useRef } from "react";
import {
FlatList,
SafeAreaView,
StatusBar,
StyleSheet,
Text,
View,
} from "react-native";
// ...
export default function App() {
const listRef = useRef < FlatList > null;
function scrollToTop() {
listRef.current?.scrollToOffset({ animated: false, offset: 0 });
}
return (
);
}
// ...
```
## Highlight matches
Algolia supports highlighting and returns the highlighted parts of a result in the response.
Build a custom `Highlight` component to highlight matches in each attribute.
The `instantsearch.js` library provides the necessary utilities to parse the highlighted parts from the Algolia response.
React InstantSearch uses InstantSearch.js behind the scenes, so you don't need to include it as a separate dependency.
Use the [`Text`](https://reactnative.dev/docs/text) component from React Native to represent each highlighted and non-highlighted part.
Then, use the custom `Highlight` component in your custom `Hit` component.
```js Highlight.js theme={"system"}
import React, { Fragment } from "react";
import { StyleSheet, Text } from "react-native";
import {
getHighlightedParts,
getPropertyByPath,
} from "instantsearch.js/es/lib/utils";
function HighlightPart({ children, isHighlighted }) {
return (
{children}
);
}
export function Highlight({ hit, attribute, separator = ", " }) {
const { value: attributeValue = "" } =
getPropertyByPath(hit._highlightResult, attribute) || {};
const parts = getHighlightedParts(attributeValue);
return (
<>
{parts.map((part, partIndex) => {
if (Array.isArray(part)) {
const isLastPart = partIndex === parts.length - 1;
return (
{part.map((subPart, subPartIndex) => (
{subPart.value}
))}
{!isLastPart && separator}
);
}
return (
{part.value}
);
})}
>
);
}
const styles = StyleSheet.create({
highlighted: {
fontWeight: "bold",
backgroundColor: "#f5df4d",
color: "#6f6106",
},
nonHighlighted: {
fontWeight: "normal",
backgroundColor: "transparent",
color: "black",
},
});
```
```js App.js theme={"system"}
// ...
import { Configure, InstantSearch } from "react-instantsearch-core";
import { Highlight } from "./Highlight";
export default function App() {
// ...
return (
);
}
// ...
function Hit({ hit }) {
return (
);
}
// ...
```
Replace `INDEX_NAME` with the name of your index.
When users type a query, the UI highlights matches in each search result.
## Add a filter
A search box is a great way to refine a search experience,
but sometimes users need to narrow down the results to find what they're looking for in a specific category.
Use the [`useRefinementList`](/doc/api-reference/widgets/refinement-list/react#hook) Hook to items based on attributes such as brand,
size, or color.
Mobile devices have limited screen real estate.
Instead of displaying filters near the list of hits as you would on a desktop website,
mobile apps commonly set filters inside a modal.
With React Native, use the [`Modal`](https://reactnative.dev/docs/modal) component.
```js Filters.js theme={"system"}
import React from "react";
import {
Button,
StyleSheet,
SafeAreaView,
Modal,
Text,
TouchableOpacity,
View,
} from "react-native";
import {
useClearRefinements,
useCurrentRefinements,
useRefinementList,
} from "react-instantsearch-core";
export function Filters({ isModalOpen, onToggleModal, onChange }) {
const { items, refine } = useRefinementList({ attribute: "brand" });
const { canRefine: canClear, refine: clear } = useClearRefinements();
const { items: currentRefinements } = useCurrentRefinements();
const totalRefinements = currentRefinements.reduce(
(acc, { refinements }) => acc + refinements.length,
0,
);
return (
<>
Filters
{totalRefinements > 0 && (
{totalRefinements}
)}
Brand
{items.map((item) => {
return (
{
refine(item.value);
onChange();
}}
>
{item.label}
{item.count}
);
})}
>
);
}
const styles = StyleSheet.create({
container: {
padding: 18,
backgroundColor: "#ffffff",
},
title: {
alignItems: "center",
},
titleText: {
fontSize: 32,
},
list: {
marginTop: 32,
},
item: {
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
borderBottomWidth: 1,
borderColor: "#ddd",
alignItems: "center",
},
itemCount: {
backgroundColor: "#252b33",
borderRadius: 24,
paddingVertical: 4,
paddingHorizontal: 8,
marginLeft: 4,
},
itemCountText: {
color: "#ffffff",
fontWeight: "800",
},
labelText: {
fontSize: 16,
},
filterListButtonContainer: {
flexDirection: "row",
},
filterListButton: {
flex: 1,
alignItems: "center",
marginTop: 18,
},
filtersButton: {
paddingVertical: 18,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
filtersButtonText: {
fontSize: 18,
textAlign: "center",
},
});
```
```js App.js theme={"system"}
import React, { useRef, useState } from "react";
import { Filters } from "./Filters";
// ...
export default function App() {
const [isModalOpen, setModalOpen] = useState(false);
// ...
return (
setModalOpen((isOpen) => !isOpen)}
onChange={scrollToTop}
/>
);
}
// ...
```
Replace `INDEX_NAME` with the name of your index.
Replace the `brand` attributes with one of your index's [faceting attributes](/doc/guides/managing-results/refine-results/faceting/how-to/declaring-attributes-for-faceting-with-dashboard).
The **Filters** button reveals filters in a modal that slides from the bottom.
It also uses the [`useCurrentRefinements`](/doc/api-reference/widgets/current-refinements/react#hook) and [`useClearRefinements`](/doc/api-reference/widgets/clear-refinements/react#hook) Hooks to display how many refinements are applied and let users clear them all at once.
## Next steps
With this tutorial, you've got a solid starting point for building dynamic search interfaces.
Some steps to improve the app are:
* [Search in multiple indices](/doc/guides/building-search-ui/ui-and-ux-patterns/multi-index-search/react) with [`Index`](/doc/api-reference/widgets/index-widget/react).
* Use a different component library like [Lottie React Native](https://github.com/lottie-react-native/lottie-react-native) or [React Native Elements](https://reactnativeelements.com/).
* Discover more Hooks by checking the [API reference](/doc/api-reference/widgets/react).
# Use Vue InstantSearch with mobile frameworks
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/native/vue
Using Vue InstantSearch with hybrid and mobile frameworks such as Weex, Quasar, or NativeScript.
While none of the following are natively supported with Vue InstantSearch, it's possible to make mobile apps with Vue using either Weex, Quasar or NativeScript.
Using Vue InstantSearch in these frameworks shouldn't be any different than on the web,
# Optimize search requests
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/optimize-search-request-usage/react
Find areas to optimize the number of search requests and reduce billing in React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Each keystroke sent to an Algolia counts as one search request.
To reduce the number of requests sent to Algolia, and potentially reduce billing, you can ["debounce searches"](/doc/guides/building-search-ui/going-further/improve-performance/js#debouncing).
## What influences search request usage
Your usage depends on factors that influence the volume of search requests, such as:
* **Where do you plan to implement Algolia?** If you implement both mobile and desktop apps, your search request usage will be more than if you just had a desktop app.
* **Will you just capture searches or also capture other elements?** Expect more search request usage if you also use Algolia to capture other elements such as navigation on [category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages), discovery solutions, and other implementations like marketing landing pages and shared widgets.
## How to optimize search request usage
You can make changes to both your **frontend and backend implementations** to ensure your search request usage is as intended.
### Optimize search requests on your frontend implementation
To find out how to optimize the number of search requests, use the browser's developer tools console to inspect Algolia API calls.
#### Check per keystroke
Type in the search box and **see how many API calls are sent to Algolia at each keystroke**.
* By default, there is one request for each keystroke and index.
* If you use [federated search](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices), you have separate requests for each queried index.
**Compare the number of search requests for three elements** - the API calls, the number of keystrokes, and the results.
* If you have more requests than your displayed indices, they could be duplicating queries to the same index or querying indices that aren't displayed.
* If you have a non-optimized multi-index search request, you are already successfully using the multi-query endpoint (federated search uses multiple indices).
* If you don't group the requests into one network request, all requests are on separate lines and billed separately.
* If you have an optimized multi-index search request and you have five different requests, the multi-query endpoint groups all five different requests to different indices.
#### Check per facet click
If your results page includes a , check whether applying it triggers an additional .
There should only be one.
#### Check on page load
* **Clear your browser's cache and reload the page**. Were any queries sent to Algolia? If yes, check to see if any Algolia results are being displayed on the page by default. It could be that a is sent to populate the page's contents, like on category pages.
* If Algolia-delivered results aren't visible on the page, **this could be a redundant query**. You can use your discretion to remove the query.
#### Check if you are using single or multi-query endpoint
How you query Algolia is important and can make a big difference to your billing.
Each line in your browser developer tool's **Network** tab is called a "network request".
* **A network request that starts with "query"** means only one request was sent to one index.
* **A network request starting with "queries"** means that two or more queries are grouped in a single network request, and it's either used to search through multiple indices or it's searching through one index but needs to make multiple queries. An example of a search through one index with multiple queries is when using [`dynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/js) which could [cause an extra search request](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js#optimize-dynamic-widget-network-requests).
**You need to optimize if you have a multi-index search implementation** and there are few calls to the multi-query endpoint.
Algolia has [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) to help you optimize the usage of multi-query endpoints,
including [an API for InstantSearch](/doc/api-reference/widgets/index-widget/js) and for frontend libraries like [Magento](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
#### Check for expected signs of usage
* Check for linear growth in **the number of queries over time**. Study the graph in the **Monitoring** > **Usage** section in your dashboard.
* Check for linear growth in Analytics for **searches in comparison to the number of users**. A high number of searches per user can mean:
* You have a [backend implementation](#optimize-search-requests-on-your-backend-and-native-mobile-implementations).
* You have a low number of users.
* You have a bug that is sending too many queries per keystroke.
* Your search is used more extensively than initially scoped. For example, after an initial scope to power your search, has Algolia now been implemented on the category pages or has it been extended to a new country? This could result in an increase in search requests.
* Check for **abnormal spikes** in search requests. For example, is there an increase during holiday or sale seasons?
* Compare **search queries and the average number of visitors**. This comparison depends on your use case and search experience. For example, if you use Algolia to browse and search, you will have more queries per user. And the number of as-you-type queries depends on how prominently the search box is displayed.
#### Check if you are making extended use of Algolia
Check if Algolia is **used across multiple domains** beyond the initial scope.
#### Check Analytics on your dashboard to see the top searches
* Check if a high number of searches are labeled as "empty search". This means that a majority of the usage is going towards browsing the content page.
* Check the **Searches** tab to see if the filters on the "empty search" queries have a category-based . This implies that Algolia is used extensively for filters and category pages. If this wasn't scoped for initially, it could explain a high search request usage.
#### Check for bots crawling your website
[Google bots](https://support.algolia.com/hc/en-us/articles/4406981900433) could cause an increase in search queries.
### Optimize search requests on your backend and native mobile implementations
Depending on your implementation, you may need to use your internal tools to investigate.
**Check the usage of each search endpoint** by looking at the incoming API operations on the Algolia dashboard's **Monitoring** > **Operations** > **API Calls** section
If you are using a multi-index search and few calls are made to the multi-query endpoint, there is room for optimization.
To start using multi-query endpoint, use Algolia's [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) or frontend libraries like
[InstantSearch for federated search](/doc/api-reference/widgets/index-widget/js) or [Magento 2](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
# Optimize Vue InstantSearch search requests
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/optimize-search-request-usage/vue
Find areas to optimize the number of search requests and reduce billing in Vue InstantSearch.
Each keystroke sent to an Algolia counts as one search request.
To reduce the number of requests sent to Algolia, and potentially reduce billing, you can ["debounce searches"](/doc/guides/building-search-ui/going-further/improve-performance/js#debouncing).
## What influences search request usage
Your usage depends on factors that influence the volume of search requests, such as:
* **Where do you plan to implement Algolia?** If you implement both mobile and desktop apps, your search request usage will be more than if you just had a desktop app.
* **Will you just capture searches or also capture other elements?** Expect more search request usage if you also use Algolia to capture other elements such as navigation on [category pages](/doc/guides/solutions/ecommerce/browse/tutorials/category-pages), discovery solutions, and other implementations like marketing landing pages and shared widgets.
## How to optimize search request usage
You can make changes to both your **frontend and backend implementations** to ensure your search request usage is as intended.
### Optimize search requests on your frontend implementation
To find out how to optimize the number of search requests, use the browser's developer tools console to inspect Algolia API calls.
#### Check per keystroke
Type in the search box and **see how many API calls are sent to Algolia at each keystroke**.
* By default, there is one request for each keystroke and index.
* If you use [federated search](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/concepts/one-or-more-indices), you have separate requests for each queried index.
**Compare the number of search requests for three elements** - the API calls, the number of keystrokes, and the results.
* If you have more requests than your displayed indices, they could be duplicating queries to the same index or querying indices that aren't displayed.
* If you have a non-optimized multi-index search request, you are already successfully using the multi-query endpoint (federated search uses multiple indices).
* If you don't group the requests into one network request, all requests are on separate lines and billed separately.
* If you have an optimized multi-index search request and you have five different requests, the multi-query endpoint groups all five different requests to different indices.
#### Check per facet click
If your results page includes a , check whether applying it triggers an additional .
There should only be one.
#### Check on page load
* **Clear your browser's cache and reload the page**. Were any queries sent to Algolia? If yes, check to see if any Algolia results are being displayed on the page by default. It could be that a is sent to populate the page's contents, like on category pages.
* If Algolia-delivered results aren't visible on the page, **this could be a redundant query**. You can use your discretion to remove the query.
#### Check if you are using single or multi-query endpoint
How you query Algolia is important and can make a big difference to your billing.
Each line in your browser developer tool's **Network** tab is called a "network request".
* **A network request that starts with "query"** means only one request was sent to one index.
* **A network request starting with "queries"** means that two or more queries are grouped in a single network request, and it's either used to search through multiple indices or it's searching through one index but needs to make multiple queries. An example of a search through one index with multiple queries is when using [`dynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/js) which could [cause an extra search request](/doc/guides/building-search-ui/ui-and-ux-patterns/facet-display/js#optimize-dynamic-widget-network-requests).
**You need to optimize if you have a multi-index search implementation** and there are few calls to the multi-query endpoint.
Algolia has [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) to help you optimize the usage of multi-query endpoints,
including [an API for InstantSearch](/doc/api-reference/widgets/index-widget/js) and for frontend libraries like [Magento](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
#### Check for expected signs of usage
* Check for linear growth in **the number of queries over time**. Study the graph in the **Monitoring** > **Usage** section in your dashboard.
* Check for linear growth in Analytics for **searches in comparison to the number of users**. A high number of searches per user can mean:
* You have a [backend implementation](#optimize-search-requests-on-your-backend-and-native-mobile-implementations).
* You have a low number of users.
* You have a bug that is sending too many queries per keystroke.
* Your search is used more extensively than initially scoped. For example, after an initial scope to power your search, has Algolia now been implemented on the category pages or has it been extended to a new country? This could result in an increase in search requests.
* Check for **abnormal spikes** in search requests. For example, is there an increase during holiday or sale seasons?
* Compare **search queries and the average number of visitors**. This comparison depends on your use case and search experience. For example, if you use Algolia to browse and search, you will have more queries per user. And the number of as-you-type queries depends on how prominently the search box is displayed.
#### Check if you are making extended use of Algolia
Check if Algolia is **used across multiple domains** beyond the initial scope.
#### Check Analytics on your dashboard to see the top searches
* Check if a high number of searches are labeled as "empty search". This means that a majority of the usage is going towards browsing the content page.
* Check the **Searches** tab to see if the filters on the "empty search" queries have a category-based . This implies that Algolia is used extensively for filters and category pages. If this wasn't scoped for initially, it could explain a high search request usage.
#### Check for bots crawling your website
[Google bots](https://support.algolia.com/hc/en-us/articles/4406981900433) could cause an increase in search queries.
### Optimize search requests on your backend and native mobile implementations
Depending on your implementation, you may need to use your internal tools to investigate.
**Check the usage of each search endpoint** by looking at the incoming API operations on the Algolia dashboard's **Monitoring** > **Operations** > **API Calls** section
If you are using a multi-index search and few calls are made to the multi-query endpoint, there is room for optimization.
To start using multi-query endpoint, use Algolia's [API clients](/doc/libraries/sdk/v1/methods/multiple-queries) or frontend libraries like
[InstantSearch for federated search](/doc/api-reference/widgets/index-widget/js) or [Magento 2](/doc/integration/magento-2/getting-started/upgrading?client=php#after-upgrading).
# Routing and URL syncing in React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/react
How to synchronize your URLs with React InstantSearch.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Synchronizing your UI with the browser URL is considered good practice.
It lets your users take one of your results pages, copy the URL, and share it.
It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.
InstantSearch provides the necessary API entries to let you synchronize the state of your search UI (your refined widgets and current ) with any kind of storage.
Use the [`routing`](/doc/api-reference/widgets/instantsearch/react#param-routing) option to synchronize UI state with the browser URL, so users can bookmark, share, and revisit the same search.
This guide covers enabling default routing, customizing URLs, and creating SEO-friendly URLs.
If you're using server-side rendering, follow the [server-side rendering](/doc/guides/building-search-ui/going-further/server-side-rendering/react) guide.
If you're using Next.js, see [`createInstantSearchRouterNext`](/doc/api-reference/widgets/instantsearch-next-router/react).
Don't configure [`initialUiState`](/doc/api-reference/widgets/instantsearch/react#param-initial-ui-state) and `routing` together.
* Use `initialUiState` to set the UI state when the search first loads, such as for a default query or filter.
* Use `routing` to keep the UI state synchronized with the URL, so users can bookmark or share a search.
## Default URLs
Set `routing` to `true` to use the default routing configuration:
a history router with simple state mapping.
This stores routing-compatible UI state in URL query parameters.
```jsx React icon=code theme={"system"}
import { InstantSearch } from "react-instantsearch";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
function App() {
return (
{/* ... */}
);
}
```
Setting `routing` to `true` is equivalent to this configuration:
```jsx React icon=code expandable theme={"system"}
import { InstantSearch } from "react-instantsearch";
import { history } from "instantsearch.js/es/lib/routers";
import { simple } from "instantsearch.js/es/lib/stateMappings";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const routing = {
router: history(),
stateMapping: simple(),
};
function App() {
return (
{/* ... */}
);
}
```
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple", "Samsung"
* **Page:** 2
This produces the following URL:
```txt theme={"system"}
https://example.org/?instant_search[query]=galaxy&instant_search[menu][categories]=Cell Phones&instant_search[refinementList][brand][0]=Apple&instant_search[refinementList][brand][0]=Samsung&instant_search[page]=2
```
By default, routing includes state from routing-compatible widgets.
With many widgets, this can create long URLs.
To create shorter or more descriptive URLs, customize the URL routing.
## Customize URL routing
You can customize which values appear and rename the URL parameters.
The `stateMapping` option maps between InstantSearch's `uiState` and the `routeState` used by the router.
Use it to rename parameters or omit values that you don't want to include in the URL.
```jsx React icon=code expandable theme={"system"}
import { InstantSearch } from "react-instantsearch";
import { history } from "instantsearch.js/es/lib/routers";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const routing = {
router: history(),
stateMapping: {
stateToRoute(uiState) {
// ...
},
routeToState(routeState) {
// ...
},
},
};
function App() {
return (
{/* ... */}
);
}
```
InstantSearch stores widget state in [`uiState`](/doc/api-reference/widgets/ui-state/react).
The following example maps that state to shorter URL parameters.
The state contains information about the user's search, including the query,
the selection,
the page being viewed,
and the widget hierarchy.
`uiState` only stores modified widget values, not defaults.
To persist this state in the URL,
InstantSearch converts the `uiState` into an object called `routeState`:
this `routeState` then becomes a URL.
Conversely, when InstantSearch reads the URL and applies it to the search,
it converts `routeState` into `uiState`.
This logic lives in two functions:
* `stateToRoute`: converts `uiState` to `routeState`.
* `routeToState`: converts `routeState` to `uiState`.
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple" and "Samsung"
* **Page:** 2
This translates into the following `uiState`:
```json JSON icon="braces" theme={"system"}
{
"indexName": {
"query": "galaxy",
"menu": {
"categories": "Cell Phones"
},
"refinementList": {
"brand": ["Apple", "Samsung"]
},
"page": 2
}
}
```
Implement `stateToRoute` to flatten this object into a URL, and `routeToState` to restore the URL into a UI state:
```jsx React icon=code expandable theme={"system"}
import { InstantSearch } from "react-instantsearch";
import { history } from "instantsearch.js/es/lib/routers";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const indexName = "instant_search";
const routing = {
router: history(),
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState[indexName];
return {
q: indexUiState.query,
categories: indexUiState.menu?.categories,
brand: indexUiState.refinementList?.brand,
page: indexUiState.page,
};
},
routeToState(routeState) {
return {
[indexName]: {
query: routeState.q,
menu: {
categories: routeState.categories,
},
refinementList: {
brand: routeState.brand,
},
page: routeState.page,
},
};
},
},
};
function App() {
return (
{/* ... */}
);
}
```
## SEO-friendly URLs
To create more descriptive URLs, move search state from query parameters into the URL path.
This is a common pattern for ecommerce category and search pages.
```txt theme={"system"}
https://example.org/search/Cell+Phones/?query=galaxy&page=2&brands=Apple&brands=Samsung
```
### Store categories in the URL path
This example stores the category in the path and the query, page, and brands as query parameters.
```jsx React icon=code expandable theme={"system"}
import { InstantSearch } from "react-instantsearch";
import { history } from "instantsearch.js/es/lib/routers";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
// Convert a category name to a URL slug.
// Replace spaces with "+" and encode other characters.
function getCategorySlug(name) {
return name.split(" ").map(encodeURIComponent).join("+");
}
// Convert a category slug to a category name.
// Replace "+" with spaces and decode other characters.
function getCategoryName(slug) {
return slug.split("+").map(decodeURIComponent).join(" ");
}
const routing = {
router: history({
windowTitle({ category, query }) {
const queryTitle = query ? `Results for "${query}"` : "Search";
if (category) {
return `${category} – ${queryTitle}`;
}
return queryTitle;
},
createURL({ qsModule, routeState, location }) {
const urlParts = location.href.match(/^(.*?)\/search/);
const baseUrl = `${urlParts ? urlParts[1] : ""}/`;
const categoryPath = routeState.category
? `${getCategorySlug(routeState.category)}/`
: "";
const queryParameters = {};
if (routeState.query) {
queryParameters.query = encodeURIComponent(routeState.query);
}
if (routeState.page !== 1) {
queryParameters.page = routeState.page;
}
if (routeState.brands) {
queryParameters.brands = routeState.brands.map(encodeURIComponent);
}
const queryString = qsModule.stringify(queryParameters, {
addQueryPrefix: true,
arrayFormat: "repeat",
});
return `${baseUrl}search/${categoryPath}${queryString}`;
},
parseURL({ qsModule, location }) {
const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
const category = getCategoryName(pathnameMatches?.[1] || "");
const {
query = "",
page,
brands = [],
} = qsModule.parse(location.search.slice(1));
// `qs` does not return an array when there's a single value.
const allBrands = Array.isArray(brands)
? brands
: [brands].filter(Boolean);
return {
query: decodeURIComponent(query),
page,
brands: allBrands.map(decodeURIComponent),
category,
};
},
}),
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState["instant_search"] || {};
return {
query: indexUiState.query,
page: indexUiState.page,
brands: indexUiState.refinementList?.brand,
category: indexUiState.menu?.categories,
};
},
routeToState(routeState) {
return {
instant_search: {
query: routeState.query,
page: routeState.page,
menu: {
categories: routeState.category,
},
refinementList: {
brand: routeState.brands,
},
},
};
},
},
};
function App() {
return (
{/* ... */}
);
}
```
This example configures the default [`history` router](/doc/api-reference/widgets/history-router/react).
The router reads and writes URLs, while `stateMapping` maps `uiState` to `routeState` and back.
When you configure the history router, you can customize these functions:
* `windowTitle`: returns the browser window title for a `routeState`.
* `createURL`: creates a URL from `routeState`. InstantSearch calls it when synchronizing the browser URL, rendering links in the `menu` widget, or when a connector calls `createURL`.
* `parseURL`: creates `routeState` from the URL when users load or reload the page or use the browser's back or forward navigation.
### Make URLs more discoverable
Shorter category URLs can be more readable and memorable.
Use a mapping object to map category names to shorter URL slugs.
Given the dataset in this guide, you can make some categories more discoverable:
* "Cameras and camcorders" → `/Cameras`
* "Car electronics and GPS" → `/Cars`
When users open `https://example.org/search/Cameras`, InstantSearch selects the "Cameras and camcorders" category.
Define mappings between category names and URL slugs:
```js JavaScript icon=code expandable theme={"system"}
// Map URL slugs to category names.
const encodedCategories = {
Cameras: "Cameras and camcorders",
Cars: "Car electronics and GPS",
Phones: "Phones",
TV: "TV and home theater",
};
// Map category names to URL slugs.
const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
const newKey = encodedCategories[key];
const newValue = key;
return {
...acc,
[newKey]: newValue,
};
}, {});
// Use the mappings when converting category names and slugs.
function getCategorySlug(name) {
const encodedName = decodedCategories[name] || name;
return encodedName.split(" ").map(encodeURIComponent).join("+");
}
function getCategoryName(slug) {
const decodedSlug = encodedCategories[slug] || slug;
return decodedSlug.split("+").map(decodeURIComponent).join(" ");
}
```
You can build these dictionaries from your Algolia .
With such a solution, you have full control over what categories are discoverable from the URL.
### About SEO
For your search results to be part of a public search engine's results, you must be selective.
Trying to index too many search results pages could be considered spam.
To do that, create a [`robots.txt`](http://www.robotstxt.org/) and host it at `https://example.org/robots.txt`.
Here's an example based on the URL scheme you created.
```txt robots.txt theme={"system"}
User-agent: *
Allow: /search/Audio/
Allow: /search/Phones/
Disallow: /search/
Allow: *
```
Explore basic InstantSearch routing in CodeSandbox.
Explore SEO-friendly routing in CodeSandbox.
# Routing and URL syncing in Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/routing-urls/vue
How to synchronize your URLs with Vue InstantSearch.
Synchronizing your UI with the browser URL is considered good practice.
It lets your users take one of your results pages, copy the URL, and share it.
It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.
InstantSearch provides the necessary API entries to let you synchronize the state of your search UI (your refined widgets and current ) with any kind of storage.
Use the [`routing`](/doc/api-reference/widgets/instantsearch/vue#param-routing) option to synchronize UI state with the browser URL, so users can bookmark, share, and revisit the same search.
Don't configure [`initial-ui-state`](/doc/api-reference/widgets/instantsearch/vue#param-initial-ui-state) and `routing` together.
* Use `initialUiState` to set the UI state when the search first loads, such as for a default query or filter.
* Use `routing` to keep the UI state synchronized with the URL, so users can bookmark or share a search.
## Routing examples
The examples in this section use Vue 2.
If you use Vue 3, adapt them using the [Vue 3 migration guide](https://v3-migration.vuejs.org/).
Run and edit the basic routing example in CodeSandbox.
Browse the source code for the basic routing example.
Run and edit the SEO-friendly routing example in CodeSandbox.
Browse the source code for the SEO-friendly routing example.
Run and edit the Vue Router example in CodeSandbox.
Browse the source code for the Vue Router example.
## Default URLs
The examples use the InstantSearch.js router.
Add `instantsearch.js` to your project dependencies alongside `vue-instantsearch`.
Configure the `routing` prop on `` to synchronize UI state with the browser URL.
The default routing setup stores routing-compatible UI state in URL query parameters.
```vue Vue icon=code expandable theme={"system"}
```
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple", "Samsung"
* **Page:** 2
This produces the following URL:
```txt theme={"system"}
https://example.org/?instant_search[query]=galaxy&instant_search[menu][categories]=Cell Phones&instant_search[refinementList][brand][0]=Apple&instant_search[refinementList][brand][0]=Samsung&instant_search[page]=2
```
By default, routing includes state from routing-compatible widgets.
With many widgets, this can create long URLs.
To create shorter or more descriptive URLs, customize the URL routing.
## Customize URL routing
You can customize which values appear and rename the URL parameters.
The `stateMapping` option maps between InstantSearch's `uiState` and the `routeState` used by the router.
Use it to rename parameters or omit values that you don't want to include in the URL.
```js JavaScript icon=code theme={"system"}
export default {
data() {
return {
searchClient: algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
),
routing: {
stateMapping: {
stateToRoute(uiState) {
// ...
},
routeToState(routeState) {
// ...
},
},
},
};
},
};
```
InstantSearch stores widget state in [`uiState`](/doc/api-reference/widgets/ui-state/vue).
The following example maps that state to shorter URL parameters.
The state contains information about the user's search, including the query,
the selection,
the page being viewed,
and the widget hierarchy.
`uiState` only stores modified widget values, not defaults.
To persist this state in the URL,
InstantSearch converts the `uiState` into an object called `routeState`:
this `routeState` then becomes a URL.
Conversely, when InstantSearch reads the URL and applies it to the search,
it converts `routeState` into `uiState`.
This logic lives in two functions:
* `stateToRoute`: converts `uiState` to `routeState`.
* `routeToState`: converts `routeState` to `uiState`.
Assume the following search UI state:
* **Query:** "galaxy"
* **Menu:**
* `categories`: "Cell Phones"
* **Refinement List:**
* `brand`: "Apple" and "Samsung"
* **Page:** 2
This translates into the following `uiState`:
```json JSON icon="braces" theme={"system"}
{
"indexName": {
"query": "galaxy",
"menu": {
"categories": "Cell Phones"
},
"refinementList": {
"brand": ["Apple", "Samsung"]
},
"page": 2
}
}
```
Implement `stateToRoute` to flatten this object into a URL, and `routeToState` to restore the URL into a UI state:
```js JavaScript icon=code expandable theme={"system"}
const indexName = "instant_search";
export default {
data() {
return {
indexName,
searchClient: algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
),
routing: {
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState[indexName];
return {
q: indexUiState.query,
categories: indexUiState.menu && indexUiState.menu.categories,
brand:
indexUiState.refinementList &&
indexUiState.refinementList.brand,
page: indexUiState.page,
};
},
routeToState(routeState) {
return {
[indexName]: {
query: routeState.q,
menu: {
categories: routeState.categories,
},
refinementList: {
brand: routeState.brand,
},
page: routeState.page,
},
};
},
},
},
};
},
};
```
### Keep unrelated URL parameters
By default, routing writes only InstantSearch state to the URL.
To preserve unrelated parameters, include them when implementing `createURL`.
The following example preserves URL parameters that start with `utm_`:
```js JavaScript icon=code expandable theme={"system"}
history({
// ... other options
parseURL({ qsModule, location }) {
return qsModule.parse(location.search.slice(1));
},
createURL({ qsModule, location, routeState }) {
const { origin, pathname, hash } = location;
const queriesFromUrl = qsModule.parse(location.search.slice(1));
// Preserve unrelated URL parameters that start with "utm_".
const utmQueries = Object.fromEntries(
Object.entries(queriesFromUrl).filter(
([key]) =>
!Object.keys(routeState).includes(key) &&
key.startsWith("utm_"),
),
);
// Combine InstantSearch state with the preserved parameters.
const queryString = qsModule.stringify(
{
...routeState,
...utmQueries,
},
{
addQueryPrefix: true,
arrayFormat: "repeat",
},
);
return `${origin}${pathname}${queryString}${hash}`;
},
});
```
### Change the name of a key in routing
To rename the `query` route parameter to `q`,
return `q` from `stateToRoute` and
map it back to `query` in `routeToState`.
## SEO-friendly URLs
This guide uses the router from InstantSearch.js.
Make sure you add `instantsearch.js` to your project's dependencies in addition to `vue-instantsearch`.
To create more descriptive URLs, move search state from query parameters into the URL path.
This is a common pattern for ecommerce category and search pages.
```txt theme={"system"}
https://example.org/search/Cell+Phones/?query=galaxy&page=2&brands=Apple&brands=Samsung
```
The category appears in the URL path, while the query, page, and brands remain query parameters.
This simplified routing assumes that only one widget controls each routed attribute.
### Store categories in the URL path
This example stores the category in the path and the query, page, and brands as query parameters.
```js JavaScript icon=code expandable theme={"system"}
import { history as historyRouter } from 'instantsearch.js/es/lib/routers';
// Convert a category name to a URL slug.
// Replace spaces with "+" and encode other characters.
function getCategorySlug(name) {
return name
.split(' ')
.map(encodeURIComponent)
.join('+');
}
// Convert a category slug to a category name.
// Replace "+" with spaces and decode other characters.
function getCategoryName(slug) {
return slug
.split('+')
.map(decodeURIComponent)
.join(' ');
}
const routing = {
router: historyRouter({
windowTitle({ category, query }) {
const queryTitle = query ? `Results for "${query}"` : 'Search';
if (category) {
return `${category} – ${queryTitle}`;
}
return queryTitle;
},
createURL({ qsModule, routeState, location }) {
const urlParts = location.href.match(/^(.*?)\/search/);
const baseUrl = `${urlParts ? urlParts[1] : ''}/`;
const categoryPath = routeState.category
? `${getCategorySlug(routeState.category)}/`
: '';
const queryParameters = {};
if (routeState.query) {
queryParameters.query = encodeURIComponent(routeState.query);
}
if (routeState.page !== 1) {
queryParameters.page = routeState.page;
}
if (routeState.brands) {
queryParameters.brands = routeState.brands.map(encodeURIComponent);
}
const queryString = qsModule.stringify(queryParameters, {
addQueryPrefix: true,
arrayFormat: 'repeat',
});
return `${baseUrl}search/${categoryPath}${queryString}`;
},
parseURL({ qsModule, location }) {
const pathnameMatches = location.pathname.match(/search\/(.*?)\/?$/);
const category = getCategoryName(
(pathnameMatches && pathnameMatches[1]) || ''
);
const { query = '', page, brands = [] } = qsModule.parse(
location.search.slice(1)
);
// `qs` doesn't return an array if there's a single value.
const allBrands = Array.isArray(brands)
? brands
: [brands].filter(Boolean);
return {
query: decodeURIComponent(query),
page,
brands: allBrands.map(decodeURIComponent),
category,
};
},
}),
stateMapping: {
stateToRoute(uiState) {
const indexUiState = uiState['instant_search'] || {};
return {
query: indexUiState.query,
page: indexUiState.page,
brands: indexUiState.refinementList && indexUiState.refinementList.brand,
category: indexUiState.menu && indexUiState.menu.categories
};
},
routeToState(routeState) {
return {
instant_search: {
query: routeState.query,
page: routeState.page,
menu: {
categories: routeState.category
},
refinementList: {
brand: routeState.brands
},
},
};
},
},
},
};
```
The [basic routing](#routing-examples) example uses the [history router](/doc/api-reference/widgets/history-router/vue).
The router reads and writes URLs, while `stateMapping` maps `uiState` to `routeState` and back.
When you configure the history router, you can customize these functions:
* `windowTitle`: returns the browser window title for a `routeState`.
* `createURL`: creates a URL from `routeState`. InstantSearch calls it when synchronizing the browser URL, rendering links in the `menu` widget, or when a connector calls `createURL`.
* `parseURL`: creates `routeState` from the URL when users load or reload the page or use the browser's back or forward navigation.
### Make URLs more discoverable
Shorter category URLs can be more readable and memorable.
Use a mapping object to map category names to shorter URL slugs.
Given the dataset in this guide, you can make some categories more discoverable:
* "Cameras and camcorders" → `/Cameras`
* "Car electronics and GPS" → `/Cars`
When users open `https://example.org/search/Cameras`, InstantSearch selects the "Cameras and camcorders" category.
Define mappings between category names and URL slugs:
```js JavaScript icon=code expandable theme={"system"}
// Map URL slugs to category names.
const encodedCategories = {
Cameras: "Cameras and camcorders",
Cars: "Car electronics and GPS",
Phones: "Phones",
TV: "TV and home theater",
};
// Map category names to URL slugs.
const decodedCategories = Object.keys(encodedCategories).reduce((acc, key) => {
const newKey = encodedCategories[key];
const newValue = key;
return {
...acc,
[newKey]: newValue,
};
}, {});
// Use the mappings when converting category names and slugs.
function getCategorySlug(name) {
const encodedName = decodedCategories[name] || name;
return encodedName.split(" ").map(encodeURIComponent).join("+");
}
function getCategoryName(slug) {
const decodedSlug = encodedCategories[slug] || slug;
return decodedSlug.split("+").map(decodeURIComponent).join(" ");
}
```
You can build these dictionaries from your Algolia .
With such a solution, you have full control over what categories are discoverable from the URL.
### About SEO
For your search results to be part of a public search engine's results, you must be selective.
Trying to index too many search results pages could be considered spam.
To do that, create a [`robots.txt`](http://www.robotstxt.org/) and host it at `https://example.org/robots.txt`.
Here's an example based on the URL scheme you created.
```txt robots.txt theme={"system"}
User-agent: *
Allow: /search/Audio/
Allow: /search/Phones/
Disallow: /search/
Allow: *
```
## Combine with Vue Router
The previous examples use the InstantSearch history router.
If your search page reads the URL through Vue Router to render content outside InstantSearch, synchronize InstantSearch with Vue Router instead.
Otherwise, keep the InstantSearch router.
Create a router object instead of using `historyRouter`.
The `router` property expects an object with these functions:
```js JavaScript icon=code theme={"system"}
const routing = {
router: {
read() {
/* Read from the URL and return a routeState */
},
write(routeState) {
/* Write to the URL */
},
createURL(routeState) {
/* Return a URL as a string */
},
onUpdate(callback) {
/* Call this callback whenever the URL changes externally */
},
dispose() {
/* Remove any listeners */
},
},
};
```
This example uses the default `stateMapping`.
Configure Vue Router to parse nested query parameters and serialize them into query strings in `main.js` first:
```js JavaScript icon=code theme={"system"}
import qs from "qs";
const router = new Router({
routes: [
// ...
],
// set custom query resolver
parseQuery(query) {
return qs.parse(query);
},
stringifyQuery(query) {
const result = qs.stringify(query);
return result ? `?${result}` : "";
},
});
```
Define the custom `router` in the `routing` object returned by `data`:
```js JavaScript icon=code expandable theme={"system"}
const vueRouter = this.$router; /* Get this from Vue Router */
const routing = {
router: {
read() {
return vueRouter.currentRoute.query;
},
write(routeState) {
vueRouter.push({
query: routeState,
});
},
createURL(routeState) {
return vueRouter.resolve({
query: routeState,
}).href;
},
onUpdate(cb) {
if (typeof window === "undefined") return;
this._removeAfterEach = vueRouter.afterEach(() => {
cb(this.read());
});
this._onPopState = () => {
cb(this.read());
};
window.addEventListener("popstate", this._onPopState);
},
dispose() {
if (typeof window === "undefined") {
return;
}
if (this._onPopState) {
window.removeEventListener("popstate", this._onPopState);
}
if (this._removeAfterEach) {
this._removeAfterEach();
}
},
},
};
```
## Combine with Nuxt
To enable routing in a [Nuxt](https://nuxtjs.org) app,
you can't use the `createServerRootMixin` factory as a mixin as usual,
because you need to access Vue Router which is only available on the component instance.
Here's the workaround:
1. Use `createServerRootMixin` in `data`, so `this.$router` is available.
2. Create an InstantSearch router that wraps Vue Router.
3. Set up `provide` as the root mixin would otherwise do.
4. Set up `findResultsState` in `serverPrefetch`.
5. Call `hydrate` in [`beforeMount`](https://v2.vuejs.org/v2/api/#beforeMount).
Set up a custom `renderToString` function.
```vue Vue 3 expandable theme={"system"}
```
```vue Vue 2 expandable theme={"system"}
```
Wrap the Vue Router for usage with Vue InstantSearch.
```vue Vue icon=code expandable theme={"system"}
```
As in Vue Router, you must set up Nuxt to write deep query strings. In Nuxt, you do this in `nuxt.config.js`:
```js JavaScript icon=code theme={"system"}
// nuxt.config.js
module.exports = {
router: {
parseQuery(queryString) {
return require("qs").parse(queryString);
},
stringifyQuery(object) {
var queryString = require("qs").stringify(object);
return queryString ? "?" + queryString : "";
},
},
};
```
## Group facet values
To group facet values such as "turquoise", "ocean", and "sky" under "blue", add the group at indexing time.
Either add a separate grouping attribute or store both the individual value and group value in the same attribute.
For example, with the following dataset:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": "turquoise"
},
{
"objectID": "2",
"color": "ocean"
},
{
"objectID": "3",
"color": "sky"
}
]
```
To facet on a separate grouping attribute, add `colorGroup` to each record:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": "turquoise",
"colorGroup": "blue"
},
{
"objectID": "2",
"color": "ocean",
"colorGroup": "blue"
},
{
"objectID": "3",
"color": "sky",
"colorGroup": "blue"
}
]
```
To facet on both individual colors and their group, store both values in the `color` attribute:
```json JSON icon=braces theme={"system"}
[
{
"objectID": "1",
"color": ["turquoise", "blue"]
},
{
"objectID": "2",
"color": ["ocean", "blue"]
},
{
"objectID": "3",
"color": ["sky", "blue"]
}
]
```
# Server-side rendering
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/react
Render your React InstantSearch app on your server.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Server-side rendering (SSR) lets you generate HTML from InstantSearch components on the server.
Integrating SSR with React InstantSearch:
* **Improves general performance:** the browser directly loads with HTML containing search results, and React preserves the existing markup (hydration) instead of re-rendering everything.
* **Improves perceived performance:** users don't see a UI flash when loading the page, but directly the search UI. This can also improve your [Largest Contentful Paint](https://web.dev/lcp/) score.
* **Improves SEO:** the content is accessible to any search engine, even those that don't use JavaScript.
Here's the SSR flow for InstantSearch:
1. On the server, **retrieve the initial search results** of the current search state.
2. Then, on the server, **render these search results to HTML** and send the response to the browser.
3. Then, on the browser, **load the JavaScript code** for InstantSearch.
4. Then, on the browser, **hydrate the server-side rendered InstantSearch app.**
React InstantSearch is compatible with server-side rendering.
The library provides an API that works with any SSR solution.
For more information, see:
* [How to install React InstantSearch](/doc/guides/building-search-ui/installation/react)
* [Get started with React InstantSearch](/doc/guides/building-search-ui/getting-started/react)
* [`getServerState`](/doc/api-reference/widgets/server-state/react)
* [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react)
* [`InstantSearchNext`](/doc/api-reference/widgets/instantsearch-next/react)
## With Next.js (App router)
As of Next.js 13, you can use the [App Router](https://nextjs.org/docs/app) to structure your app.
The App Router has a different approach to data fetching than the Pages Router,
which changes the approach to server-side rendering.
To support server-side rendering for the App Router, use the `react-instantsearch-nextjs` package.
It provides an `InstantSearchNext` component that replaces your [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) component.
### Install `react-instantsearch-nextjs`
First, make sure you have the correct dependencies installed:
* `react-instantsearch` >= `7.1.0`
* `next` >= `13.14.0`
Then, install the `react-instantsearch-nextjs` package:
```sh npm theme={"system"}
npm install react-instantsearch-nextjs
```
```sh yarn theme={"system"}
yarn add react-instantsearch-nextjs
```
### Usage
Your search component must be in its own file, and it shouldn't be named `page.js` or `page.tsx`.
To render the component in the browser and allow users to interact with it, include the "use client" directive at the top of your code.
```jsx React icon=code theme={"system"}
"use client"; // [!code ++]
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
export function Search() {
return (
{/* other widgets */}
);
}
```
Import the `InstantSearchNext` component from the `react-instantsearch-nextjs` package,
and replace the [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) component with it,
without changing the props.
```diff React icon=code theme={"system"}
'use client';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
- InstantSearch,
SearchBox,
} from 'react-instantsearch';
+import { InstantSearchNext } from 'react-instantsearch-nextjs';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
export function Search() {
return (
-
+
{/* other widgets */}
-
+
);
}
```
To serve your search page at `/search`, create an `app/search` directory.
Inside it, create a `page.js` file (or `page.tsx` if you're using TypeScript).
Make sure to [configure your route segment to be dynamic](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes)
so that Next.js generates a new page for each request.
```jsx React icon=code theme={"system"}
// app/search/page.js or app/search/page.tsx
import { Search } from "./Search"; // change this with the path to your component
export const dynamic = "force-dynamic";
export default function Page() {
return ;
}
```
You can now visit `/search` to see your server-side rendered search page.
If you were previously using [`getServerState`](/doc/api-reference/widgets/server-state/react) in `getServerSideProps` with the Pages Router, remove any references to it.
It's not needed with the App Router.
### Composing Hooks
Due to the way the App Router and `InstantSearchNext` are implemented,
you can't compose Hooks the same way as you would normally do in React InstantSearch.
All React InstantSearch Hooks have a `skipSuspense` option that you can set to `true` to skip the suspense behavior.
This should be done to all Hooks but the last one in your custom component.
```jsx React icon=code theme={"system"}
export function MyCustomFilters() {
const { items: brands } = useRefinementList(
{
attribute: 'brand',
},
{ skipSuspense: true }
);
const { items: categories } = useRefinementList({ attribute: 'category' });
return (
// Render your filters here
);
}
```
That way, categories will properly render on the server.
You will see a warning in the console if more widgets are rendered after suspense.
This may be a false positive if you are using multi-index or dynamic widgets.
You can ignore it in that case, and set the `ignoreMultipleHooksWarning` prop to `true` on `InstantSearchNext`.
### Static site generation with dynamic route segments
You can generate a static version of your search page at build time using Next's [`generateStaticParams`](https://nextjs.org/docs/app/api-reference/functions/generate-static-params).
Static site generation (pre-rendering) and server-side rendering are essentially the same.
The key difference is timing:
static generation occurs at build time,
while server-side rendering occurs on request.
Adopt the same [usage recommendations](#usage) for both server-side rendering and static site generation.
```jsx React icon=code theme={"system"}
// app/[slug]/page.js or app/[slug]/page.tsx
export const dynamicParams = true;
export async function generateStaticParams() {
const posts = await fetch(/* ... */).then((res) => res.json());
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Page({ params }) {
const { slug } = await params;
/* ... */
return ;
}
```
To statically render all paths at runtime,
set the [`dynamic` route segment](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes) to `'force-static'`.
### Enabling routing
To enable routing, add a boolean `routing` prop to `InstantSearchNext`.
```jsx React icon=code theme={"system"}
function Search() {
return (
{/* widgets */}
);
}
```
To customize the way InstantSearch maps the state to the route,
pass an object to `routing`, where `router` has the same options as [`history`](/doc/api-reference/widgets/history-router/react),
and `stateMapping` has the same options as the one in [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react).
```jsx React icon=code theme={"system"}
{/* widgets */}
;
```
### Dynamic routes
If you have a dynamic route, such as `/search/[brand]`, you may get CLS (Cumulative Layout Shift) issues when navigating between pages,
especially if you are using Next.js `Link` components.
To avoid this, use the `createInstantSearchNextInstance` function from the `react-instantsearch-router-nextjs` package to create an instance which will be shared across all pages.
```jsx React icon=code theme={"system"}
import {
createInstantSearchNextInstance,
InstantSearchNext,
} from "react-instantsearch-nextjs";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
const categoryPageInstance = createInstantSearchNextInstance();
export function BrandPageSearch({ brand }) {
return (
{/* other widgets */}
);
}
```
You can see a full example of this in the [Next.js dynamic routes example](https://github.com/algolia/instantsearch/blob/master/examples/react/next-app-router/app/\[category]/Search.tsx).
## With the Next.js Pages Router
Server-side rendering a page with the Pages Router in Next.js is split in two parts:
a function that returns data from the server, and a React component for the page that receives this data.
On the page, wrap the search experience with the [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react) component.
This provider receives the server state and forwards it to the entire InstantSearch app.
### Server-side rendering
In Next's [`getServerSideProps`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props),
you can use [`getServerState`](/doc/api-reference/widgets/server-state/react) to return the server state as a prop.
To support routing, you can use the `createInstantSearchRouterNext` function from the `react-instantsearch-router-nextjs` package.
```jsx JavaScript theme={"system"}
// index.js
import { renderToString } from "react-dom/server";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from "react-instantsearch";
import { history } from "instantsearch.js/es/lib/routers/index.js";
import singletonRouter from "next/router";
import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs";
const searchClient = algoliasearch("undefined", "undefined");
export default function SearchPage({ serverState, serverUrl }) {
return (
{/* Widgets */}
);
}
export async function getServerSideProps({ req }) {
const protocol = req.headers.referer?.split["://"](0) || "https";
const serverUrl = `${protocol}://${req.headers.host}${req.url}`;
const serverState = await getServerState(
,
{ renderToString },
);
return {
props: {
serverState,
serverUrl,
},
};
}
```
```tsx TypeScript theme={"system"}
// index.tsx
import { renderToString } from 'react-dom/server';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchServerState,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
import { history } from 'instantsearch.js/es/lib/routers/index.js';
import singletonRouter from 'next/router';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
const searchClient = algoliasearch('undefined', 'undefined');
type SearchPageProps = {
serverState?: InstantSearchServerState;
serverUrl: URL;
};
export default function SearchPage({ serverState, serverUrl }: SearchPageProps) {
return (
{/* Widgets */}
);
}
export async function getServerSideProps({ req }) {
const protocol = req.headers.referer?.split('://')[0] || 'https';
const serverUrl = `${protocol}://${req.headers.host}${req.url}`;
const serverState = await getServerState(
,
{ renderToString }
);
return {
props: {
serverState,
serverUrl,
},
};
}
```
Check the complete [SSR example with Next.js](https://codesandbox.io/s/github/algolia/instantsearch/tree/master/examples/react/next?file=/pages/index.tsx).
### Static site generation
You can generate a static version of your search page at build time using Next's [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching/get-static-props).
Static site generation (or pre-rendering) is essentially the same thing as [server-side rendering](/doc/guides/building-search-ui/going-further/server-side-rendering/react#server-side-rendering), except the latter happens at request time, while the former happens at build time.
You can use the same [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react) and [`getServerState`](/doc/api-reference/widgets/server-state/react) APIs for both [server-side rendering](#server-side-rendering) and static site generation.
```js JavaScript theme={"system"}
// index.js
import { renderToString } from "react-dom/server";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from "react-instantsearch";
const searchClient = algoliasearch("undefined", "undefined");
export default function SearchPage({ serverState }) {
return (
{/* Widgets */}
);
}
export async function getStaticProps() {
const serverState = await getServerState(, { renderToString });
return {
props: {
serverState,
},
};
}
```
```tsx TypeScript theme={"system"}
// index.tsx
import { renderToString } from 'react-dom/server';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchServerState,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
const searchClient = algoliasearch('undefined', 'undefined');
type SearchPageProps = {
serverState?: InstantSearchServerState;
};
export default function SearchPage({ serverState }: SearchPageProps) {
return (
{/* Widgets */}
);
}
export async function getStaticProps() {
const serverState = await getServerState(, {
renderToString
});
return {
props: {
serverState,
},
};
}
```
### Dynamic routes
If you want to generate pages dynamically—for example,
one for each brand—you can use Next's [`getStaticPaths`](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths) API.
The following example uses [dynamic routes](https://nextjs.org/docs/routing/dynamic-routes) along with `getStaticPaths` to create one page per brand.
```js JavaScript theme={"system"}
// pages/brands/[brand].js
import { renderToString } from "react-dom/server";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
InstantSearch,
InstantSearchSSRProvider,
SearchBox,
Hits,
Configure,
getServerState,
} from "react-instantsearch";
const searchClient = algoliasearch("undefined", "undefined");
export default function BrandPage({ brand, serverState }) {
return (
);
}
export async function getStaticPaths() {
return {
// You can retrieve your brands from an API, a database, a file, etc.
paths: [{ params: { brand: "Apple" } }, { params: { brand: "Samsung" } }],
fallback: "blocking", // or `true` or `false`
};
}
export async function getStaticProps({ params }) {
if (!params) {
return { notFound: true };
}
const serverState = await getServerState(, {
renderToString,
});
return {
props: {
brand: params.brand,
serverState,
},
};
}
```
```tsx TypeScript theme={"system"}
// pages/brands/[brand].tsx
import { renderToString } from 'react-dom/server';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import {
InstantSearch,
InstantSearchServerState,
InstantSearchSSRProvider,
SearchBox
Hits,
Configure,
getServerState,
} from 'react-instantsearch';
import { GetStaticPathsResult, GetStaticPropsContext } from 'next';
const searchClient = algoliasearch(
'undefined',
'undefined'
);
type BrandPageProps = {
brand?: string;
serverState?: InstantSearchServerState;
};
export default function BrandPage({ brand, serverState }: BrandPageProps) {
return (
);
}
type Params = {
brand: string;
};
export function getStaticPaths(): GetStaticPathsResult {
return {
// You can retrieve your brands from an API, a database, a file, etc.
paths: [{ params: { brand: 'Apple' } }, { params: { brand: 'Samsung' } }],
fallback: 'blocking', // or `true` or `false`
};
}
export async function getStaticProps({
params,
}: GetStaticPropsContext) {
if (!params) {
return { notFound: true };
}
const serverState = await getServerState(
,
{ renderToString }
);
return {
props: {
brand: params.brand,
serverState,
},
};
}
```
If you have a reasonable amount of paths to generate and this number doesn't change much, you can generate them all at build time.
In this case, you can set `fallback: false`, which will serve a 404 page to users who try to visit a path that doesn't exist (for example, a brand that isn't in your dataset).
If there are many categories and generating them all significantly slows down your build,
you can pre-render only a subset of them (for example, the most popular ones) and generate the rest on the fly.
With `fallback: true`, whenever a user visits a path that doesn't exist, your `getStaticProps` code runs on the server and the page is generated once for all subsequent users.
Users see a loading screen that you can implement with [`router.isFallback`](https://nextjs.org/docs/pages/api-reference/functions/get-static-paths#fallback-pages) until the page is ready.
With `fallback: 'blocking'`, the scenario is the same as with `fallback: true` but there's no loading screen.
The server only returns the HTML once the page is generated.
## With Remix
[Remix](https://remix.run/) is a full-stack web framework that encourages usage of runtime servers, notably for server-side rendering.
Server-side rendering a page in Remix is split in two parts:
* A [loader](https://remix.run/docs/en/v1/api/conventions#loader) that returns data from the server,
* A React component for the page that receives this data.
In the page, you need to wrap the search experience with the [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react) component.
This provider receives the server state and forwards it to the entire InstantSearch app.
In Remix' loader, you can use [`getServerState`](/doc/api-reference/widgets/server-state/react) to return the server state.
To support routing, forward the server's request URL to the [`history`](/doc/api-reference/widgets/history-router/js) router.
```js JavaScript theme={"system"}
// index.js
import { renderToString } from "react-dom/server";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from "react-instantsearch";
import { history } from "instantsearch.js/cjs/lib/routers/index.js";
import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
const searchClient = algoliasearch("undefined", "undefined");
export async function loader({ request }) {
const serverUrl = request.url;
const serverState = await getServerState(, {
renderToString,
});
return json({
serverState,
serverUrl,
});
}
function Search({ serverState, serverUrl }) {
return (
{/* Widgets */}
);
}
export default function HomePage() {
const { serverState, serverUrl } = useLoaderData();
return ;
}
```
```tsx TypeScript theme={"system"}
// index.tsx
import { renderToString } from 'react-dom/server';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import type { InstantSearchServerState } from 'react-instantsearch';
import {
InstantSearch,
InstantSearchSSRProvider,
getServerState,
} from 'react-instantsearch';
import { history } from 'instantsearch.js/cjs/lib/routers/index.js';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import type { LoaderFunction } from '@remix-run/node';
const searchClient = algoliasearch('undefined', 'undefined');
export const loader: LoaderFunction = async ({ request }) => {
const serverUrl = request.url;
const serverState = await getServerState(
,
{ renderToString }
);
return json({
serverState,
serverUrl,
});
};
type SearchProps = {
serverState?: InstantSearchServerState;
serverUrl?: string;
};
function Search({ serverState, serverUrl }: SearchProps) {
return (
{/* Widgets */}
);
}
export default function HomePage() {
const { serverState, serverUrl } = useLoaderData() as SearchProps;
return ;
}
```
Check out the complete examples:
* [SSR with Remix](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/react-instantsearch/remix?file=/app/routes/index.tsx).
* [SSR with Express](https://codesandbox.io/s/github/algolia/instantsearch/tree/master/examples/react/ssr?file=/src/App.js).
## With a custom server
This guide shows how to server-side render your app with [Express](https://github.com/expressjs/express).
However, you can follow the same approach with any Node.js server.
The example in this guide has three files:
* `App.js`: the React component shared between the server and the browser
* `server.js`: the server entry to a Node.js HTTP server
* `browser.js`: the browser entry (which gets compiled to `assets/bundle.js`)
`App.js` is the main entry point to your React app.
It exports an `App` component that you can render both on the server and in the browser.
The [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react) component receives the server state and forwards it to [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react).
```js JavaScript theme={"system"}
// App.js
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch, InstantSearchSSRProvider } from "react-instantsearch";
const searchClient = algoliasearch("undefined", "undefined");
function App({ serverState }) {
return (
{/* Widgets */}
);
}
export default App;
```
```tsx TypeScript theme={"system"}
// App.tsx
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import React from 'react';
import {
InstantSearch,
InstantSearchServerState,
InstantSearchSSRProvider,
} from 'react-instantsearch';
const searchClient = algoliasearch('undefined', 'undefined');
type AppProps = {
serverState?: InstantSearchServerState;
};
function App({ serverState }: AppProps) {
return (
{/* Widgets */}
);
}
export default App;
```
When you receive the request on the server, you need to retrieve the server state so you can pass it down to `App`.
This is what [`getServerState`](/doc/api-reference/widgets/server-state/react) does: it receives your InstantSearch app and computes a search state from it.
In the `server.js` file:
```js JavaScript icon=code theme={"system"}
import express from "express";
import React from "react";
import { renderToString } from "react-dom/server";
import { getServerState } from "react-instantsearch";
import App from "./App";
const app = express();
app.get("/", async (req, res) => {
const serverState = await getServerState(, { renderToString });
const html = renderToString();
res.send(
`
${html}
`,
);
});
app.listen(8080);
```
Here, the server:
1. **Retrieves the server state** with [`getServerState`](/doc/api-reference/widgets/server-state/react).
2. **Renders the `App` as HTML** with this server state.
3. **Sends the HTML** to the browser.
Since you're sending plain HTML to the browser,
you need a way to forward the server state object so you can reuse it in your InstantSearch app.
To do so, you can serialize it and store it on the `window` object (here on the `__SERVER_STATE__` global), for later reuse in `browser.js`.
Once the browser has received HTML from the server, the final step is to connect this markup to the interactive app.
This step is called [hydration](https://reactjs.org/docs/react-dom.html#hydrate).
In the `browser.js` file:
```js JavaScript icon=code theme={"system"}
import React from "react";
import { hydrate } from "react-dom";
import App from "./App";
hydrate(
,
document.querySelector("#root"),
);
delete window.__SERVER_STATE__;
```
Deleting `__SERVER_STATE__` from the global object allows the server state to be garbage collected.
Server-side rendered search experiences should be able to generate HTML based on the current URL.
You can use the [`history`](/doc/api-reference/widgets/history-router/js) router to synchronize [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) with the browser URL.
```diff JavaScript theme={"system"}
// App.js
import { liteClient as algoliasearch } from 'algoliasearch/lite';
+import { history } from 'instantsearch.js/es/lib/routers';
+// or cjs if you're running in a CommonJS environment
+// import { history } from 'instantsearch.js/cjs/lib/routers';
import React from 'react';
import {
InstantSearch,
InstantSearchSSRProvider,
} from 'react-instantsearch';
const searchClient = algoliasearch('undefined', 'undefined');
-function App({ serverState }) {
+function App({ serverState, serverUrl }) {
return (
- typeof window === 'undefined' ? serverUrl : window.location,
- }),
- }}
>
{/* Widgets */}
);
}
export default App;
```
```diff TypeScript theme={"system"}
// App.tsx
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import React from 'react';
+import { history } from 'instantsearch.js/es/lib/routers';
+// or cjs if you're running in a CommonJS environment
+// import { history } from 'instantsearch.js/cjs/lib/routers';
import {
InstantSearch,
InstantSearchServerState,
InstantSearchSSRProvider,
} from 'react-instantsearch';
const searchClient = algoliasearch('undefined', 'undefined');
type AppProps = {
serverState?: InstantSearchServerState;
+ serverUrl: URL;
};
-function App({ serverState }: AppProps) {
+function App({ serverState, serverUrl }: AppProps) {
return (
+ typeof window === 'undefined' ? serverUrl : window.location,
+ }),
+ }}
>
{/* Widgets */}
);
}
export default App;
```
You can rely on `window.location` when rendering in the browser,
and use the `location` provided by the server when rendering on the server.
In the `server.js` file, recreate the URL and pass it to the `App`:
```diff JavaScript icon=code theme={"system"}
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch';
import App from './App';
const app = express();
app.get('/', async (req, res) => {
+ const serverUrl = new URL(
+ `${req.protocol}://${req.get('host')}${req.originalUrl}`
+ );
- const serverState = await getServerState(, { renderToString });
+ const serverState = await getServerState(, {
+ renderToString,
+ });
- const html = renderToString();
+ const html = renderToString();
res.send(
`
${html}
`
);
});
app.listen(8080);
```
# Server-side rendering
Source: https://www.algolia.com/doc/guides/building-search-ui/going-further/server-side-rendering/vue
Render your Vue InstantSearch app on your server.
You're reading the documentation for Vue InstantSearch v4.
Read the migration guide to learn [how to upgrade from v3 to v4](/doc/guides/building-search-ui/upgrade-guides/vue#upgrade-from-v3-to-v4).
You can still find the [v3 documentation for this page](https://algolia.com/old-docs/deprecated/instantsearch/vue/v3/guides/server-side-rendering/).
Server-side rendering (SSR) describes a technique for rendering websites.
When you request an SSR page, the HTML is rendered on the server and then sent to your client.
The main steps to implement server-side rendering with Algolia are:
On the server:
1. Request search results from Algolia
2. Render the Vue app with the results of the request
3. Store the search results in the page
4. Return the HTML page as a string
On the client:
1. Read the search results from the page
2. Render (or hydrate) the Vue app with the search results
You can build server-side rendered apps with Vue.js in different ways—for example,
using the [Vue CLI](https://cli.vuejs.org/) or [Nuxt.js](https://nuxt.com).
For code examples, see these GitHub repositories:
* [InstantSearch with Vue 3, SSR, and Vue CLI](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/vue3-ssr-vue-cli)
* [InstantSearch with Vue 3, SSR, and Vite](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/vue3-ssr-vite)
## With Vue CLI
Run and edit the Server-side rendering example in CodeSandbox.
Browse the source for the Server-side rendering example on GitHub.
Use the [Vue CLI](https://cli.vuejs.org) to create the app and add the SSR plugin:
```sh Command line icon=square-terminal theme={"system"}
vue create algolia-ssr-example
cd algolia-ssr-example
vue add router
vue add @akryum/ssr
```
Run `npm run ssr:serve`.
See [How to install Vue InstantSearch](/doc/guides/building-search-ui/installation/vue).
Add the following to the `src/main.js` file:
```js Vue 3 theme={"system"}
import {
AisInstantSearchSsr,
createServerRootMixin,
} from "vue-instantsearch/vue3/es";
Vue.use(VueInstantSearch);
```
```js Vue 2 theme={"system"}
import VueInstantSearch from "vue-instantsearch";
Vue.use(VueInstantSearch);
```
Create a new page `src/views/Search.vue` and build a search interface:
```vue Vue icon=code theme={"system"}
```
Add route to this page in `src/router.js`:
```js JavaScript icon=code theme={"system"}
import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Search from "./views/Search.vue";
Vue.use(Router);
export function createRouter() {
return new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
// ...
{
path: "/search",
name: "search",
component: Search,
},
],
});
}
```
Update in `src/App.vue`:
```vue Vue icon=code theme={"system"}
Home |
About |
Search
```
Add `instantsearch.css` to `public/index.html`:
```html HTML icon=code-xml theme={"system"}
```
Vue InstantSearch uses ES modules, but the app runs on the server, with Node.js.
That's why you need to add these modules to the `vue.config.js` configuration file:
```js JavaScript icon=code theme={"system"}
module.exports = {
pluginOptions: {
ssr: {
nodeExternalsWhitelist: [
/\.css$/,
/\?vue&type=style/,
/vue-instantsearch/,
/instantsearch.js/,
],
},
},
};
```
At this point, Vue.js renders the app on the server.
But when you go to `/search` in your browser, you won't see the search results on the page.
That's because, by default, Vue InstantSearch starts searching and showing results after the page is rendered for the first time.
To perform searches on the backend as well, you need to create a backend instance in `src/main.js`:
```js Vue 3 theme={"system"}
import VueInstantSearch, {
createServerRootMixin,
} from "vue-instantsearch/vue3/es";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
export async function createApp({
renderToString,
beforeApp = () => {},
afterApp = () => {},
} = {}) {
const router = createRouter();
//Provide access to all components
Vue.use(VueInstantSearch);
await beforeApp({
router,
});
const app = new Vue({
// Provide access to the instance
mixins: [
createServerRootMixin({
searchClient,
indexName: "instant_search",
}),
],
serverPrefetch() {
return this.instantsearch.findResultsState({
component: this,
renderToString,
});
},
beforeMount() {
if (typeof window === "object" && window.__ALGOLIA_STATE__) {
this.instantsearch.hydrate(window.__ALGOLIA_STATE__);
delete window.__ALGOLIA_STATE__;
}
},
router,
render: (h) => h(App),
});
const result = {
app,
router,
};
await afterApp(result);
return result;
}
```
```js Vue 2 theme={"system"}
import VueInstantSearch, { createServerRootMixin } from "vue-instantsearch";
import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
export async function createApp({
renderToString,
beforeApp = () => {},
afterApp = () => {},
} = {}) {
const router = createRouter();
// Provide access to all components
Vue.use(VueInstantSearch);
await beforeApp({
router,
});
const app = new Vue({
// Provide access to the instance
mixins: [
createServerRootMixin({
searchClient,
indexName: "instant_search",
}),
],
serverPrefetch() {
return this.instantsearch.findResultsState({
component: this,
renderToString,
});
},
beforeMount() {
if (typeof window === "object" && window.__ALGOLIA_STATE__) {
this.instantsearch.hydrate(window.__ALGOLIA_STATE__);
delete window.__ALGOLIA_STATE__;
}
},
router,
render: (h) => h(App),
});
const result = {
app,
router,
};
await afterApp(result);
return result;
}
```
The Vue app can now inject the backend instance of Vue InstantSearch.
In the main file, replace `ais-instant-search` with `ais-instant-search-ssr`.
You can also remove its props since they're now passed to the `createServerRootMixin` function.
```vue Vue icon=code theme={"system"}
```
To save the results on the backend, add the InstantSearch state to the `context` using the `getState` function:
```js JavaScript icon=code theme={"system"}
import _renderToString from "vue-server-renderer/basic";
import { createApp } from "./main";
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err);
resolve(res);
});
});
}
export default (context) => {
return new Promise(async (resolve, reject) => {
// Read the provided instance
const { app, router, instantsearch } = await createApp({ renderToString });
router.push(context.url);
router.onReady(() => {
// Save the results once rendered fully.
context.rendered = () => {
context.algoliaState = app.instantsearch.getState();
};
const matchedComponents = router.getMatchedComponents();
// Find the root component that handles the rendering
Promise.all(
matchedComponents.map((Component) => {
if (Component.asyncData) {
return Component.asyncData({
route: router.currentRoute,
});
}
}),
).then(() => resolve(app));
}, reject);
});
};
```
Rehydrate the app with the initial request once you start searching.
For this, you need to save the data on the page.
Vue CLI provides a way to read the value on the `context` and save it in `public/index.html`:
```html HTML theme={"system"}
{{{ renderState() }}}
{{{ renderState({ contextKey: 'algoliaState', windowKey: '__ALGOLIA_STATE__' }) }}}
{{{ renderScripts() }}}
```
## With Nuxt 2
The following section describes how to set up server-side rendering with Vue InstantSearch and Nuxt 2.
[Check the community](https://www.algolia.com/developers/code-exchange/?query=nuxt\&page=1) for solutions for Nuxt 3.
Run and edit the Server-side rendering example in CodeSandbox.
Browse the source for the Server-side rendering example on GitHub.
Create a [Nuxt](https://nuxtjs.org) app and add `vue-instantsearch`:
```sh Command line icon=square-terminal theme={"system"}
npx create-nuxt-app algolia-nuxt-example
cd algolia-nuxt-example
npm install vue-instantsearch algoliasearch
```
Vue InstantSearch uses ES modules, but the app runs on the server, with Node.js.
That's why you need to add these modules to the `nuxt.config.js` configuration file:
```js JavaScript icon=code theme={"system"}
module.exports = {
build: {
transpile: ["vue-instantsearch", "instantsearch.js/es"],
},
};
```
Create a new page `pages/search.vue` and build a Vue InstantSearch interface:
```vue Vue icon=code theme={"system"}
```
Add the component declarations and the style sheet:
```js Vue 3 theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
AisHighlight,
AisHits,
AisInstantSearch,
AisPagination,
AisRefinementList,
AisSearchBox,
AisStats,
createServerRootMixin,
} from "vue-instantsearch/vue3/es";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
export default {
components: {
AisInstantSearch,
AisRefinementList,
AisHits,
AisHighlight,
AisSearchBox,
AisStats,
AisPagination,
},
data() {
return {
searchClient,
};
},
head() {
return {
link: [
{
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite-min.css",
},
],
};
},
};
```
```js Vue 2 theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
AisHighlight,
AisHits,
AisInstantSearch,
AisPagination,
AisRefinementList,
AisSearchBox,
AisStats,
createServerRootMixin,
} from "vue-instantsearch";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
export default {
components: {
AisInstantSearch,
AisRefinementList,
AisHits,
AisHighlight,
AisSearchBox,
AisStats,
AisPagination,
},
data() {
return {
searchClient,
};
},
head() {
return {
link: [
{
rel: "stylesheet",
href: "https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite-min.css",
},
],
};
},
};
```
1. Add `createServerRootMixin` to create a reusable search instance.
2. Add `findResultsState` in `serverPrefetch` to perform a search query in the backend.
3. Call the `hydrate` method in `beforeMount`.
4. Replace `ais-instant-search` with `ais-instant-search-ssr`
Add the `createRootMixin`:
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
# Install React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/installation/react
Learn how to install React InstantSearch from npm and include it in your React app.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Install React InstantSearch as an npm package or include it in your app with a CDN link.
React InstantSearch is composed of two separate packages:
* [`react-instantsearch`](https://www.npmjs.com/package/react-instantsearch) which exposes all UI widgets and re-exports all Hooks.
* [`react-instantsearch-core`](https://www.npmjs.com/package/react-instantsearch-core) which only exposes all Hooks. **You only need this package if you're using React InstantSearch with React Native.**
The code examples on this page use the React InstantSearch library with the `algoliasearch/lite` client,
which is smaller but doesn't support indexing your data.
To perform indexing operations, import the regular `algoliasearch` client.
## Install React InstantSearch as an npm package
If you're using a package manager and a build tool, you can install React InstantSearch from npm.
For the web:
```sh npm theme={"system"}
npm install algoliasearch react-instantsearch
```
```sh yarn theme={"system"}
yarn add algoliasearch react-instantsearch
```
```sh pnpm theme={"system"}
pnpm add algoliasearch react-instantsearch
```
For React Native:
```sh npm theme={"system"}
npm install algoliasearch react-instantsearch-core
```
```sh yarn theme={"system"}
yarn add algoliasearch react-instantsearch-core
```
```sh pnpm theme={"system"}
pnpm add algoliasearch react-instantsearch-core
```
Then in your app, import the module:
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const indexName = "INDEX_NAME";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
);
}
```
[`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) is the root React InstantSearch component.
All widgets must be wrapped within it
In a production environment, [secure your API keys](/doc/guides/security/security-best-practices#secure-your-api-keys) as environment variables.
## Include React InstantSearch with a CDN
If you don't use a package manager and a build tool,
you can include React InstantSearch directly from the [jsDelivr](https://www.jsdelivr.com) CDN.
```html HTML icon=code-xml theme={"system"}
```
```html HTML icon=code-xml theme={"system"}
```
You now have access to the `ReactInstantSearch` object on the global `window` object.
```jsx React icon=code theme={"system"}
const { liteClient: algoliasearch } = window["algoliasearch/lite"];
const { InstantSearch } = ReactInstantSearch;
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const indexName = "INDEX_NAME";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
{/* Add widgets here */}
);
}
```
[`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) is the root React InstantSearch component.
All widgets must be wrapped within it.
In a production environment, [secure your API keys](/doc/guides/security/security-best-practices#secure-your-api-keys) as environment variables.
## Send click and conversion events
Capturing real-time user interactions as events gives you actionable insights from [click and conversion metrics](/doc/guides/search-analytics/concepts/metrics#click-through-rate),
and they help you increase your customer engagement and conversions.
Events are used to activate Algolia features and products like [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started),
[Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking),
[Query Categorization](/doc/guides/algolia-ai/query-categorization),
[Recommend](/doc/guides/algolia-recommend/overview),
and [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization).
To send click and conversion events when users interact with your search UI, set the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option to `true`.
## TypeScript support
These packages ship with TypeScript types:
* `react-instantsearch` (v7)
* `react-instantsearch-core` (v7)
The following packages don't ship with TypeScript types,
but have [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) definitions you can use:
* `react-instantsearch` (v6)
* `react-instantsearch-core` (v6)
* `react-instantsearch-dom` (v6)
* `react-instantsearch-native` (v6)
If you have issues when using them, [contact the DefinitelyTyped community](https://github.com/DefinitelyTyped/DefinitelyTyped/issues).
The `react-instantsearch-dom-maps` package doesn't ship with TypeScript types and doesn't have any DefinitelyTyped definitions.
## Browser support
Algolia supports the last two versions of the major browsers:
Chrome, Edge, Firefox, Safari.
To support [Internet Explorer 11](https://wikipedia.org/wiki/Internet_Explorer_11) you need [polyfills](https://wikipedia.org/wiki/Polyfill_\(programming\)) for:
`Array.prototype.find`, `Array.prototype.includes`, `Promise`, `Object.entries`, and `Object.assign`.
The code samples in this documentation use a JavaScript syntax that isn't natively supported by older browsers like Internet Explorer 11.
If your site needs to support older browsers, use a tool like [Babel](https://babeljs.io/repl) to make your code work in the browsers you target.
# How to install Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/installation/vue
Learn how to install Vue InstantSearch from npm and load it as a Vue plugin in your app.
Install Vue InstantSearch with a packaging system or with a direct link in your web page.
If you have a JavaScript build tool (for example if you've created your project using the Vue command-line tool),
install Vue InstantSearch from npm:
```sh Command line icon=square-terminal theme={"system"}
npm install algoliasearch vue-instantsearch
```
In your `main js` file, load the package as a Vue plugin:
```js Vue 3 theme={"system"}
import { createApp } from "vue";
import App from "./App.vue";
import InstantSearch from "vue-instantsearch/vue3/es";
const app = createApp(App);
app.use(InstantSearch);
app.mount("#app");
```
```js Vue 2 theme={"system"}
import Vue from "vue";
import App from "./App.vue";
import InstantSearch from "vue-instantsearch";
Vue.use(InstantSearch);
new Vue({
el: "#app",
render: (h) => h(App),
});
```
This lets you use all the widgets directly in your components.
```html HTML icon=code-xml theme={"system"}
```
```html HTML icon=code-xml theme={"system"}
```
```html HTML icon=code-xml theme={"system"}
```
For example in an `#app` div:
```vue Vue icon=code theme={"system"}
```
Add a script tag with the instantiation of the plugin and the app:
```js Vue 3 theme={"system"}
```
```js Vue 2 theme={"system"}
```
[`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue) is the root Vue InstantSearch component.
All widgets must be wrapped within it.
In a production environment, [secure your API keys](/doc/guides/security/security-best-practices#secure-your-api-keys) as environment variables.
## Send click and conversion events
Capturing real-time user interactions as events gives you actionable insights through [click and conversion metrics](/doc/guides/search-analytics/concepts/metrics#click-through-rate),
and they help you increase your customer engagement and conversions.
Events are used to activate Algolia features and products like [NeuralSearch](/doc/guides/ai-relevance/neuralsearch/get-started),
[Dynamic Re-Ranking](/doc/guides/algolia-ai/re-ranking),
[Query Categorization](/doc/guides/algolia-ai/query-categorization),
[Recommend](/doc/guides/algolia-recommend/overview),
and [Personalization](/doc/guides/personalization/classic-personalization/what-is-personalization).
To send click and conversion events when users interact with your search UI, set the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option to `true`.
## Optimize your build with tree shaking
Tree shaking can be done with modern build tools.
The goal is to have code that's actually used in your built site.
In the context of Vue InstantSearch this means not including the widgets that aren't used.
You can do this by using the components directly, rather than the `Vue.use(InstantSearch)` call.
In every component you're using InstantSearch widgets, you can import and instantiate them like this:
```js Vue 3 theme={"system"}
import { AisInstantSearch, AisSearchBox } from "vue-instantsearch/vue3/es";
export default {
components: {
AisInstantSearch,
AisSearchBox,
},
};
```
```js Vue 2 theme={"system"}
import { AisInstantSearch, AisSearchBox } from "vue-instantsearch";
export default {
components: {
AisInstantSearch,
AisSearchBox,
},
};
```
The build tool you're using will then be able to only include the widgets that you're actually using.
## Browser support
Algolia supports the last two versions of the major browsers (Chrome, Edge, Firefox, Safari).
To support [Internet Explorer 11](https://wikipedia.org/wiki/Internet_Explorer_11) you need [polyfills](https://wikipedia.org/wiki/Polyfill_\(programming\)) for:
`Array.prototype.find`, `Array.prototype.includes`, `Promise`, `Object.entries`, and `Object.assign`.
The code samples in this documentation use a JavaScript syntax that isn't natively supported by older browsers like Internet Explorer 11.
If your site needs to support older browsers, use a tool like [Babel](https://babeljs.io/repl) to make your code work in the browsers you target.
# UI examples for InstantSearch.js
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/demos/js
See a few examples of user interfaces built with InstantSearch.js.
## Ecommerce
[View demo](https://instantsearchjs.netlify.app/examples/js/e-commerce/)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/js/e-commerce)
[View dataset](https://github.com/algolia/datasets/tree/master/ecommerce)
## Media
[View demo](https://instantsearchjs.netlify.app/examples/js/media/)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/js/media)
## Tourism
[View demo](https://instantsearchjs.netlify.app/examples/js/tourism/)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/js/tourism)
# UI examples for React InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/demos/react
See a few examples for user interfaces built with React InstantSearch.
## Ecommerce
* [View demo](https://instantsearchjs.netlify.app/examples/react/e-commerce)
* [View source code](https://github.com/algolia/instantsearch/tree/master/examples/react/e-commerce)
* [View dataset](https://github.com/algolia/datasets/tree/master/ecommerce)
## Default theme
* [View demo](https://codesandbox.io/s/github/algolia/instantsearch/tree/master/examples/react/default-theme)
* [View source code](https://github.com/algolia/instantsearch/tree/master/examples/react/default-theme)
* [View dataset](https://github.com/algolia/datasets/tree/master/ecommerce)
# UI examples for Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/demos/vue
See a few examples of user interfaces built with Vue InstantSearch.
## Ecommerce
[View demo](https://instantsearchjs.netlify.app/examples/vue/e-commerce)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/vue/e-commerce)
[View dataset](https://github.com/algolia/datasets/tree/master/ecommerce)
## Media
[View demo](https://instantsearchjs.netlify.app/examples/vue/media)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/vue/media)
[View dataset](https://github.com/algolia/datasets/tree/master/movies)
## Default theme
[View demo](https://instantsearchjs.netlify.app/examples/vue/default-theme)
[View source code](https://github.com/algolia/instantsearch/tree/master/examples/vue/default-theme)
# SEO checklist
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/seo/js
Every step you can take to make your InstantSearch.js search experience SEO-friendly.
Show more
```
2. Each page can be accessed directly through a URL.
3. Each page URL has a canonical link to its primary page.
If your pagination occurs on scroll (as is the case with the infinite hits widget),
make sure you still provide a "show more" link that uses plain URLs.
Some search engines use the HTML `link` elements with attributes `rel="next"` and `rel="prev"` in the `` of your page to infer the relationship between component URLs in a paginated series.
These elements help but not all search engines use them for indexing.
For more information, see:
* [Pagination with rel="next" and rel="prev"](https://developers.google.com/search/blog/2011/09/pagination-with-relnext-and-relprev)
* [Infinite scroll search-friendly recommendations](https://developers.google.com/search/blog/2014/02/infinite-scroll-search-friendly)
## Your category URLs are referenced in a sitemap
A sitemap is an XML file that tells search engine crawlers which pages are central to your site.
Having a sitemap is particularly recommended for large websites with many unrelated pages.
When deciding which pages to put in your sitemap,
a good rule is to include only your canonical pages.
If you're not using a content management system (CMS) that automatically generates a sitemap for you,
you can generate one based on your Algolia indices.
For more information, see:
* [Generate a sitemap from an Algolia index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index)
* [Learn about sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
## Your canonical pages have unique titles and descriptions
The `title` and `description` meta tags give search engine users important insight into the content of a search result and its relevance.
This information often determines which search engine result a person clicks on,
so it's crucial to use [high-quality titles and descriptions](https://developers.google.com/search/docs/appearance/title-link) when you build your web pages.
Algolia doesn't add meta tags to your pages.
You have to do that yourself using the tools you use to build your website.
## Your widget's markup is semantic
InstantSearch widgets provide a default semantic markup that should cover most cases,
but if you're using connectors to customize your UI,
make sure to properly use markup according to its meaning and purpose.
For example, use heading elements (`h1` to `h6`) for headings,
paragraph elements (`p`) for paragraphs,
list elements (`ul`, `ol`, or `dl`) for lists, and tables (`table`) for data tables.
Semantic markup helps search engines identify and categorize your content with minimal effort.
### Dos and don'ts
Do this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Not this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Because your site's are unique,
you need to customize the markup template for the hits or infinite hits widgets.
Here's a possible template for a product search result on an ecommerce website:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
While Algolia uses semantic markup (the content is isolated in an `article` element, a `header` section with the product name in an `h2` element, a product image with an `alt` attribute, and the main description in a `p` element),
you're missing some information details that could be useful for a search engine ("TV and home theater" is the product category, "69" is its price).
You can solve this by using structured data.
### Structured data
Structured data is a standardized format for providing information about a page and classifying its content. With structured data, you can give search engines insight into the meaning of your pages.
Here's how to add structured data to the previous example:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
This markup is now much more complete: it explicitly references what the content represents (product name, category, price, rating). You have also added content for search engines using `link` and `meta` tags.
Here are some examples of templates with structured data for InstantSearch widgets:
```html Breadcrumb theme={"system"}
```
Search engines support a variety of structured data, and you're encouraged to mark up your content with it.
For more information, see:
* [Web semantics](https://developers.google.com/search/blog/2012/07/on-web-semantics)
* [HTML elements reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
* [HTML standard](https://html.spec.whatwg.org)
* [Google-supported structured data](https://developers.google.com/search/docs/appearance/structured-data/search-gallery)
* [Introduction to structured data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
* [Schema.org](https://schema.org)
* [Structured data testing tool](https://developers.google.com/search/docs/appearance/structured-data)
## Your website is mobile-friendly
Due to the large number of users who browse the web on mobile,
search engines value [mobile-friendly websites](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) and only index content visible on a mobile device.
Therefore, your mobile and desktop sites must share the same content, structured data, title, and description meta tags.
## Your site is using a pre-rendering technique
Algolia can power a wide array of search experiences, but the most potent implementations often rely on client-side JavaScript.
That's why InstantSearch was built: it's a suite of search widgets compatible with the most popular frontend frameworks.
Not all search engine crawlers can [process JavaScript successfully or immediately](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics).
Fortunately, there are many ways around it.
### Server-side rendering (SSR)
This technique fetches your data and renders a JavaScript website on the server before sending it to the browser.
React InstantSearch and Vue InstantSearch support dynamic rendering.
For more information, see:
* [Server-side Rendering with Vue InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/vue)
* [Server-side Rendering with React InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/react).
# SEO checklist
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/seo/react
Every step you can take to make your React InstantSearch search experience SEO-friendly.
Show more
```
2. Each page can be accessed directly through a URL.
3. Each page URL has a canonical link to its primary page.
If your pagination occurs on scroll (as is the case with the infinite hits widget),
make sure you still provide a "show more" link that uses plain URLs.
Some search engines use the HTML `link` elements with attributes `rel="next"` and `rel="prev"` in the `` of your page to infer the relationship between component URLs in a paginated series.
These elements help but not all search engines use them for indexing.
For more information, see:
* [Pagination with rel="next" and rel="prev"](https://developers.google.com/search/blog/2011/09/pagination-with-relnext-and-relprev)
* [Infinite scroll search-friendly recommendations](https://developers.google.com/search/blog/2014/02/infinite-scroll-search-friendly)
## Your category URLs are referenced in a sitemap
A sitemap is an XML file that tells search engine crawlers which pages are central to your site.
Having a sitemap is particularly recommended for large websites with many unrelated pages.
When deciding which pages to put in your sitemap,
a good rule is to include only your canonical pages.
If you're not using a content management system (CMS) that automatically generates a sitemap for you,
you can generate one based on your Algolia indices.
For more information, see:
* [Generate a sitemap from an Algolia index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index)
* [Learn about sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
## Your canonical pages have unique titles and descriptions
The `title` and `description` meta tags give search engine users important insight into the content of a search result and its relevance.
This information often determines which search engine result a person clicks on,
so it's crucial to use [high-quality titles and descriptions](https://developers.google.com/search/docs/appearance/title-link) when you build your web pages.
Algolia doesn't add meta tags to your pages.
You have to do that yourself using the tools you use to build your website.
## Your widget's markup is semantic
InstantSearch widgets provide a default semantic markup that should cover most cases,
but if you're using connectors to customize your UI,
make sure to properly use markup according to its meaning and purpose.
For example, use heading elements (`h1` to `h6`) for headings,
paragraph elements (`p`) for paragraphs,
list elements (`ul`, `ol`, or `dl`) for lists, and tables (`table`) for data tables.
Semantic markup helps search engines identify and categorize your content with minimal effort.
### Dos and don'ts
Do this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Not this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Because your site's are unique,
you need to customize the markup template for the hits or infinite hits widgets.
Here's a possible template for a product search result on an ecommerce website:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
While Algolia uses semantic markup (the content is isolated in an `article` element, a `header` section with the product name in an `h2` element, a product image with an `alt` attribute, and the main description in a `p` element),
you're missing some information details that could be useful for a search engine ("TV and home theater" is the product category, "69" is its price).
You can solve this by using structured data.
### Structured data
Structured data is a standardized format for providing information about a page and classifying its content. With structured data, you can give search engines insight into the meaning of your pages.
Here's how to add structured data to the previous example:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
This markup is now much more complete: it explicitly references what the content represents (product name, category, price, rating). You have also added content for search engines using `link` and `meta` tags.
Here are some examples of templates with structured data for InstantSearch widgets:
```html Breadcrumb theme={"system"}
```
Search engines support a variety of structured data, and you're encouraged to mark up your content with it.
For more information, see:
* [Web semantics](https://developers.google.com/search/blog/2012/07/on-web-semantics)
* [HTML elements reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
* [HTML standard](https://html.spec.whatwg.org)
* [Google-supported structured data](https://developers.google.com/search/docs/appearance/structured-data/search-gallery)
* [Introduction to structured data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
* [Schema.org](https://schema.org)
* [Structured data testing tool](https://developers.google.com/search/docs/appearance/structured-data)
## Your website is mobile-friendly
Due to the large number of users who browse the web on mobile,
search engines value [mobile-friendly websites](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) and only index content visible on a mobile device.
Therefore, your mobile and desktop sites must share the same content, structured data, title, and description meta tags.
## Your site is using a pre-rendering technique
Algolia can power a wide array of search experiences, but the most potent implementations often rely on client-side JavaScript.
That's why InstantSearch was built: it's a suite of search widgets compatible with the most popular frontend frameworks.
Not all search engine crawlers can [process JavaScript successfully or immediately](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics).
Fortunately, there are many ways around it.
### Server-side rendering (SSR)
This technique fetches your data and renders a JavaScript website on the server before sending it to the browser.
React InstantSearch and Vue InstantSearch support dynamic rendering.
For more information, see:
* [Server-side Rendering with Vue InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/vue)
* [Server-side Rendering with React InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/react).
# SEO checklist
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/seo/vue
Every step you can take to make your Vue InstantSearch search experience SEO-friendly.
Show more
```
2. Each page can be accessed directly through a URL.
3. Each page URL has a canonical link to its primary page.
If your pagination occurs on scroll (as is the case with the infinite hits widget),
make sure you still provide a "show more" link that uses plain URLs.
Some search engines use the HTML `link` elements with attributes `rel="next"` and `rel="prev"` in the `` of your page to infer the relationship between component URLs in a paginated series.
These elements help but not all search engines use them for indexing.
For more information, see:
* [Pagination with rel="next" and rel="prev"](https://developers.google.com/search/blog/2011/09/pagination-with-relnext-and-relprev)
* [Infinite scroll search-friendly recommendations](https://developers.google.com/search/blog/2014/02/infinite-scroll-search-friendly)
## Your category URLs are referenced in a sitemap
A sitemap is an XML file that tells search engine crawlers which pages are central to your site.
Having a sitemap is particularly recommended for large websites with many unrelated pages.
When deciding which pages to put in your sitemap,
a good rule is to include only your canonical pages.
If you're not using a content management system (CMS) that automatically generates a sitemap for you,
you can generate one based on your Algolia indices.
For more information, see:
* [Generate a sitemap from an Algolia index](/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/tutorial/generate-sitemap-from-index)
* [Learn about sitemaps](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview)
## Your canonical pages have unique titles and descriptions
The `title` and `description` meta tags give search engine users important insight into the content of a search result and its relevance.
This information often determines which search engine result a person clicks on,
so it's crucial to use [high-quality titles and descriptions](https://developers.google.com/search/docs/appearance/title-link) when you build your web pages.
Algolia doesn't add meta tags to your pages.
You have to do that yourself using the tools you use to build your website.
## Your widget's markup is semantic
InstantSearch widgets provide a default semantic markup that should cover most cases,
but if you're using connectors to customize your UI,
make sure to properly use markup according to its meaning and purpose.
For example, use heading elements (`h1` to `h6`) for headings,
paragraph elements (`p`) for paragraphs,
list elements (`ul`, `ol`, or `dl`) for lists, and tables (`table`) for data tables.
Semantic markup helps search engines identify and categorize your content with minimal effort.
### Dos and don'ts
Do this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Not this:
```html HTML icon="code-xml" theme={"system"}
Heading
Body text.
More body text.
```
Because your site's are unique,
you need to customize the markup template for the hits or infinite hits widgets.
Here's a possible template for a product search result on an ecommerce website:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
While Algolia uses semantic markup (the content is isolated in an `article` element, a `header` section with the product name in an `h2` element, a product image with an `alt` attribute, and the main description in a `p` element),
you're missing some information details that could be useful for a search engine ("TV and home theater" is the product category, "69" is its price).
You can solve this by using structured data.
### Structured data
Structured data is a standardized format for providing information about a page and classifying its content. With structured data, you can give search engines insight into the meaning of your pages.
Here's how to add structured data to the previous example:
```html HTML icon="code-xml" theme={"system"}
Google Chromecast Ultra
TV and home theater
Enjoy all your favorite movies, shows, games, and music plus the latest 4K content with Chromecast Ultra, a streaming device that…
```
This markup is now much more complete: it explicitly references what the content represents (product name, category, price, rating). You have also added content for search engines using `link` and `meta` tags.
Here are some examples of templates with structured data for InstantSearch widgets:
```html Breadcrumb theme={"system"}
```
Search engines support a variety of structured data, and you're encouraged to mark up your content with it.
For more information, see:
* [Web semantics](https://developers.google.com/search/blog/2012/07/on-web-semantics)
* [HTML elements reference](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
* [HTML standard](https://html.spec.whatwg.org)
* [Google-supported structured data](https://developers.google.com/search/docs/appearance/structured-data/search-gallery)
* [Introduction to structured data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
* [Schema.org](https://schema.org)
* [Structured data testing tool](https://developers.google.com/search/docs/appearance/structured-data)
## Your website is mobile-friendly
Due to the large number of users who browse the web on mobile,
search engines value [mobile-friendly websites](https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing) and only index content visible on a mobile device.
Therefore, your mobile and desktop sites must share the same content, structured data, title, and description meta tags.
## Your site is using a pre-rendering technique
Algolia can power a wide array of search experiences, but the most potent implementations often rely on client-side JavaScript.
That's why InstantSearch was built: it's a suite of search widgets compatible with the most popular frontend frameworks.
Not all search engine crawlers can [process JavaScript successfully or immediately](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics).
Fortunately, there are many ways around it.
### Server-side rendering (SSR)
This technique fetches your data and renders a JavaScript website on the server before sending it to the browser.
React InstantSearch and Vue InstantSearch support dynamic rendering.
For more information, see:
* [Server-side Rendering with Vue InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/vue)
* [Server-side Rendering with React InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/react).
# UI Design Kit (Figma)
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/ui-kit/js
Design best-in-class search and discovery experiences with Algolia's UI Design Kit.
If you have to start from zero, designing a complete and delightful user experience can be overwhelming.
The free and open source UI Design Kit (Figma) includes screens and components you can use as starting point for designing your own ecommerce experiences.
## How to use the UI kit?
If you want to see all components and pages:
1. Go to the [Figma community page](https://www.figma.com/community/file/981543186947734892) and click **Duplicate** to add the UI kit to your Figma account.
2. Open the UI kit in Figma.
3. Go to the **Get started: Search & Discovery** page to see how the resources are organized and learn how you can customize them to match your needs and brand guidelines.
4. Go to the **Brand** page and change the basic design elements: logos, colors, typography, icons, and screens.
## Guiding principles
The UI kit follows these principles:
* **Mobile-first design** with rich experiences on larger device screens
* **Neutral UI and modular components** that are customizable to your brand
* **Works with all devices** to support today's user journeys across multiple devices
* **Unified search results and browse pages** for a unique and seamless experience
* **Incorporate UI/UX best practices** from Algolia's expertise backed by research and guidelines from industry leaders, for example,
[Google's UX playbook](https://services.google.com/fh/files/events/pdf_retail_ux_playbook.pdf), [Baymard Institute](https://baymard.com/blog)
* **Inspired by Atomic Design**. Putting components at the center of the design system enables a smooth implementation in modern component-centric frameworks like React or Vue.js.
## What's included in the UI kit?
The Search and Discovery UI Kit includes mobile-first responsive pages for:
* Homepage
* Multi-category autocomplete
* Search and browse product listing
* and sort panel
* Product detail with recommendations
### Homepage
A dynamic home page allows shoppers to engage with navigation and search and discover personalized content according to their preferences, navigation history across devices, and trending searches.
### Global navigation header
All pages show a global navigation header with access to the main menu and search.
Depending on your needs, you can show the search as:
* **Search button:** suitable for compact headers. This option provides direct access to search and the shopping cart but relies on the menu for everything else, such as the user account or a store finder.
* **Prominent search box:** suitable for web stores with large inventories or complex categories. This option shows a search box on all pages and provides direct access to extra functions, such as the user account or a store finder.
### Multi-category autocomplete
Build an all-in-one federated search experience with a full-page autocomplete overlay available in three main states:
* Empty query state
* Search state
* No search results state
#### Empty query state
As soon as users interact with the search box, the empty query state offers them the opportunity to:
* Resume an earlier search, including searches performed on a different device.
* Discover new products by selecting trending searches or selecting a promoted content tile.
* Access other resources such as a store finder or an FAQ.
The search box at the top optimizes the display of the search results for the limited space once the keyboard opens.
See the [Autocomplete documentation](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete) for more information.
The **image search** feature is increasingly popular on fashion and retail ecommerce websites. Depending on how you implement image search, users can explore similar-looking products or find a specific product based on its barcode or QR code. For example, users can scan a product's code in a retail store and check a description of it and other variations online.
#### Search state
With autocomplete, users get a mix of results across different content types at every keystroke.
The results adapt to their but also earlier searches.
Users can either jump to a product or article they find interesting or continue to a product listing page after selecting a query term, a brand, or a category.
### Search and browse product listing
Users expect the same experience, whether they explore products on the search results page or browse products from a category page. That's why both pages follow the same structure and incorporate the same capabilities.
#### Search results page
Give your users access to all relevant content from multiple categories in a single view, such as blog articles or shopping guides.
To enrich the user experience, you can **display a promotion banner in the search results**. Depending on the context, you can show banners, based on search queries, current category, or set of filters.
For example, you can display:
* A visual banner promoting a given brand on queries containing that brand name
* A banner "Learn about Free Delivery and Return policy" on the queries "delivery" or "return"
* A "Get to the Size" guide on query "size" or "size help"
#### Browse page
**Breadcrumbs** are as crucial for users as for SEO. They enable users to:
* Understand where they are in your website's content hierarchy
* Get back to the parent pages with a single tap or click
#### Magazine layout
Like a traditional paper magazine, the magazine layout complements product listing tiles with rich content tiles that show editorial or promotional content.
Mixing content types increases diversity, which helps to break up long product listings.
The magazine layout also exposes more content to the users,
which helps with their exploration and evaluation,
leading them to buy quicker.
You can promote brands by displaying:
* Promoted products
* Promoted rich content
* Suggested filters or captured user preferences, such as size or color
### Filter and sort panel
As a vital part of the product listing page, the filter and sort panel shows the available refinements for the current search results.
At the same time, users have complete control to sort and refine the listed products.
### Product detail page with recommendations
Showing recommended products is a great way to increase the average order value by bringing more items to users' attention.
To learn more, see [Algolia Recommend](/doc/guides/algolia-recommend/overview).
#### Searchable reviews
Shoppers rely more than ever on other shoppers' reviews and inputs. Users can search through reviews using keywords and filters, for example, rating, user profile, or language.
To implement searchable reviews,
add the reviews as a new and use InstantSearch to implement the frontend experience.
#### Related items
Maximize and catalog exposure by using collaborative filtering recommendations.
#### Frequently bought together
Increase cross-selling by showing products that complement the current selection.
# UI Design Kit (Figma)
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/ui-kit/react
Design best-in-class search and discovery experiences with Algolia's UI Design Kit.
If you have to start from zero, designing a complete and delightful user experience can be overwhelming.
The free and open source UI Design Kit (Figma) includes screens and components you can use as starting point for designing your own ecommerce experiences.
## How to use the UI kit?
If you want to see all components and pages:
1. Go to the [Figma community page](https://www.figma.com/community/file/981543186947734892) and click **Duplicate** to add the UI kit to your Figma account.
2. Open the UI kit in Figma.
3. Go to the **Get started: Search & Discovery** page to see how the resources are organized and learn how you can customize them to match your needs and brand guidelines.
4. Go to the **Brand** page and change the basic design elements: logos, colors, typography, icons, and screens.
## Guiding principles
The UI kit follows these principles:
* **Mobile-first design** with rich experiences on larger device screens
* **Neutral UI and modular components** that are customizable to your brand
* **Works with all devices** to support today's user journeys across multiple devices
* **Unified search results and browse pages** for a unique and seamless experience
* **Incorporate UI/UX best practices** from Algolia's expertise backed by research and guidelines from industry leaders, for example,
[Google's UX playbook](https://services.google.com/fh/files/events/pdf_retail_ux_playbook.pdf), [Baymard Institute](https://baymard.com/blog)
* **Inspired by Atomic Design**. Putting components at the center of the design system enables a smooth implementation in modern component-centric frameworks like React or Vue.js.
## What's included in the UI kit?
The Search and Discovery UI Kit includes mobile-first responsive pages for:
* Homepage
* Multi-category autocomplete
* Search and browse product listing
* and sort panel
* Product detail with recommendations
### Homepage
A dynamic home page allows shoppers to engage with navigation and search and discover personalized content according to their preferences, navigation history across devices, and trending searches.
### Global navigation header
All pages show a global navigation header with access to the main menu and search.
Depending on your needs, you can show the search as:
* **Search button:** suitable for compact headers. This option provides direct access to search and the shopping cart but relies on the menu for everything else, such as the user account or a store finder.
* **Prominent search box:** suitable for web stores with large inventories or complex categories. This option shows a search box on all pages and provides direct access to extra functions, such as the user account or a store finder.
### Multi-category autocomplete
Build an all-in-one federated search experience with a full-page autocomplete overlay available in three main states:
* Empty query state
* Search state
* No search results state
#### Empty query state
As soon as users interact with the search box, the empty query state offers them the opportunity to:
* Resume an earlier search, including searches performed on a different device.
* Discover new products by selecting trending searches or selecting a promoted content tile.
* Access other resources such as a store finder or an FAQ.
The search box at the top optimizes the display of the search results for the limited space once the keyboard opens.
See the [Autocomplete documentation](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete) for more information.
The **image search** feature is increasingly popular on fashion and retail ecommerce websites. Depending on how you implement image search, users can explore similar-looking products or find a specific product based on its barcode or QR code. For example, users can scan a product's code in a retail store and check a description of it and other variations online.
#### Search state
With autocomplete, users get a mix of results across different content types at every keystroke.
The results adapt to their but also earlier searches.
Users can either jump to a product or article they find interesting or continue to a product listing page after selecting a query term, a brand, or a category.
### Search and browse product listing
Users expect the same experience, whether they explore products on the search results page or browse products from a category page. That's why both pages follow the same structure and incorporate the same capabilities.
#### Search results page
Give your users access to all relevant content from multiple categories in a single view, such as blog articles or shopping guides.
To enrich the user experience, you can **display a promotion banner in the search results**. Depending on the context, you can show banners, based on search queries, current category, or set of filters.
For example, you can display:
* A visual banner promoting a given brand on queries containing that brand name
* A banner "Learn about Free Delivery and Return policy" on the queries "delivery" or "return"
* A "Get to the Size" guide on query "size" or "size help"
#### Browse page
**Breadcrumbs** are as crucial for users as for SEO. They enable users to:
* Understand where they are in your website's content hierarchy
* Get back to the parent pages with a single tap or click
#### Magazine layout
Like a traditional paper magazine, the magazine layout complements product listing tiles with rich content tiles that show editorial or promotional content.
Mixing content types increases diversity, which helps to break up long product listings.
The magazine layout also exposes more content to the users,
which helps with their exploration and evaluation,
leading them to buy quicker.
You can promote brands by displaying:
* Promoted products
* Promoted rich content
* Suggested filters or captured user preferences, such as size or color
### Filter and sort panel
As a vital part of the product listing page, the filter and sort panel shows the available refinements for the current search results.
At the same time, users have complete control to sort and refine the listed products.
### Product detail page with recommendations
Showing recommended products is a great way to increase the average order value by bringing more items to users' attention.
To learn more, see [Algolia Recommend](/doc/guides/algolia-recommend/overview).
#### Searchable reviews
Shoppers rely more than ever on other shoppers' reviews and inputs. Users can search through reviews using keywords and filters, for example, rating, user profile, or language.
To implement searchable reviews,
add the reviews as a new and use InstantSearch to implement the frontend experience.
#### Related items
Maximize and catalog exposure by using collaborative filtering recommendations.
#### Frequently bought together
Increase cross-selling by showing products that complement the current selection.
# UI Design Kit (Figma)
Source: https://www.algolia.com/doc/guides/building-search-ui/resources/ui-kit/vue
Design best-in-class search and discovery experiences with Algolia's UI Design Kit.
If you have to start from zero, designing a complete and delightful user experience can be overwhelming.
The free and open source UI Design Kit (Figma) includes screens and components you can use as starting point for designing your own ecommerce experiences.
## How to use the UI kit?
If you want to see all components and pages:
1. Go to the [Figma community page](https://www.figma.com/community/file/981543186947734892) and click **Duplicate** to add the UI kit to your Figma account.
2. Open the UI kit in Figma.
3. Go to the **Get started: Search & Discovery** page to see how the resources are organized and learn how you can customize them to match your needs and brand guidelines.
4. Go to the **Brand** page and change the basic design elements: logos, colors, typography, icons, and screens.
## Guiding principles
The UI kit follows these principles:
* **Mobile-first design** with rich experiences on larger device screens
* **Neutral UI and modular components** that are customizable to your brand
* **Works with all devices** to support today's user journeys across multiple devices
* **Unified search results and browse pages** for a unique and seamless experience
* **Incorporate UI/UX best practices** from Algolia's expertise backed by research and guidelines from industry leaders, for example,
[Google's UX playbook](https://services.google.com/fh/files/events/pdf_retail_ux_playbook.pdf), [Baymard Institute](https://baymard.com/blog)
* **Inspired by Atomic Design**. Putting components at the center of the design system enables a smooth implementation in modern component-centric frameworks like React or Vue.js.
## What's included in the UI kit?
The Search and Discovery UI Kit includes mobile-first responsive pages for:
* Homepage
* Multi-category autocomplete
* Search and browse product listing
* and sort panel
* Product detail with recommendations
### Homepage
A dynamic home page allows shoppers to engage with navigation and search and discover personalized content according to their preferences, navigation history across devices, and trending searches.
### Global navigation header
All pages show a global navigation header with access to the main menu and search.
Depending on your needs, you can show the search as:
* **Search button:** suitable for compact headers. This option provides direct access to search and the shopping cart but relies on the menu for everything else, such as the user account or a store finder.
* **Prominent search box:** suitable for web stores with large inventories or complex categories. This option shows a search box on all pages and provides direct access to extra functions, such as the user account or a store finder.
### Multi-category autocomplete
Build an all-in-one federated search experience with a full-page autocomplete overlay available in three main states:
* Empty query state
* Search state
* No search results state
#### Empty query state
As soon as users interact with the search box, the empty query state offers them the opportunity to:
* Resume an earlier search, including searches performed on a different device.
* Discover new products by selecting trending searches or selecting a promoted content tile.
* Access other resources such as a store finder or an FAQ.
The search box at the top optimizes the display of the search results for the limited space once the keyboard opens.
See the [Autocomplete documentation](/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete) for more information.
The **image search** feature is increasingly popular on fashion and retail ecommerce websites. Depending on how you implement image search, users can explore similar-looking products or find a specific product based on its barcode or QR code. For example, users can scan a product's code in a retail store and check a description of it and other variations online.
#### Search state
With autocomplete, users get a mix of results across different content types at every keystroke.
The results adapt to their but also earlier searches.
Users can either jump to a product or article they find interesting or continue to a product listing page after selecting a query term, a brand, or a category.
### Search and browse product listing
Users expect the same experience, whether they explore products on the search results page or browse products from a category page. That's why both pages follow the same structure and incorporate the same capabilities.
#### Search results page
Give your users access to all relevant content from multiple categories in a single view, such as blog articles or shopping guides.
To enrich the user experience, you can **display a promotion banner in the search results**. Depending on the context, you can show banners, based on search queries, current category, or set of filters.
For example, you can display:
* A visual banner promoting a given brand on queries containing that brand name
* A banner "Learn about Free Delivery and Return policy" on the queries "delivery" or "return"
* A "Get to the Size" guide on query "size" or "size help"
#### Browse page
**Breadcrumbs** are as crucial for users as for SEO. They enable users to:
* Understand where they are in your website's content hierarchy
* Get back to the parent pages with a single tap or click
#### Magazine layout
Like a traditional paper magazine, the magazine layout complements product listing tiles with rich content tiles that show editorial or promotional content.
Mixing content types increases diversity, which helps to break up long product listings.
The magazine layout also exposes more content to the users,
which helps with their exploration and evaluation,
leading them to buy quicker.
You can promote brands by displaying:
* Promoted products
* Promoted rich content
* Suggested filters or captured user preferences, such as size or color
### Filter and sort panel
As a vital part of the product listing page, the filter and sort panel shows the available refinements for the current search results.
At the same time, users have complete control to sort and refine the listed products.
### Product detail page with recommendations
Showing recommended products is a great way to increase the average order value by bringing more items to users' attention.
To learn more, see [Algolia Recommend](/doc/guides/algolia-recommend/overview).
#### Searchable reviews
Shoppers rely more than ever on other shoppers' reviews and inputs. Users can search through reviews using keywords and filters, for example, rating, user profile, or language.
To implement searchable reviews,
add the reviews as a new and use InstantSearch to implement the frontend experience.
#### Related items
Maximize and catalog exposure by using collaborative filtering recommendations.
#### Frequently bought together
Increase cross-selling by showing products that complement the current selection.
# Styling and theming
Source: https://www.algolia.com/doc/guides/building-search-ui/styling/react
Learn how to customize React InstantSearch appearance using CSS variables and themes.
InstantSearch.css provides a set of themes to style your search experiences with minimal effort.
The themes are designed to be customizable through [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) (CSS custom properties), allowing you to match your brand identity while maintaining the core layout and features.
CSS variables are currently only supported for [`chat`](/doc/api-reference/widgets/chat/react) and [`autocomplete`](/doc/api-reference/widgets/autocomplete/react) widgets.
## Loading themes
InstantSearch.css includes two main themes:
* **`satellite.css`** - A complete theme with modern styling
* **`algolia.css`** - Algolia-branded theme
Both themes include `reset.css`, a minimal reset theme that provides base styles.
### Use a CDN
The themes are available on jsDelivr:
```html HTML icon=code-xml theme={"system"}
```
### Using npm
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
Then import it in your React app:
```jsx React icon=code theme={"system"}
// Satellite theme
import 'instantsearch.css/themes/satellite.css';
// Or Algolia theme
// import 'instantsearch.css/themes/algolia.css';
```
### Loading individual component styles
If you only need specific components, you can import their styles individually to reduce bundle size. This is currently available for `chat` and `autocomplete` components:
```jsx React icon=code theme={"system"}
// Import only chat component styles
import 'instantsearch.css/components/chat.css';
// Or import the minified version
// import 'instantsearch.css/components/chat-min.css';
```
```jsx React icon=code theme={"system"}
// Import only autocomplete component styles
import 'instantsearch.css/components/autocomplete.css';
// Or import the minified version
// import 'instantsearch.css/components/autocomplete-min.css';
```
Individual component styles include all CSS variables and theming capabilities of the full themes.
You can still customize them using the same CSS variable overrides.
## Development tools
InstantSearch.css provides developer tools that help you customize CSS variables visually in your browser. The developer tools display an interactive panel where you can:
* Modify CSS variables in real-time
* Toggle between theme modes (Auto, Light, Dark)
* Export your customized variables to clipboard
### Use the developer tools
Import the developer tools in your React app:
```jsx React icon=code theme={"system"}
import 'instantsearch.css/devtools/inject';
```
The developer tools panel will appear in the bottom-left corner of your browser, allowing you to interactively customize colors, spacing, typography, and more.
The developer tools are intended for development only.
Don't include them in your production build.
### Programmatic usage
You can also create the developer tools programmatically with custom options:
```jsx React icon=code theme={"system"}
import { createInstantSearchDevtools } from 'instantsearch.css/devtools';
createInstantSearchDevtools({
// Optional: custom container
container: document.getElementById('devtools'),
// Optional: custom styling
style: {
position: 'fixed',
top: '1rem',
right: '1rem',
zIndex: '1000',
},
});
```
## Customization example
You can override any CSS variable to match your brand:
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 124, 58, 237;
/* Spacing */
--ais-spacing-factor: 1.5;
}
```
## Dark mode
The themes support dark mode in two ways:
### Using data-theme attribute
```jsx React icon=code theme={"system"}
function App() {
return (
{/* Your widgets */}
);
}
```
### Using CSS class
```jsx React icon=code theme={"system"}
function App() {
return (
{/* Your widgets */}
);
}
```
Both approaches automatically adjust the following variables:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
/* Text colors */
--ais-text-color-rgb: 255, 255, 255;
--ais-primary-color-rgb: 110, 160, 255;
--ais-muted-color-rgb: 190, 190, 190;
--ais-button-text-color-rgb: 255, 255, 255;
/* Border color */
--ais-border-color-rgb: 100, 100, 100;
/* Background color */
--ais-background-color-rgb: 38, 38, 38;
/* Shadow color */
--ais-shadow-color-rgb: 0, 0, 0;
}
```
### Custom dark mode colors
You can override dark mode colors:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
--ais-primary-color-rgb: 147, 197, 253;
--ais-background-color-rgb: 15, 23, 42;
}
```
## Complete variable reference
For a complete list of all available CSS variables with detailed descriptions, see the tables below:
### Text colors
| Variable | Default | Description |
| - | - | - |
| `--ais-text-color-rgb` | `38, 38, 38` | RGB values for the base text color |
| `--ais-text-color-alpha` | `1` | Alpha value for the base text color |
| `--ais-primary-color-rgb` | `30, 89, 255` | RGB values for the accent color |
| `--ais-primary-color-alpha` | `1` | Alpha value for the accent color |
| `--ais-muted-color-rgb` | `82, 82, 82` | RGB values for muted/secondary text |
| `--ais-muted-color-alpha` | `1` | Alpha value for muted/secondary text |
| `--ais-button-text-color-rgb` | `255, 255, 255` | RGB values for button text |
| `--ais-button-text-color-alpha` | `1` | Alpha value for button text |
### Border, background, and shadow colors
| Variable | Default | Description |
| - | - | - |
| `--ais-border-color-rgb` | `150, 150, 150` | RGB values for borders |
| `--ais-border-color-alpha` | `1` | Alpha value for borders |
| `--ais-background-color-rgb` | `255, 255, 255` | RGB values for backgrounds |
| `--ais-background-color-alpha` | `1` | Alpha value for backgrounds |
| `--ais-shadow-color-rgb` | `23, 23, 23` | RGB values for shadow colors |
### Spacing
| Variable | Default | Description |
| - | - | - |
| `--ais-base-unit` | `16` | Base unit value to calculate font sizes and spacing |
| `--ais-spacing-factor` | `1` (tap: `1.2`) | Multiplier for spacing calculations |
| `--ais-spacing` | calculated | Computed spacing value |
### Typography
| Variable | Default | Description |
| - | - | - |
| `--ais-font-size` | `16px` | Base font size derived from `--ais-base-unit` |
| `--ais-font-weight-medium` | `500` | Medium font weight |
| `--ais-font-weight-semibold` | `600` | Semibold font weight |
| `--ais-font-weight-bold` | `700` | Bold font weight |
### Icons
| Variable | Default | Description |
| - | - | - |
| `--ais-icon-size` | `20px` | Icon size |
| `--ais-icon-stroke-width` | `1.6` | Icon stroke width |
### Shadows
| Variable | Default | Description |
| - | - | - |
| `--ais-shadow-sm` | `0px 0px 0px 1px rgba(..., 0.05), 0px 1px 3px 0px rgba(..., 0.25)` | Small shadow for subtle depth |
| `--ais-shadow-md` | `0px 0px 0px 1px rgba(..., 0.05), 0px 4px 8px -2px rgba(..., 0.25)` | Medium shadow for cards |
| `--ais-shadow-lg` | `0 0 0 1px rgba(..., 0.05), 0 6px 16px -4px rgba(..., 0.15)` | Large shadow for modals |
### Border radius
| Variable | Default | Description |
| - | - | - |
| `--ais-border-radius-sm` | `4px` | Small border radius |
| `--ais-border-radius-md` | `8px` | Medium border radius |
| `--ais-border-radius-lg` | `16px` | Large border radius |
| `--ais-border-radius-full` | `9999px` | Full border radius for pills |
### Transitions
| Variable | Default | Description |
| - | - | - |
| `--ais-transition-duration` | `0.3s` | Duration for transitions |
| `--ais-transition-timing-function` | `cubic-bezier(0.4, 0, 0.2, 1)` | Timing function for transitions |
### Z-index
| Variable | Default | Description |
| - | - | - |
| `--ais-z-index-chat` | `9999` | Z-index for chat component |
| `--ais-z-index-autocomplete` | `10000` | Z-index for autocomplete component |
### Component-specific
#### Chat component
| Variable | Default | Description |
| - | - | - |
| `--ais-chat-width` | `22.5rem` | Default width of chat panel |
| `--ais-chat-height` | `70%` | Default height of chat panel |
| `--ais-chat-maximized-width` | `70%` | Width when maximized |
| `--ais-chat-maximized-height` | `100%` | Height when maximized |
| `--ais-chat-margin` | `1.5rem` | Margin around chat panel |
| `--ais-chat-carousel-item-width` | `calc(var(--ais-spacing) * 10)` | Width of carousel items |
#### Autocomplete component
| Variable | Default | Description |
| - | - | - |
| `--ais-autocomplete-search-input-height` | `44px` | Height of search input |
| `--ais-autocomplete-panel-max-height` | `650px` | Maximum height of autocomplete panel |
# Styling and theming
Source: https://www.algolia.com/doc/guides/building-search-ui/styling/vue
Learn how to customize Vue InstantSearch appearance using CSS variables and themes.
InstantSearch.css provides a set of themes to style your search experiences with minimal effort.
The themes are designed to be customizable through [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) (CSS custom properties), allowing you to match your brand identity while maintaining the core layout and features.
CSS variables are currently only supported for the [`autocomplete`](/doc/api-reference/widgets/autocomplete/vue) widget.
## Loading themes
InstantSearch.css includes two main themes:
* **`satellite.css`** - A complete theme with modern styling
* **`algolia.css`** - Algolia-branded theme
Both themes include `reset.css`, a minimal reset theme that provides base styles.
### Use a CDN
The themes are available on jsDelivr:
```html HTML icon=code-xml theme={"system"}
```
### Using npm
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
Then import it in your Vue app:
```js JavaScript icon=code theme={"system"}
// In your main.js or App.vue
// Satellite theme
import 'instantsearch.css/themes/satellite.css';
// Or Algolia theme
// import 'instantsearch.css/themes/algolia.css';
```
### Loading individual component styles
If you only need specific components, you can import their styles individually to reduce bundle size. This is currently available for the `autocomplete` component:
```js JavaScript icon=code theme={"system"}
// Import only autocomplete component styles
import 'instantsearch.css/components/autocomplete.css';
// Or import the minified version
// import 'instantsearch.css/components/autocomplete-min.css';
```
Individual component styles include all CSS variables and theming capabilities of the full themes.
You can still customize them using the same CSS variable overrides.
## Development tools
InstantSearch.css provides developer tools that help you customize CSS variables visually in your browser. The developer tools display an interactive panel where you can:
* Modify CSS variables in real-time
* Toggle between theme modes (Auto, Light, Dark)
* Export your customized variables to clipboard
### Use the developer tools
Import the developer tools in your Vue app:
```js JavaScript icon=code theme={"system"}
// In your main.js or App.vue
import 'instantsearch.css/devtools/inject';
```
The developer tools panel will appear in the bottom-left corner of your browser, allowing you to interactively customize colors, spacing, typography, and more.
The developer tools are intended for development only.
Don't include them in your production build.
### Programmatic usage
You can also create the developer tools programmatically with custom options:
```js JavaScript icon=code theme={"system"}
import { createInstantSearchDevtools } from 'instantsearch.css/devtools';
createInstantSearchDevtools({
// Optional: custom container
container: document.getElementById('devtools'),
// Optional: custom styling
style: {
position: 'fixed',
top: '1rem',
right: '1rem',
zIndex: '1000',
},
});
```
## Customization example
You can override any CSS variable to match your brand:
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 124, 58, 237;
/* Spacing */
--ais-spacing-factor: 1.5;
}
```
## Dark mode
The themes support dark mode in two ways:
### Using data-theme attribute
```vue Vue icon=code theme={"system"}
```
### Using CSS class
```vue Vue icon=code theme={"system"}
```
Both approaches automatically adjust the following variables:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
/* Text colors */
--ais-text-color-rgb: 255, 255, 255;
--ais-primary-color-rgb: 110, 160, 255;
--ais-muted-color-rgb: 190, 190, 190;
--ais-button-text-color-rgb: 255, 255, 255;
/* Border color */
--ais-border-color-rgb: 100, 100, 100;
/* Background color */
--ais-background-color-rgb: 38, 38, 38;
/* Shadow color */
--ais-shadow-color-rgb: 0, 0, 0;
}
```
### Custom dark mode colors
You can override dark mode colors:
```css CSS icon=paintbrush theme={"system"}
:root[data-theme='dark'],
.dark {
--ais-primary-color-rgb: 147, 197, 253;
--ais-background-color-rgb: 15, 23, 42;
}
```
## Complete variable reference
For a complete list of all available CSS variables with detailed descriptions, see the tables below:
### Text colors
| Variable | Default | Description |
| - | - | - |
| `--ais-text-color-rgb` | `38, 38, 38` | RGB values for the base text color |
| `--ais-text-color-alpha` | `1` | Alpha value for the base text color |
| `--ais-primary-color-rgb` | `30, 89, 255` | RGB values for the accent color |
| `--ais-primary-color-alpha` | `1` | Alpha value for the accent color |
| `--ais-muted-color-rgb` | `82, 82, 82` | RGB values for muted/secondary text |
| `--ais-muted-color-alpha` | `1` | Alpha value for muted/secondary text |
| `--ais-button-text-color-rgb` | `255, 255, 255` | RGB values for button text |
| `--ais-button-text-color-alpha` | `1` | Alpha value for button text |
### Border, background, and shadow colors
| Variable | Default | Description |
| - | - | - |
| `--ais-border-color-rgb` | `150, 150, 150` | RGB values for borders |
| `--ais-border-color-alpha` | `1` | Alpha value for borders |
| `--ais-background-color-rgb` | `255, 255, 255` | RGB values for backgrounds |
| `--ais-background-color-alpha` | `1` | Alpha value for backgrounds |
| `--ais-shadow-color-rgb` | `23, 23, 23` | RGB values for shadow colors |
### Spacing
| Variable | Default | Description |
| - | - | - |
| `--ais-base-unit` | `16` | Base unit value to calculate font sizes and spacing |
| `--ais-spacing-factor` | `1` (tap: `1.2`) | Multiplier for spacing calculations |
| `--ais-spacing` | calculated | Computed spacing value |
### Typography
| Variable | Default | Description |
| - | - | - |
| `--ais-font-size` | `16px` | Base font size derived from `--ais-base-unit` |
| `--ais-font-weight-medium` | `500` | Medium font weight |
| `--ais-font-weight-semibold` | `600` | Semibold font weight |
| `--ais-font-weight-bold` | `700` | Bold font weight |
### Icons
| Variable | Default | Description |
| - | - | - |
| `--ais-icon-size` | `20px` | Icon size |
| `--ais-icon-stroke-width` | `1.6` | Icon stroke width |
### Shadows
| Variable | Default | Description |
| - | - | - |
| `--ais-shadow-sm` | `0px 0px 0px 1px rgba(..., 0.05), 0px 1px 3px 0px rgba(..., 0.25)` | Small shadow for subtle depth |
| `--ais-shadow-md` | `0px 0px 0px 1px rgba(..., 0.05), 0px 4px 8px -2px rgba(..., 0.25)` | Medium shadow for cards |
| `--ais-shadow-lg` | `0 0 0 1px rgba(..., 0.05), 0 6px 16px -4px rgba(..., 0.15)` | Large shadow for modals |
### Border radius
| Variable | Default | Description |
| - | - | - |
| `--ais-border-radius-sm` | `4px` | Small border radius |
| `--ais-border-radius-md` | `8px` | Medium border radius |
| `--ais-border-radius-lg` | `16px` | Large border radius |
| `--ais-border-radius-full` | `9999px` | Full border radius for pills |
### Transitions
| Variable | Default | Description |
| - | - | - |
| `--ais-transition-duration` | `0.3s` | Duration for transitions |
| `--ais-transition-timing-function` | `cubic-bezier(0.4, 0, 0.2, 1)` | Timing function for transitions |
### Z-index
| Variable | Default | Description |
| - | - | - |
| `--ais-z-index-chat` | `9999` | Z-index for chat component |
| `--ais-z-index-autocomplete` | `10000` | Z-index for autocomplete component |
### Component-specific
#### Chat component
| Variable | Default | Description |
| - | - | - |
| `--ais-chat-width` | `22.5rem` | Default width of chat panel |
| `--ais-chat-height` | `70%` | Default height of chat panel |
| `--ais-chat-maximized-width` | `70%` | Width when maximized |
| `--ais-chat-maximized-height` | `100%` | Height when maximized |
| `--ais-chat-margin` | `1.5rem` | Margin around chat panel |
| `--ais-chat-carousel-item-width` | `calc(var(--ais-spacing) * 10)` | Width of carousel items |
#### Autocomplete component
| Variable | Default | Description |
| - | - | - |
| `--ais-autocomplete-search-input-height` | `44px` | Height of search input |
| `--ais-autocomplete-panel-max-height` | `650px` | Maximum height of autocomplete panel |
# Federated two-column autocomplete
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/federated/react
Build a two-column autocomplete with recent searches, Query Suggestions, and product previews.
This example builds on the [`Autocomplete`](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/react) widget guide.
It creates a federated autocomplete experience with recent searches and Query Suggestions on the left, and product previews on the right.
Run and edit the federated autocomplete example in CodeSandbox.
Browse the source code for the federated autocomplete example on GitHub.
The `panelComponent` arranges the sources into two columns, and each source brings its own header and item components:
```jsx React icon=code expandable theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
InstantSearch,
Autocomplete,
Highlight,
} from "react-instantsearch";
import "instantsearch.css/themes/satellite.css";
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76",
);
// Right column: product previews with image, name, price, and highlight
function ProductItem({ item }) {
return (
),
itemComponent: ProductItem,
},
]}
// Access built-in sources by their element names and index sources by index name.
panelComponent={({ elements }) => (
{elements.recent}
{elements.suggestions}
{elements["instant_search"]}
)}
/>
);
}
```
Add CSS for the columns, product rows, and state-specific elements.
The [example style sheet](https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch/autocomplete/src/App.css) includes classes for:
* Two-column grids
* Product rows
* Shortcut cards
* "See all" and no-results states
## Add a shortcuts panel
You can show promotional cards or shortcuts when the query is empty.
Define them as static data and render them in the `panelComponent`.
When users type a query, show product previews instead:
```jsx React icon=code expandable theme={"system"}
// Quick-access cards shown in the empty state.
const QUICK_ACCESS = [
{
title: "Spring sale",
subtitle: "up to 60% off",
image: "https://picsum.photos/seed/spring-sale/300/200",
href: "#",
},
{
title: "New collection",
subtitle: "spring / summer",
image: "https://picsum.photos/seed/new-collection/300/200",
href: "#",
},
];
panelComponent={({ elements, indices }) => {
const products = indices.find(
(index) => index.indexName === "instant_search",
);
const isEmptyQuery = products?.results?.query === "";
return (
);
}}
```
For dynamic cards, return custom data from an [Algolia rule](/doc/guides/managing-results/rules/rules-overview).
In the `panelComponent`, read the data from `results.userData` for the products index instead of using a static array.
## Add a category suggestion
To show a category suggestion in the left column, find the products index and read the top product's `categories` from `results.hits[0]` in the `panelComponent`.
Guard against a missing or empty `categories` attribute, then render the category after the suggestions:
```jsx React icon=code theme={"system"}
const products = indices.find(
(index) => index.indexName === "instant_search",
);
// Derive a category from the top product, if any.
const categories = products?.results?.hits?.[0]?.categories ?? [];
// In the left column, after the suggestions:
{elements.suggestions}
{categories.length > 0 && (
{categories.join(" › ")}
)}
```
## Show a "See all" link
The index source doesn't have a `footerComponent`.
To show a **See all** link with the total number of matching results,
read the products index's `results.nbHits` in the `panelComponent` and render the link after the product previews:
```jsx React icon=code theme={"system"}
const products = indices.find(
(index) => index.indexName === "instant_search",
);
const nbHits = products?.results?.nbHits ?? 0;
// In the right column of the panelComponent, after the product elements:
{elements["instant_search"]}
{nbHits > 5 && (
See all {nbHits.toLocaleString()} results
)}
```
## Show the no-results state
When a result source doesn't have any items, the widget doesn't render its `itemComponent`.
Use the source's `noResultsComponent` to show a message such as "No products found", as shown in the main example.
## Combine the empty and typing states
The final `panelComponent` combines two states:
* For an empty query, show recent and popular searches in the left column and shortcut cards in the right column.
* After users enter a query, show Query Suggestions and [a category](#add-a-category-suggestion) in the left column and product previews in the right column.
To switch between the states:
* Remove the `headerComponent` from `showQuerySuggestions`.
* Render the suggestions heading in the `panelComponent`. Use "Popular searches" for an empty query and "Suggestions" for a non-empty query.
* Show the heading only when the Query Suggestions source has results.
For the assembled `panelComponent`, see [`src/App.jsx`](https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch/autocomplete/src/App.jsx).
## See also
* [Autocomplete](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/react) guide for the widget basics.
* [Rich text box with mentions](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/mentions/react) for a headless pattern with React Hooks.
# Build a mentions text box
Source: https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/examples/mentions/react
Build a headless social mentions text box with the`useAutocomplete` Hook.
This example builds on the [`Autocomplete`](/doc/guides/building-search-ui/ui-and-ux-patterns/autocomplete/react) widget guide.
Use this guide to build a headless mentions text box with the `useAutocomplete` Hook.
This widget is and is subject to change in minor versions.
## Build a custom UI with `useAutocomplete`
The widget renders its own input and panel.
Use the `useAutocomplete` Hook when you need full control over the markup.
It's useful for custom input elements, different panel structures, or inline type-ahead patterns such as `@` mentions.
It gives you the autocomplete state and a `refine` function, leaving all the markup to you:
```jsx React icon=code expandable theme={"system"}
import { useAutocomplete, Highlight } from "react-instantsearch";
function CustomAutocomplete(props) {
const { indices, currentRefinement, refine } = useAutocomplete(props);
const hits = indices[0]?.hits ?? [];
return (
refine(event.currentTarget.value)}
/>
{hits.map((hit) => (
))}
);
}
```
Render `CustomAutocomplete` inside your [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) provider.
Each entry in `indices` has the shape `{ indexName, indexId, hits, results, sendEvent }`.
The Hook searches the Algolia indices in your widget tree,
the root index and any nested [`Index`](/doc/api-reference/widgets/index-widget/react) components,
so you don't pass index names to the Hook.
Because you render the markup, you're responsible for keyboard navigation, active-item state, and ARIA attributes.
If you need the same accessible combobox behavior without building it yourself, use the [`Autocomplete`](/doc/api-reference/widgets/autocomplete/react) widget instead of the Hook.
## Build a rich text box with mentions
Beyond redirecting to a search page, you can use an autocomplete as a secondary search pattern to improve the typing experience.
For example, the social media mentions feature lets users mention another user with the "@" character, which opens a panel of matching accounts so you can complete your message with the right username.
The text box provides type-ahead suggestions.
The panel doesn't block users.
They can keep typing and ignore the suggestions, or select one to complete the message.
The compose box doesn't process a query from a search input.
Instead, it parses the content of a text box and detects when you're trying to mention someone.
To replicate this, you need full control over the markup, so you use the `useAutocomplete` Hook rather than the widget.
This example searches the public `autocomplete_twitter_accounts` index, whose records include a `name`, a `handle`, and an `image`.
The Hook exposes `indices` and `refine`, but not `getSources` or `onSelect` from `autocomplete-core`.
You manage the active token, caret position, and selection in the component's state.
Run and edit the mentions example in CodeSandbox.
Browse the source code for the mentions example on GitHub.
### Render the text box
Render a `textarea` for the message and, when the panel is open, a `ul` for the suggestions.
This example uses a `textarea` element instead of an `input` for free-form, multiline plain text.
```jsx React icon=code theme={"system"}
function Mentions() {
return (
);
}
```
### Detect a mention
If you pass the full text box value to `refine`, Algolia searches the entire message.
You only want to search when the caret sits inside a mention, and you only want to send the mention itself, not the whole message.
To do that, tokenize the text and find the word under the caret, then check whether it's a valid username:
```jsx React icon=code expandable theme={"system"}
// The word under the caret, with its [start, end] range in the text.
function getActiveToken(input, cursor) {
const re = /\S+/g;
let match;
while ((match = re.exec(input))) {
const [start, end] = [match.index, match.index + match[0].length];
if (start <= cursor && cursor <= end) {
return { word: match[0], range: [start, end] };
}
}
return null;
}
// A mention is "@" followed by 1–15 word characters.
const isMention = (word) => /^@\w{1,15}$/.test(word);
```
### Position the panel
When users mention someone, the panel should follow the caret instead of sitting at the bottom of the text box.
To find the caret's pixel position, install [`textarea-caret`](https://www.npmjs.com/package/textarea-caret):
```sh npm theme={"system"}
npm install textarea-caret@3
```
```sh yarn theme={"system"}
yarn add textarea-caret@3
```
`getCaretCoordinates(textarea, position)` returns the `top`, `left`, and `height` of the caret at a given offset.
Use it to place the panel just below the `@` of the active mention.
### Search for accounts and render the panel
Connect the Hook to the text box.
On every input, find the active token.
If it's a mention, call `refine` with the text after `@` and show the matching accounts.
Otherwise, hide the panel.
The Hook gives you `indices[0].hits` and a `refine` function.
You manage the remaining behavior, including the active token, panel state, and selection handling.
This excerpt shows the Hook wiring.
The imports, search client, caret positioning, selection handling, and the panel markup are in [the complete `src/App.jsx`](https://github.com/algolia/doc-code-samples/tree/master/react-instantsearch/autocomplete-mentions/src/App.jsx).
```jsx React icon=code expandable theme={"system"}
function Mentions(props) {
const { indices, refine } = useAutocomplete(props);
const inputRef = useRef(null);
const [value, setValue] = useState("");
const [activeToken, setActiveToken] = useState(null);
const hits = indices[0]?.hits ?? [];
const isOpen = activeToken && isMention(activeToken.word) && hits.length > 0;
// Only search when the caret sits inside a mention.
function onInput() {
const cursor = inputRef.current?.selectionEnd ?? 0;
const token = getActiveToken(inputRef.current?.value ?? "", cursor);
setActiveToken(token);
if (token && isMention(token.word)) {
refine(token.word.slice(1));
}
}
return (
);
}
```
When users type `@` followed by a name, highlighted results appear.
### Select an account
The goal of the mention feature is to help users find an account and autocomplete its handle.
For example, when users type a few letters after `@`,
the panel opens with matching accounts.
Selecting an account replaces the typed text with the account's handle, such as "@jwestlakedc", and closes the panel.
When the user picks an account, replace the active token with the correct handle and move the caret after it.
Use `onMouseDown` with `preventDefault` instead of `onClick`.
This keeps focus on the `textarea`.
```jsx React icon=code theme={"system"}
function onSelect(hit) {
const [start, end] = activeToken.range;
const replacement = `@${hit.handle} `;
const caretPosition = start + replacement.length;
setValue(value.slice(0, start) + replacement + value.slice(end));
setActiveToken(null);
requestAnimationFrame(() => {
inputRef.current?.focus();
inputRef.current?.setSelectionRange(caretPosition, caretPosition);
});
}
```
### Navigate in the text box
Typing isn't the only action in a text box.
Users can also edit their text or move the caret to a different position.
When the caret lands on a mention, the panel should open.
When it leaves a mention, the panel should close.
Handle `onClick` and `onKeyUp` in addition to `onChange`, and run the same active-token check again:
```jsx React icon=code theme={"system"}
}
+ submitIconComponent={({ classNames }) => (
+
Submit
+ )}
- reset={
Reset
}
+ resetIconComponent={({ classNames }) => (
+
Reset
+ )}
- loadingIndicator={
Loading
}
+ loadingIconComponent={({ classNames }) => (
+
Loading
+ )}
/>
);
}
```
##### Replace `focusShortcuts` with custom code
The `focusShortcuts` prop was removed.
If you want to focus the search box with custom keyboard shortcuts, you can set it up yourself with custom code.
For example, you can use [`react-use`](https://github.com/streamich/react-use) with the [`useKey`](https://github.com/streamich/react-use/blob/master/docs/useKey.md) Hook.
```diff React icon=code theme={"system"}
+import { useKey } from 'react-use';
function Search() {
+ useKey('s', (event) => {
+ event.preventDefault();
+ document.querySelector('.ais-SearchBox-input').focus();
+ });
return (
);
}
```
##### Replace translations.placeholder with placeholder
The `placeholder` translation now has its own top-level prop, `placeholder`.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
#### `Snippet`
##### Replace `tagName` with `highlightedTagName`
The [`Highlight`](/doc/api-reference/widgets/highlight/react) component now takes an optional `highlightedTagName` prop which replaces `tagName`.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
The default value also changed from `"em"` to `"mark"` for `highlightedTagName` (formerly `tagName`).
If `tagName` is to `"mark"` in your app, you can remove it.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
Conversely, if you were relying on the default value for `tagName`, set it explicitly.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
#### `SortBy`
##### Replace `defaultRefinement` with `initialUiState` on `InstantSearch`
The [`SortBy`](/doc/api-reference/widgets/sort-by/react) component no longer accepts a `defaultRefinement` prop.
Instead, you can specify an initial UI state on your `InstantSearch` component.
```diff JavaScript theme={"system"}
function App() {
return (
>: {
+ sortBy: 'instant_search',
+ },
+ }}
>
);
}
```
#### `Stats`
##### Replace translations
The `translations` key have changed.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
#### `ToggleRefinement`
##### Replace `defaultRefinement` with `initialUiState` on `InstantSearch`
The [`ToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react) component no longer accepts a `defaultRefinement` prop.
Instead, you can specify an initial UI state on your `InstantSearch` component.
```diff JavaScript theme={"system"}
function App() {
return (
>: {
+ toggle: {
+ free_shipping: true
+ },
+ },
+ }}
>
);
}
```
##### Replace value with on and off
By default, [`ToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react) uses respectively `true` and `undefined` for new optional props `on` and `off`.
You can replace the `value` prop with `on` (and `off`, if needed).
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
### Connectors
React InstantSearch v7 provides the same connectors as React InstantSearch v6 except the `connectStateResults` connector, which isn't available in React InstantSearch v7.
If you were using connectors with higher-order components (HOCs), you can migrate to Hooks.
```diff JavaScript theme={"system"}
-import { connectSearchBox } from 'react-instantsearch-dom';
+import { useSearchBox } from 'react-instantsearch';
function SearchBox({
- currentRefinement,
+ query,
isSearchStalled,
refine
}) {
return (
);
}
-const CustomSearchBox = connectSearchBox(SearchBox);
+function CustomSearchBox(props) {
+ const searchBoxApi = useSearchBox(props);
+ return ;
+}
```
#### `connectStateResults`
React InstantSearch v7 doesn't have the `connectStateResults` connector.
You can use the [`useInstantSearch`](/doc/api-reference/widgets/use-instantsearch/react) Hook instead.
### Connectors and Hooks
React InstantSearch v7 is a bridge between InstantSearch.js connectors and React Hooks.
The connectors from React InstantSearch and InstantSearch.js were historically different APIs, meaning there are differences between the props that React InstantSearch connectors accept and the new Hooks.
The React InstantSearch v7 package uses TypeScript natively.
If your editor supports code completion (IntelliSense), you can use it to discover the new props.
### Higher-Order Components (HOCs)
React InstantSearch v7 doesn't come with Higher Order Components (HOCs).
However, you can create them by wrapping existing Hooks into simple HOCs:
```jsx React icon=code theme={"system"}
import { useSearchBox } from "react-instantsearch";
const connectSearchBox = (Component) => {
const SearchBox = (props) => {
const data = useSearchBox();
return ;
};
return SearchBox;
};
function RawSearchBox(props) {
const { refine, query } = props;
return (
);
}
const SearchBox = connectSearchBox(RawSearchBox);
```
You can also combine that with the [`useConnector`](/doc/api-reference/widgets/connector/react) Hook to create a connector.
```jsx React icon=code theme={"system"}
import { useConnector } from "react-instantsearch";
const connectCustom = (Component) => {
const Custom = (props) => {
// myConnector.connectCustom is an InstantSearch.js connector
const data = useConnector(myConnector.connectCustom, props);
return ;
};
return Custom;
};
function RawCustom(props) {
const { refine, currentRefinement } = props;
return (
);
}
const Custom = connectCustom(RawCustom);
```
### Creating connectors
The connector API has changed to use InstantSearch.js connectors.
**The previous `createConnector()` function is no longer available.**
React InstantSearch v7 works with all InstantSearch.js connectors—official Algolia connectors, and community ones.
To create your own Hook, you can use an existing connector or [create your InstantSearch.js connector](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react#build-a-custom-connector).
For more information, see [Custom Hooks](/doc/guides/building-search-ui/what-is-instantsearch/react#custom-hooks).
### Routing
Routing now follows the InstantSearch.js routing APIs with the `InstantSearch` [routing](/doc/api-reference/widgets/instantsearch/react#param-routing) prop.
For more information, see:
* [InstantSearch routing prop](/doc/api-reference/widgets/instantsearch/react#param-routing)
* [Sync Your URLs with InstantSearch.js](/doc/guides/building-search-ui/going-further/routing-urls/js)
### Server-side rendering (SSR)
The server APIs have been simplified in React InstantSearch v7.
Replace `findResultsState()` with [`getServerState()`](/doc/api-reference/widgets/server-state/react).
This new API accepts an element ``.
You can pass props directly to ``.
In your server code, you don't need to provide your index name and your search client anymore, because they're already in your `App`.
```diff server.js icon=code theme={"system"}
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
-import { findResultsState } from 'react-instantsearch-dom/server';
+import { getServerState } from 'react-instantsearch';
import App from './App';
-const indexName = 'instant_search';
-const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
const app = express();
app.get('/', async (req, res) => {
- const searchState = { /* ... */ };
- const serverState = {
- resultsState: await findResultsState(App, {
- searchClient,
- indexName,
- searchState,
- }),
- searchState,
- };
+ const serverState = await getServerState();
const html = renderToString();
res.send(
`
${html}
`
);
});
app.listen(8080);
```
You don't need to pass any props to `InstantSearch` to support SSR,
only wrap the component to render on the server with [`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react).
Then, pass the server state by spreading the [`getServerState()`](/doc/api-reference/widgets/server-state/react) prop.
```diff App.js icon=code theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import React from 'react';
-import { InstantSearch } from 'react-instantsearch-dom';
+import {
+ InstantSearch,
+ InstantSearchSSRProvider,
+} from 'react-instantsearch';
const searchClient = algoliasearch('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
function App(props) {
return (
+
{/* Widgets */}
+
);
}
export default App;
```
For more information, see:
* **[`getServerState`](/doc/api-reference/widgets/server-state/react)**
* **[`InstantSearchSSRProvider`](/doc/api-reference/widgets/ssr-provider/react)**
* **[Server-side rendering with React InstantSearch](/doc/guides/building-search-ui/going-further/server-side-rendering/react)**
### Types
Types are now available in React InstantSearch v7.
You can uninstall all InstantSearch types coming from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped).
```sh Command line icon=square-terminal theme={"system"}
npm uninstall @types/react-instantsearch-dom @types/react-instantsearch-core @types/react-instantsearch
```
## Migrate from React InstantSearch Hooks to React InstantSearch v7
From v7, React InstantSearch Hooks is called React InstantSearch.
The migration is straightforward as the APIs are the same.
Only package names have changed, and several APIs have been deprecated.
### Package names
| Before | After |
| - | - |
| `react-instantsearch-hooks` | `react-instantsearch-core` |
| `react-instantsearch-hooks-web` | `react-instantsearch` |
| `react-instantsearch-hooks-server` | `react-instantsearch` |
| `react-instantsearch-hooks-router-nextjs` | `react-instantsearch-router-nextjs` |
Update your `package.json` file:
```diff package.json icon=braces theme={"system"}
{
"dependencies": {
- "react-instantsearch-hooks": "^6.42.0",
+ "react-instantsearch-core": "^7.0.0",
- "react-instantsearch-hooks-web": "^6.42.0",
+ "react-instantsearch": "^7.0.0",
- "react-instantsearch-hooks-server": "^6.42.0",
- "react-instantsearch-hooks-router-nextjs": "^6.42.0",
+ "react-instantsearch-router-nextjs": "^7.0.0",
}
}
```
After updating your `package.json` file, run `npm install` to install the updated dependencies.
You can then change your imports:
```diff JavaScript icon=code theme={"system"}
-import { useInstantSearch } from 'react-instantsearch-hooks';
+import { useInstantSearch } from 'react-instantsearch-core';
-import { SearchBox } from 'react-instantsearch-hooks-web';
-import { getServerState } from 'react-instantsearch-hooks-server';
+import { SearchBox, getServerState } from 'react-instantsearch';
-import { createInstantSearchRouterNext } from 'react-instantsearch-hooks-router-nextjs';
+import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
```
The `react-instantsearch-hooks-server` functions are now included in `react-instantsearch`.
### Codemods
To automatically update your code to use the new package names and APIs from React InstantSearch v7, Algolia provides a [codemod](https://www.codeshiftcommunity.com/docs/).
First, uninstall the `react-instantsearch-hooks-*` packages and install their `react-instantsearch-*` counterparts.
```sh Command line icon=square-terminal theme={"system"}
npm uninstall react-instantsearch-hooks-web react-instantsearch-hooks react-instantsearch-hooks-server
npm install react-instantsearch react-instantsearch-core
```
Assuming your source code is in the `src/` folder, run the following command:
```sh Command line icon=square-terminal theme={"system"}
npx @codeshift/cli -e js,jsx,ts,tsx --packages 'instantsearch-codemods#rish-to-ris' src/
```
Afterwards, run your code formatter, since codemods might output code that doesn't follow your code style:
```sh yarn theme={"system"}
yarn prettier --write 'src/**/*.{js,jsx,ts,tsx}'
# or
yarn eslint --fix src/
```
```sh npm theme={"system"}
npm exec prettier --write 'src/**/*.{js,jsx,ts,tsx}'
# or
npm exec eslint --fix src/
```
### `use` to `addMiddlewares`
[`useInstantSearch`](/doc/api-reference/widgets/use-instantsearch/react)'s `use` function has been renamed to `addMiddlewares`.
```diff JavaScript icon=code theme={"system"}
import { useInstantSearch } from 'react-instantsearch-core';
function Middleware() {
- const { use } = useInstantSearch();
+ const { addMiddlewares } = useInstantSearch();
useEffect(() => {
const middleware = /* ... */;
- return use(middleware);
+ return addMiddlewares(middleware);
- }, [use]);
+ }, [addMiddlewares]);
return null;
}
```
### `getServerState`
The [`getServerState`](/doc/api-reference/widgets/server-state/react) function now requires `renderToString` from `react-dom/server` to be passed as a second argument.
```diff JavaScript icon=code theme={"system"}
+import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch';
function App() {
return (
/* ... */
);
}
async function getServerSideProps() {
- const serverState = await getServerState();
+ const serverState = await getServerState(, { renderToString });
return {
props: {
serverState,
},
};
}
```
### `Stats`
The `translations` prop of the [`Stats`](/doc/api-reference/widgets/stats/react) widget has changed and now accepts a `rootElementText` function instead of a `stats` function.
```diff React icon=code theme={"system"}
function Search() {
return (
);
}
```
## Upgrade event tracking
**Starting from v6.43.0, React InstantSearch Hooks simplifies the event tracking process with the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option.**
You no longer need to install the [`search-insights`](/doc/libraries/search-insights) library or set up the [`insights`](/doc/api-reference/widgets/insights/react) middleware yourself.
Here are some benefits when using the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option:
* It better handles the . Once you set it, all search and event tracking calls include the token.
* It automatically sends default events from built-in widgets such as [`RefinementList`](/doc/api-reference/widgets/refinement-list/react), [`Menu`](/doc/api-reference/widgets/menu/react), etc.
You can also change the event payloads, or remove them altogether.
* It lets you send custom events from your custom widgets.
* It simplifies forwarding events to third-party trackers.
If you've been tracking events directly with [`search-insights`](/doc/libraries/search-insights) or with the [`insights`](/doc/api-reference/widgets/insights/react) middleware, follow these steps:
1. Upgrade `react-instantsearch-hooks` to v6.43.0 or greater
2. [Migrate](#use-the-insights-option) from using the [`insights`](/doc/api-reference/widgets/insights/react) middleware to the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option
3. Either [update](#update-search-insights) or [remove](#remove-search-insights) the [`search-insights`](/doc/libraries/search-insights) library
### Use the `insights` option
**Starting from v6.43.0, InstantSearch lets you enable event tracking with the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option.** You no longer need to set up the [`insights`](/doc/api-reference/widgets/insights/react) middleware yourself.
```jsx React icon=code theme={"system"}
function App() {
return (
{/* Widgets */}
);
}
```
If you had already set up the [`insights`](/doc/api-reference/widgets/insights/react) middleware in your code, you can now remove it and move its configuration to the [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights) option.
```diff React icon=code theme={"system"}
-import { createInsightsMiddleware } from 'instantsearch.js/es/middlewares';
-import { useInstantSearch } from 'react-instantsearch-hooks-web';
-import { useLayoutEffect } from 'react';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
- function InsightsMiddleware() {
- const { addMiddlewares } = useInstantSearch();
-
- useLayoutEffect(() => {
- const middleware = createInsightsMiddleware({
- insightsClient: window.aa,
- insightsInitParams: {
- useCookie: false,
- }
- });
-
- return addMiddlewares(middleware);
- }, [addMiddlewares]);
-
- return null;
-}
function App() {
return (
{/* Widgets */}
);
}
```
### Update `search-insights`
**Starting from v4.55.0, InstantSearch can load [`search-insights`](/doc/libraries/search-insights) for you so the [`insightsClient`](/doc/api-reference/widgets/insights/react#param-insights-client) option is no longer required.**
If you prefer loading it yourself, make sure to update [`search-insights`](/doc/libraries/search-insights) to at least v2.4.0 and forward the reference to [`insights`](/doc/api-reference/widgets/instantsearch/react#param-insights).
If you're using the UMD bundle with a `script` tag,
make sure to update the full code snippet (not just the version):
```html HTML icon=code-xml theme={"system"}
```
If you're using a package manager, you can upgrade it to the latest version.
```sh Command line icon=square-terminal theme={"system"}
npm install search-insights
```
```js UMD theme={"system"}
function App() {
return (
{/* ... */}
);
}
```
```js Module theme={"system"}
import aa from 'search-insights';
function App() {
return (
{/* ... */}
);
}
```
Otherwise, you can [remove it and let InstantSearch handle it for you](#remove-search-insights).
### Remove `search-insights`
**Starting from v4.55.0, InstantSearch loads [`search-insights`](/doc/libraries/search-insights) for you from jsDelivr if not detected in the page.** If you've installed [`search-insights`](/doc/libraries/search-insights), you can now remove it.
If you're using the UMD bundle with a `script` tag, you can remove the snippet:
```diff HTML icon=code-xml theme={"system"}
-
```
If you're using a package manager, you can upgrade it to the latest version.
```sh Command line icon=square-terminal theme={"system"}
npm uninstall search-insights
```
Then you can remove the reference to [`search-insights`](/doc/libraries/search-insights) from your code:
```diff UMD theme={"system"}
function App() {
return (
{/* ... */}
);
}
```
```diff Module theme={"system"}
- const aa = require('search-insights');
// or
- import aa from 'search-insights';
function App() {
return (
{/* ... */}
);
}
```
InstantSearch loads [`search-insights`](/doc/libraries/search-insights) from the jsDelivr CDN, which requires that your site or app allows script execution from foreign resources.
Check the [security best practices](/doc/guides/security/security-best-practices) for recommendations.
## Upgrade to Recommend
The [`RelatedProducts`](/doc/api-reference/widgets/related-products/react) widget is now available instead of the previous experimental widget `EXPERIMENTAL_RelatedItems`. To migrate, you change the widget name and update the props:
```diff React icon=code theme={"system"}
-import { EXPERIMENTAL_RelatedItems } from 'react-instantsearch';
+import { RelatedProducts } from 'react-instantsearch';
function App() {
return (
-
-
+
);
}
```
# Upgrade Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/upgrade-guides/vue
Upgrade your Vue InstantSearch version
## Upgrade event tracking
**Starting from v4.9.0, Vue InstantSearch simplifies the event tracking process with the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option.**
You no longer need to install the [`search-insights`](/doc/libraries/search-insights) library or set up the [`insights`](/doc/api-reference/widgets/insights/vue) middleware yourself.
Here are some benefits when using the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option:
* It better handles the . Once you set it, all search and event tracking calls include the token.
* It automatically sends default events from built-in widgets such as [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue), [`ais-menu`](/doc/api-reference/widgets/menu/vue), etc.
You can also change the event payloads, or remove them altogether.
* It lets you send custom events from your custom widgets.
* It simplifies forwarding events to third-party trackers.
If you've been tracking events directly with [`search-insights`](/doc/libraries/search-insights) or with the [`insights`](/doc/api-reference/widgets/insights/vue) middleware, follow these steps:
1. Upgrade Vue InstantSearch to v4.9.0 or greater
2. [Migrate](#use-the-insights-option) from using the [`insights`](/doc/api-reference/widgets/insights/vue) middleware to the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option
3. Either [update](#update-search-insights) or [remove](#remove-search-insights) the [`search-insights`](/doc/libraries/search-insights) library
### Use the Insights option
**Starting from v4.9.0, Vue InstantSearch lets you enable event tracking with the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option.** You no longer need to set up the [`insights`](/doc/api-reference/widgets/insights/vue) middleware yourself.
```vue Vue icon=code theme={"system"}
```
If you had already set up the [`insights`](/doc/api-reference/widgets/insights/vue) middleware in your code, you can now remove it and move its configuration to the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option.
```diff Vue theme={"system"}
```
### Update `search-insights`
**Starting from v4.9.0, Vue InstantSearch can load [`search-insights`](/doc/libraries/search-insights) for you so the [`insightsClient`](/doc/api-reference/widgets/insights/vue#param-insights-client) option is no longer required.**
If you prefer loading it yourself, make sure to update [`search-insights`](/doc/libraries/search-insights) to at least v2.4.0 and forward the reference to [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights).
If you're using the UMD bundle with a `script` tag, make sure to update the full code snippet (not just the version):
```html HTML icon=code-xml theme={"system"}
```
If you're using a package manager, you can upgrade it to the latest version.
```sh Command line icon=square-terminal theme={"system"}
npm install search-insights
```
Now you can pass the reference to the [`insights`](/doc/api-reference/widgets/instantsearch/vue#param-insights) option.
```vue UMD theme={"system"}
```
```vue Module theme={"system"}
```
Otherwise, you can [remove it and let Vue InstantSearch handle it for you](#remove-search-insights).
### Remove `search-insights`
**Starting from v4.9.0, Vue InstantSearch loads [`search-insights`](/doc/libraries/search-insights) for you from jsDelivr if not detected in the page.** If you've installed [`search-insights`](/doc/libraries/search-insights), you can now remove it.
If you're using the UMD bundle with a `script` tag, you can remove the snippet in any page that uses Vue InstantSearch:
```diff HTML icon="code=xml" theme={"system"}
-
```
If you're using a package manager, you can remove it from your dependencies.
```sh Command line icon=square-terminal theme={"system"}
npm uninstall search-insights
```
Then you can remove the reference to [`search-insights`](/doc/libraries/search-insights) from your code:
```diff UMD theme={"system"}
```
```diff Module theme={"system"}
```
Vue InstantSearch loads [`search-insights`](/doc/libraries/search-insights) from the jsDelivr CDN, which requires that your site or app allows script execution from foreign resources.
Check the [security best practices](/doc/guides/security/security-best-practices) for recommendations.
## Upgrade from v3 to v4
You can use Vue InstantSearch v4 with Vue 2 and Vue 3.
### Server-side rendering
Using Vue InstantSearch v4 introduces one breaking change in the server-side rendering.
`findResultsState` in `serverPrefetch` now takes `component` and `renderToString: (app) => Promise` in the first argument.
#### Vue 3
```js JavaScript icon=code theme={"system"}
import { renderToString } from '@vue/server-renderer';
// ...
serverPrefetch() {
return this.instantsearch.findResultsState({
component: this,
renderToString,
});
},
```
#### Vue 2
The renderToString function from `vue-server-renderer/basic` is callback-based.
You need to promisify it like this:
```js JavaScript icon=code theme={"system"}
import _renderToString from 'vue-server-renderer/basic';
function renderToString(app) {
return new Promise((resolve, reject) => {
_renderToString(app, (err, res) => {
if (err) reject(err);
resolve(res);
});
});
}
// ...
serverPrefetch() {
return this.instantsearch.findResultsState({
component: this,
renderToString,
});
},
```
### If you upgrade to Vue 3
#### Import the library
The path to import Vue InstantSearch has changed for Vue 3, and is now:
```js JavaScript icon=code theme={"system"}
import InstantSearch from "vue-instantsearch/vue3/es";
// ...
app.use(InstantSearch);
```
#### Vue Router v4
If you're using Vue Router and upgrading it to v4, [`currentRoute`](https://router.vuejs.org/api/#currentroute) is now a reference instead of an object.
```js JavaScript icon=code theme={"system"}
// previously
vueRouter.currentRoute.query;
// now
vueRouter.currentRoute.value.query;
```
It's not directly related to Vue InstantSearch,
but if you are upgrading to Vue Router 4 along with Vue 3,
you will encounter this issue anyway.
It's mentioned here for your information.
## Upgrade from v2 to v3
This guide contains all the major changes that were introduced in v3, and how to migrate from v2.
Vue InstantSearch v3 uses InstantSearch.js v4, so all breaking changes using the InstantSearch.js API also apply here.
You can find more information in its respective [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/js).
### Federated search (multi-index)
If you were using a [`ais-configure`](/doc/api-reference/widgets/configure/vue) or a [`ais-search-box`](/doc/api-reference/widgets/search-box/vue) to synchronize between two [`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue) instances, you can now replace either one of them with the new [`ais-index`](/doc/api-reference/widgets/index-widget/vue) widget like this:
```vue Vue icon=code theme={"system"}
```
### ais-autocomplete
The `indices` option has been removed in favor of [`ais-index`](/doc/api-reference/widgets/index-widget/vue).
This means that:
```vue Vue icon=code theme={"system"}
```
Should be replaced with:
```vue Vue icon=code theme={"system"}
```
### `ais-state-results`
This widget used to expose the search results only, you can now access the search parameters as well.
This lets you use information to perform view logic instead of waiting for the response to retrieve the data.
```diff Vue icon=code theme={"system"}
-
+
```
The results you have access to are always scoped to the current .
### Routing and URLs
Since the introduction of federated search, it's now possible to have multiple indices in a single app.
This means that the default state mapping for routing now takes multiple indices into account.
If you want to keep on using the same URLs as before, make the following change:
```diff JavaScript icon=code theme={"system"}
- import { simple as simpleMapping } from 'instantsearch.js/es/lib/stateMappings';
+ import { singleIndex as singleIndexMapping } from 'instantsearch.js/es/lib/stateMappings';
export default {
data() {
return {
routing: {
- stateMapping: simpleMapping(),
+ stateMapping: singleIndexMapping('myIndex'),
}
}
}
}
```
For more information about custom routers,
consult the [InstantSearch.js upgrade guide](/doc/guides/building-search-ui/upgrade-guides/js#routing).
### Server-side rendering (SSR)
This major version of Vue InstantSearch also includes an overhaul of the server-side rendering implementation. This takes two points into consideration:
* work fully with `serverPrefetch`
* no repetition needed between the template and parameters
To do this the following changes have been made:
1. `createInstantSearch` -> `createServerRootMixin`
```js JavaScript icon=code theme={"system"}
import { createServerRootMixin } from "vue-instantsearch";
const app = new Vue({
mixins: [
createServerRootMixin({
searchClient,
indexName: "instant_search",
}),
],
serverPrefetch() {
return this.instantsearch.findResultsState(this);
},
beforeMount() {
if (typeof window === "object" && window.__ALGOLIA_STATE__) {
this.instantsearch.hydrate(window.__ALGOLIA_STATE__);
delete window.__ALGOLIA_STATE__;
}
},
router,
render: (h) => h(App),
});
```
2. Using a compatible router
The default routers in InstantSearch.js are based around browser location,
and thus won't work on the server. An example router for Vue SSR can be made like this:
```js JavaScript icon=code theme={"system"}
import qs from "qs";
// get this from the environment, will be different in nuxt vs. vue ssr
const serverUrl = context ? context.url : undefined;
const router = {
read() {
const url = serverUrl
? serverUrl
: typeof window.location === "object"
? window.location.href
: "";
const search = url.slice(url.indexOf("?"));
return qs.parse(search, {
ignoreQueryPrefix: true,
});
},
write(routeState) {
const query = qs.stringify(routeState, {
addQueryPrefix: true,
});
if (typeof history === "object") {
history.pushState(routeState, null, query);
}
},
createURL(routeState) {
const query = qs.stringify(routeState, {
addQueryPrefix: true,
});
return query;
},
onUpdate(callback) {
if (typeof window !== "object") {
return;
}
this._onPopState = () => {
if (this.writeTimer) {
window.clearTimeout(this.writeTimer);
this.writeTimer = undefined;
}
callback(this.read());
};
window.addEventListener("popstate", this._onPopState);
},
dispose() {
if (this._onPopState && typeof window == "object") {
window.removeEventListener("popstate", this._onPopState);
}
// Don't write on dispose to prevent double entries on navigation
},
};
```
A full example can be found in the [GitHub repository](https://github.com/algolia/instantsearch/tree/master/examples/vue).
Read more detail in the dedicated [SSR guide](/doc/guides/building-search-ui/going-further/server-side-rendering/vue).
### Helper v3.x.x
This release includes version 3 of the [`algoliasearch-helper` package](https://www.npmjs.com/package/algoliasearch-helper/).
**If you are using the built-in widgets or connectors, nothing changes for you.**
This version of `algoliasearch-helper` no longer includes [Lodash](https://lodash.com/), which significantly reduces its bundle size (from 27.5 KB to 9.1 KB Gzipped).
## Upgrade from v1 to v2
This guide contains all the major changes that were introduced in v2 and how to migrate from v1.
You can still find the documentation for [Vue InstantSearch v1](https://community.algolia.com/vue-instantsearch).
### Renamed components
Some components have been renamed to be more consistent with other InstantSearch flavors.
* `ais-results` -> `ais-hits`
* `ais-tree-menu` -> `ais-hierarchical-menu`
* `ais-clear` -> `ais-clear-refinements`
* `ais-results-per-page-selector` -> `ais-hits-per-page`
* `ais-rating` -> `ais-rating-menu`
* `ais-sort-by-selector` -> `ais-sort-by`
* `ais-index` -> `ais-instant-search`
All individual component exports have also been renamed from, for example, `SearchBox` to `AisSearchBox`.
This is to make it more ergonomic to use them as components in a Vue file with the same name as expected.
The `Component` mixin has been renamed to `createWidgetMixin({ connector })`.
Read more about that in the [custom component guide](/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue).
### New components
* [`ais-configure`](/doc/api-reference/widgets/configure/vue)
This widget is the replacement of `query-parameters` on `ais-index`.
* [`ais-state-results`](/doc/api-reference/widgets/state-results/vue)
This component can be used for conditional rendering, and getting information that's not returned in `ais-hits`.
* [`ais-breadcrumb`](/doc/api-reference/widgets/breadcrumb/vue)
To be used together with `ais-hierarchical-menu`.
* [`ais-menu-select`](/doc/api-reference/widgets/menu-select/vue)
A single-selectable menu, rendered inside a `select`
* [`ais-current-refinements`](/doc/api-reference/widgets/current-refinements/vue)
Shows the current refinements, and allows them to be unset.
* [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue)
Replaces `:stack="true"` on `ais-results` (now called `ais-hits`).
* [`ais-numeric-menu`](/doc/api-reference/widgets/numeric-menu/vue)
Statically set numerical ranges can be used to refine using this widget.
* [`ais-panel`](/doc/api-reference/widgets/panel/vue)
Wrap a widget in `ais-panel` to be able to give it a header and a footer.
Replaces those options on each widget.
* [`ais-toggle-refinement`](/doc/api-reference/widgets/toggle-refinement/vue)
Toggle a boolean refinement either refined/unrefined or refinement/another refinement.
Useful for toggles or buttons with two states.
### Renamed options
Some options have been renamed. Largely those are:
* attribute-name -> attribute
* results -> hits
* anything in a list -> items / item
* header / footer -> wrap the widget in an `ais-panel`
If you see anything not mentioned here, [complete the feedback form](https://github.com/algolia/instantsearch/issues/new/choose).
### Removed options
* `query-parameters`
This is now handled with the [`ais-configure`](/doc/api-reference/widgets/configure/vue) widget.
Each query parameter becomes a prop on Configure.
* `query`
You can now synchronize the query across indices either by using a `v-model` on two `ais-search-box`es of which you hide one, or with [`ais-configure`](/doc/api-reference/widgets/configure/vue) on both indices, and synchronizing between those like that.
* `appId` and `apiKey`
This is now handled by the `search-client` prop.
Search client is what gets returned if you call `algoliasearch`.
```diff Vue icon=code theme={"system"}
-
+ My app
+
-
+
```
* `:stack="true"`
When you used to put this on `ais-results` (now called [`ais-hits`](/doc/api-reference/widgets/hits/vue)),
it allows to load next pages without pagination,
but with a "next page" button,
as well as showing all pages rather than a single one.
Replaced by [`ais-infinite-hits`](/doc/api-reference/widgets/infinite-hits/vue).
* `auto-search`
This option is removed in favor of a more complete way of not doing queries using the search client.
For more information, see [Conditional requests](/doc/guides/building-search-ui/going-further/conditional-display/vue).
### Removed components
* `ais-input`
This component has been removed with as alternative having two options:
1. use [`ais-search-box`](/doc/api-reference/widgets/search-box/vue) and style it to not include the extra items
2. use the default slot of `ais-search-box` to use it with your own custom input ([see live](https://community.algolia.com/vue-instantsearch/stories/?selectedKind=SearchBox)):
```vue Vue icon=code theme={"system"}
```
### CSS class names
All CSS class names are now different, since Algolia follows the [SUIT CSS](https://suitcss.github.io/) methodology now, rather than the previous, slightly wrong, implementation of 'block, element, and modifier' (BEM).
Since the DOM output is also different in most widgets, it's best to start styling over from scratch on these widgets.
Each widget lists the CSS classes it uses in its documentation page.
### Known limitations
#### SSR (server-side rendering)
The implementation of server-side rendering with Vue InstantSearch slightly changed, with a simpler API.
Read more on how to use it in the [SSR guide](/doc/guides/building-search-ui/going-further/server-side-rendering/vue).
#### Search store
The search store no longer exists.
Custom widgets are either made by making a connector, or a combination of new widgets.
You no longer need to copy a widget to give it custom rendering.
Now you can fill in the `default` slot, which will have everything needed to render that component.
If you're using this, and have suggestions or questions, [complete the feedback form](https://github.com/algolia/instantsearch/issues/new/choose).
#### Routing
You're now able to use routing in InstantSearch with, or without [Vue Router](/doc/guides/building-search-ui/going-further/routing-urls/vue).
#### Changing props on ais-instant-search
It's possible to change props on `ais-instant-search`, except `routing`.
If you have a need for that to be changed as well, [complete the feedback form](https://github.com/algolia/instantsearch/issues/new/choose).
# What is InstantSearch iOS
Source: https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/ios
InstantSearch iOS is an open source library for building search interfaces.
Starting May 1, 2024,
Apple requires all iOS apps to include a privacy manifest.
For more information, see [Privacy manifest](/doc/guides/building-search-ui/resources/privacy-manifest/ios).
InstantSearch focuses on enhancing your frontend with widgets that you can combine to create unique search interfaces.
## InstantSearch iOS
InstantSearch iOS is a library that helps you integrate various search interfaces with Algolia, built on top of the [Swift API client](/doc/libraries/sdk/install).
### Accelerate development
InstantSearch iOS provides out of the box components that work together: [search box](/doc/api-reference/widgets/search-box/ios), [list of results](/doc/api-reference/widgets/hits/ios), [filtering](/doc/api-reference/widgets/refinement-list/ios) interfaces, [sorting](/doc/api-reference/widgets/sort-by/ios) interfaces, [hierarchical menu](/doc/api-reference/widgets/hierarchical-menu/ios), [federated search](/doc/api-reference/widgets/multi-hits/ios), [suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/ios), [highlighting](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js), [loading](/doc/api-reference/widgets/loading/ios), and more.
It connects to Algolia servers, performs API calls for you and automatically updates the user interface with the latest state.
### Manage complexity
InstantSearch iOS lets you focus on the look and feel of your user interface, by handling the search-specific logic for you,
and simplifying management of complex state like filters, hierarchical menus or federated search.
### Integrate with existing code
InstantSearch fits into existing code: it follows a strict separation of concerns, has few dependencies, and doesn't make assumptions on how your app is built.
### Build for all Apple platforms
InstantSearch iOS supports the full range of Apple platforms: iPadOS, macOS, [tvOS](https://github.com/algolia/instantsearch-ios/tree/demos-v1/Examples/TVSearch), [watchOS](https://github.com/algolia/instantsearch-ios/tree/demos-v1/Examples/WatchSearch%20WatchKit%20Extension).
Build a perfect search and discovery experience for any device.
### Customize components
Components shipped in InstantSearch can be customised and configured, from high-level parameters to custom presentation logic.
Most common search use-cases are covered with these components.
Beyond those, writing your own component only requires implementing a single interface.
InstantSearch is a powerful tool for builders that want to stay in control.
[Get started](/doc/guides/building-search-ui/getting-started/ios) with InstantSearch iOS now.
## How InstantSearch works
InstantSearch iOS has been designed in such a way that you have full control of your UI.
Each component respects the single responsibility principle, as well as being extendible and customizable.
This gives developers the control over all aspects of building a search experience: whether it's related to the framework, the UI, or any other use case.
Below is an overview of all components that you have access to:
### Network requests and entities
#### `Searcher`
The `Searcher` takes care of the search requests to Algolia.
It abstracts the complexity of the REST API, the retry logic, the sequencing, and more.
#### `FilterState`
This component takes care of keeping the state of the filters.
Filters in a search experience are complex: you can have facets, numeric filters or tags.
You can have conjunctive facets (AND), disjunctive facets (OR), or hierarchical facets.
In short, there's a lot of cases to handle, and the `FilterState` helps you define how you want your filters to be applied, in a declarative way.
#### `Query`
This component helps you define any [search API parameter](/doc/api-reference/search-api-parameters) that you want to add to your search requests.
### Use cases
#### Interactors
Interactors contain the business logic of a specific search element or view, as well as the UI State of this specific element or view.
Most of the time, you don't need to write your own Interactors: the ones of the InstantSearch SDK are enough.
#### Connectors
These aren't components in and of themselves, but most components have a `connect` method.
This lets them establish a connection with other components and lets them react to a change happening in the connected component.
### Interface adapters
#### Controllers
Controller components play the role of an adapter to the UI element of a specific framework.
For example, a `TextFieldController` is an adapter for a `UITextField` in iOS, and conforms to the `SearchBoxController` protocol.
The use of having such component is to make the library extendible: the same `Interactor` can connect to any controller that implements a `SearchBoxController`.
In this way, if you have your own type of `textField`, all you have to do is implement your own class that follows the `SearchBoxController` protocol and connect it to the already defined `SearchBoxInteractor`.
Many controllers are available by default.
#### Presenters
A `Presenter` component lets you control the output shown in search views.
For example, when displaying a refinement list (list of filters), a `SelectableFacet` is given to you which contains the title of the facet, the count associated with this facet, and whether or not it has been selected by users.
Given this information, you are free to decide how to you want to display them in your UI.
### View
The `View` components are the Apple Native components.
Whether they're from UIKit, AppKit, or others, you can use them alongside the defined controllers to define any UI in your search experience.
## Join the community
Ask questions and find answers on those following platforms.
* [Algolia Community Discord](https://alg.li/discord)
* [Stack Overflow](https://stackoverflow.com/questions/tagged/instantsearch)
* [GitHub issues](https://github.com/algolia/instantsearch-ios/issues)
* [Changelog](https://github.com/algolia/instantsearch-ios/blob/master/CHANGELOG.md).
# What is React InstantSearch?
Source: https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/react
React InstantSearch is an open source library for building search interfaces.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
InstantSearch focuses on enhancing your frontend with primitives that you can combine to create unique search interfaces.
InstantSearch supports [server-side rendering](/doc/guides/building-search-ui/going-further/server-side-rendering/react) and offers [full routing capabilities](/doc/guides/building-search-ui/going-further/routing-urls/react).
Run and edit the What is React InstantSearch? example in CodeSandbox.
InstantSearch offers three levels of increasing control over your UI:
1. Start with a **predefined widget** that you can configure and style with CSS.
2. To change the render output of a widget, **use Hooks** to render what you want.
3. To implement something that doesn't exist, create a **custom Hook**.
## Predefined widgets
The recommended way to use InstantSearch is with its predefined widgets such as [`SearchBox`](/doc/api-reference/widgets/search-box/react).
InstantSearch includes a [set of widgets](/doc/guides/building-search-ui/widgets/showcase/react) that are most often used in search experiences.
Widgets provide features and a rendered output.
You can place them anywhere on your UI, configure them, and style them with CSS.
For example, add the [`RefinementList`](/doc/api-reference/widgets/refinement-list/react)\` widget and ask it to show a list of brands, so your users can refine their search using those brands.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, RefinementList } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const indexName = "INDEX_NAME";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
);
}
```
### The InstantSearch wrapper
The [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) wrapper communicates between your app and Algolia.
This is where you add all the widgets.
It accepts a search client and an name.
Refinement list widget
### CSS theme
The predefined widgets in React InstantSearch are compatible with the default CSS theme:
Default theme preview
For more information, see [Style your widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#style-your-widgets).
If you use Hooks and you want to use the default theme in your app,
follow the markup from the predefined widgets and use the InstantSearch class names.
## Use Hooks
Algolia's predefined widgets, with their fixed behavior and output, may not fully meet your requirements.
For example, you might want to use React InstantSearch with a component library like [Material UI](https://mui.com/material-ui/) or render to a non-DOM target like [React Native](https://reactnative.dev/).
To address these limitations, use [Hooks](https://react.dev/reference/react/hooks) to construct the UI you want:
* [`useBreadcrumb`](/doc/api-reference/widgets/breadcrumb/react#hook)
* [`useClearRefinements`](/doc/api-reference/widgets/clear-refinements/react#hook)
* [`useConfigure`](/doc/api-reference/widgets/configure/react#hook)
* [`useCurrentRefinements`](/doc/api-reference/widgets/current-refinements/react#hook)
* [`useDynamicWidgets`](/doc/api-reference/widgets/dynamic-widgets/react#hook)
* [`useHierarchicalMenu`](/doc/api-reference/widgets/hierarchical-menu/react#hook)
* [`useHits`](/doc/api-reference/widgets/hits/react#hook)
* [`useHitsPerPage`](/doc/api-reference/widgets/hits-per-page/react#hook)
* [`useInfiniteHits`](/doc/api-reference/widgets/infinite-hits/react#hook)
* [`useMenu`](/doc/api-reference/widgets/menu/react#hook)
* [`usePagination`](/doc/api-reference/widgets/pagination/react#hook)
* [`usePoweredBy`](/doc/api-reference/widgets/powered-by/react#hook)
* [`useRange`](/doc/api-reference/widgets/range-input/react#hook)
* [`useRefinementList`](/doc/api-reference/widgets/refinement-list/react#hook)
* [`useSearchBox`](/doc/api-reference/widgets/search-box/react#hook)
* [`useSortBy`](/doc/api-reference/widgets/sort-by/react#hook)
* [`useStats`](/doc/api-reference/widgets/stats/react#hook)
* [`useToggleRefinement`](/doc/api-reference/widgets/toggle-refinement/react#hook)
For more information, see [Customize a React InstantSearch widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react).
### Example component
Here's an example of a custom `RefinementList` component created with
[`useRefinementList`](/doc/api-reference/widgets/refinement-list/react#hook),
and mounted in the [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) provider.
```jsx App.js theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch";
import { RefinementList } from "./RefinementList";
const searchClient = algoliasearch("undefined", "undefined");
function App() {
return (
);
}
```
```jsx RefinementList.js theme={"system"}
import { useRefinementList } from "react-instantsearch";
export function RefinementList(props) {
// Retrieves the refinement `items` and the `refine` function to update the
// refinement
const { items, refine } = useRefinementList(props);
return (
{items.map((item) => (
))}
);
}
function cx(...classNames) {
return classNames.filter(Boolean).join(" ");
}
```
### Pass stable references
When you provide a function to Hooks,
make sure to pass a stable reference to avoid rendering endlessly
(for example, with [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback)).
You don't need to stabilize objects and arrays, because React reuses them between renders unless you create new ones.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React, { useCallback } from "react";
import { InstantSearch, useHits } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
function Search({ cartItems }) {
const transformItems = useCallback(
(items) =>
items.map((item) => ({
...item,
isInCart: Boolean(
cartItems.find((cartItem) => cartItem.objectID === item.objectID),
),
})),
[cartItems],
);
const { hits } = useHits({ transformItems });
return <>{/* Your JSX */}>;
}
function App({ cartItems }) {
return (
);
}
```
## Custom Hooks
React InstantSearch works with all InstantSearch.js connectors: from Algolia, from the community, or even your own.
To create a Hook:
1. [Create a connector](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react#build-a-custom-connector).
2. Use [`useConnector`](/doc/api-reference/widgets/connector/react) to turn the connector into a Hook:
```js useMyWidget.js theme={"system"}
import { useConnector } from "react-instantsearch";
import { connectMyWidget } from "./connectMyWidget";
export function useMyWidget(widgetParams) {
return useConnector(connectMyWidget, widgetParams);
}
```
```jsx MyComponent.js theme={"system"}
import { useMyWidget } from "./useMyWidget";
export function MyComponent(props) {
const data = useMyWidget(props);
// Render based on the data returned by the Hook
return null;
}
```
```jsx App.js theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch";
import { MyComponent } from "./MyComponent";
const searchClient = algoliasearch("undefined", "undefined");
function App() {
return (
);
}
```
You can now use your new Hook and component anywhere within [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react).
## Resources
Use the following links and resources to learn more.
### See also
* [Install React InstantSearch](/doc/guides/building-search-ui/installation/react)
* [Get started](/doc/guides/building-search-ui/getting-started/react)
* [API reference](/doc/api-reference/widgets/react)
* [Code Exchange](https://www.algolia.com/developers/code-exchange/?category=frontend-tools)
* [Support knowledge base](https://support.algolia.com/hc/en-us/sections/19109461315345-InstantSearch-React-InstantSearch-V7)
### Join the community
Ask questions and find answers on those following platforms.
* [Algolia Community Discord](https://alg.li/discord)
* [Stack Overflow](https://stackoverflow.com/questions/tagged/instantsearch)
* [GitHub issues](https://github.com/algolia/instantsearch/issues)
* [Changelog](https://github.com/algolia/instantsearch-android/blob/master/CHANGELOG.md).
# What is Vue InstantSearch?
Source: https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/vue
What is the Vue InstantSearch UI library and what are the available customization APIs.
Vue InstantSearch is an open source UI library for Vue that lets you build a search interface in your frontend app.
InstantSearch focuses on enhancing your frontend with widgets that you can combine to create unique search interfaces.
Vue InstantSearch supports [server-side rendering](/doc/guides/building-search-ui/going-further/server-side-rendering/vue) and offers [full routing capabilities](/doc/guides/building-search-ui/going-further/routing-urls/vue).
Run and edit the What is Vue InstantSearch? example in CodeSandbox.
InstantSearch offers three levels of increasing control over your UI:
1. Start with a **predefined widget** that you can configure and style with CSS.
2. To change its render output (DOM or Native), **customize a predefined widget** to render what you want.
3. To implement something that doesn't exist, create a **custom widget**.
## Predefined widgets
The recommended way to use InstantSearch is with its predefined widgets such as [`ais-search-box`](/doc/api-reference/widgets/search-box/vue).
InstantSearch includes a [set of widgets](/doc/guides/building-search-ui/widgets/showcase/vue) that are most often used in search experiences.
Widgets provide features and a rendered output.
You can place them anywhere on your UI, configure them, and style them with CSS.
For example, add the [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue)\` widget and ask it to show a list of brands, so your users can refine their search using those brands.
```vue Vue theme={"system"}
```
Within the Vue InstantSearch documentation, widgets are components, and vice-versa.
When you see the word widget in this documentation, consider it as a Vue component.
### The InstantSearch wrapper
The [`ais-instant-search`](/doc/api-reference/widgets/instantsearch/vue) wrapper communicates between your app and Algolia.
This is where you add all the widgets.
It accepts a search client and an name.
```vue Vue theme={"system"}
```
For more information, see [Vue InstantSearch API reference](/doc/api-reference/widgets/instantsearch/vue) and [Getting started](/doc/guides/building-search-ui/getting-started/vue).
### CSS theme
The predefined widgets in Vue InstantSearch are compatible with the default CSS theme:
For more information, see [Style your widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue#style-your-widgets).
## Customize widgets
Algolia's predefined widgets, with their fixed behavior and output, may not fully meet your requirements.
For example, you might want to customize the rendering of the [`ais-menu`](/doc/api-reference/widgets/menu/vue) widget to display as a [`select`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select) element instead of a list of links.
You may also want to render it as a keyboard-controlled slideshow of images.
To address these limitations, Vue InstantSearch uses "connectors" to redefine widget behavior and DOM output.
For more information, see [Customize a Vue InstantSearch widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue).
## Custom widgets
If none of the previous solutions work for you and you want to create a new widget from scratch, InstantSearch provides a third layer of API for this: **creating custom widgets**.
This requires two APIs: the first one lets you create a new connector, and the second one lets you create a new widget.
Both solutions give you full control of the render and behavior.
When building a new widget, be prepared to dive deep into the Algolia semantics to achieve what you want.
For more information, see [Create your own Vue InstantSearch widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue).
## Resources
Use the following links and resources to learn more.
### See also
* [Install Vue InstantSearch](/doc/guides/building-search-ui/installation/vue)
* [Get started](/doc/guides/building-search-ui/getting-started/vue)
* [API reference](/doc/api-reference/widgets/vue)
* [Code Exchange](https://www.algolia.com/developers/code-exchange/?category=frontend-tools)
* [Support knowledge base](https://support.algolia.com/hc/en-us/sections/4406469062417-InstantSearch-Vue-InstantSearch)
### Join the community
Ask questions and find answers on those following platforms.
* [Algolia Community Discord](https://alg.li/discord)
* [Stack Overflow](https://stackoverflow.com/questions/tagged/instantsearch)
* [Changelog](https://github.com/algolia/instantsearch/blob/master/packages/vue-instantsearch/CHANGELOG.md).
# Create your own widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/react
Learn how you can create a custom widget for your React InstantSearch app.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
React InstantSearch comes with [multiple widgets](/doc/api-reference/widgets/react) that you can [extensively customize](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react), but you can also use connectors to change your rendering.
You can also use connectors to completely change your rendering.
If no existing widget or connector suits your needs, you can create your own.
Creating custom widgets is the most advanced way to tailor your search experience.
It requires in-depth knowledge of InstantSearch and Algolia.
This guide covers how to build a custom widget:
* [Build a custom connector](#build-a-custom-connector)
* [Build a UI with a connector](#render-a-custom-user-interface)
If you're creating a custom InstantSearch widget because you didn't find a built-in option for your use case,
consider [opening a feature request](https://github.com/algolia/instantsearch/discussions/new?category=ideas\&labels=triage\&title=Feature%20request%3A%20) to describe what you're trying to build.
## When to create custom widgets
You can create a new widget when **none of the [existing widgets](/doc/api-reference/widgets/react) fit your functional needs.**
However, if you're trying to redefine the UI or DOM output of a widget,
[extend it instead by using its connector counterpart](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#customize-the-complete-ui-of-the-widgets).
**Existing widgets and connectors should fit most of your use cases.**
Look into them before creating custom connectors.
For example, to create buttons that set predefined queries, you could use [`useSearchBox`](/doc/api-reference/widgets/search-box/react#hook).
Although you're not rendering a search box, the connector provides the necessary APIs for this, so there's no need to re-develop it.
**For help, [explain your situation and ask questions on GitHub](https://github.com/algolia/instantsearch/discussions/new?category=q-a\&labels=triage).**
You'll see references to [InstantSearch.js](/doc/guides/building-search-ui/what-is-instantsearch/js) and its APIs throughout the guide.
React InstantSearch relies on this core library and bridges them with a thin adapter.
If you're using TypeScript, install `algoliasearch-helper` and `instantsearch.js` as development dependencies to access the necessary types.
Make sure to use the same versions as the ones in your React InstantSearch version.
Once you're done building an InstantSearch.js connector, you'll [turn it into a Hook](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react#build-a-custom-connector).
## Build a custom connector
When creating a custom widget, start by **writing a connector that encapsulates all the logic of your widget**, yet keeps the rendering separate.
**This guide uses the example of a negative refinement list widget**.
It's similar to a [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) but, instead of filtering on the selected items,
it excludes them from the search.
For example, selecting the brand "Apple" would results to all matching that aren't Apple products.
### Write the connector function
Create a `connectNegativeRefinementList` function that takes a render and an unmount function.
It should return a `negativeRefinementList` function (the widget factory) that takes widget parameters and returns an object (the widget).
For the sake of simplicity, the only parameter that the widget accepts is `attribute`.
This lets users specify which record attribute to filter on.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
};
};
}
const noop = () => {};
```
```ts TypeScript theme={"system"}
import type { SearchResults } from "algoliasearch-helper";
import type { Connector } from "instantsearch.js";
export type NegativeRefinementListConnectorParams = {
attribute: string;
};
type NegativeRefinementListRenderState = {
items: SearchResults.FacetValue[];
refine(value: string): void;
};
type NegativeRefinementListWidgetDescription = {
$$type: "myOrganization.negativeRefinementList";
renderState: NegativeRefinementListRenderState;
indexRenderState: {
negativeRefinementList: {
[attribute: string]: NegativeRefinementListRenderState;
};
};
indexUiState: {
negativeRefinementList: {
[attribute: string]: string[];
};
};
};
type NegativeRefinementListConnector = Connector<
NegativeRefinementListWidgetDescription,
NegativeRefinementListConnectorParams
>;
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
};
};
};
const noop = () => {};
```
Your custom connector needs an identifier.
The naming convention is `"myOrganization.myWidget"` (for example, `"microsoft.negativeRefinementList"`).
If you don't have an organization, you can use your name.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
$$type: "myOrganization.negativeRefinementList",
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
$$type: "myOrganization.negativeRefinementList",
};
};
};
// …
```
## Compute the render state
At this point, the widget doesn't perform any custom logic.
To make it functional, you need to hook into the InstantSearch lifecycle to update search parameters, extract response data, and expose values for rendering.
#### Widget render state
Implement the `getWidgetRenderState` method.
This is where you consume data from the API response.
It should return an object with the data and APIs you want to expose to the render function.
For the negative refinement list, you need to expose:
* The `items` to display in the list.
* A `refine` function to trigger a new search from the UI with new items to exclude.
The widget parameters are also passed under the `widgetParams` key. This is necessary for internal purposes.
In the returned object from the `negativeRefinementList` function, add a function `getWidgetRenderState`.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
getWidgetRenderState({ results, helper }) {
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
getWidgetRenderState({ results, helper }) {
// …
},
};
};
};
// …
```
You need to expose a `refine` function to toggle exclusions for the given attribute.
**The `refine` function must keep the same reference whenever `getWidgetRenderState` is called** so that UI frameworks can consider it stable between renders.
To do so, create a `connectorState` object outside the widget and attach the `refine` function to it.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// An empty `connectorState` object is used to store information
// that needs to be shared across multiple method calls.
const connectorState = {};
return {
getWidgetRenderState({ results, helper }) {
// To ensure `refine` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.refine) {
connectorState.refine = (value) =>
helper.toggleFacetExclusion(attribute, value).search();
}
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
type ConnectorState = {
refine?(value: string): void;
};
// An empty `connectorState` object is used to store information
// that needs to be shared across multiple method calls.
const connectorState: ConnectorState = {};
return {
getWidgetRenderState({ results, helper }) {
// To ensure `refine` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.refine) {
connectorState.refine = (value) =>
helper.toggleFacetExclusion(attribute, value).search();
}
// …
},
};
};
};
// …
```
You need to be able to consume the facet values from the API response so you can display them on the UI,
let users click them, and so on.
Find them on the exposed `results` and store them in an `items` variable.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// Retrieve the facet values for the given attribute and sort them by
// ascending name. Store the facet values in the `items` variable.
const items =
results.getFacetValues(attribute, {
sortBy: ["name:asc"],
}) || [];
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// Retrieve facet values from the results for the given attribute
// and sort them by ascending name.
// Later on, you could let users pass a `sortBy` parameter.
const items =
(results.getFacetValues(attribute, {
sortBy: ["name:asc"],
}) as SearchResults.FacetValue[]) || [];
// …
},
};
};
};
// …
```
You need to handle what happens when there are no search results.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// When there are no results, return the API with default values.
// This shows a fallback UI while waiting for results.
if (!results) {
return { items: [], refine: connectorState.refine, widgetParams };
}
// …
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
// When there are no results, return the API with default values.
// It's helpful to render a default UI until results are available.
if (!results) {
return { items: [], refine: connectorState.refine, widgetParams };
}
// …
},
};
};
};
// …
```
Return the `items` containing the facet values and the `refine` function.
You should also return the parameters passed to the widget under the `widgetParams` key.
You can use all this data and APIs in the render function later.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
return {
items,
// Toggles exclusion of a facet value when selected.
// If the value is already excluded, the exclusion is unset.
// Otherwise, it's added to the exclusion list.
// Then, a search is triggered.
refine: connectorState.refine,
widgetParams,
};
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
getWidgetRenderState({ results, helper }) {
// …
return {
items,
// A function to toggle a value when selected.
// If the value is already excluded, the exclusion is unset.
// Otherwise, it's added to the exclusion list.
// Then, a search is triggered.
refine: connectorState.refine,
widgetParams,
};
},
};
};
};
// …
```
#### Global render state
In InstantSearch, each widget you add registers its render state in one global object.
You need to specify how to store your widget render state in this global tree by implementing the `getRenderState` method.
You might use multiple negative refinement lists in your application but with different attributes.
For example,
you might want to exclude by brand and by categories.
Here, you want to store each widget's render state individually so they don't override each other.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getRenderState(renderState, renderOptions) {
// The global render state is merged with a new one to store the render
// state of the current widget.
return {
...renderState,
negativeRefinementList: {
...renderState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's render state is stored
// by the `attribute` it affects.
[attribute]: this.getWidgetRenderState(renderOptions),
},
};
},
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getRenderState(renderState, renderOptions) {
// The global render state is merged with a new one to store the render
// state of the current widget.
return {
...renderState,
negativeRefinementList: {
...renderState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's render state is stored
// by the `attribute` it affects.
[attribute]: this.getWidgetRenderState(renderOptions),
},
};
},
};
};
};
// …
```
### Set up the lifecycle
When you add InstantSearch widgets to your app, they go through several steps in response to internal events.
**These steps are the InstantSearch lifecycle.**
You must register lifecycle hooks on your widget to run code at the `init`, `render`, and `dispose` stages.
Use these functions to call the user-provided render and unmount functions with the correct information.
The lifecycle code shouldn't change, so you can copy the code from this step without modifying it.
Most of the custom logic happens in `getWidgetRenderState`.
The `init` step runs when the app starts (before the initial search is performed).
**Don't use this function to add Algolia-related logic**.
Instead, use `getWidgetSearchParameters`, `getWidgetUiState`, or `getWidgetRenderState`.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=true` lets you perform
// conditional logic in the render function.
true,
);
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
init(initOptions) {
const { instantSearchInstance } = initOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(initOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=true` lets you perform
// conditional logic in the render function.
true,
);
},
// …
};
};
};
// …
```
The `render` step runs whenever new results come back from Algolia.
It's usually triggered by search state changes,
such as when a user submits a new query or clicks on a filter.
During this step, the widget can react to the updated search results by re-rendering with the new information.
This lets the widget remain synchronized with the current state of the search experience.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=false` lets you perform
// conditional logic in the render function.
false,
);
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
render(renderOptions) {
const { instantSearchInstance } = renderOptions;
renderFn(
// The render state is the data provided to the render function,
// necessary to build the UI.
{
...this.getWidgetRenderState(renderOptions),
instantSearchInstance,
},
// Calling the function with `isFirstRender=false` lets you perform
// conditional logic in the render function.
false,
);
},
// …
};
};
};
// …
```
The `dispose` step runs when removing the widget.
Use it to clean up anything the widget created during its "lifetime" such as search parameters, UI, and event listeners.
This helps prevent memory leaks and ensures the widget doesn't continue affecting the search experience once it's no longer in use.
```js JavaScript icon="code" theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList({ attribute }) {
return {
// …
dispose(disposeOptions) {
unmountFn();
},
// …
};
};
}
// …
```
```ts TypeScript icon="code" theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList({ attribute }) {
return {
// …
dispose(disposeOptions) {
unmountFn();
},
// …
};
};
};
// …
```
### Interact with routing
An important aspect of building an InstantSearch widget is how to make it work with [routing](/doc/guides/building-search-ui/going-further/routing-urls/react).
**Your custom widget should be able to synchronize its state with the browser URL** so you can share a link to your search experience in any given state.
#### Setting the widget UI state
In InstantSearch, routing uses an internal [`uiState`](/doc/api-reference/widgets/ui-state/react) object to derive the route.
As with the [render state](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react#compute-the-render-state), you need to specify how to store your widget UI state in the global UI state by implementing the `getWidgetUiState` method.
As with `getRenderState`, since you might use the widget multiple times with different attributes, you need to store each widget's UI state individually so they don't override each other.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetUiState(uiState, { searchParameters }) {
// The global UI state is merged with a new one to store the UI
// state of the current widget.
return {
...uiState,
negativeRefinementList: {
...uiState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's UI state is stored by // the `attribute` it impacts. [attribute]: searchParameters.getExcludeRefinements(attribute), }, }; }, }; }; } // …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetUiState(uiState, { searchParameters }) {
// The global UI state is merged with a new one to store the UI
// state of the current widget.
return {
...uiState,
negativeRefinementList: {
...uiState.negativeRefinementList,
// You can use multiple `negativeRefinementList` widgets in a single
// app so you need to register each of them separately.
// Each `negativeRefinementList` widget's UI state is stored by // the `attribute` it impacts. [attribute]: searchParameters.getExcludeRefinements(attribute), }, }; }, }; }; } // …
```
#### Set the widget search parameters
When initializing InstantSearch state from a URL, you need to convert the URL into [search parameters](/doc/api-reference/search-api-parameters) to trigger the first search.
To define how to derive search parameters from the UI state, use the `getWidgetSearchParameters` method.
This method lets you modify the existing search parameters based on the widget's configuration and the current UI state.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetSearchParameters(searchParameters, { uiState }) {
const state = searchParameters.addFacet(attribute);
const values = uiState.negativeRefinementList?.[attribute];
if (Array.isArray(values)) {
return values.reduce(
(acc, curr) => acc.addExcludeRefinement(attribute, curr),
state,
);
}
return state;
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
import type { SearchResults, SearchParameters } from "algoliasearch-helper";
// …
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop,
) => {
return function negativeRefinementList(widgetParams) {
const { attribute } = widgetParams;
return {
// …
getWidgetSearchParameters(searchParameters, { uiState }) {
const state = searchParameters.addFacet(attribute);
const values = uiState.negativeRefinementList?.[attribute];
if (Array.isArray(values)) {
return values.reduce(
(acc, curr) => acc.addExcludeRefinement(attribute, curr),
state,
);
}
return state;
},
};
};
};
// …
```
## Sending events to the Insights API
To better understand your users, you could capture when they use the widget to exclude refinements.
The Insights API lets you collect such events from the frontend so that you can, later on, unlock features such as [Algolia Recommend](/doc/guides/algolia-recommend/overview), [Click, conversion, and revenue analytics](/doc/guides/search-analytics/overview), and [other features](/doc/guides/sending-events).
You can set up your widget so it automatically sends the right events to Algolia Insights when using the [Insights middleware](/doc/api-reference/widgets/insights/react).
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// To ensure `sendEvent` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.sendEvent) {
connectorState.sendEvent = (
eventType,
facetValue,
eventName = 'Negative Filter Applied'
) => {
if (helper.state.isExcludeRefined(attribute, facetValue)) {
instantSearchInstance.sendEventToInsights({
insightsMethod: 'clickedFilters',
widgetType: this.$$type,
eventType,
payload: {
eventName,
index: helper.getIndex(),
filters: [`${attribute}:-${facetValue}`],
},
attribute,
});
}
};
}
if (!connectorState.refine) {
connectorState.refine = (value) => {
helper.toggleFacetExclusion(attribute, value);
// Send `click` event once the facet is toggled.
connectorState.sendEvent('click', value);
return helper.search();
};
}
// …
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop
) => {
return function negativeRefinementList(widgetParams) {
// …
type ConnectorState = {
// …
sendEvent?(
eventType: 'view' | 'click' | 'conversion',
facetValue: string,
eventName?: string
): void;
};
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// To ensure `sendEvent` keeps the same reference across renders, create
// and store it once outside the method scope.
if (!connectorState.sendEvent) {
connectorState.sendEvent = (
eventType,
facetValue,
eventName = 'Negative Filter Applied'
) => {
if (helper.state.isExcludeRefined(attribute, facetValue)) {
instantSearchInstance.sendEventToInsights({
insightsMethod: 'clickedFilters',
widgetType: this.$$type,
eventType,
payload: {
eventName,
index: helper.getIndex(),
filters: [`${attribute}:-${facetValue}`],
},
attribute,
});
}
};
}
if (!connectorState.refine) {
connectorState.refine = (value) => {
helper.toggleFacetExclusion(attribute, value);
// Send `click` event once the facet is toggled.
connectorState.sendEvent!('click', value);
return helper.search();
};
}
// …
},
};
};
}
// …
```
Now when clicking on a refinement, it automatically sends an event to Algolia Insights.
Note that this can only work when [providing an Insights client with the Insights middleware](/doc/api-reference/widgets/insights/react).
You can make the connector even more flexible by providing the `sendEvent` function to the render function.
This lets you customize events depending on the use case.
```js JavaScript theme={"system"}
export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// …
return {
// …
sendEvent: connectorState.sendEvent,
};
},
};
};
}
// …
```
```ts TypeScript theme={"system"}
export const connectNegativeRefinementList: NegativeRefinementListConnector = (
renderFn,
unmountFn = noop
) => {
return function negativeRefinementList(widgetParams) {
// …
return {
// …
getWidgetRenderState({ results, helper, instantSearchInstance }) {
// …
return {
// …
sendEvent: connectorState.sendEvent,
};
},
};
};
}
// …
```
## Using a custom connector as a Hook
To make a connector more idiomatically consumable in a React context, you need to turn it into a Hook.
React InstantSearch exposes [`useConnector`](/doc/api-reference/widgets/connector/react) to use InstantSearch.js connectors as Hooks.
```js JavaScript theme={"system"}
import { useConnector } from 'react-instantsearch';
import { connectNegativeRefinementList } from './connectNegativeRefinementList';
export function useNegativeRefinementList(props, additionalWidgetProperties) {
return useConnector(
connectNegativeRefinementList,
props,
additionalWidgetProperties
);
}
```
```ts TypeScript theme={"system"}
import { useConnector } from 'react-instantsearch';
import { connectNegativeRefinementList } from './connectNegativeRefinementList';
import type { AdditionalWidgetProperties } from 'react-instantsearch';
import type { NegativeRefinementListConnectorParams } from './connectNegativeRefinementList';
export type NegativeRefinementListParams =
NegativeRefinementListConnectorParams;
export function useNegativeRefinementList(
props: NegativeRefinementListParams,
additionalWidgetProperties: AdditionalWidgetProperties
) {
return useConnector(
connectNegativeRefinementList,
props,
additionalWidgetProperties
);
}
```
You can use the Hook in any React component nested under [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) to consume the [`uiState`](/doc/api-reference/widgets/ui-state/react) from your negative refinement and interact with it.
```jsx React icon=code theme={"system"}
function NegativeCategoriesList() {
const { items, refine } = useNegativeRefinementList({
attribute: "brand",
});
return <>{/* Your JSX */}>;
}
```
## Render a custom user interface
An InstantSearch widget is a custom connector with a render function.
In a React context, it translates to a component that consumes a Hook and renders a UI.
In this example, the `NegativeRefinementList` component uses the `useNegativeRefinementList` Hook to build a reactive, stateful UI.
This widget is usable in any React InstantSearch app.
```jsx JavaScript theme={"system"}
import React from "react";
import { useNegativeRefinementList } from "./useNegativeRefinementList";
export function NegativeRefinementList(props) {
const { items, refine, canRefine } = useNegativeRefinementList(props, {
// This is helpful for debugging purposes and allows to differentiate
// between the connector and the widget.
$$widgetType: "myOrganization.negativeRefinementList",
});
return (
{items.map((item) => (
))}
);
}
function cx(...classNames) {
return classNames.filter(Boolean).join(" ");
}
```
```tsx TypeScript theme={"system"}
import React from "react";
import { useNegativeRefinementList } from "./useNegativeRefinementList";
import type { NegativeRefinementListParams } from "./useNegativeRefinementList";
export function NegativeRefinementList(props: NegativeRefinementListParams) {
const { items, refine, canRefine } = useNegativeRefinementList(props, {
// This is helpful for debugging purposes and allows to differentiate
// between the connector and the widget.
$$widgetType: "myOrganization.negativeRefinementList",
});
return (
{items.map((item) => (
))}
);
}
function cx(
...classNames: Array
) {
return classNames.filter(Boolean).join(" ");
}
```
InstantSearch widgets use a [standardized class naming convention](#expose-standard-options).
The widget is now [usable in an InstantSearch application](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react#use-the-custom-widget).
Still, if you want to reuse or distribute the widget, you can [further tweak the API](#making-the-widget-reusable) to use the same standards as the built-in React InstantSearch widgets.
## Making the widget reusable
You might want to reuse your widget within your app, share it across multiple projects, or even publish it on [npm](https://www.npmjs.com/) for others to enjoy.
To do so, **you can provide APIs to allow customization** while abstracting the complexity away.
InstantSearch exposes consistent APIs.
You can follow the same guidelines and conventions in your own widgets and connectors.
### Forwarding root props
Forward props to the root element to support styling, accessibility, and testing.
```diff JavaScript theme={"system"}
export function NegativeRefinementList({ attribute, ...props }) {
// …
return (
+
);
}
```
### Expose standard classes
Widgets expose a class on each DOM element to help you style them.
Built-in InstantSearch widgets use the [SUITCSS](https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md#components) component syntax:
```txt theme={"system"}
[-][-descendantName][--modifierName]
```
* **Every class starts with the `ais-` namespace** (for Algolia InstantSearch). This helps target all InstantSearch elements with selectors like `[class^="ais-"]`.
* **Every class has a component name mapped to the widget name.** In the example on this page, the widget uses the `NegativeRefinementList` component name. Component names are always in [Pascal case](https://en.wiktionary.org/wiki/Pascal_case).
* **Each element is identified with a descendant name.** In the example on this page, each item of the widget uses the `-item` descendant name. Descendant names are always in camel case.
* If an element has multiple states, **identify each state with a "modifier".** In the example on this page, the selected item of the widget uses the `--selected` modifier. Modifiers are always in camel case and prefixed with two hyphens. You should include the modified class on the element in addition to the base component class (for example, `ais-NegativeRefinementList-item` and `ais-NegativeRefinementList-item--selected`).
See these conventions [in action](#render-a-custom-user-interface).
If you're using a CSS linter like [Stylelint](https://stylelint.io/),
you can validate your InstantSearch classes with the following regular expression:
```txt theme={"system"}
^ais-(?:[A-Z][a-z]+)+(?:-[a-z]+(?:[A-Z][a-z]+)*)?(?:--[a-z]+(?:[A-Z][a-z]+)*)?$
```
### Pass custom classes
Widgets expose [standardized class names](#expose-standard-options) to let users write custom CSS but you could open the styling API further to allow passing classes directly on each element.
This lets users of class-based CSS frameworks like [Bootstrap](https://getbootstrap.com/) or [Tailwind CSS](https://tailwindcss.com/) use them without friction or workarounds.
In built-in widgets, the convention is to provide a prop that takes an object of named classes.
```jsx JavaScript theme={"system"}
export function NegativeRefinementList({
attribute,
classNames = {},
...props
}) {
// …
return (
);
}
```
### Customize the UI
**UI customizability is an important aspect of building reusable InstantSearch widgets.**
If you need to internationalize your app, control the markup, or change icons, you shouldn't have to opt out of using widgets and resort to using connectors just to tweak the UI.
#### Translations
In React InstantSearch, widgets expose a `translations` prop—a dictionary to customize the UI text and support internationalization.
```jsx JavaScript theme={"system"}
export function NegativeRefinementList({
attribute,
translations,
classNames = {},
...props
}) {
// …
const { countText } = {
countText({ count }) {
return `${count} hits`;
},
...translations,
};
return (
);
}
```
Translations are strings, but you can use functions that return strings if you need to expose data.
#### Templates
If users need to control the markup, they shouldn't have to reach out to connectors.
Instead, you can let them template parts of the UI.
For example, users may want to change the rendering of each item to display a custom checkbox that requires additional markup.
In React InstantSearch, widgets expose component props that accept React components.
```jsx JavaScript theme={"system"}
export function NegativeRefinementList({
attribute,
listItemComponent: ListItemIcon = DefaultListItemIcon,
translations,
classNames = {},
...props
}) {
// …
return (
);
}
```
### Expose standard options
Each widget and connector exposes options specific to its use case.
Some options are available across multiple widgets and connectors to address common issues and offer a consistent experience.
In addition to root props, classes, translations, and templates,
you can expose the following options on widgets and connectors when relevant.
#### Target a record attribute
When a widget needs to target a specific record attribute, expose an `attribute` option in the connector.
This is the case for most refinement widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) so you can select the specific attribute to refine.
The `attribute` option typically accepts a string.
For deeply nested objects, you can accept dot-separated values like `"brand.name"` or an array of strings like `["brand", "name"]`.
#### Include or exclude attributes
When a widget manipulates selected refinements, you may want to let users select what attributes to include or exclude.
This is the case with widgets like [`CurrentRefinements`](/doc/api-reference/widgets/current-refinements/react) or [`ClearRefinements`](/doc/api-reference/widgets/clear-refinements/react) so you can hand-pick the attributes to manipulate.
#### Limit items
When a widget manipulates items, notably facet refinements, you may want to let users limit the number of items to retrieve.
Widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) or [`Menu`](/doc/api-reference/widgets/menu/react) expose a `limit` option, which is directly forwarded to the Algolia search engine with the [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet) search parameter.
Such widgets expose two extra options to let users toggle more facets: `showMore`, a boolean option to enable the feature, and `showMoreLimit`, to define the maximum number of items to display if the widget is showing more items.
#### Transform items
When a widget manipulates items like hits or facets, users may want to change them before rendering.
To do so, expose a `transformItems` option for transforming, removing, or reordering items on the connector.
The `transformItems` option is a function that receives the items and should return a new array of the same shape.
The default value is an identity function.
## Use the custom widget
You can use a custom widget like any widget provided by the library.
It takes the parameters to forward to the connectors,
HTML props for the root element, and an object for classes.
```js JavaScript icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch";
import { NegativeRefinementList } from "./NegativeRefinementList";
const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");
function App() {
return (
{/* … */}
);
}
```
This is what the widget looks like in an app:
## Next steps
You now have a good starting point to take full control of your InstantSearch experience.
You can further improve it by:
* Extending the widget's API with useful parameters such as those in [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#instance-options).
* Open sourcing your custom widget on [GitHub](https://github.com/) and publish it on [npm](https://www.npmjs.com/) (if you do, [let Algolia know.](https://github.com/algolia/instantsearch/discussions/new?category=show-and-tell)).
# Create your widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue
Learn how you can create a custom widget for your Vue InstantSearch app.
You can build your own components when the provided ones aren't sufficient.
If you're creating a custom InstantSearch widget because you didn't find a built-in option for your use case,
consider [opening a feature request](https://github.com/algolia/instantsearch/discussions/new?category=ideas\&labels=triage\&title=Feature%20request%3A%20) to describe what you're trying to build.
## Existing connectors
Sometimes you might want to create a component which uses a piece of data not provided by any of the widgets.
You also might want to make a custom component for having access to the data in different places than the template.
You can do this with the `createWidgetMixin` function exposed by Vue InstantSearch.
It works together with the [connectors](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#customize-the-complete-ui-of-the-widgets) from InstantSearch.js.
Choose the [`connectMenu`](/doc/api-reference/widgets/menu/js) connector for this example.
```vue Vue 3 theme={"system"}
```
```vue Vue 2 theme={"system"}
```
All information from that connector will then be available to your template as well as your other Vue lifecycle methods (after `created`).
All information will be available on `this.state` on your instance, and will be `null` initially
(so make sure your code is safe by wrapping usage in the template for example in an `v-if="state"`).
All connectors expect to be passed widget parameters.
Since these change over the lifecycle of a component, and are usually passed through props, the widget mixin expects a property `widgetParams` on the component's instance.
Create it with a `computed` property, based on props:
```js Vue icon=code theme={"system"}
export default {
// ...
props: {
attribute: {
type: String,
required: true,
},
},
computed: {
widgetParams() {
return {
attribute: this.attribute,
};
},
},
};
```
Use `state` here to filter over the items.
```js Vue icon=code theme={"system"}
export default {
// ...
computed: {
// ...
items() {
// Not needed here if you v-if in the template
// Only labels of three character long allowed
return this.state.items.filter(item => item.label.length === 3)
},
},
};
```
This is also possible with the `transform-items` prop on [`ais-menu`](/doc/api-reference/widgets/menu/vue)).
For more information, see [UI state](/doc/api-reference/widgets/ui-state/vue).
```vue Vue theme={"system"}
```
## Custom connectors
If you want to make your own connector, you can do that using a function with this signature:
```js JavaScript icon=code theme={"system"}
const connector = (renderFn, unmountFn) => (widgetParams = {}) => ({
init({ instantSearchInstance }) {
renderFn(
{
/* Anything you put here comes in this.state */
},
true
);
},
render({ results, instantSearchInstance }) {
renderFn(
{
/* Anything you put here comes in this.state */
},
false
);
},
dispose() {
unmountFn();
},
});
```
That lets you access the lowest level of abstraction,
including the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js#algolia-search-helper).
# Customize existing widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react
Learn how you can customize an existing React InstantSearch widget.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
React InstantSearch offers various APIs to let you **customize your search and discovery experience without considering its inner workings.**
For example, you can customize how the experience looks:
* [Highlight and snippet your search results](#highlight-and-snippet-your-search-results)
* [Style your widgets](#style-your-widgets)
* [Translate your widgets](#translate-your-widgets)
* [Using custom components with Hooks](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#custom-components-with-hooks)
* [Building a virtual widget with Hooks](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#build-virtual-widgets-with-hooks)
* [Using custom components without Hooks](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#custom-components-without-hooks)
You can also adjust the data sent to or received from Algolia:
* [Change the list of items in widgets](#change-the-list-of-items-in-widgets)
* [Apply default values to widgets](#apply-default-values-to-widgets)
* [Manually set search parameters](#manually-set-search-parameters)
## Highlight and snippet your search results
Search helps users understand what they're seeing in the results.
When users run a ,
they should be able to tell why a result matches.
Algolia highlights the matched text in each result.
Algolia can also generate snippets:
short excerpts from longer text, truncated to a fixed length around the match.
Snippeted attributes are also highlighted.
When working with [`Snippet`](/doc/api-reference/widgets/snippet/react),
the attribute must be set up in [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet) either inside the Algolia dashboard or at runtime.
### On the web
React InstantSearch provides the [`Highlight`](/doc/api-reference/widgets/highlight/react) and [`Snippet`](/doc/api-reference/widgets/snippet/react) components to highlight and snippet your Algolia search results. Both widgets take two props:
* `attribute`: the path to the highlighted attribute of the hit
* `hit`: a single result object
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { Highlight, Hits, InstantSearch, Snippet } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
function Hit({ hit }) {
return (
);
}
function App() {
return (
);
}
```
### With React Native
The [`Highlight`](/doc/api-reference/widgets/highlight/react) and [`Snippet`](/doc/api-reference/widgets/snippet/react) components are designed for the web platform.
To [highlight matches in a React Native app](/doc/guides/building-search-ui/going-further/native/react#highlight-matches),
you can build a custom `Highlight` component using the provided [`getHighlightedParts`](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.js/src/lib/utils/getHighlightedParts.ts) and [`getPropertyByPath`](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.js/src/lib/utils/getPropertyByPath.ts) utilities from `instantsearch.js`.
## Style your widgets
All React InstantSearch widgets use a set of conventional CSS classes compatible with the InstantSearch CSS themes.
If you don't want to use the themes, you can either write your own theme by following the existing classes and customizing the styles, or override the classes to pass yours instead.
### Load the InstantSearch theme
React InstantSearch doesn't inject any styles by default but comes with two optional themes: **Algolia** and **Satellite**.
#### From a CDN
The themes are available on jsDelivr.
Unminified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite.css`
Minified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite-min.css`
The `reset` style sheet is included by default in the Satellite theme,
so you don't need to import it separately.
Either copy the files into your own app or use a direct link to jsDelivr.
```html HTML icon=code-xml theme={"system"}
```
#### Using npm
If your project imports CSS into JavaScript files using build tools,
you can install the theme with npm.
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
```sh pnpm theme={"system"}
pnpm add instantsearch.css
```
Then, you can import the theme directly into your app.
```js JavaScript icon=code theme={"system"}
import "instantsearch.css/themes/satellite.css";
```
### Write your own theme
You can create your own theme based on the InstantSearch CSS classes.
Either inspect the DOM with your developer tools and style accordingly or [reuse an existing theme and customize it](https://github.com/algolia/instantsearch/blob/master/packages/instantsearch.css/src/themes/algolia.scss).
```css CSS icon=paintbrush theme={"system"}
.ais-Breadcrumb-item--selected,
.ais-HierarchicalMenu-item--selected,
.ais-Menu-item--selected {
font-weight: semibold;
}
/* ... */
```
### Customize with CSS variables
The InstantSearch.css themes provide CSS variables that you can override to customize colors, spacing, typography, and more without writing custom CSS classes.
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 62, 52, 211;
/* Larger spacing */
--ais-spacing-factor: 1.5;
}
```
For a complete list of available CSS variables and customization options, see [Styling and theming](/doc/guides/building-search-ui/styling/react).
### Pass custom CSS classes to widgets
If you're using a class-based CSS framework like [Bootstrap](https://getbootstrap.com/) or [Tailwind CSS](https://tailwindcss.com/),
you can pass your own CSS classes to the `classNames` prop to style each element of the widgets.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
);
}
```
If you only want to pass classes to the root element, use the [`className`](https://react.dev/learn#adding-styles) React prop.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
);
}
```
## Translate your widgets
You can customize every piece of static text in React InstantSearch widgets,
such as "Load more" or "Show more" with the `translations` prop.
This is useful if you want to change the text or work with a localized app.
The `translations` prop is a key-value mapping.
Each translation is either a `string` or a `function` that exposes some state.
You can find the exact signature of the prop for each widget in their API reference.
### With strings
Most translations expect string values.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
);
}
```
### With functions
For dynamic values, translations expose a function.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
isShowingMore ? "Voir moins" : "Voir plus",
}}
/>
);
}
```
## Change the list of items in widgets
Every widget and Hook that handles a list of items exposes a `transformItems` prop that lets you transform the items before displaying them in the UI.
This is useful to sort or filter items.
You can also use Hooks to add items that don't come from Algolia, or append cached results to the list of displayed items.
### Sort items
#### Using `transformItems`
This example uses the `transformItems` prop to order the current refinements by ascending `attribute`:
```jsx React icon=code theme={"system"}
// ...
import { orderBy } from "lodash";
// ...
function App() {
return (
{/* ... */}
orderBy(items, "attribute", "asc")}
/>
);
}
```
#### Using `sortBy`
If you want to sort items in a [`RefinementList`](/doc/api-reference/widgets/refinement-list/react), use the [`sortBy`](/doc/api-reference/widgets/refinement-list/react#param-sort-by) prop.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
);
}
```
### Filter items
This example uses the `transformItems` prop to filter out items when the `count` is lower than 150.
```jsx React icon=code theme={"system"}
// ...
function App() {
return (
{/* ... */}
items.filter((item) => item.count >= 150)}
/>
);
}
```
### Display static values
The facet values exposed in widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) or [`Menu`](/doc/api-reference/widgets/menu/react) are dynamic, and update with the context of the search.
However, sometimes you may want to display a static list that never changes.
You can do so using `transformItems`.
This example uses `transformItems` to display a static list of values.
This [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) always and only displays the items "iPad" and "Printers".
```jsx React icon=code theme={"system"}
// ...
import { InstantSearch, RefinementList } from "react-instantsearch-web";
// ...
const staticItems = [
{ label: "Appliances", value: "Appliances" },
{ label: "Cell Phones", value: "Cell Phones" },
];
function App() {
return (
{/* ... */}
{
return staticItems.map((staticItem) => ({
...staticItem,
...items.find((item) => item.value === staticItem.value),
}));
}}
/>
);
}
```
### Display facets with no matches
Showing facets that don't match the current query helps users explore and refine results.
Algolia returns only facet values that match the current query.
To show non-matching values, add frontend logic.
For example, you could cache facet values the first time you receive them.
Then, if Algolia returns fewer values than your facet limit,
append the cached values.
This approach has limitations:
* It doesn't work with [search for facet values](/doc/guides/managing-results/refine-results/faceting#search-for-facet-values) because Algolia doesn't return non-matching values. As a result, highlighting doesn't work for cached values.
* You may need to re-apply sorting in the [`transformItems`](#using-transformitems) function because InstantSearch applies its internal sorting before calling the function.
```jsx App.js theme={"system"}
import React from "react";
import { InstantSearch } from "react-instantsearch";
import { CustomRefinementList } from "./CustomRefinementList";
import { indexName, searchClient } from "./searchClient";
function App() {
return (
);
}
```
```jsx CustomRefinementList.js theme={"system"}
import React from "react";
import { useRefinementList } from "react-instantsearch";
import { indexName, searchClient } from "./searchClient";
import { uniqBy } from "./uniqBy";
const brandAttribute = "brand";
const brandLimit = 10;
const initialFacets = [];
// On the first load, we store all facets returned by Algolia
// so we can append them to the returned facets when Algolia returns enough.
// We set their `count` to 0 because they'll show up when nothing
// else matches.
// Note that this triggers another query.
searchClient
.searchForFacetValues([
{
indexName,
params: {
facetName: brandAttribute,
facetQuery: "",
maxFacetHits: brandLimit,
},
},
])
.then(([{ facetHits }]) => {
initialFacets.push(
...facetHits.map((facet) => ({
...facet,
label: facet.value,
value: facet.value,
isRefined: false,
count: 0,
})),
);
});
// If Algolia doesn't return enough results, we lose track of a
// potentially refined facet.
// For example, if you refine on "Apple", then search for "chromecast",
// "Apple" is no longer returned, and we don't know that it was selected
// based on the initial facets.
// We need to keep track of the last state to reflect the fact that it
// was refined in the UI.
const currentRefinement = new Set();
export function CustomRefinementList(props) {
const { items, refine } = useRefinementList(props);
function toggle(item) {
currentRefinement.clear();
item.value.forEach((value) => currentRefinement.add(value));
// didn't use new Set([...]) because IE 11 doesn't support it.
// ref: http://kangax.github.io/compat-table/es6/#test-Set
refine(item.value);
}
// If a cached facet is already returned by Algolia, we want it to be
// displayed rather than to display its cached value.
// You might need to sort the items again here because the internal
// sorting already happens before `items` is provided by `connectRefinementList`.
const combinedItems = uniqBy([...items, ...initialFacets], "label")
.slice(0, brandLimit)
.map((item) => {
// Here we re-calculate `isRefined` and `value` for each item
// based on `currentRefinement` which we cached at `toggle`.
const isRefined = currentRefinement.has(item.label);
const value =
isRefined === false
? [...currentRefinement, item.label]
: [...currentRefinement].filter((label) => label !== item.label);
return {
...item,
isRefined,
value,
};
});
return (
{combinedItems.map((item, index) => (
))}
);
}
```
```jsx searchClient.js theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
export const indexName = "instant_search";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
export const searchClient = algoliasearch(appID, apiKey);
```
```jsx uniqBy.js theme={"system"}
export function uniqBy(items, property) {
const seen = {};
return items.filter((item) => {
const val = item[property];
if (seen[val]) {
return false;
} else {
seen[val] = true;
return true;
}
});
}
```
This solution comes with limitations:
* Facet hits from a [faceted search](/doc/guides/managing-results/refine-results/faceting#search-for-facet-values) won't work because Algolia only returns matching facets (the highlighting can't apply to cached items).
* You might need to sort again in the custom widget because the internal sorting happens before rendering.
## Apply default values to widgets
When first loading the page, you might want to assign default values to some widgets.
For example, you may want to set a default filter based on a user setting, pre-select an item in a [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) when you're on a category page, or sync the UI with your URL state.
### Providing an initial state
Providing an initial state is useful when you want to start from a given [`uiState`](/doc/api-reference/widgets/ui-state/react) but you expect it to change from user interactions with the widgets.
This example provides an [`initialUiState`](/doc/api-reference/widgets/instantsearch/react#param-initial-ui-state) to React InstantSearch.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch, Pagination, SearchBox } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
const indexName = "instant_search";
function App() {
return (
{/* ... */}
);
}
```
Since [`initialUiState`](/doc/api-reference/widgets/instantsearch/react#param-initial-ui-state)
sets the initial state of your UI (widgets),
you must also add the corresponding widgets to your implementation,
or [use virtual widgets](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#build-virtual-widgets-with-hooks).
### Sync UI state and URL
Syncing your UI with the browser URL is a good practice.
It lets users take one of your results pages,
copy the URL, and share it.
It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.
To sync the UI state and the URL, use the [`routing`](/doc/api-reference/widgets/instantsearch/react#param-routing) option.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, SearchBox } from "react-instantsearch";
const searchClient = algoliasearch(
ALGOLIA_APPLICATION_ID,
ALGOLIA_SEARCH_API_KEY,
);
function App() {
return (
{/* ... */}
);
}
```
Note the presence of the [`SearchBox`](/doc/api-reference/widgets/search-box/react) widget.
When using [`routing`](/doc/api-reference/widgets/instantsearch/react#param-routing),
you must also add the corresponding widgets to your implementation.
To learn more, see [Sync your URLs](/doc/guides/building-search-ui/going-further/routing-urls/react).
## Manually set search parameters
Algolia supports a [wide range of search parameters](/doc/api-reference/search-api-parameters).
If you want to use a search parameter that isn't covered by any widget or Hook,
use the [`Configure`](/doc/api-reference/widgets/configure/react) widget.
Using [`Configure`](/doc/api-reference/widgets/configure/react) is also useful to provide a search parameter that users aren't meant to interact with directly.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { Configure, InstantSearch } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
);
}
```
### Filter results without using widgets or Hooks
If you need to set a fixed filter without letting users change it,
pass [`filters`](/doc/api-reference/api-parameters/filters) to the [`Configure`](/doc/api-reference/widgets/configure/react) widget.
This can be useful, for example, to reflect user preferences saved in your app.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { Configure, InstantSearch } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
);
}
```
Don't set filters on [`Configure`](/doc/api-reference/widgets/configure/react) for an attribute already managed by a widget, or they will conflict.
### Dynamically update search parameters
The [`InstantSearch`](/doc/api-reference/widgets/instantsearch/react) root component exposes an [`onStateChange`](/doc/api-reference/widgets/instantsearch/react#param-on-state-change) prop.
This function triggers whenever the UI state changes (such as typing in a search box or selecting a refinement).
You get a chance to alter the next UI state or perform custom logic before applying it.
In this example, there's a static "All" item at the top of the categories refinement list.
When checked, it clears all existing category refinements.
This is achieved by intercepting the next UI state in [`onStateChange`](/doc/api-reference/widgets/instantsearch/react#param-on-state-change) before it's applied and changing it with custom logic.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch, RefinementList } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
const indexName = "instant_search";
export function App() {
return (
{
const categories = uiState[indexName].refinementList?.categories || [];
const [lastSelectedCategory] = categories.slice(-1);
setUiState({
...uiState,
[indexName]: {
...uiState[indexName],
refinementList: {
...uiState[indexName].refinementList,
categories: categories.filter((category) =>
lastSelectedCategory === "All" ? false : category !== "All",
),
},
},
});
}}
>
[
{
label: "All",
value: "All",
count: null,
isRefined: items.every((item) => !item.isRefined),
},
].concat(items)
}
/>
{/* ... */}
);
}
```
If you've set a search parameter using [`Configure`](/doc/api-reference/widgets/configure/react), you can update it like any React state.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React, { useEffect, useState } from "react";
import { Configure, InstantSearch } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
const languages = [
{ label: "Français", value: "fr_FR" },
{ label: "English", value: "en_US" },
];
export function App() {
const [language, setLanguage] = useState(languages[0].value);
return (
<>
{/* ... */}
>
);
}
```
## Customize the complete UI of the widgets
Hooks are the headless counterparts of widgets.
They return APIs to build the UI as you see fit.
If you feel limited by the provided customization options,
**you can use Hooks to control the render output.**
This is useful when [using React InstantSearch together with a component library](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#custom-components-with-hooks) or when [rendering to a non-DOM target like React Native](/doc/guides/building-search-ui/going-further/native/react).
Hooks can also be helpful if you want to create a custom behavior or a radically different UI based on the APIs of an existing widget.
For example, to set pre-determined search queries by clicking on a button,
you could use [`useSearchBox`](/doc/api-reference/widgets/search-box/react#hook) and render a button that sets a given query on click.
```jsx React icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch, useSearchBox } from "react-instantsearch";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
function App() {
return (
);
}
function PopularQueries({ queries, ...props }) {
const { refine } = useSearchBox(props);
return (
{queries.map((query) => (
))}
);
}
```
Another example is when you need to create a [virtual widget](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#build-virtual-widgets-with-hooks) to apply a refinement without displaying anything on the page.
### When not to use Hooks
When you need to customize the UI of a widget, you might be tempted to use Hooks.
While this grants you complete control, it also means **you become responsible for adequately implementing UI logic and accessibility.**
The provided UI components already handle this complexity and provide many customization options for you to change the way they look.
If you only need to make visual changes to a widget, **use customization options instead of reaching for Hooks.**
To summarize, avoid using Hooks to:
* Customize styles ([write your own theme](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#write-your-own-theme), or [pass custom `classNames`](#pass-custom-css-classes-to-widgets) instead)
* Set DOM props on the top-level element (forward them on the widget directly instead)
* Transform the items before rendering them ([use `transformItems` when applicable](#change-the-list-of-items-in-widgets) instead)
* Apply default values ([use `initialUiState`](#apply-default-values-to-widgets) instead)
### Custom components with Hooks
If you're using a component library like [Material UI](https://mui.com/) or your own,
you might want to use these components instead of the provided ones.
You can use Hooks to access the necessary state and APIs to stitch React InstantSearch together with your custom components.
```js App.js theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch } from "react-instantsearch";
import { CustomSortBy } from "./CustomSortBy";
const searchClient = algoliasearch("undefined", "undefined");
function App() {
return (
);
}
```
```js CustomSortBy.js theme={"system"}
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import React from "react";
import { useSortBy } from "react-instantsearch";
export function CustomSortBy(props) {
const { currentRefinement, options, refine } = useSortBy(props);
return (
Sort by
);
}
```
If you want to customize a widget, click its entry in the [React InstantSearch API reference](/doc/api-reference/widgets/react) and then look up the **Hook** section on that page.
### Build virtual widgets with Hooks
A virtual widget is an InstantSearch widget mounted in the app **but which doesn't render anything.**
Widgets do more than displaying a UI: they each interact with a piece of the UI state that maps to one or more Algolia search parameters. For example, the [`SearchBox`](/doc/api-reference/widgets/search-box/react) controls the [`query`](/doc/api-reference/api-parameters/query),
the [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) interacts with [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), etc.
**When mounting a widget or using the corresponding Hook, it's reflected in the InstantSearch UI state and included in search requests.**
For example, consider an ecommerce website where you want to have dedicated pages for your product categories.
The categories are provided by your server, and passed to InstantSearch using [`initialUiState`](#providing-an-initial-state).
The [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) widget doesn't need to be displayed, but InstantSearch needs to be aware of its state.
You can solve this by using a virtual widget.
```js VirtualRefinementList.js theme={"system"}
import React from "react";
import { useRefinementList } from "react-instantsearch";
export function VirtualRefinementList(props) {
useRefinementList(props);
return null;
}
```
```jsx App.jsx theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch } from "react-instantsearch";
import { VirtualRefinementList } from "./VirtualRefinementList";
const searchClient = algoliasearch("undefined", "undefined");
// The category page value can be retrieved from the server.
const categoryPage = "Audio";
export function App() {
return (
{/* ... */}
);
}
```
The virtual widget `VirtualRefinementList` refines results from Algolia according to the category retrieved from your server,
like a hidden filter that's always active.
### Custom components without Hooks
If you're using a component library like [Material UI](https://mui.com/) or your own,
you might want to use these components instead of the provided ones.
If your app uses class components instead of Hooks,
you can manually create a Higher Order Component (HOC) from the Hook and then use the HOC to access the necessary state and APIs to stitch React InstantSearch together with your custom components.
```js App.js theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import React from "react";
import { InstantSearch } from "react-instantsearch";
import { CustomSortBy } from "./CustomSortBy";
const searchClient = algoliasearch("undefined", "undefined");
function App() {
return (
);
}
```
```js CustomSortBy.js theme={"system"}
import Box from "@mui/material/Box";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import Select from "@mui/material/Select";
import React from "react";
import { useSortBy } from "react-instantsearch";
const connectSortBy = (Component) => (props) => {
const data = useSortBy(props);
return ;
};
function RawCustomSortBy(props) {
const { currentRefinement, options, refine } = props;
return (
Sort by
);
}
export const CustomSortBy = connectSortBy(RawCustomSortBy);
```
If you want to customize a widget, click its entry in the [React InstantSearch API reference](/doc/api-reference/widgets/react) and then look up the **Hook** section on that page.
## Next steps
You now have a good starting point to create an even more custom experience with React InstantSearch.
Next up, you could improve this app by:
* [Create your own widget](/doc/guides/building-search-ui/widgets/create-your-own-widgets/react).
* Checking the [API reference](/doc/api-reference/widgets/instantsearch/react) to learn more about the widgets and Hooks.
# Customize existing widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/customize-an-existing-widget/vue
Learn how you can customize an existing Vue InstantSearch widget.
This demo is working example of a menu widget rendered as a `select` HTML element.
Run and edit the Customize existing widgets example in CodeSandbox.
Browse the source for the Customize existing widgets example on GitHub.
The code that uses the default slots to extend the menu widget can be found in [`src/components/MenuSelect.vue`](https://github.com/algolia/doc-code-samples/blob/master/vue-instantsearch/extending-widgets).
This code has been specifically created for Vue 2.
Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
## Highlight and snippet your search results
Search helps users understand what they're seeing in the results.
When users run a ,
they should be able to tell why a result matches.
Algolia highlights the matched text in each result.
Algolia can also generate snippets:
short excerpts from longer text, truncated to a fixed length around the match.
Highlighting is based on the results and you need to customize the [`ais-hits`](/doc/api-reference/widgets/hits/vue) widget to use the Highlighter. The [`ais-highlight`](/doc/api-reference/widgets/highlight/vue) and the [`ais-snippet`](/doc/api-reference/widgets/snippet/vue) widgets take two props:
* `attribute`: the path to the highlighted attribute of the hit (which can be either a string or an array of strings)
* `hit`: a single result object
### Usage notes
* Use the [`ais-highlight`](/doc/api-reference/widgets/highlight/vue) widget when you want to display the regular value of an attribute.
* Use the [`ais-snippet`](/doc/api-reference/widgets/snippet/vue) widget when you want to display the snippet version of an attribute.
To use this widget, the attribute name passed to the `attribute` prop must be present in
"Attributes to snippet" on the Algolia dashboard or configured as [`attributesToSnippet`](/doc/api-reference/api-parameters/attributesToSnippet) with a `set settings` call to the Algolia API.
### Example
This example uses the item slot of the Hit widget.
In the results the `name` field is highlighted.
These examples use the [`mark`](https://developer.mozilla.org/en/docs/Web/HTML/Element/mark) tag to highlight.
This is a tag specially made for highlighting pieces of text.
```vue Vue icon=code theme={"system"}
```
## Style your widgets
All Vue InstantSearch widgets use a set of conventional CSS classes compatible with the InstantSearch CSS themes.
If you don't want to use the themes, you can either write your own theme by following the existing classes and customizing the styles, or override the classes to pass yours instead.
### Load the InstantSearch theme
Vue InstantSearch doesn't inject any styles by default but comes with two optional themes: **Algolia** and **Satellite**.
#### From a CDN
The themes are available on jsDelivr.
Unminified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite.css`
Minified:
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/reset-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/algolia-min.css`
* `https://cdn.jsdelivr.net/npm/instantsearch.css@8.24.0/themes/satellite-min.css`
The `reset` style sheet is included by default in the Satellite theme,
so you don't need to import it separately.
Either copy the files into your own app or use a direct link to jsDelivr.
```html HTML icon=code-xml theme={"system"}
```
#### Using npm
If your project imports CSS into JavaScript files using build tools,
you can install the theme with npm.
```sh npm theme={"system"}
npm install instantsearch.css
```
```sh yarn theme={"system"}
yarn add instantsearch.css
```
```sh pnpm theme={"system"}
pnpm add instantsearch.css
```
Then, you can import the theme directly into your app.
```js JavaScript icon=code theme={"system"}
import "instantsearch.css/themes/satellite.css";
```
### Customize with CSS variables
The InstantSearch.css themes provide CSS variables that you can override to customize colors, spacing, typography, and more without writing custom CSS classes.
```css CSS icon=paintbrush theme={"system"}
:root {
/* Brand colors */
--ais-primary-color-rgb: 62, 52, 211;
/* Larger spacing */
--ais-spacing-factor: 1.5;
}
```
For a complete list of available CSS variables and customization options, see [Styling and theming](/doc/guides/building-search-ui/styling/vue).
## Translate your widgets
Vue InstantSearch doesn't have a dedicated API for translating text, but it has slots in every component.
When there's data needed for rendering this is provided too.
For example in [`ais-pagination`](/doc/api-reference/widgets/pagination/vue):
```vue Vue icon=code theme={"system"}
first
prev
{{value.toLocaleString()}}
next
last
```
## Templating
Vue InstantSearch supports templating with slots.
With them you can customize the markup of each sub part of your components. For example in [`ais-pagination`](/doc/api-reference/widgets/pagination/vue):
```vue Vue icon=code theme={"system"}
first
prev
{{value.toLocaleString()}}
next
last
```
## Change the list of items in widgets
Vue InstantSearch provides two APIs for manipulating lists:
* `sortBy`, this is the most straightforward API and obviously limited to sorting
* `transformItems`, this is the most flexible solutions but it requires more involvement on your side
The `transform-items` prop is a function that takes the whole list of items as a parameter and it will expect to receive in return another array of items.
Most of the examples in this guide will use this API.
### Sorting
#### Using `sortBy`
```vue Vue icon=code theme={"system"}
```
#### Using `transformItems`
```vue Vue icon=code theme={"system"}
```
### Filtering
This example filters out items when the count is lower than 150:
```vue Vue icon=code theme={"system"}
```
### Add manual values
By default, the values in a [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) or a [`ais-menu`](/doc/api-reference/widgets/menu/vue) are dynamic.
This means that the values are updated with the context of the search.
Most of the time this is the expected behavior, but sometimes you may want to have a static list of values that never change.
This example, uses a static list of values:
```vue Vue icon=code theme={"system"}
```
### Display facets with no matches
Showing facets that don't match the current query helps users explore and refine results.
Algolia returns only facet values that match the current query.
To show non-matching values, add frontend logic.
For example, you could cache facet values the first time you receive them.
Then, if Algolia returns fewer values than your facet limit,
append the cached values.
This approach has limitations:
* It doesn't work with [search for facet values](/doc/guides/managing-results/refine-results/faceting#search-for-facet-values) because Algolia doesn't return non-matching values. As a result, highlighting doesn't work for cached values.
* You may need to re-apply sorting in the [`transformItems`](#using-transformitems) function because InstantSearch applies its internal sorting before calling the function.
```vue Vue icon=code theme={"system"}
```
### Search long lists of facet values
Use the `searchable` prop to add a search box to supported widgets:
```vue Vue icon=code theme={"system"}
```
## Apply default value to widgets
A question that comes up frequently is "How to instantiate a [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) widget with a pre-selected item?".
For this use case, you can use the [`ais-configure`](/doc/api-reference/widgets/configure/vue) widget.
The following example instantiates a search page with a default query of "apple" and will show a category menu where the item "Cell Phones" is already selected:
```vue Vue icon=code theme={"system"}
```
## Provide search parameters
Algolia has a [wide range of parameters](/doc/api-reference/search-api-parameters).
If one of the parameters you want to use isn't covered by any widget, use the [`ais-configure`](/doc/api-reference/widgets/configure/vue) widget.
Here's an example configuring the [number of results per page](/doc/api-reference/api-parameters/hitsPerPage):
```vue Vue icon=code theme={"system"}
```
### Dynamic update of search parameters
Updating the props of the [`ais-configure`](/doc/api-reference/widgets/configure/vue) widget will dynamically change the search parameters and will trigger a new search.
### Filter your results without widgets
Widgets already provide a lot of different ways to filter your results but sometimes you might have more complicated use cases that require the [`filters`](/doc/api-reference/api-parameters/filters) search parameter.
Don't use filters on a attribute already used with a widget, it will conflict.
```vue Vue icon=code theme={"system"}
```
For more information, see [Filters and facet filters](/doc/guides/managing-results/refine-results/filtering/in-depth/filters-and-facetfilters).
## Customize the complete UI of the widgets
Extending Vue InstantSearch widgets is the second layer of the API.
Read about the two others possibilities in the ["What is InstantSearch?" guide](/doc/guides/building-search-ui/what-is-instantsearch/vue).
Unlike other flavors of InstantSearch, Vue InstantSearch doesn't provide connectors.
Instead, each widget provides a top level slot to allow for complete customization while being able to reuse the logic of the widget.
### When to extend widgets?
*Extending widgets* means being able to redefine the rendering output of an existing widget.
For example, if you want to render the Menu widget as an HTML `select` element, you must extend the Menu widget.
Here are some common examples that require the usage of the connectors API:
* When you want to display the widgets using another UI library
* When you want to have full control on the rendering without having to re-implement business logic
* As soon as you hit a feature wall using the default widgets
### How widgets are built
Vue InstantSearch widgets have two parts:
* Business logic code
* Rendering code
The business logic is called `connectors`.
Connectors are provided by InstantSearch.js and their interfaces are exposed through a scope on the default slot.
### Connectors render API
The aim is to share as much of a common API between all connectors as possible.
Once you know how to use one connector, you can use them all.
#### Slot scopes
Most of the connectors will use the same naming for properties passed down to the slot.
* `items[]`: array of items to display, for example the brands list of a custom Refinement List. Every extended widget displaying a list gets an items property to the data passed to its render function.
* `refine(value|item.value)`: will refine the current s[`uiState`](/doc/api-reference/widgets/ui-state/vue) of the widget. Examples include: updating the query for a custom SearchBox or selecting a new item in a custom RefinementList.
* `currentRefinement`: currently applied refinement value (usually the call value of refine()).
* `createURL(value|item.value)`: will return a full URL you can display for the specific refine value given you are using the [routing feature](/doc/guides/building-search-ui/going-further/routing-urls/vue).
An item is an object that you will find in `items` arrays.
The shape of those objects is always the same.
* `item.value`: The underlying precomputed state value to pass to `refine` or `createURL`
* `item.label`: The label to display, for example "Samsung"
* `item.count`: The number of hits matching this item
* `item.isRefined`: Is the item currently selected as a filter
Some connectors will have more data than others.
Read their API reference to know more.
Connectors for every widget are documented in the API reference, for example the [`ais-menu`](/doc/api-reference/widgets/menu/vue) widget.
### Extending widget example
You can change the complete rendering with the `default` slot of every widget.
Here's an example of that for [`ais-menu`](/doc/api-reference/widgets/menu/vue):
```vue Vue icon=code theme={"system"}
```
# Change browser defaults
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/disabling-default-browser-behavior/react
Change the default properties for the HTML input element when using custom search boxes.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
**If you don't use the built-in InstantSearch or Autocomplete components to render a search box, you need to change the default properties of the HTML input element** to turn off the default autocomplete behavior of the browser.
A classic search HTML input looks like:
```html HTML icon=code-xml theme={"system"}
```
When a user starts typing into this input, the browser may use its default search-as-you-type behavior, which interferes with the custom search box.
This guide describes how to avoid this behavior.
## Update input attributes
Set these attributes on the HTML input element to turn off the browser's default behavior:
```html HTML icon=code-xml theme={"system"}
```
In Chrome, `autocomplete="off"` doesn't work.
In Chrome, you can [turn off autocomplete](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion#Disabling_autocompletion)
by setting the attribute to an invalid value like `autocomplete="nope"`.
The same solution doesn't work in Firefox (it disregards the invalid value and reverts to the default autocompletion behavior).
## See also
For more information, see these references:
* [How to turn autocomplete off](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion)
* [Autocomplete](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocomplete)
* [`autocorrect` attribute](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocorrect)
* [`autocapitalize` attribute](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocapitalize)
* [Spellcheck](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-spellcheck)
# Change browser defaults for Vue InstantSearch
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/disabling-default-browser-behavior/vue
Change the default properties for the HTML input element when using custom search boxes.
**If you don't use the built-in InstantSearch or Autocomplete components to render a search box, you need to change the default properties of the HTML input element** to turn off the default autocomplete behavior of the browser.
A classic search HTML input looks like:
```html HTML theme={"system"}
```
When a user starts typing into this input, the browser may use its default search-as-you-type behavior, which interferes with the custom search box.
This guide describes how to avoid this behavior.
## Updating input attributes
Set these attributes on the HTML input element to turn off the browser's default behavior:
```html HTML theme={"system"}
```
In Chrome, `autocomplete="off"` doesn't work.
In Chrome, you can [turn off autocomplete](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion#Disabling_autocompletion) by setting the attribute to an invalid value like `autocomplete="nope"`.
The same solution doesn't work in Firefox (it disregards the invalid value and reverts to the default autocompletion behavior).
## More information about input attributes
For more information, see these MDN references:
* [How to turn autocomplete off](https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion)
* [Autocomplete](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocomplete)
* [Autocorrect](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocorrect)
* [Autocapitalize](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-autocapitalize)
* [Spellcheck](https://developer.mozilla.org/en/docs/Web/HTML/Element/Input#attr-spellcheck)
# Show and hide React InstantSearch widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/show-and-hide-widgets/react
Learn how you can prevent common issues when showing and hiding React InstantSearch widgets.
This is the **React InstantSearch v7** documentation.
If you're upgrading from v6, see the [upgrade guide](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-v6-to-react-instantsearch-v7).
If you were using React InstantSearch Hooks,
this v7 documentation applies—just check for [necessary changes](/doc/guides/building-search-ui/upgrade-guides/react#migrate-from-react-instantsearch-hooks-to-react-instantsearch-v7).
To continue using v6, you can find the [archived documentation](https://algolia.com/old-docs/deprecated/instantsearch/react/v6/api-reference/instantsearch/).
Widgets do more than display a UI: they each modify the [`uiState`](/doc/api-reference/widgets/ui-state/react) that maps to one or more Algolia search parameters.
For example,
the [`SearchBox`](/doc/api-reference/widgets/search-box/react) controls the [`query`](/doc/api-reference/api-parameters/query),
the [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) interacts with [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), parameter.
**When a widget is mounted or unmounted, this is reflected in the InstantSearch UI state and included in the .**
Consider what happens in a mobile search interface with filters ([`RefinementList`](/doc/api-reference/widgets/refinement-list/react)): when a user clicks the **Filters** button, a dialog opens and displays the refinements.
In many component libraries, the `Dialog` component mounts and unmounts its content when toggled.
**This is problematic when used with InstantSearch components.**
For example, if you have a [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) widget nested in the dialog:
* The widget wouldn't be mounted on the first app load because the dialog box is closed.
* When the dialog box opens, the widget mounts, adding it to the InstantSearch state and triggering a new request, even before a refinement has been selected.
* When the dialog box closes, the widget unmounts, removing it from the InstantSearch state and losing all selected refinements.
To keep the state available after unmount, you can either:
* [Enable `preserveSharedStateOnUnmount`](#enable-preservesharedstateonunmount)
* [Keep the widget mounted but hidden](#keep-the-widget-mounted-but-hidden)
* [Persist the state on unmount](#persist-the-state-on-unmount) using a virtual widget
* [Use the Hook in a parent component](#use-the-hook-in-a-parent-component)
## Enable `preserveSharedStateOnUnmount`
Enabling [`preserveSharedStateOnUnmount`](/doc/api-reference/widgets/instantsearch/react#param-future-preserve-shared-state-on-unmount) prevents a widget's state from being cleared when it's unmounted, as long as other widgets share the same refinements.
Enabling this option changes how `dispose` is used in the InstantSearch lifecycle.
By default when a widget is removed, it provides a cleared version of the state that will be propagated throughout the other widgets.
```jsx Search.jsx theme={"system"}
import React, { useState } from "react";
import { RefinementList, useRefinementList } from "react-instantsearch";
export function Search() {
useRefinementList({ attribute: "brand" });
const [showMenu, setShowMenu] = useState(false);
return (
<>
{showMenu && }
{/* ... */}
>
);
}
```
```jsx App.jsx theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch";
import { Search } from "./Search";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
export function App() {
return (
);
}
```
This option is available from v7.2.0 and **will be the only option from React InstantSearch v8**.
Find out more about this option in the [API reference](/doc/api-reference/widgets/instantsearch/react#param-future-preserve-shared-state-on-unmount).
## Keep the widget mounted but hidden
The most straightforward way to retain a widget's state and refinements is to **avoid unmounting it**.
You can, for example, hide the content of the dialog with CSS:
```jsx Dialog.jsx theme={"system"}
import React from "react";
export function Dialog({ open, children }) {
return (
{children}
);
}
```
```jsx App.jsx theme={"system"}
import React, { useState } from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
CurrentRefinements,
InstantSearch,
RefinementList,
} from "react-instantsearch";
import { Dialog } from "./Dialog";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
export function App() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
{/* ... */}
);
}
```
If you're using a component library, you can verify whether the dialog component lets you avoid unmounting its content.
For example, the [`Dialog` component from Headless UI](https://headlessui.com/react/dialog) lets you turn off unmounting with the [`unmount` option](https://headlessui.com/react/dialog#dialog).
If you can't avoid unmounting, you can try [persisting the state on unmount](#persist-the-state-on-unmount).
## Persist the state on unmount
If you can't prevent unmounting a widget, you can keep track of the InstantSearch UI state to preserve it and apply it back when the dialog unmounts.
```jsx Filters.jsx theme={"system"}
import React, { useEffect, useRef } from "react";
import {
RangeInput,
RefinementList,
useInstantSearch,
useRange,
useRefinementList,
} from "react-instantsearch";
export function Filters() {
const { uiState, setUiState } = useInstantSearch();
const uiStateRef = useRef(uiState);
// Keep up to date uiState in a reference
useEffect(() => {
uiStateRef.current = uiState;
}, [uiState]);
// Apply latest uiState to InstantSearch as the component is unmounted
useEffect(() => {
return () => {
setTimeout(() => setUiState(uiStateRef.current));
};
}, [setUiState]);
return (
Brands
Price range
);
}
export function VirtualFilters() {
useRefinementList({ attribute: "brand" });
useRange({ attribute: "price" });
return null;
}
```
```jsx App.jsx theme={"system"}
import React, { useState } from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { CurrentRefinements, InstantSearch } from "react-instantsearch";
import { Filters, VirtualFilters } from "./Filters";
import { Dialog } from "./Dialog";
const searchClient = algoliasearch("undefined", "undefined");
export function App() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
{/* ... */}
);
}
```
Two components are used: `Filters` and `VirtualFilters`.
`Filters` renders [`RefinementList`](/doc/api-reference/widgets/refinement-list/react) and
[`RangeInput`](/doc/api-reference/widgets/range-input/react) for the `brand` and `price` attributes.
This component is nested in `Dialog` (see `App.jsx`), so the widgets are mounted and unmounted as users toggle the dialog.
To avoid losing applied filters, the `VirtualFilters` uses [`useRefinementList`](/doc/api-reference/widgets/refinement-list/react#hook) and [`useRange`](/doc/api-reference/widgets/range-input/react#hook) which register themselves in InstantSearch for the same `brand` and `price` attributes as the widgets.
The component is "renderless", users don't interact with it, but it allows persisting the state for `brand` and `price` within InstantSearch even when the widgets are unmounted.
## Use the Hook in a parent component
In some situations you may want to use Hooks instead of widgets—for example,
if you're using [React Native](/doc/guides/building-search-ui/going-further/native/react),
or if you want to fully control what's rendered.
In this case you can use Hooks in a parent component which isn't subject to being mounted and unmounted.
```jsx Filters.jsx theme={"system"}
import React, { useState } from "react";
import { useRefinementList } from "react-instantsearch";
import { Dialog } from "./Dialog";
export function Filters() {
const { items, refine } = useRefinementList({ attribute: "brand" });
const [dialogOpen, setDialogOpen] = useState(false);
return (
);
}
```
```jsx App.jsx theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { CurrentRefinements, InstantSearch } from "react-instantsearch";
import { Filters } from "./Filters";
const appID = "ALGOLIA_APPLICATION_ID";
const apiKey = "ALGOLIA_SEARCH_API_KEY";
const searchClient = algoliasearch(appID, apiKey);
export function App() {
return (
);
}
```
When using Hooks, you're in charge of [rendering the UI](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/react#customize-the-complete-ui-of-the-widgets) and setting up UI events.
While this provides full control, it also requires more work on your end.
If the only reason for you to use Hooks is to fix mounting and unmounting issues, it's strongly recommended try [hiding the widget](#keep-the-widget-mounted-but-hidden) or [persisting the state on unmount instead](#persist-the-state-on-unmount).
# Show and hide widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/show-and-hide-widgets/vue
Learn how you can prevent common issues when showing and hiding Vue InstantSearch widgets.
Widgets do more than display a UI—they each interact with a piece of the [`uiState`](/doc/api-reference/widgets/ui-state/vue) that maps to one or more Algolia search parameters.
For example, the [`ais-search-box`](/doc/api-reference/widgets/search-box/vue) controls the [`query`](/doc/api-reference/api-parameters/query), the [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) interacts with [`facetFilters`](/doc/api-reference/api-parameters/facetFilters), parameter.
**When a widget is mounted or unmounted, this is reflected in the InstantSearch UI state and included in the .**
Consider what happens in a mobile search interface with filters ([`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue)): when a user clicks the **Filters** button, a dialog opens and displays the refinements.
In many component libraries, the `Dialog` component mounts and unmounts its content when toggled.
**This is problematic when used with InstantSearch components.**
For example, if you have a [`ais-refinement-list`](/doc/api-reference/widgets/refinement-list/vue) widget nested in the dialog:
* The widget wouldn't be mounted on the first app load because the dialog box is closed.
* When the dialog box opens, the widget mounts, adding it to the InstantSearch state and triggering a new request, even before a refinement has been selected.
* When the dialog box closes, the widget unmounts, removing it from the InstantSearch state and losing all selected refinements.
To keep the state available after unmount, you can either:
* [Enable `preserveSharedStateOnUnmount`](#enable-preservesharedstateonunmount)
* [Keep the widget mounted but hidden](#keep-the-widget-mounted-but-hidden)
## Enable `preserveSharedStateOnUnmount`
Enabling [`preserveSharedStateOnUnmount`](/doc/api-reference/widgets/instantsearch/vue#param-future-preserve-shared-state-on-unmount) prevents a widget's state from being cleared when it's unmounted, as long as other widgets share the same refinements.
Enabling this option changes how `dispose` is used in the InstantSearch lifecycle.
By default when a widget is removed, it provides a cleared version of the state that will be propagated throughout the other widgets.
```html theme={"system"}
```
This option is available from v4.11.0 and **will be the only option from Vue InstantSearch v5**.
## Keep the widget mounted but hidden
The most straightforward way to retain a widget's state and refinements is to **avoid unmounting it**.
You can, for example, hide the content of the dialog with CSS:
```html theme={"system"}
```
If you're using a component library,
you can verify whether the dialog component lets you avoid unmounting its content.
For example, the [`Dialog` component from Headless UI](https://headlessui.com/v1/vue/dialog) lets you turn off unmounting with the `unmount` option.
# Showcase for React InstantSearch widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react
All React InstantSearch widgets in action.
For a list of available widgets, see [Widgets](/doc/api-reference/widgets/react).
# Showcase for Vue InstantSearch widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/vue
All Vue InstantSearch widgets in action.
For a list of available widgets, see [Widgets](/doc/api-reference/widgets/vue).
# Get started with Algolia Experiences
Source: https://www.algolia.com/doc/guides/building-search-ui/algolia-experiences/get-started
Create your first experience and embed it on your site with a single script tag.
This is a **pilot feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/). Functionality may change before general availability.
Algolia Experiences lets you add hosted, managed search and discovery interfaces to your site.
You configure each experience visually in the Algolia dashboard,
then add a single `script` tag to your website.
You don't have to write frontend code, or keep libraries up to date.
Algolia manages this for you.
Compared to [InstantSearch](/doc/guides/building-search-ui/what-is-instantsearch/js), Algolia Experiences is:
* **Managed**: Algolia hosts the UI and ships updates. You don't pin versions or upgrade libraries.
* **Configurable, not coded**: You build the experience in the dashboard instead of writing frontend code.
* **Opinionated by default**: A production-ready design ships out of the box. You customize it through theming controls instead of CSS.
Choose InstantSearch when you need full control over the UI in your own codebase.
Choose Algolia Experiences when you want the implementation handled for you.
The first available experience is **Autocomplete**.
## Before you begin
You need an Algolia application with at least one that contains records.
## Create your experience in the dashboard
In the Algolia dashboard, open the **Experiences** section and click **Create a new experience**.
By default, the editor adds an autocomplete block to an element with the ID `autocomplete`.
The **container** option defines where the autocomplete input renders on your page.
It must be a CSS selector that points to an existing HTML element.
To add the autocomplete to a different HTML element, update the container option to match your element.
Make sure the element exists in your page **before the script runs.**
Select an index for the autocomplete.
Connect a [Query Suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js) index and enable recent searches.
Choose between a drop-down menu or a dialog, and one or two columns.
* **Dropdown**: As people type, results appear in a panel anchored right below the search input.
Use this when search is one feature among many on the page, like a product listing with a filter-style search box or an embedded search on a marketing page.
* **Dialog**: Clicking the search input opens a focused window on top of your page, with the rest of the page dimmed in the background.
Use this when search is the primary reason people visit the page, like documentation search or full-site search.
On mobile, both layouts open in full screen.
Adjust colors, typography, and shape controls to match your brand.
Click **Save**.
## Embed the experience on your website
When you first save your experience, an integration dialog opens automatically.
You can reopen it at any time from the **Integration help** button () at the top right of the editor.
The dialog offers two options: **Production** and **Staging (optional)**.
Use the production snippet to embed your experience on your live site.
If your site is a React single-page app, follow [Use in React](/doc/guides/building-search-ui/algolia-experiences/use-in-react) instead.
A plain `script` tag only runs once and doesn't re-initialize after client-side route changes.
1. In the **Integration help** dialog, select **Production** and copy the snippet.
2. Paste the snippet into your page, before the closing `body` tag.
3. Make sure the container element listed in the snippet's comment block (for example, `#autocomplete`) exists on your page.
Your page should look similar to this:
```html theme={"system"}
```
Save your changes and refresh your page.
You'll see the autocomplete input.
Click it and start typing.
## Next steps
Your experience is now live. From here, you can:
* [Preview changes against your staging environment](/doc/guides/building-search-ui/algolia-experiences/preview-changes) before they reach your users.
* [Use Algolia Experiences in a React app](/doc/guides/building-search-ui/algolia-experiences/use-in-react) if your site is a single-page app.
* Check the [troubleshooting guide](/doc/guides/building-search-ui/algolia-experiences/troubleshooting) if your experience doesn't behave as expected, or if your site uses a content security policy.
# Edit and preview your experience from your website
Source: https://www.algolia.com/doc/guides/building-search-ui/algolia-experiences/preview-changes
Open the Algolia Experiences editor on your staging site to preview changes against real content before publishing to production.
This is a **pilot feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/). Functionality may change before general availability.
Algolia Experiences lets you preview unsaved changes against a staging environment before publishing them to production.
Previewing live helps you:
* Verify changes against your real content and layout, not a mocked preview.
* Iterate on theme and layout side by side with the editor and your site.
* Catch issues before end users see them, since saved changes go live immediately.
This guide assumes you've already [created an experience and embedded the production snippet](/doc/guides/building-search-ui/algolia-experiences/get-started) on your site.
## Embed the preview snippet on your staging environment
In the editor, open the **Integration help** dialog and select **Staging**.
Copy the preview snippet.
Paste the preview snippet before the closing `body` tag of the pages in your staging environment where you want to test the experience.
Open a page that includes the preview snippet.
In the bottom-left corner of your page, click the Algolia button.
The editor opens as a side panel in the same browser tab, next to your site.
The first time you click the Algolia button in a new browser,
you need to authenticate.
You can now edit the experience and see your changes against your staging content in real time.
When you're happy with the result, save to apply the changes to every page that embeds the experience.
# Troubleshoot Algolia Experiences
Source: https://www.algolia.com/doc/guides/building-search-ui/algolia-experiences/troubleshooting
Content security policy directives and answers to common questions about embedding Algolia Experiences.
This is a **pilot feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/). Functionality may change before general availability.
This page covers the content security policy directives Algolia Experiences needs,
and answers to common questions you might run into after [embedding an experience](/doc/guides/building-search-ui/algolia-experiences/get-started) on your site.
## Content security policy
If your site uses a [content security policy (CSP)](/doc/guides/security/security-best-practices#content-security-policy),
add the following directives so Algolia Experiences can load and fetch its configuration:
```txt theme={"system"}
connect-src https://*.algolia.com https://*.algolia.net https://*.algolianet.com https://*.algolia.io;
script-src https://cdn.jsdelivr.net https://*.algolia.com;
```
* `*.algolia.com` covers the Experiences API (`experiences.algolia.com`), the bundle resolver (`experiences.resolver.algolia.com`), and the runtime bundle (`experiences.cdn.algolia.com`).
* `cdn.jsdelivr.net` hosts the loader script. It's already required if you use InstantSearch or the [Insights](/doc/libraries/search-insights) library.
## Common issues
### The autocomplete doesn't appear
Check that the container element exists on your page and matches the selector configured in the editor.
### You don't see your latest changes
Hard-refresh the page.
Your browser can cache the bundle and the configuration.
### CSP errors in the browser console
See [Content security policy](#content-security-policy) for the directives Algolia Experiences needs.
### The experience disappears after a route change
If your site is a React single-page app, the default `script` snippet only loads the library once and doesn't re-initialize after client-side navigation.
Follow [Use in React](/doc/guides/building-search-ui/algolia-experiences/use-in-react) to wire up a Hook that calls `run()` and `dispose()` on each route change.
# Use Algolia Experiences in a React app
Source: https://www.algolia.com/doc/guides/building-search-ui/algolia-experiences/use-in-react
Embed an Algolia Experience in a React single-page app with a custom Hook that re-initializes on route changes.
This is a **pilot feature** according to [Algolia's Terms of Service ("Beta Services")](https://www.algolia.com/policies/terms/). Functionality may change before general availability.
In a React single-page app, routes change without a full page reload, so a plain `script` tag alone can't re-initialize the experience between routes.
This guide shows how to create a custom Hook that manages the Algolia Experiences lifecycle in your React codebase.
## Before you begin
You need:
* A React app with a client-side router that exposes the current path on navigation, such as [React Router](https://reactrouter.com/), [Remix](https://remix.run/), or [TanStack Router](https://tanstack.com/router).
* An Algolia Experience created in the dashboard. See [Get started](/doc/guides/building-search-ui/algolia-experiences/get-started).
## Declare the global type
If you use TypeScript, extend the `Window` interface so `window.AlgoliaExperiences` is typed:
```ts global.d.ts theme={"system"}
interface Window {
AlgoliaExperiences?: { run: () => void; dispose: () => void };
}
```
## Create the Hook
Create a Hook that loads the library script on first mount, calls `run()` on later route changes, and calls `dispose()` on cleanup:
```ts useAlgoliaExperiences.ts expandable theme={"system"}
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
export function useAlgoliaExperiences(opts: {
src: string;
appId: string;
apiKey: string;
experienceId: string;
}) {
const { pathname } = useLocation();
const injected = useRef(false);
useEffect(() => {
if (!injected.current) {
injected.current = true;
const { src, ...params } = opts;
const url = new URL(src);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const script = document.createElement('script');
script.src = url.href;
document.head.appendChild(script);
} else {
window.AlgoliaExperiences?.run();
}
return () => window.AlgoliaExperiences?.dispose();
}, [pathname]);
}
```
```js useAlgoliaExperiences.js expandable theme={"system"}
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
export function useAlgoliaExperiences(opts) {
const { pathname } = useLocation();
const injected = useRef(false);
useEffect(() => {
if (!injected.current) {
injected.current = true;
const { src, ...params } = opts;
const url = new URL(src);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const script = document.createElement('script');
script.src = url.href;
document.head.appendChild(script);
} else {
window.AlgoliaExperiences?.run();
}
return () => window.AlgoliaExperiences?.dispose();
}, [pathname]);
}
```
The example imports `useLocation` from React Router.
If your app uses a different router, swap it for the equivalent path Hook from that router.
The Hook only needs the current path as its effect dependency.
The Hook manages the experience lifecycle:
1. On first mount, it builds the loader URL with your credentials as query parameters and appends the script to `head`.
2. On later route changes, calls `run()` to remount the experience in the current page's container.
3. On unmount or before the next route change, calls `dispose()` to clean up the previous instance.
## Use the Hook in your layout
Call the Hook from a component that wraps all routes where you want the experience to appear.
```tsx Layout.tsx theme={"system"}
import { Outlet } from 'react-router-dom';
import { useAlgoliaExperiences } from './useAlgoliaExperiences';
function Layout() {
useAlgoliaExperiences({
src: 'https://cdn.jsdelivr.net/npm/@algolia/experiences/dist/experiences.js',
appId: 'ALGOLIA_APPLICATION_ID',
apiKey: 'ALGOLIA_SEARCH_API_KEY',
experienceId: 'EXPERIENCE_ID',
});
return ;
}
```
```jsx Layout.jsx theme={"system"}
import { Outlet } from 'react-router-dom';
import { useAlgoliaExperiences } from './useAlgoliaExperiences';
function Layout() {
useAlgoliaExperiences({
src: 'https://cdn.jsdelivr.net/npm/@algolia/experiences/dist/experiences.js',
appId: 'ALGOLIA_APPLICATION_ID',
apiKey: 'ALGOLIA_SEARCH_API_KEY',
experienceId: 'EXPERIENCE_ID',
});
return ;
}
```
Make sure the container element configured in the editor (for example, ``) renders on the pages where the experience should appear.
## Next steps
* Verify the experience renders on every route, including after browser back and forward navigation.
* Check the [troubleshooting guide](/doc/guides/building-search-ui/algolia-experiences/troubleshooting) for content security policy directives and common issues.
# Components
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/flutter
Use Algolia's UI template components to assemble a search and discovery experience.
Each UI template comes with a set of functional blocks ("components") and a demo site consisting of pages created with those components.
For example, the Ecommerce UI template's demo site has:
* [A home page](#home-page)
* [Product listing pages](#product-listing-page)
* Product detail pages.
These pre-packaged components are designed to be as independent as possible,
so you can choose the ones you're interested in.
They're typically composed of InstantSearch widgets,
Autocomplete plugins, and non-Algolia components.
The Ecommerce UI template components are cross-platform,
all components are optimized to render well on both iOS and Android.
## Data sources
The ecommerce experience requires sources of entity objects.
In this example, there are three entity objects:
products, query suggestions and search.
| Component | Description |
| - | - |
| [Product repository](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/product-repository/flutter) | Repository providing access to products information. |
| [Suggestion repository](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/suggestion-repository/flutter) | Repository providing access to query suggestions and queries history.. |
| [Search repository](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/search-repository/flutter) | Repository providing access to products indices. |
## Home page
| Component | Description |
| - | - |
| [Showcase](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/showcase/flutter) | Organizes media-rich records into scannable rows |
## Product listing page
### Display
| Component | Description |
| - | - |
| [Display products](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/display-products/flutter) | Display multiple products using different layouts (as a grid or list) with infinite scrolling |
| [Product card](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/product-card/flutter) | Map your record structure so that you can display a product card |
| [No results](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/no-results-handler/flutter) | Display different options to users when there are no results for a specific search |
### Search
| Component | Description |
| - | - |
| [Autocomplete](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/search-autocomplete/flutter) | Display search autocomplete view with search history and query suggestions |
### Filtering
| Component | Description |
| - | - |
| [Filters](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/filters-refinements/flutter) | Filter results from a sidebar drawer |
| [Facets](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/refinement-widgets/flutter) | Filter product results with facets |
| [Sort](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/sort/flutter) | Sort products according to different criteria |
# ProductRepository
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/product-repository/flutter
The ProductRepository component provides access to product information.
The `ProductRepository` component implements the repository design pattern.
It includes methods to fetch product information.
It wraps [Flutter helpers](https://pub.dev/packages/algolia_helper_flutter) and the [Dart API client](https://pub.dev/packages/algoliasearch).
```dart Usage icon=code theme={"system"}
class ProductRepository {
/// Hits Searcher with static search query `shoes`.
final _shoesSearcher = HitsSearcher.create(
applicationID: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
state: const SearchState(
indexName: Credentials.hitsIndex,
query: 'shoes',
),
);
/// Hits Searcher with static rule contexts `home-spring-summer-2021`.
final _seasonalProductsSearcher = HitsSearcher.create(
applicationID: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
state: const SearchState(
indexName: Credentials.hitsIndex,
ruleContexts: ['home-spring-summer-2021'],
),
);
/// Hits Searcher with static search query `jacket`.
final _recommendedProductsSearcher = HitsSearcher.create(
applicationID: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
state: const SearchState(
indexName: Credentials.hitsIndex,
query: 'jacket',
),
);
/// Algolia API client instance.
final SearchClient _algoliaClient = SearchClient(
appId: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
);
/// Disposable components composite
final CompositeDisposable _components = CompositeDisposable();
/// Product repository constructor.
ProductRepository() {
_components
..add(_shoesSearcher)
..add(_seasonalProductsSearcher)
..add(_recommendedProductsSearcher);
}
/// Get stream of shoes.
Stream> get shoes => _shoesSearcher.responses
.map((response) => response.hits.map(Product.fromJson).toList());
/// Get stream of seasonal products.
Stream> get seasonalProducts =>
_seasonalProductsSearcher.responses
.map((response) => response.hits.map(Product.fromJson).toList());
/// Get stream of recommended products.
Stream> get recommendedProducts =>
_recommendedProductsSearcher.responses.map((response) =>
response.hits.map((hit) => Product.fromJson(hit)).toList());
/// Get product by ID.
Future getProduct(String productID) async {
final rawProduct = await _algoliaClient.get(
path: '/indexes/${Credentials.hitsIndex}/$productID')
as Map;
final product = Product.fromJson(rawProduct);
return product;
}
/// Dispose of underlying resources.
void dispose() {
_components.dispose();
}
}
```
# SearchRepository
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/search-repository/flutter
Repository providing access to products indices.
The `SearchRepository` component provides convenient methods to run search queries,
apply filters and display facets list.
It encapsulates [Flutter helpers](https://pub.dev/packages/algolia_helper_flutter).
```dart Usage icon=code theme={"system"}
class SearchRepository {
/// Component holding search filters
final _filterState = FilterState();
/// Products Hits Searcher.
late final _hitsSearcher = HitsSearcher(
applicationID: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
indexName: Credentials.hitsIndex,
)..connectFilterState(_filterState);
/// Brands facet lists
late final _brandFacetList = _hitsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'brand',
);
/// Size facet lists
late final _sizeFacetList = _hitsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'available_sizes',
);
/// Disposable components composite.
final CompositeDisposable _components = CompositeDisposable();
/// Search repository constructor.
SearchRepository() {
_components
..add(_filterState)
..add(_hitsSearcher)
..add(_brandFacetList)
..add(_sizeFacetList);
}
/// Set search page.
void setPage(int page) {
_hitsSearcher.applyState((state) => state.copyWith(page: page));
}
/// Get products list by query.
void search(String query) {
_hitsSearcher.query(query);
}
/// Get stream of latest search result
Stream get searchMetadata =>
_hitsSearcher.responses.map(SearchMetadata.fromResponse);
/// Get stream of latest search page
Stream get productsPage =>
_hitsSearcher.responses.map(ProductsPage.fromResponse);
Stream get appliedFiltersCount =>
_filterState.filters.map((event) => event.getFilters().length);
/// Get currently selected index
Stream get selectedIndex =>
_hitsSearcher.state.map((state) => SortIndex.of(state.indexName));
/// Update target index
void selectIndexName(String indexName) {
_hitsSearcher
.applyState((state) => state.copyWith(indexName: indexName, page: 0));
}
/// Get stream of list of brand facets
Stream> get brandFacets => _brandFacetList.facets;
/// Get stream of list of size facets
Stream> get sizeFacets => _sizeFacetList.facets;
/// Toggle selection of a brand facet
void toggleBrand(String brand) {
_brandFacetList.toggle(brand);
_hitsSearcher.applyState((state) => state.copyWith(page: 0));
}
/// Toggle selection of a size facet
void toggleSize(String size) {
_sizeFacetList.toggle(size);
_hitsSearcher.applyState((state) => state.copyWith(page: 0));
}
/// Clear all filters
void clearFilters() {
_filterState.clear();
_hitsSearcher.applyState((state) => state.copyWith(page: 0));
}
/// Dispose of underlying resources.
void dispose() {
_components.dispose();
}
}
```
# SuggestionRepository
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/data-sources/suggestion-repository/flutter
Repository providing access to query suggestions and queries history.
`SuggestionRepository` provides methods to get query suggestions.
Algolia has two types of query suggestions:
* Search completions updated on each keystroke.
* Search history, which stores submitted queries.
The `SuggestionRepository` component wraps [Flutter helpers](https://pub.dev/packages/algolia_helper_flutter).
It also stores query history locally, in memory.
```dart Usage icon=code theme={"system"}
class SuggestionRepository {
/// Hits Searcher for suggestions index
final _suggestionsSearcher = HitsSearcher(
applicationID: Credentials.applicationID,
apiKey: Credentials.searchOnlyKey,
indexName: Credentials.suggestionsIndex,
);
/// Get query suggestions for a given query string.
void query(String query) {
_suggestionsSearcher.query(query);
}
/// Get query suggestions stream
late final Stream> suggestions = _suggestionsSearcher
.responses
.map((response) => response.hits.map(QuerySuggestion.fromJson).toList());
/// In-memory store of submitted queries.
final BehaviorSubject> _history =
BehaviorSubject.seeded(['jackets']);
/// Stream of previously submitted queries.
Stream> get history => _history;
/// Add a query to queries history store.
void addToHistory(String query) {
if (query.isEmpty) return;
final _current = _history.value;
_current.removeWhere((element) => element == query);
_current.add(query);
_history.sink.add(_current);
}
/// Remove a query from queries history store.
void removeFromHistory(String query) {
final _current = _history.value;
_current.removeWhere((element) => element == query);
_history.sink.add(_current);
}
/// Clear everything from queries history store.
void clearHistory() {
_history.sink.add([]);
}
/// Dispose of underlying resources.
void dispose() {
_suggestionsSearcher.dispose();
}
}
```
# Display products
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/display-products/flutter
The "Display products" components display multiple products.
The *Display products* components lets you display multiple products using different layouts.
Paged hits widgets allow loading more product pages while scrolling.
Use:
* The [`HitsListView` widget](#products-list) to display a list view of products
* The [`PagedHitsListView` widget](#paged-products-list) to display infinite list view of products
* The [`PagedHitsGridView` widget](#paged-products-grid-list) to display infinite grid list view of products
* The [`ModeSwitcherView` widget](#mode-switcher) to switch between list view modes
## Products list
The `HitsListView` widgets displays multiple products as a horizontal or vertical list view.
### Code summary
You can customize `HitsListView` in:
* [The home screen](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/home_screen.dart#L142-L154)
* [The `ProductsView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/components/products_view.dart#L30-L33)
* [The `HitsListView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/components/hits_list_view.dart)
#### Usage and props
```dart Usage theme={"system"}
class HitsListView extends StatelessWidget {
const HitsListView(
{Key? key,
required this.items,
required this.productWidget,
this.scrollDirection = Axis.vertical})
: super(key: key);
final Stream> items;
final ProductWidgetBuilder productWidget;
final Axis scrollDirection;
@override
Widget build(BuildContext context) {
return StreamBuilder>(
stream: items,
builder: (context, snapshot) {
if (snapshot.hasData) {
final products = snapshot.data ?? [];
return ListView.separated(
padding: const EdgeInsets.all(8),
scrollDirection: scrollDirection,
itemCount: products.length,
itemBuilder: (BuildContext context, int index) {
return productWidget(context, products[index]);
},
separatorBuilder: (context, index) => const SizedBox(width: 10));
} else {
return const Center(child: CircularProgressIndicator());
}
},
);
}
}
```
```dart Props theme={"system"}
final Stream> items; // Stream of products hits to display
final ProductWidgetBuilder productWidget; // View to display a product
final Axis scrollDirection; // List scroll direction
```
## Paged products list
The `PagedHitsListView` widgets displays multiple products as a list view.
As users scroll down the page, more products get loaded automatically.
### Code summary
You can customize `PagedHitsListView` in:
* [The search results screen](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/search_results_screen.dart#L84-L87)
* [The `PagedHitsListView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/components/paged_hits_list_view.dart)
#### Usage and props
```dart Usage theme={"system"}
class PagedHitsListView extends StatelessWidget {
const PagedHitsListView(
{Key? key,
required this.pagingController,
this.onHitClick,
this.noItemsFound})
: super(key: key);
final PagingController pagingController;
final Function(String)? onHitClick;
final WidgetBuilder? noItemsFound;
@override
Widget build(BuildContext context) {
return PagedListView.separated(
shrinkWrap: true,
pagingController: pagingController,
separatorBuilder: (context, index) => const SizedBox(height: 10),
builderDelegate: PagedChildBuilderDelegate(
noItemsFoundIndicatorBuilder: noItemsFound,
itemBuilder: (context, item, index) => ProductItemView(
product: item,
imageAlignment: Alignment.bottomCenter,
onProductPressed: (objectID) => onHitClick?.call(objectID)),
),
);
}
}
```
```dart Props theme={"system"}
final PagingController pagingController; // Controller for paged widget
final Function(String)? onHitClick; // Callback on product click
final WidgetBuilder? noItemsFound; // No results view
```
## Paged products grid list
The `PagedHitsListView` widgets displays multiple products as a grid list view.
As users scroll down the page, more products get loaded automatically.
### Code summary
You can customize `PagedHitsGridView` in:
* [The search results screen](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/search_results_screen.dart#L89-L92)
* [The `PagedHitsGridView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/components/paged_hits_grid_view.dart)
## Mode switcher
The `ModeSwitcherView` widget allows users to switch between a grid and a product list.
This lets them see more details about the products, such as extended descriptions or larger product images.
### Code summary
You can customize `ModeSwitcherView` in:
* [The search results file](https://github.com/algolia/pwa-ecom-ui-template/blob/v0.8.6/components/refinements-bar/refinements-bar.tsx#L58)
* [The `ModeSwitcherView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/search_results_screen.dart#L68-L70)
#### Usage and props
```dart Usage icon=code theme={"system"}
class ModeSwitcherView extends StatelessWidget {
const ModeSwitcherView(
{Key? key, required this.currentDisplay, this.onPressed})
: super(key: key);
final HitsDisplay currentDisplay;
final Function(HitsDisplay)? onPressed;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('Display'),
const SizedBox(width: 10),
SizedBox(
height: 20,
width: 20,
child: IconButton(
splashRadius: 10,
padding: const EdgeInsets.all(0.0),
onPressed: () => onPressed?.call(HitsDisplay.grid),
icon: Icon(Icons.grid_view,
size: 20,
color: HitsDisplay.grid == currentDisplay ? Colors.blue : null),
),
),
const SizedBox(width: 10),
SizedBox(
height: 20,
width: 20,
child: IconButton(
splashRadius: 10,
padding: const EdgeInsets.all(0.0),
onPressed: () => onPressed?.call(HitsDisplay.list),
icon: Icon(Icons.view_list,
size: 20,
color: HitsDisplay.list == currentDisplay ? Colors.blue : null),
),
),
],
);
}
}
```
```dart Props theme={"system"}
final HitsDisplay currentDisplay; // Current hits list mode
final Function(HitsDisplay)? onPressed; // Callback on mode click
```
## External packages used
The *paged list products views* components depend on the following Flutter packages:
| Package | Description | Used by |
| - | - | - |
| [`infinite_scroll_pagination`](https://pub.dev/packages/infinite_scroll_pagination) | Lazily load and display pages of items as users scrolls down the screen | `PagedHitsListView`, `PagedHitsGridView` |
# No results handler
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/no-results-handler/flutter
The "NoResultsView" component displays different options to users when there are no results for a specific search.
The `NoResultsView` component guides users when there are no results for a specific search by providing them with different ways to continue.
In the following example, the "beautiful black bag" doesn't match any result.
The `NoResultsView` component hides all other components and lists two options to help users find relevant results:
* Check the query spelling.
* Searching again using more general terms.
## Code summary
You can customize `NoResultsView` in:
* [The search results screen](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/search_results_screen.dart#L104-L106)
* [The `NoResultsView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/components/no_results_view.dart)
### Usage
```dart Usage icon=code theme={"system"}
class NoResultsView extends StatelessWidget {
const NoResultsView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Try the following:",
style: Theme.of(context).textTheme.bodyText1),
const SizedBox(height: 4),
Text("• Check your spelling",
style: Theme.of(context).textTheme.bodyText2),
Text("• Searching again using more general terms",
style: Theme.of(context).textTheme.bodyText2),
],
),
);
}
}
```
## External packages used
The `NoResultsView` widget is used with paging hits widgets which depend on the following Flutter packages:
| Package | Description | Used by |
| - | - | - |
| [`infinite_scroll_pagination`](https://pub.dev/packages/infinite_scroll_pagination) | Lazily load and display pages of items as users scroll down the screen | `PagedHitsListView`, `PagedHitsGridView` |
# Product card
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-display/product-card/flutter
The "Product card" components show your product data from an Algolia index as a product card.
Use:
* The [`ProductCardView` widget](#product-card) to display product information in a list or grid.
* The [`ProductItemView` widget](#product-item) to display product information with details in a list.
## Product card
The `ProductCardView` component lets you display product information.
For example, you can show product cards in a list or grid view.
### Code summary
You can customize `ProductCardView` in:
* [The `ProductCardView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/widgets/product_card_view.dart)
* [The `PagedHitsGridView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/components/paged_hits_grid_view.dart#L31-L35)
* [The home screen](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/home_screen.dart#L73-L79)
* Its props.
#### Usage and props
```dart Usage theme={"system"}
class ProductCardView extends StatelessWidget {
const ProductCardView(
{Key? key,
required this.product,
this.imageAlignment = Alignment.bottomCenter,
this.onTap})
: super(key: key);
final Product product;
final Alignment imageAlignment;
final Function(String)? onTap;
@override
Widget build(BuildContext context) {
final priceValue = (product.price?.onSales ?? false)
? product.price?.discountedValue
: product.price?.value;
final crossedValue =
(product.price?.onSales ?? false) ? product.price?.value : null;
return GestureDetector(
onTap: () => onTap?.call(product.objectID!),
child: SizedBox(
width: 150,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
SizedBox(
height: 100,
width: MediaQuery.of(context).size.width,
child: Image.network('${product.image}',
alignment: imageAlignment, fit: BoxFit.cover)),
if (product.price?.onSales == true)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(" ON SALE ${product.price?.discountLevel}% ",
style: Theme.of(context).textTheme.caption?.copyWith(
color: Colors.white,
backgroundColor: AppTheme.darkPink)),
)
],
),
const SizedBox(height: 8),
SizedBox(
child: Text('${product.brand}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: Theme.of(context).textTheme.caption)),
SizedBox(
child: Text('${product.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: Theme.of(context).textTheme.bodyText2)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: ColorIndicatorView(product: product),
),
Row(
children: [
Text('$priceValue €',
maxLines: 1,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.vividOrange)),
if (crossedValue != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text('$crossedValue €',
maxLines: 1,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context)
.textTheme
.caption
?.copyWith(decoration: TextDecoration.lineThrough)),
),
],
),
RatingView(
value: product.reviews?.rating?.toInt() ?? 0,
reviewsCount: product.reviews?.count?.toInt() ?? 0),
]),
),
);
}
}
```
```dart Props theme={"system"}
final Product product; // Product item to display
final Alignment imageAlignment; // How to align the image within its bounds
final Function(String)? onTap; // Callback in item click
```
## Product item
The `ProductItemView` component lets you display product information.
For example, you can show product item with details in a list.
### Code summary
You can customize `ProductItemView` in:
* [The `ProductItemView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/widgets/product_item_view.dart)
* [The `PagedHitsListView` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/products/components/paged_hits_list_view.dart#L26-L30)
* Its props.
#### Usage and props
```dart Usage theme={"system"}
class ProductItemView extends StatelessWidget {
const ProductItemView(
{Key? key,
required this.product,
this.imageAlignment = Alignment.center,
this.onProductPressed})
: super(key: key);
final Product product;
final Alignment imageAlignment;
final Function(String)? onProductPressed;
@override
Widget build(BuildContext context) {
final priceValue = (product.price?.onSales ?? false)
? product.price?.discountedValue
: product.price?.value;
final crossedValue =
(product.price?.onSales ?? false) ? product.price?.value : null;
return GestureDetector(
onTap: () {
onProductPressed?.call(product.objectID!);
},
child: SizedBox(
child: Row(mainAxisSize: MainAxisSize.min, children: [
Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
SizedBox(
height: 100,
width: 100,
child: Image.network('${product.image}',
alignment: imageAlignment, fit: BoxFit.cover)),
if (product.price?.onSales == true)
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
" ON SALE ${product.price?.discountLevel}% ",
style: Theme.of(context).textTheme.caption?.copyWith(
color: Colors.white,
backgroundColor: AppTheme.darkPink),
))
],
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${product.brand}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: Theme.of(context).textTheme.caption),
Text('${product.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: Theme.of(context).textTheme.bodyText1),
if (product.description?.isNotEmpty == true)
Text('${product.description}',
maxLines: 2,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: Theme.of(context)
.textTheme
.bodyText2
?.copyWith(fontSize: 12, color: Colors.grey)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: ColorIndicatorView(product: product),
),
Row(
children: [
Text('$priceValue €',
maxLines: 1,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context).textTheme.bodyText2?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.vividOrange)),
if (crossedValue != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text('$crossedValue €',
maxLines: 1,
overflow: TextOverflow.clip,
softWrap: false,
style: Theme.of(context)
.textTheme
.caption
?.copyWith(
decoration: TextDecoration.lineThrough)),
),
],
),
RatingView(
value: product.reviews?.rating?.toInt() ?? 0,
reviewsCount: product.reviews?.count?.toInt() ?? 0),
],
),
),
]),
),
);
}
}
```
```dart Props theme={"system"}
final Product product; // Product item to display
final Alignment imageAlignment; // How to align the image within its bounds
final Function(String)? onProductPressed; // Callback in item click
```
# Refinements
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/filters-refinements/flutter
Refinement components let users filter results.
The *Filters and refinements* components allow users to filter results from a drawer.
The drawer can be shown or hidden.
## Filters and sort
The `FiltersScreen` component provides a way to filter the results.
Each filter is displayed in an silver list.
You can show or hide the filters.
### Code summary
To customize the `FiltersScreen` component, edit the [filters\_screen.dart](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/filters/filters_screen.dart) file.
#### Usage and props
```dart Usage icon=code theme={"system"}
class FiltersScreen extends StatefulWidget {
const FiltersScreen({super.key});
@override
State createState() => _FiltersScreenState();
}
enum FiltersSection { none, sort, brand, size }
class _FiltersScreenState extends State {
FiltersSection activeSection = FiltersSection.none;
bool _isActive(FiltersSection section) => section == activeSection;
@override
Widget build(BuildContext context) {
final searchRepository = context.read();
return Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top,
bottom: MediaQuery.of(context).padding.bottom,
left: 10,
right: 10),
child: Column(
children: [
const FiltersHeaderView(),
const SizedBox(
height: 10,
),
Expanded(
child: CustomScrollView(
slivers: [
_sortHeader(searchRepository.selectedIndex),
if (_isActive(FiltersSection.sort))
SortSelectorView(
sorts: searchRepository.selectedIndex,
onToggle: searchRepository.selectIndexName,
),
_brandHeader(),
if (_isActive(FiltersSection.brand))
BrandSelectorView(
facets: searchRepository.brandFacets,
onToggle: searchRepository.toggleBrand,
),
_sizeHeader(),
if (_isActive(FiltersSection.size))
SizeSelectorView(
facets: searchRepository.sizeFacets,
onToggle: searchRepository.toggleSize,
),
],
),
),
const Divider(),
FiltersFooterView(
metadata: searchRepository.searchMetadata,
onClear: searchRepository.clearFilters,
),
],
));
}
Widget _sortHeader(Stream sorts) {
const section = FiltersSection.sort;
final isActive = _isActive(section);
return ExpandableHeaderView(
title: SortTitleView(
sorts: sorts,
isActive: isActive,
),
isActive: isActive,
onToggle: () => _toggleSection(section),
);
}
Widget _brandHeader() {
const section = FiltersSection.brand;
return ExpandableHeaderView(
title: const Text('Brand',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
isActive: _isActive(section),
onToggle: () => _toggleSection(section),
);
}
Widget _sizeHeader() {
const section = FiltersSection.size;
return ExpandableHeaderView(
title: const Text('Size',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
isActive: _isActive(section),
onToggle: () => _toggleSection(section),
);
}
_toggleSection(FiltersSection section) => setState(
() => activeSection = _isActive(section) ? FiltersSection.none : section);
}
```
### Components
`FilterScreen` uses the following components:
| Component | Description |
| - | - |
| [Facets](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/refinement-widgets/flutter) | Filter product results with facets |
| [Sort](/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/sort/flutter) | Sort products according to different criteria |
# Refinement widgets
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/refinement-widgets/flutter
Refinement widgets let you filter product results with facets.
Configure your filters with `FacetList` fields in the [`search_repository.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/data/search_repository.dart) file.
Behind the scenes, the `FacetList` uses a `FilterState` to listen to each change and
a `HitsSearcher` to update the search state and return each from search operations.
You can refine these facets:
* [Brand](#brand-filter)
* [Size](#size-filter)
## Brand filter
Use the `Brand` filter to only show search results from one or more brands.
### Configure the brand filter
To configure the brand filter, edit the [`search_repository.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/data/search_repository.dart#L20-L24) file:
```dart Dart icon=code theme={"system"}
late final _brandFacetList = _hitsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'brand',
);
```
### Code summary
You can customize the filters view in [brand\_selector\_view.dart](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/filters/components/brand_selector_view.dart) file.
#### Usage and props
```dart Usage theme={"system"}
class BrandSelectorView extends StatelessWidget {
const BrandSelectorView(
{super.key, required this.facets, required this.onToggle});
final Stream> facets;
final Function(String) onToggle;
@override
Widget build(BuildContext context) {
return StreamBuilder>(
stream: facets,
builder: (context, snapshot) {
final facets = snapshot.data ?? [];
return SliverFixedExtentList(
itemExtent: 44,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final facet = facets[index];
return InkWell(
child: Row(children: [
Icon(
facet.isSelected
? Icons.check_box
: Icons.check_box_outline_blank,
),
const SizedBox(
width: 5,
),
Text(facet.item.value),
const Spacer(),
Text(facet.item.count > 0 ? '${facet.item.count}' : ''),
]),
onTap: () => onToggle(facet.item.value),
);
},
childCount: facets.length,
));
});
}
}
```
```dart Props theme={"system"}
Stream> facets; // Stream of facet lists (w/ selection status)
Function(String) onToggle; // Callback on facet toggle
```
## Size filter
Use the `Size` filter to only show search results matching one or more product sizes.
### Configure the size filter
To configure the size filter, edit the [`search_repository.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/data/search_repository.dart#L27-L31) file:
```dart Dart icon=code theme={"system"}
late final _sizeFacetList = _hitsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'available_sizes',
);
```
### Code summary
You can customize the filters view in [size\_selector\_view.dart](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/filters/components/size_selector_view.dart) file.
#### Usage and props
```dart Usage theme={"system"}
class SizeSelectorView extends StatelessWidget {
const SizeSelectorView(
{super.key, required this.facets, required this.onToggle});
final Stream> facets;
final Function(String) onToggle;
@override
Widget build(BuildContext context) {
return StreamBuilder>(
stream: facets,
builder: (context, snapshot) {
final facets = snapshot.data ?? [];
return SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 65.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 2.5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final facet = facets[index];
if (facet.isSelected) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.darkBlue,
padding: const EdgeInsets.all(2),
),
onPressed: () => onToggle(facet.item.value),
child: Text(facet.item.value));
} else {
return OutlinedButton(
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.darkBlue,
side: const BorderSide(
width: 1.0,
color: AppTheme.darkBlue,
style: BorderStyle.solid),
padding: const EdgeInsets.all(2),
),
onPressed: () => onToggle(facet.item.value),
child: Text(facet.item.value));
}
},
childCount: facets.length,
));
});
}
}
```
```dart Props theme={"system"}
Stream> facets; // Stream of facet lists (w/ selection status)
Function(String) onToggle; // Callback on facet toggle
```
# Sort UI components
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/product-listing-page-filter-and-nav/sort/flutter
Sort components let users sort products by different attributes, for example, by price or popularity in ascending or descending order.
The `Sort` component lets users sort products according to different criteria, such as "Price Low to High" or "Most popular".
## Configure
To configure the `Sort` filter, edit the [`search_repository.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/data/search_repository.dart#L67-L74) file:
```dart Usage icon=code theme={"system"}
/// Get currently selected index
Stream get selectedIndex =>
_hitsSearcher.state.map((state) => SortIndex.of(state.indexName));
/// Update target index
void selectIndexName(String indexName) {
_hitsSearcher
.applyState((state) => state.copyWith(indexName: indexName, page: 0));
}
```
The `SortIndex` class is defined in the [`sort_index.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/model/sort_index.dart) file.
## Code summary
To customize the filters view, edit these files:
* [`sort_selector_view.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/filters/components/sort_selector_view.dart)
* [`sort_title_view.dart`](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/filters/components/sort_title_view.dart)
### Usage and props
```dart Usage theme={"system"}
class SortSelectorView extends StatelessWidget {
const SortSelectorView(
{super.key, required this.sorts, required this.onToggle});
final Stream sorts;
final Function(String) onToggle;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: sorts,
builder: (context, snapshot) {
final selectedIndex = snapshot.data;
return SliverFixedExtentList(
itemExtent: 40,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final item = SortIndex.values[index];
return InkWell(
onTap: () => onToggle(item.indexName),
child: Text(
item.title,
style: TextStyle(
fontWeight: item == selectedIndex
? FontWeight.bold
: FontWeight.normal),
));
},
childCount: SortIndex.values.length,
));
});
}
}
```
```dart Props theme={"system"}
Stream sorts; // Stream of sorting indexes
Function(String) onToggle; // Callback on sorting index select
```
```dart Usage theme={"system"}
class SortTitleView extends StatelessWidget {
const SortTitleView(
{super.key, required this.sorts, required this.isActive});
final Stream sorts;
final bool isActive;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Sort',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600)),
if (!isActive)
StreamBuilder(
stream: sorts,
builder: (context, snapshot) =>
Text(snapshot.hasData ? snapshot.data!.title : '')),
],
);
}
}
```
```dart Props theme={"system"}
Stream sorts; // Stream of sorting indexes
bool isActive; // Tells if sort view is active
```
# Autocomplete
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/search-autocomplete/flutter
Algolia's Autocomplete component presents a search interface.
## Code summary
The `Autocomplete` component is used on the Ecommerce UI demo app search page when you tap the search box in the main page.
It consists of two sections: one presenting the search history and the second one presenting the query suggestions.
The [`AppBar`](https://api.flutter.dev/flutter/material/AppBar-class.html) includes a `SearchHeaderView` widget representing the search box on the autocomplete screen.
```dart Usage icon=code theme={"system"}
class SearchHeaderView extends StatelessWidget {
const SearchHeaderView(
{Key? key, required this.controller, this.onSubmitted, this.onChanged})
: super(key: key);
final TextEditingController controller;
final ValueChanged? onChanged;
final ValueChanged? onSubmitted;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: controller,
onSubmitted: onSubmitted,
onChanged: onChanged,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: "Search products, articles, faq, ..."),
),
),
if (controller.text.isNotEmpty)
IconButton(
iconSize: 34,
onPressed: controller.clear,
icon: const Icon(Icons.clear),
color: AppTheme.darkBlue),
const SizedBox(width: 8)
],
);
}
}
```
Each section consists of couple of [`SliverAppBar`](https://api.flutter.dev/flutter/material/SliverAppBar-class.html) and [`SliverList`](https://api.flutter.dev/flutter/widgets/SliverList-class.html) components representing the section header and section body respectively.
```dart Usage icon=code theme={"system"}
Widget _sectionHeader(Stream> itemsStream, Widget title) =>
StreamBuilder>(
stream: itemsStream,
builder: (context, snapshot) {
final suggestions = snapshot.data ?? [];
return SliverSafeArea(
top: false,
bottom: false,
sliver: SliverPadding(
padding: const EdgeInsets.only(left: 15),
sliver: SliverToBoxAdapter(
child:
suggestions.isEmpty ? const SizedBox.shrink() : title,
)));
});
Widget _sectionBody(
BuildContext context,
Stream> itemsStream,
Function(Item) rowBuilder,
) =>
StreamBuilder>(
stream: itemsStream,
builder: (context, snapshot) {
final suggestions = snapshot.data ?? [];
if (suggestions.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverSafeArea(
top: false,
sliver: SliverPadding(
padding: const EdgeInsets.only(left: 15),
sliver: SliverFixedExtentList(
itemExtent: 44,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final item = suggestions[index];
return InkWell(
onTap: () {
final query = item.toString();
_onSubmitSearch(query, context);
},
child: rowBuilder(item));
},
childCount: suggestions.length,
))));
});
```
## Search history section
All the submitted search queries are stored in the Autocomplete screen state.
Users can delete each history item or remove them all by tapping the corresponding button in the section header.
Each history row is represented by a `HistoryRowView`:
```dart Usage icon=code theme={"system"}
class HistoryRowView extends StatelessWidget {
const HistoryRowView({Key? key, required this.suggestion, this.onRemove})
: super(key: key);
final String suggestion;
final Function(String)? onRemove;
@override
Widget build(BuildContext context) {
return Row(children: [
const Icon(Icons.refresh),
const SizedBox(
width: 10,
),
Text(suggestion, style: const TextStyle(fontSize: 16)),
const Spacer(),
IconButton(
onPressed: () => onRemove?.call(suggestion),
icon: const Icon(Icons.close)),
]);
}
}
```
## Query Suggestions section
By default it shows the list of popular searches.
When user starts typing a search query the list is refreshing automatically and presents the highlighted search completions.
Each suggestion row is represented by a `SuggestionRowView`:
```dart Usage icon=code theme={"system"}
class SuggestionRowView extends StatelessWidget {
const SuggestionRowView({Key? key, required this.suggestion, this.onComplete})
: super(key: key);
final QuerySuggestion suggestion;
final Function(String)? onComplete;
@override
Widget build(BuildContext context) {
return Row(children: [
const Icon(Icons.search),
const SizedBox(
width: 10,
),
RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black),
children: suggestion.highlighted!.toInlineSpans())),
const Spacer(),
IconButton(
onPressed: () => onComplete?.call(suggestion.query),
icon: const Icon(Icons.north_west),
)
]);
}
}
```
The highlighting of the suggestion row is possible through `HighlightedString.toInlineSpans()` from [Flutter helpers](https://pub.dev/packages/algolia_helper_flutter).
The entire autocomplete logic can be found in the `autocomplete_screen` file.
You can customize `Autocomplete` in:
* The [`autocomplete_screen` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/search/autocomplete_screen.dart)
* The [`history_row_view` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/search/components/history_row_view.dart)
* The [`suggestion_row_view` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/search/components/suggestion_row_view.dart)
# Showcase
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/components/in-depth/showcase/flutter
Algolia's Showcase component organizes media-rich records into scannable rows.
The home page is where the user's discovery of your products starts,
and it's the place to promote sets of products based on your business needs and audience type.
Instead of overwhelming users with a large grid of products, use showcases to group products into scannable sections such as "most popular", "trending", and "just for you".
Algolia's `Showcase` component works well on both desktop and mobile and lets you set up several rows on your home page using different settings according to your needs.
For instance, you can customize them to adjust aspects such as the number of items per page and their order.
## Code summary
The `Showcase` component is used on the [Ecommerce UI demo site](https://algolia-pwa-ecom-ui-template.netlify.app) home page.
It consists of three rows, all with the same index name but different search parameters.
You can customize `Showcase` in:
* The [`home_screen` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/home_screen.dart)
* The [`products_view` file](https://github.com/algolia/flutter-ecom-ui-template/blob/0.1.0/lib/ui/screens/home/components/products_view.dart)
* Its props
### Usage and props
```dart Usage theme={"system"}
class ProductsView extends StatelessWidget {
const ProductsView({
Key? key,
required this.title,
required this.items,
required this.productWidget,
}) : super(key: key);
final String title;
final Stream> items;
final ProductWidgetBuilder productWidget;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SectionHeader(title: title),
Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
height: 200.0,
child: HitsListView(
items: items,
productWidget: productWidget,
scrollDirection: Axis.horizontal))
],
);
}
}
```
```dart Props theme={"system"}
final String title; // The title for the component.
final List items; // The list of product items to show.
final ProductWidgetBuilder productWidget; // Renders each hit from the results.
// You can find an example component called ProductCardView.
// It receives the product hit as a property and does the mapping between
// the dataset attributes and the ProductCardView props to display a Showcase item.
```
# Get started with the Ecommerce UI template
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/getting-started/flutter
Learn how to build an online store front with Algolia's Ecommerce UI template.
You can use the sample data, or customize the app to connect your data.
You can run the Ecommerce UI template in one of two ways:
* **Using the `latency` Algolia application.** If you use the `latency` application, you can't customize or index configurations (such as settings, Rules, or synonyms).
* **Using your own Algolia .** To do this, you must [send your data](/doc/guides/sending-and-managing-data/send-and-update-your-data) to your Algolia .
## Run the Ecommerce UI template
1. [Install Flutter](https://docs.flutter.dev/get-started/install)
2. [Fork the Flutter ecommerce UI template repository](https://github.com/algolia/flutter-ecom-ui-template)
3. [Clone the forked repository](https://help.github.com/articles/cloning-a-repository) to your local machine
4. Install the dependencies: `flutter pub get`
5. Configure the [environment variables](/doc/guides/building-search-ui/ecommerce-ui-template/getting-started/flutter#connect-your-algolia-app-to-the-ui)
6. Start the application: `flutter run`
### Connect your Algolia app to the UI
Change the values of the Algolia credentials in the `lib/credentials.dart` file.
To use the sample data from Algolia's Ecommerce UI demo, leave the values as they are.
The following variables are used:
| Name | Value |
| - | - |
| `applicationID` | Your Algolia |
| `searchOnlyKey` | Your Algolia search |
| `hitsIndex` | Your Algolia index name |
| `suggestionsIndex` | Your Algolia Query Suggestions index name |
## API client
To use the Algolia API from your Dart app, you have these options:
* Use the [Dart API client](https://pub.dev/packages/algoliasearch) (used in this guide)
* Call the [API methods](/doc/rest-api) directly using the [`http`](https://pub.dev/packages/http) package.
## Structure
If you want to experiment with customization, the Ecommerce UI template is structured as follows:
* `lib/data`: services and data providers
* `lib/model`: model objects for products data representation
* `lib/ui/screens`: applications screens and their components
* `lib/ui/widgets`: standalone UI widgets used on multiple screens
### Use your own data and configurations
The demo application data and configurations can be found in the `dataset` folder as JSON files that you can [upload to Algolia](/doc/guides/sending-and-managing-data/send-and-update-your-data):
* `products_configuration.json`
* `products_dataset.json`
* `products_query_suggestions_configuration.json`
* `products_query_suggestions_dataset.json`
## Example product record
The following is an example of a typical product record:
```json JSON icon=braces theme={"system"}
{
"objectID": "A0E200000002GZB",
"name": "Michael Kors – shopper "Jet Set Travel"",
"description": "The sleek leather shopper from Michael Kors is the perfect every day bag, which offers enough space for the most important essentials in the office while traveling or shopping. The longer handles allow you to carry the bag comfortably on the shoulder, while the black leather and silver tag provides subtle elegance. A real investment piece that will accompany you from season to season.",
"brand": "Michael Kors",
"gender": "women",
"hierarchical_categories": {
"lvl0": "Women",
"lvl1": "Women > Bags",
"lvl2": "Women > Bags > Shopper"
},
"image_urls": [
"https://res.cloudinary.com/hilnmyskv/image/upload/v1638375538/flagship_sunrise/A0E200000002GZB_0.jpg"
],
"reviews": {
"rating": 4,
"count": 42,
"bayesian_avg": 3.906976744186046
},
"color": {
"filter_group": "black;#333",
"original_name": "black"
},
"available_sizes": [
"one size"
],
"price": {
"currency": "EUR",
"value": 343.75,
"discounted_value": 0,
"discount_level": -100,
"on_sales": false
}
}
```
# Ecommerce UI template
Source: https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/overview/flutter
Build an attractive, first-class search and discovery experience for your online store built on top of best practices.
Get inspired and speed up the implementation of Algolia in your Flutter based cross platform app (Android/iOS).
The Algolia [Flutter helpers](https://pub.dev/packages/algolia_helper_flutter) outlined in this guide will help you build an attractive, first-class search and discovery experience.
## Start with a template, not a blank page
The Ecommerce UI template has significant benefits for developers creating an online store:
* **It's production-ready.**
As a developer, you can tailor the UI template to your business using pre-packaged, customizable components.
You can re-use as much of the template as you like and customize the rest.
* You get a beautiful, native search and discovery experience.
* The UI template focuses on great user experience and performance.
## What's a UI template?
The Ecommerce UI template helps mobile developers and UI/UX designers build an online store that matches your business needs.
It's a starter package, designed to be mobile-first, optimized for UI/UX using [Algolia's UI design kit](/doc/guides/building-search-ui/resources/ui-kit/flutter), and embracing search and discovery best practices.
The UI template has two main screens:
* **Home page,** which focuses on product discovery
* **Product listing page,** which unifies search and product discovery into a single experience allowing users to find products by combining keyword-based search and facet-based navigation
The UI template uses the best of the Algolia platform:
* [Search and browse](/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria)
* [Query suggestions](/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/js)
* [Dynamic Re-ranking](/doc/guides/algolia-ai/re-ranking)
* [Sorting](/doc/guides/managing-results/refine-results/sorting#exhaustive-and-relevant-sorting)
The UI template is built with:
* [**Flutter**](https://flutter.dev) for building cross-platform native apps
* [**Algolia Flutter Helpers**](https://pub.dev/packages/algolia_helper_flutter) to implement advanced search features
* [**Algolia Dart API Client**](https://pub.dev/packages/algoliasearch) to connect to Algolia's APIs
To start a new project, see [Getting started](/doc/guides/building-search-ui/ecommerce-ui-template/getting-started/flutter).
## Components
To get an overview of the UI template components, see [Components](/doc/guides/building-search-ui/ecommerce-ui-template/components/flutter)
# Send click and conversion events with InstantSearch Android
Source: https://www.algolia.com/doc/guides/building-search-ui/events/android
Unlock Algolia's most powerful features by sending click and conversion events from your InstantSearch Android app.
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
To send events from your InstantSearch Android app, follow these steps:
1. Add the `insights` client.
2. Add [click events](#send-click-events) when users click search results.
3. [Track conversions](#send-conversion-events) that start in your InstantSearch app.
## Add the InstantSearch Insights library
Add the library as a dependency to your `build.gradle` file:
```groovy Gradle theme={"system"}
implementation "com.algolia:instantsearch-insights-android:4.+"
```
## Add the Insights client
To add the Insights client, you'll need your Algolia application ID, (search) API key, and the name.
You can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account/api-keys).
```kotlin Kotlin theme={"system"}
val appID = "applicationID"
val apiKey = "apiKey"
val indexName = "indexName"
val configuration = Insights.Configuration(
connectTimeoutInMilliseconds = 5000,
readTimeoutInMilliseconds = 5000
)
registerInsights(context, appID, apiKey, indexName, configuration)
```
To send events without timestamps, and be automatically attributed on the server, set the `generateTimestamps` parameter to `false`:
```kotlin Kotlin theme={"system"}
val configuration = Insights.Configuration(generateTimestamps = false)
registerInsights(context, appID, apiKey, indexName, configuration)
```
## Set the user token
All events must have a to specify the user it relates to.
You can set it in three ways:
* Globally, for all events
* Per app, for every event tracked by the app
* Individually, for each event
```kotlin Kotlin theme={"system"}
// Global userToken default value
val configuration = Insights.Configuration(
connectTimeoutInMilliseconds = 5000,
readTimeoutInMilliseconds = 5000,
defaultUserToken = "userToken"
)
registerInsights(context, appID, apiKey, indexName, configuration)
// App userToken overrides global default
sharedInsights(indexName).apply {
userToken = "userToken"
}
// Event userToken overrides previous defaults
sharedInsights?.clickedObjectIDsAfterSearch(
eventName = "eventName",
indexName = "indexName",
userToken = "userToken",
timestamp = System.currentTimeMillis(),
queryID = "queryId",
objectIDs = listOf("objectID1"),
positions = listOf(1)
)
```
### User opt-out
You can exclude users who opted out from tracking with the following code:
```kotlin Kotlin theme={"system"}
sharedInsights(indexName)?.enabled = false
// Alternatively, by getting the latest registered Insights instance
sharedInsights?.enabled = false
```
## Enable automatic view events tracking
InstantSearch can automatically send view events when hits are returned from Algolia. To enable this feature, set `isAutoSendingHitsViewEvents` to `true` when you initialize [`HitsSearcher`](/doc/api-reference/widgets/instantsearch/android#hitssearcher):
```kotlin Kotlin theme={"system"}
val searcher = HitsSearcher(
applicationID = "ALGOLIA_APPLICATION_ID",
apiKey = "ALGOLIA_SEARCH_API_KEY",
indexName = "indexName",
isAutoSendingHitsViewEvents = true
)
```
## Create a `HitsTracker` to track events from search results
```kotlin Kotlin theme={"system"}
val hitsTracker = HitsTracker(
eventName = "hits",
searcher = searcher,
insights = sharedInsights(indexName)
)
```
## Create a `FilterTracker` to track events from filters
```kotlin Kotlin theme={"system"}
val filterTracker = FilterTracker(
eventName = "demo",
searcher = searcher,
insights = sharedInsights(indexName)
)
```
## Send events
Use the `trackView`, `trackClick`, and `trackConversion` methods to send events.
When you add events, check them in the [**Events Debugger**](https://dashboard.algolia.com/events/debugger) in the Algolia dashboard.
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
### Send view events
```kotlin Kotlin theme={"system"}
// HitsTracker
hitsTracker.trackView(hit)
// FilterTracker
filterTracker.trackView(facet)
```
### Send click events
```kotlin Kotlin theme={"system"}
// HitsTracker
hitsTracker.trackClick(hit, position)
// FilterTracker
filterTracker.trackClick(facet)
```
### Send conversion events
```kotlin Kotlin theme={"system"}
// HitsTracker
hitsTracker.trackConvert(hit)
// FilterTracker
filterTracker.trackConversion(facet)
```
If you upgraded from InstantSearch Android 3.x to 4.x:
* Your hit model must include an `objectID` and implement the [Indexable interface](/doc/guides/building-search-ui/upgrade-guides/android#indexable-interface).
* Facet results are now `FacetHits` (they used to be `Facet`).
Conversions often happen outside your search pages.
For example, the Order completed event for a successful purchase happens in the shopping cart.
To capture these conversions, [keep track of the query ID](/doc/guides/sending-events/guides/queryid) across your app.
### Event batching
By default, events are sent in batches of 10.
To customize this, adjust the `minBatchSize` parameter:
```kotlin Kotlin theme={"system"}
// Customize at registration
registerInsights(context, appID, apiKey, indexName, configuration).apply {
minBatchSize = 1
}
// Customize by getting shared insights
sharedInsights(indexName).apply {
minBatchSize = 1
}
```
## Enable logging
You can also check if you've sent an event by enabling logging:
```kotlin Kotlin theme={"system"}
// Check at insights registration
registerInsights(context, appID, apiKey, indexName, configuration).apply {
loggingEnabled = true
}
// Check by getting shared insights
sharedInsights(indexName).apply {
loggingEnabled = true
}
```
After you've enabled it, check the output for success messages or errors.
```txt theme={"system"}
// Success
[Algolia Insights - appName] Sync succeeded for EventsPackage(id: "37E9A093-8F86-4049-9937-23E99E4E4B33", events: [{
eventName = "search result click";
eventType = click;
index = "my index";
objectIDs = (
1234567
);
positions = (
3
);
queryID = 08a76asda34fl30b7d06b7aa19a9e0;
timestamp = 1545069313405;
userToken = "C1D1322E-8CBF-432F-9875-BE3B5AFDA498";
}], region: nil)
//Error
[Algolia Insights - appName] The objectID field is missing (Code: 422)
```
For the `positions` parameter of the [`clickedObjectIdsAfterSearch`](/doc/libraries/sdk/v1/methods/clicked-object-ids-after-search) method, the first object in the list of search results has a value of 1 (not 0), the second has a value of 2.
# Send click and conversion events with the Flutter Helper
Source: https://www.algolia.com/doc/guides/building-search-ui/events/flutter
Unlock Algolia's most powerful features by sending click and conversion events from your Flutter app.
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
## Add the Insights Dart library
To send events to Algolia from your Flutter app, add the Insights Dart library to your project:
1. Add `algolia_insights: ^1.0.0` to your `pubspec.yaml` file.
2. Run `pub get` console command.
3. Add `import 'package:algolia_insights/algolia_insights.dart';` to your source files.
## Add the Insights client
To add the Insights client, you'll need your Algolia application ID, (search) API key, and a `userToken` to identify your users.
You can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account/api-keys).
```dart Dart theme={"system"}
final insights = Insights('ALGOLIA_APPLICATION_ID', 'ALGOLIA_SEARCH_API_KEY');
```
## Customizing the user token
You can set a custom by setting the `userToken` property on the Insights instance.
The user token is used to track events for a specific user.
If the user token isn't specified, the system will automatically generate a token and store it on the device.
This auto-generated user token ensures that events are still associated with a unique user identifier, even if a custom token isn't explicitly provided.
```dart Dart theme={"system"}
insights.userToken = 'user-123';
```
## Send events from the HitsSearcher component
If you're using multiple [`Hits Searcher`](/doc/api-reference/widgets/instantsearch/flutter) widgets,
you must enable sending click events on each one separately.
This lets you choose which click events to track and which to ignore.
The `HitsSearcher` component sends view events automatically.
It sets the [`clickAnalytics`](/doc/api-reference/api-parameters/clickAnalytics) parameter to get the `queryID`.
With the `queryID` parameter available, you can send click and conversion events:
```dart Dart theme={"system"}
final searcher = HitsSearcher(
applicationID: 'ALGOLIA_APPLICATION_ID',
apiKey: 'ALGOLIA_SEARCH_API_KEY',
indexName: 'YourIndexName',
);
searcher.eventTracker.clickedObjects(
eventName: 'Clicked Objects',
objectIDs: ['object1', 'object2', 'object3'],
positions: [1, 2, 3]);
searcher.eventTracker.viewedObjects(
eventName: 'Viewed Objects',
objectIDs: ['object1', 'object2', 'object3']);
searcher.eventTracker.convertedObjects(
eventName: 'Converted Objects',
objectIDs: ['object1', 'object2', 'object3']);
```
To turn off both automatic and manual events in the `HitsSearcher` class, set:
```dart Dart theme={"system"}
searcher.eventTracker.isEnabled = false;
```
This can be useful when you want to pause event tracking temporarily or when working in a development environment.
For the `positions` parameter of the [`clickedObjectIdsAfterSearch`](/doc/libraries/sdk/v1/methods/clicked-object-ids-after-search) method, the first object in the list of search results has a value of 1 (not 0), the second has a value of 2.
## Send events from the `FacetList` component
The `FacetList` component inherits the application ID, API key and name parameters from the `HitsSearcher`.
It also provides methods to track view, conversion, and click events for a .
```dart Dart theme={"system"}
final searcher = HitsSearcher(applicationID: 'ALGOLIA_APPLICATION_ID',
apiKey: 'ALGOLIA_SEARCH_API_KEY',
indexName: 'YourIndexName');
final facetList = searcher.buildFacetList(
filterState: filterState,
attribute: 'actors',
);
facetList.eventTracker.clickedFilters(eventName: 'Clicked Filters',
values: ['Samuel L. Jackson']);
facetList.eventTracker.viewedFilters(eventName: 'Viewed Filters',
values: ['Samuel L. Jackson']);
facetList.eventTracker.convertedFilters(eventName: 'Converted Filters',
values: ['Samuel L. Jackson']);
```
To turn off both automatic and manual event sending in the `FacetList` class, set:
```dart Dart theme={"system"}
facetList.eventTracker.isEnabled = false;
```
This can be useful when you want to pause event tracking temporarily or when working in a development environment.
## Send click, conversion, and view events from other sources
Use the following methods to send click, conversion, or view events:
```dart Dart theme={"system"}
insights.clickedObjectsAfterSearch(
indexName: 'YourIndexName',
eventName: 'Clicked Item',
queryID: 'query id',
objectIDs: ['objectID1', 'objectID2', 'objectID3'],
positions: [5, 6, 7],
);
insights.convertedObjectsAfterSearch(
indexName: 'YourIndexName',
eventName: 'Purchased Item',
queryID: 'query id',
objectIDs: ['objectID1', 'objectID2', 'objectID3']
);
insights.viewedObjects(
indexName: 'YourIndexName',
eventName: 'Viewed Item',
objectIDs: ['objectID1', 'objectID2', 'objectID3']
);
```
Conversions often happen outside your search pages.
For example, the Order completed event for a successful purchase happens in the shopping cart.
To capture these conversions, [keep track of the query ID](/doc/guides/sending-events/guides/queryid) across your app.
When you add events, check them in the [Events Debugger](https://dashboard.algolia.com/events/debugger) in the Algolia dashboard.
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
# Send click and conversion events with InstantSearch iOS
Source: https://www.algolia.com/doc/guides/building-search-ui/events/ios
Unlock Algolia's most powerful features by sending click and conversion events from your InstantSearch iOS app.
Starting May 1, 2024,
Apple requires all iOS apps to include a privacy manifest.
For more information, see [Privacy manifest](/doc/guides/building-search-ui/resources/privacy-manifest/ios).
Events are actions that users take on your app or website.
They unlock powerful features, such as recommendations,
personalization, smarter search results,
and analytics that help you optimize your user experience.
For more information, see [Choose how to send events](/doc/guides/sending-events/getting-started).
To send events from your InstantSearch iOS app, follow these steps:
1. Add the `insights` client.
2. Add [click events](#send-click-events-from-hits-widgets) when users click search results.
3. [Track conversions](#send-click-conversion-and-view-events) that start in your InstantSearch app.
## Add the InstantSearch Insights library
Add the library as a dependency using Swift Package Manager or CocoaPods, and add `import InstantSearchInsights` to your source files.
### Swift Package Manager
To use Swift Package Manager, open your project with Xcode.
Go to **File > Add Packages** and enter the URL of the InstantSearch iOS library (`https://github.com/algolia/instantsearch-ios`).
In the **Choose Package Products for instantsearch-ios** dialog,
include `InstantSearchInsights`
If you're a framework author and use InstantSearch Insights as a dependency, update your `Package.swift` file:
```swift Swift icon=code theme={"system"}
let package = Package(
dependencies: [
.package(name: "InstantSearch", url: "https://github.com/algolia/instantsearch-ios", from: "8.0.0")
]
)
```
Add `.product(name: "InstantSearchInsights", package: "InstantSearch")` to your target dependencies list.
### CocoaPods
If you use [CocoaPods](https://cocoapods.org/),
add the following line to your `Podfile`:
```ruby Ruby theme={"system"}
pod("InstantSearch/Insights", "~> 8.0")
```
Run `pod update` to download the dependencies.
## Add the Insights client
To add the Insights client, you'll need your Algolia application ID, (search) API key, and a to identify your users.
You can find your application ID and API key in the [Algolia dashboard](https://dashboard.algolia.com/account/api-keys).
```swift Swift icon=code theme={"system"}
Insights.register(appId: "testApp", apiKey: "testKey", userToken: "testToken")
```
To send events without timestamps, and be automatically attributed on the server, set the `generateTimestamps` parameter to `false`:
```swift Swift icon=code theme={"system"}
Insights.register(appId: "testApp",
apiKey: "testKey",
userToken: "testToken",
generateTimestamps = false)
```
To specify the region you prefer to use, set the `region` parameter to `.de` or `.us`:
```swift Swift icon=code theme={"system"}
Insights.register(appId: "testApp",
apiKey: "testKey",
userToken: "testToken",
region: .de)
```
### Customize the events flush delay
By default, the client sends events to Algolia every 30 minutes.
You can adjust this delay by changing the `flushDelay` value (in seconds):
```swift Swift icon=code theme={"system"}
Insights.flushDelay = 60
```
### Set the Insights API region
By default, each time you send an event, Algolia geo-routes the API call so that the call targets the closest servers.
The analytics API supports two regions: United States and Germany.
You can specify the region you prefer to use as follows.
```swift Swift icon=code theme={"system"}
Insights.region = .de
```
## Enable automatic view events
InstantSearch can automatically send view events when hits are returned from Algolia. To enable this feature, set `isAutoSendingHitsViewEvents` to `true` when you initialize [`HitsSearcher`](/doc/api-reference/widgets/instantsearch/ios#hitssearcher):
```swift Swift icon=code theme={"system"}
let searcher = try HitsSearcher(appID: "ALGOLIA_APPLICATION_ID",
apiKey: "ALGOLIA_SEARCH_API_KEY",
indexName: "indexName",
isAutoSendingHitsViewEvents: true)
```
## Send click events from hits widgets
To send click events when users select a search result in a [`Hits`](/doc/api-reference/widgets/hits/ios) widget,
use the following code:
```swift Swift icon=code theme={"system"}
let searcher = try HitsSearcher(appID: "ALGOLIA_APPLICATION_ID",
apiKey: "ALGOLIA_SEARCH_API_KEY",
indexName: "indexName")
let hitsInteractor = HitsInteractor>()
// Establish a connection between the searcher and hits interactor
hitsInteractor.connectSearcher(searcher)
// Declare a HitsTracker which takes care of activating clickAnalytics
// in the Query and extracts queryID from a received result for
// the index in the question
let hitsTracker = HitsTracker(eventName: "didClickItem", searcher: searcher)
// Did click the 10th item in the list
let hit = hitsInteractor.hit(atIndex: 10)!
hitsTracker.trackClick(for: hit, position: 10)
```
If you're using multiple [`Hits`](/doc/api-reference/widgets/hits/ios) widgets, you must enable sending click events on each one separately.
Check your click events in the [**Events Debugger**](https://dashboard.algolia.com/events/debugger).
## Send click, conversion, and view events
Use the following methods to send click, conversion, or view events:
```swift Swift icon=code theme={"system"}
Insights.shared?.clickedAfterSearch(eventName: "click event",
indexName: "indexName",
objectID: "object id",
position: 1,
queryID: "query id")
Insights.shared?.convertedAfterSearch(eventName: "conversion event",
indexName: "indexName",
objectIDs: ["obj1", "obj2"],
queryID: "query id")
Insights.shared?.viewed(eventName: "view event",
indexName: "indexName",
filters: ["brand:apple"])
```
Conversions often happen outside your search pages.
For example, the Order completed event for a successful purchase happens in the shopping cart.
To capture these conversions, [keep track of the query ID](/doc/guides/sending-events/guides/queryid) across your app.
When you add events, check them in the [**Events Debugger**](https://dashboard.algolia.com/events/debugger) in the Algolia dashboard.
For more information, see [Validate your events](/doc/guides/sending-events/guides/validate).
## Enable logging
You can also check if you've sent an event by enabling logging:
```swift Swift icon=code theme={"system"}
Insights.shared(appId: "ALGOLIA_APPLICATION_ID")?.isLoggingEnabled = true
```
After you've enabled it, check the output for success messages or errors.
```txt theme={"system"}
// Success
[Algolia Insights - appName] Sync succeded for EventsPackage(id: "37E9A093-8F86-4049-9937-23E99E4E4B33", events: [{
eventName = "search result click";
eventType = click;
index = "my index";
objectIDs = (
1234567
);
positions = (
3
);
queryID = 08a76asda34fl30b7d06b7aa19a9e0;
timestamp = 1545069313405;
userToken = "C1D1322E-8CBF-432F-9875-BE3B5AFDA498";
}], region: nil)
//Error
[Algolia Insights - appName] The objectID field is missing (Code: 422)
```
For the `positions` parameter of the [`clickedObjectIdsAfterSearch`](/doc/libraries/sdk/v1/methods/clicked-object-ids-after-search) method,
the first object in the list of search results has a value of 1 (not 0), the second has a value of 2.
# Get started with InstantSearch Android
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/android
Build an InstantSearch Android page from start to finish.
To get started, see:
* [Get started with declarative UI](/doc/guides/building-search-ui/getting-started/how-to/declarative/android)
* [Get started with imperative UI](/doc/guides/building-search-ui/getting-started/how-to/programmatically/android)
If you don't want to use the InstantSearch Android library,
you can use the [Kotlin API client](/doc/libraries/sdk/install).
# Get started with Flutter Helper
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/flutter
Build a cross-platform search experience with Flutter and the Algolia Flutter Helper library.
This search experience includes:
* A search box to type your
* Statistics about the current search
* A list to display search results with infinite scrolling
* A refinement list to let users results
For more information about the library, see [Algolia Flutter Helper library](https://pub.dev/packages/algolia_helper_flutter).
## Prepare your project
Before you can use Algolia, you need an Algolia account.
You can [create a new one for free](https://dashboard.algolia.com/users/sign_up) or use the following credentials
for an example products dataset:
* : `latency`
* Search : `927c3fe76d4b52c5a2912973f35a3077`
* name: `STAGING_native_ecom_demo_products`
### Create a new Flutter app
In a terminal, run:
```sh Command line icon=square-terminal theme={"system"}
flutter create algoliasearch
```
### Add project dependencies
This tutorial uses the [Algolia Flutter Helper library](https://pub.dev/packages/algolia_helper_flutter) to integrate Algolia and the [Infinite Scroll Pagination](https://pub.dev/packages/infinite_scroll_pagination) library for infinite scrolling.
Add `algolia_helper_flutter` and `infinite_scroll_pagination` as dependencies to your project:
```yaml pubspec.yaml icon=braces theme={"system"}
dependencies:
algolia_helper_flutter: ^1.0.0
infinite_scroll_pagination: ^3.2.0
```
In a terminal, run:
```sh Command line icon=square-terminal theme={"system"}
flutter pub get
```
## Create a basic search interface
Build a basic search interface with a search box and a search metadata panel for showing the number of search results.
Open the file `./lib/main.dart` and look for the `_MyHomePageState` class.
Remove its sample variables and method declarations (`_counter`, `_incrementCounter`),
then import the Flutter Helper library:
```dart Dart icon=code theme={"system"}
import 'package:algolia_helper_flutter/algolia_helper_flutter.dart';
```
Add the `_productsSearcher` property of the `HitsSearcher` type with your Algolia credentials as parameters.
The `HitsSearcher` component performs search requests and obtains search results.
```dart Dart icon=code theme={"system"}
final _productsSearcher = HitsSearcher(
applicationID: 'latency',
apiKey: '927c3fe76d4b52c5a2912973f35a3077',
indexName: 'STAGING_native_ecom_demo_products',
);
```
Add the `_searchTextController` property to `_MyHomePageState`.
It controls and listens to the state of the `TextField` component you use as the search box.
```dart Dart icon=code theme={"system"}
final _searchTextController = TextEditingController();
```
Add a `SearchMetadata` class with the metadata of the latest search.
In this example, it only contains the `nbHits` value, which is the number of search results.
The `SearchMetadata` class also has a `fromResponse` factory method which extracts the `nbHits` value from the `SearchResponse`.
```dart Dart icon=code theme={"system"}
class SearchMetadata {
final int nbHits;
const SearchMetadata(this.nbHits);
factory SearchMetadata.fromResponse(SearchResponse response) =>
SearchMetadata(response.nbHits);
}
```
Add the `_searchMetadata` stream which listens to `_productSearcher` responses and transforms them to `SearchMetaData` instance.
```dart Dart icon=code theme={"system"}
Stream get _searchMetadata =>
_productsSearcher.responses.map(SearchMetadata.fromResponse);
```
Override the `build` method containing the user interface declaration.
The interface is based on the `Scaffold` component.
Add the `AppBar` with "Algolia and Flutter" as its title,
and the `Column` component as its body:
```dart Dart icon=code theme={"system"}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Algolia and Flutter'),
),
body: Center(
child: Column(
children: [],
),
),
);
}
```
The `Column`'s body consists of three children:
the search box, the metadata panel and the hits list.
```dart Dart icon=code theme={"system"}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Algolia and Flutter')),
body: Center(
child: Column(
children: [
SizedBox(
height: 44,
child: TextField(
controller: _searchTextController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon: Icon(Icons.search),
),
),
),
],
),
),
);
}
```
Save your changes in the `main.dart` file,
then build and run your application by running `flutter run` in a terminal or [your development tool](https://docs.flutter.dev/get-started/test-drive).
The simulator shows the app bar with title and the search box below it.
Add a `Text` widget embedded in `Padding` and `StreamBuilder` widgets to show the search metadata.
The `StreamBuilder` widget ensures update of the `Text` on each `_searchMetadata` stream change.
```dart Dart icon=code theme={"system"}
StreamBuilder(
stream: _searchMetadata,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text('${snapshot.data!.nbHits} hits'),
);
},
)
```
Add `StreamBuilder` as the second child to the main `Column` widget.
```dart Dart icon=code theme={"system"}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Algolia and Flutter')),
body: Center(
child: Column(
children: [
SizedBox(
height: 44,
child: TextField(
controller: _searchTextController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon: Icon(Icons.search),
),
),
),
StreamBuilder(
stream: _searchMetadata,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text('${snapshot.data!.nbHits} hits'),
);
},
),
],
),
),
);
}
```
Build and run the app.
You might see the centered text with the hits count below the search box.
If you type into the search box, the displayed value remains unchanged.
This happens because the `_searchTextController` and `_productsSearcher` aren't connected.
To fix it, override the `initState` method of the `_MyHomePageState` class and add a listener to the `_searchTextController` that propagates the input text to the `_productsSearcher`.
```dart Dart icon=code theme={"system"}
void initState() {
super.initState();
_searchTextController.addListener(
() => _productsSearcher.query(_searchTextController.text),
);
}
```
The search metadata panel now updates dynamically on each change of the search box.
To free up resources, dispose the `_searchTextController` and `_productsSearcher` by overriding the `dispose` method of the `_MyHomePageState`.
```dart Dart icon=code theme={"system"}
@override
void dispose() {
_searchTextController.dispose();
_productsSearcher.dispose();
super.dispose();
}
```
## Results list and infinite scrolling
To show search results and their number with infinite scrolling:
Add a `Product` class that represents a search hit.
To keep this example simple,
it contains a name and an image URL field.
Declare a `fromJson` constructor method for creating `Product` from a JSON string.
```dart Dart icon=code theme={"system"}
class Product {
final String name;
final String image;
Product(this.name, this.image);
static Product fromJson(Map json) {
return Product(json['name'], json['image_urls'][0]);
}
}
```
```dart Dart icon=code theme={"system"}
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
```
Add the `_pagingController` component that handles the infinite scrolling logic as `_MyHomePageState` class property.
```dart Dart icon=code theme={"system"}
final PagingController _pagingController = PagingController(firstPageKey: 0);
```
To declare the `HitsPage` class, which represents a page of search results, call the `fromResponse` factory method which builds a `HitsPage` from a `SearchResponse`.
```dart Dart icon=code theme={"system"}
class HitsPage {
const HitsPage(this.items, this.pageKey, this.nextPageKey);
final List items;
final int pageKey;
final int? nextPageKey;
factory HitsPage.fromResponse(SearchResponse response) {
final items = response.hits.map(Product.fromJson).toList();
final isLastPage = response.page >= response.nbPages;
final nextPageKey = isLastPage ? null : response.page + 1;
return HitsPage(items, response.page, nextPageKey);
}
}
```
1. Add the `_searchPage` stream which listens to `_productSearcher` responses and transforms it to `HitsPage` object.
```dart Dart icon=code theme={"system"}
Stream get _searchPage => _productsSearcher.responses.map(HitsPage.fromResponse);
```
2. Add the `_hits` function to the `_MyHomePageState` class which builds the list of search results.
It returns `PagedListView`, a component of the `infinite_scroll_pagination` library, taking `_pagingController` as parameter.
```dart Dart icon=code theme={"system"}
Widget _hits(BuildContext context) => PagedListView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
noItemsFoundIndicatorBuilder: (_) =>
const Center(child: Text('No results found')),
itemBuilder: (_, item, __) => Container(
color: Colors.white,
height: 80,
padding: const EdgeInsets.all(8),
child: Row(
children: [
SizedBox(width: 50, child: Image.network(item.image)),
const SizedBox(width: 20),
Expanded(child: Text(item.name)),
],
),
),
),
);
```
3. Add the results of the `_hits` function as the third child to the main `Column` widget embedded in the `Expanded` widget so that it can fill the available screen space.
```dart Dart icon=code theme={"system"}
Column(children: [
SizedBox(
height: 44,
child: TextField(
controller: _searchTextController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon: Icon(Icons.search),
),
)),
StreamBuilder(
stream: _searchMetadata,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text('${snapshot.data!.nbHits} hits'),
);
},
),
Expanded(
child: _hits(context),
)
],)
```
Build and run the app.
You can now see the loading indicator instead of search results.
This happens because the `_pagingController` and the `_productsSearcher` aren't connected.
To update the `_pagingController` whenever a new results page is fetched,
add a listener to `__searchPage` in the `initState` method.
Add a call to `_pagingController.refresh()` to the `_searchTextController` listener callback.
```dart Dart icon=code theme={"system"}
@override
void initState() {
super.initState();
_searchTextController.addListener(
() => _productsSearcher.applyState(
(state) => state.copyWith(query: _searchTextController.text, page: 0),
),
);
_searchPage
.listen((page) {
if (page.pageKey == 0) {
_pagingController.refresh();
}
_pagingController.appendPage(page.items, page.nextPageKey);
})
.onError((error) => _pagingController.error = error);
}
```
Build and run the app.
Now it displays the list of search results.
Scroll to the bottom.
Instead of the next results page the loading indicator appears.
Although `_pagingController` triggered a request for next page, this request wasn't processed.
To fix it, complete the `initState` method by adding a page request listener to `_pagingController`.
It triggers the loading of the next page in the `_productSearcher`.
```dart Dart icon=code theme={"system"}
@override
void initState() {
super.initState();
_searchTextController.addListener(
() => _productsSearcher.applyState(
(state) => state.copyWith(query: _searchTextController.text, page: 0),
),
);
_searchPage
.listen((page) {
if (page.pageKey == 0) {
_pagingController.refresh();
}
_pagingController.appendPage(page.items, page.nextPageKey);
})
.onError((error) => _pagingController.error = error);
_pagingController.addPageRequestListener(
(pageKey) =>
_productsSearcher.applyState((state) => state.copyWith(page: pageKey)),
);
}
```
Now infinite scrolling is working as expected.
You now get the basic search experience with search box, metadata, and results.
Consider disposing the `_pagingController` in the `dispose` method of the `_MyHomePageState` to free up the resources properly.
```dart Dart icon=code theme={"system"}
@override
void dispose() {
_searchTextController.dispose();
_productSearcher.dispose();
_pagingController.dispose();
super.dispose();
}
```
## Implement results filtering
To add an extra screen to implement filtering of the search results:
Enable search results filtering by adding a `FilterState` property to `_MyHomePageState`.
`FilterState` is a component that stores the state of applied filters and provides an interface to alter the state.
```dart Dart icon=code theme={"system"}
final _filterState = FilterState();
```
Add the `FacetList` property which manages the appearance of the list of refinement facets for a designated attribute.
Here, the `brand` attribute is used.
```dart Dart icon=code theme={"system"}
late final _facetList = _productsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'brand',
);
```
1. Add the `_filters` method to present the filtering interface as a list of `CheckboxListTiles` embedded in the `Scaffold` widget.
The `FacetList` class provides a `facets` stream, combining the facets themselves and their selection state as well as a `toggle` method that allows to change this state.
```dart Dart icon=code theme={"system"}
Widget _filters(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Filters')),
body: StreamBuilder>>(
stream: _facetList.facets,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
final selectableFacets = snapshot.data!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: selectableFacets.length,
itemBuilder: (_, index) {
final selectableFacet = selectableFacets[index];
return CheckboxListTile(
value: selectableFacet.isSelected,
title: Text(
"${selectableFacet.item.value} (${selectableFacet.item.count})",
),
onChanged: (_) {
_facetList.toggle(selectableFacet.item.value);
},
);
},
);
},
),
);
```
2. To present the filters screen, add a `GlobalKey` property to the `_MyHomePageState` class.
```dart Dart icon=code theme={"system"}
final GlobalKey _mainScaffoldKey = GlobalKey();
```
* Assign this key to the key property of `Scaffold` in its constructor.
* Add `IconButton` to the `actions` list of the `AppBar`.
This triggers opening the end drawer.
* Assign the `endDrawer` property of `Scaffold` with filters the widget embedded in the `Drawer` widget.
```dart Dart icon=code theme={"system"}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _mainScaffoldKey,
appBar: AppBar(
title: const Text('Algolia and Flutter'),
actions: [
IconButton(
onPressed: () => _mainScaffoldKey.currentState?.openEndDrawer(),
icon: const Icon(Icons.filter_list_sharp))
],
),
endDrawer: Drawer(
child: _filters(context),
),
body: (/ ... /),
);
}
```
Build and run the app.
The app bar now displays the filters button which shows the list of facet values (individual brands) for the brand attribute.
A selection of these values doesn't affect the search results.
To fix it, connect `FilterState` to `HitsState` in the `initState` method,
so that each change of `FilterState` triggers a new search request.
Also, each filter state change might refresh the `_pagingController` to remove the obsolete loaded pages.
Add the corresponding listener to the `_filterState.filters` stream.
```dart Dart icon=code theme={"system"}
@override
void initState() {
super.initState();
_searchTextController.addListener(
() => _productsSearcher.applyState(
(state) => state.copyWith(query: _searchTextController.text, page: 0),
),
);
_searchPage
.listen((page) {
if (page.pageKey == 0) {
_pagingController.refresh();
}
_pagingController.appendPage(page.items, page.nextPageKey);
})
.onError((error) => _pagingController.error = error);
_pagingController.addPageRequestListener(
(pageKey) =>
_productsSearcher.applyState((state) => state.copyWith(page: pageKey)),
);
_productsSearcher.connectFilterState(_filterState);
_filterState.filters.listen((_) => _pagingController.refresh());
}
```
The selection and deselection of the brand in the facet list now triggers a new search with applied filters.
Dispose of `_filterState` and `_facetList` in the `dispose` method of the `_MyHomePageState`.
```dart Dart icon=code theme={"system"}
@override
void dispose() {
_searchTextController.dispose();
_productSearcher.dispose();
_pagingController.dispose();
_filterState.dispose();
_facetList.dispose();
super.dispose();
}
```
## The final result
Find the source code in the [Algolia Flutter playground](https://github.com/algolia/flutter-playground/blob/main/lib/main.dart) repository on GitHub.
The final version of the `main.dart` file should look as follows:
```dart Dart icon=code expandable theme={"system"}
import 'package:algolia_helper_flutter/algolia_helper_flutter.dart';
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
void main() {
runApp(const MyApp());
}
class SearchMetadata {
final int nbHits;
const SearchMetadata(this.nbHits);
factory SearchMetadata.fromResponse(SearchResponse response) =>
SearchMetadata(response.nbHits);
}
class Product {
final String name;
final String image;
Product(this.name, this.image);
static Product fromJson(Map json) {
return Product(json['name'], json['image_urls'][0]);
}
}
class HitsPage {
const HitsPage(this.items, this.pageKey, this.nextPageKey);
final List items;
final int pageKey;
final int? nextPageKey;
factory HitsPage.fromResponse(SearchResponse response) {
final items = response.hits.map(Product.fromJson).toList();
final isLastPage = response.page >= response.nbPages;
final nextPageKey = isLastPage ? null : response.page + 1;
return HitsPage(items, response.page, nextPageKey);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
final _searchTextController = TextEditingController();
final _productsSearcher = HitsSearcher(
applicationID: 'latency',
apiKey: '927c3fe76d4b52c5a2912973f35a3077',
indexName: 'STAGING_native_ecom_demo_products',
);
Stream get _searchMetadata =>
_productsSearcher.responses.map(SearchMetadata.fromResponse);
final PagingController _pagingController = PagingController(
firstPageKey: 0,
);
Stream get _searchPage =>
_productsSearcher.responses.map(HitsPage.fromResponse);
final GlobalKey _mainScaffoldKey = GlobalKey();
final _filterState = FilterState();
late final _facetList = _productsSearcher.buildFacetList(
filterState: _filterState,
attribute: 'brand',
);
@override
void initState() {
super.initState();
_searchTextController.addListener(
() => _productsSearcher.applyState(
(state) => state.copyWith(query: _searchTextController.text, page: 0),
),
);
_searchPage
.listen((page) {
if (page.pageKey == 0) {
_pagingController.refresh();
}
_pagingController.appendPage(page.items, page.nextPageKey);
})
.onError((error) => _pagingController.error = error);
_pagingController.addPageRequestListener(
(pageKey) => _productsSearcher.applyState(
(state) => state.copyWith(page: pageKey),
),
);
_productsSearcher.connectFilterState(_filterState);
_filterState.filters.listen((_) => _pagingController.refresh());
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _mainScaffoldKey,
appBar: AppBar(
title: const Text('Algolia and Flutter'),
actions: [
IconButton(
onPressed: () => _mainScaffoldKey.currentState?.openEndDrawer(),
icon: const Icon(Icons.filter_list_sharp),
),
],
),
endDrawer: Drawer(child: _filters(context)),
body: Center(
child: Column(
children: [
SizedBox(
height: 44,
child: TextField(
controller: _searchTextController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon: Icon(Icons.search),
),
),
),
StreamBuilder(
stream: _searchMetadata,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text('${snapshot.data!.nbHits} hits'),
);
},
),
Expanded(child: _hits(context)),
],
),
),
);
}
Widget _hits(BuildContext context) => PagedListView(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
noItemsFoundIndicatorBuilder: (_) =>
const Center(child: Text('No results found')),
itemBuilder: (_, item, __) => Container(
color: Colors.white,
height: 80,
padding: const EdgeInsets.all(8),
child: Row(
children: [
SizedBox(width: 50, child: Image.network(item.image)),
const SizedBox(width: 20),
Expanded(child: Text(item.name)),
],
),
),
),
);
Widget _filters(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Filters')),
body: StreamBuilder>>(
stream: _facetList.facets,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
final selectableFacets = snapshot.data!;
return ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: selectableFacets.length,
itemBuilder: (_, index) {
final selectableFacet = selectableFacets[index];
return CheckboxListTile(
value: selectableFacet.isSelected,
title: Text(
"${selectableFacet.item.value} (${selectableFacet.item.count})",
),
onChanged: (_) {
_facetList.toggle(selectableFacet.item.value);
},
);
},
);
},
),
);
@override
void dispose() {
_searchTextController.dispose();
_productSearcher.dispose();
_pagingController.dispose();
_filterState.dispose();
_facetList.dispose();
super.dispose();
}
}
```
## Next steps
This examples shows how to bridge native search with the Algolia Flutter Helper library.
You can use it as a basis for more complex applications.
* To explore the components in more detail, see the [API reference](https://pub.dev/documentation/algolia_helper_flutter/latest/algolia_helper_flutter/algolia_helper_flutter-library.html).
* To explore a complete ecommerce app built with Flutter, see the [Flutter Ecommerce UI Template](/doc/guides/building-search-ui/ecommerce-ui-template/overview/flutter).
# Get started with declarative UI
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/how-to/declarative/android
Learn how to build a search experience with InstantSearch Android and Compose UI.
This guide explains, step by step, how to build a voice search experience using the libraries provided by Algolia and [Compose UI](https://developer.android.com/compose).
This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch.
It includes:
* A search box for users to type queries
* A list to display search results
* A list to results
* Statistics about the search
## Before you begin
To use InstantSearch, you need an Algolia account.
[Sign up](https://dashboard.algolia.com/users/sign_up) for a new account or use the following credentials
(which include a pre-loaded dataset of products appropriate for this guide):
* : `latency`
* Search : `1f6fd3a6fb973cb08419fe7d288fa4db`
- name: `instant_search`
### Create a new project and add InstantSearch Android
In Android Studio, create a new project:
* Select **Phone and Tablet** template
* Select **Empty Compose Activity** screen
### Add project dependencies
In your `gradle/libs.version.toml` file, add the following:
```toml TOML icon=file-cog theme={"system"}
[versions]
instantsearchCompose = "4.+"
[libraries]
instantsearch-android = { module = "com.algolia:instantsearch-android", version.ref = "instantsearchCompose" }
instantsearch-android-paging3 = { module = "com.algolia:instantsearch-android-paging3", version.ref = "instantsearchCompose" }
instantsearch-compose = { module = "com.algolia:instantsearch-compose", version.ref = "instantsearchCompose" }
```
In your `build.gradle.kts` file,
under the `app` module,
add the following in the `dependencies` block:
```kotlin Kotlin icon=code theme={"system"}
implementation(libs.instantsearch.compose)
implementation(libs.instantsearch.android)
implementation(libs.instantsearch.android.paging3)
```
You also need to add dependencies for Paging and extended Material Icons,
in `gradle/libs.version.toml`:
```toml TOML icon=file-cog theme={"system"}
[versions]
pagingCompose = "3.3.6"
materialIconsExtended = "1.1.0"
[libraries]
androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" }
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "materialIconsExtended" }
```
In `build.gradle.kts`:
```kotlin Kotlin icon=code theme={"system"}
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.material.icons.extended)
```
To perform network operations in your application, `AndroidManifest.xml` must include the following permissions:
```xml XML icon=code-xml theme={"system"}
```
Set up [`kotlinx.serialization`](https://github.com/Kotlin/kotlinx.serialization#setup) by adding the serialization plugin to your `build.gradle`:
```kotlin Kotlin icon=code theme={"system"}
plugins {
// ...
kotlin("plugin.serialization") version "2.2.0"
}
```
## Implementation
### Application architecture overview
* `MainActivity`: this activity controls displayed views
* `MainViewModel`: a `ViewModel` from Android Architecture Components. The business logic lives here
* `Search`: composes the search UI
### Define your data class
Define a structure that represents the in your index.
For simplicity's sake, this structure only provides the name of the product.
Add the following data class definition to the `Product.kt` file:
```kotlin Kotlin icon=code theme={"system"}
@Serializable
data class Product(
val name: String
)
```
### Add search business logic
You need three components for the basic search experience:
* `HitsSearcher` performs search requests and obtains search results.
* `SearchBoxConnector` handles a textual input and triggers a search request when needed.
* `Paginator` displays hits and manages the pagination logic.
The `setupConnections` method establishes the connections between these components to make them work together seamlessly.
The central part of your search experience is the `Searcher`.
The `Searcher` performs a and obtains search results.
Most InstantSearch components connected to the `Searcher`.
In this tutorial you're targeting one [index](https://support.algolia.com/hc/en-us/articles/4406981910289-What-is-an-index-), so instantiate a `HitsSearcher` with the proper credentials.
Create a new `MainViewModel.kt` file and add the following:
```kotlin Kotlin icon=code theme={"system"}
class MainViewModel : ViewModel() {
private val searcher = HitsSearcher(
applicationID = "latency",
apiKey = "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName = "instant_search"
)
// Search box
val searchBoxState = SearchBoxState()
val searchBoxConnector = SearchBoxConnector(searcher)
// Hits
val hitsPaginator = Paginator(searcher) { it.deserialize(Product.serializer()) }
val connections = ConnectionHandler(searchBoxConnector)
init {
connections += searchBoxConnector.connectView(searchBoxState)
connections += searchBoxConnector.connectPaginator(hitsPaginator)
}
override fun onCleared() {
super.onCleared()
searcher.cancel()
}
}
```
Most InstantSearch components should connect **and disconnect** in accordance to the [Android Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) to avoid memory leaks.
A `ConnectionHandler` handles a set of `Connection` s for you: each `+=` call with a component implementing the `Connection` interface connects it and makes it active.
Whenever you want to free resources or deactivate a component, call the `disconnect` method.
Get an instance of your `ViewModel` in your `MainActivity` by adding the following:
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : ComponentActivity() {
val viewModel: MainViewModel by viewModels()
//...
}
```
A `ViewModel` is a good place to put your data sources.
This way, the data persists during configuration changes.
### Create a basic search experience: `SearchBox`
Create a `SearchScreen.kt` file that holds the search UI.
Add a composable function `ProductsList` to display a list of products, the hit row represented by a column with a `Text` presenting the name of the item and a `Divider`:
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun ProductsList(
modifier: Modifier = Modifier,
pagingHits: LazyPagingItems,
listState: LazyListState
) {
LazyColumn(modifier, listState) {
items(pagingHits.itemCount) { index ->
val item = pagingHits[index] ?: return@items
Text(
modifier = modifier
.fillMaxWidth()
.padding(14.dp),
text = item.name,
style = MaterialTheme.typography.bodyLarge
)
HorizontalDivider(
modifier = modifier
.fillMaxWidth()
.width(1.dp)
)
}
}
}
```
Complete your search experience by putting the `SearchBox` and `ProductsList` views together:
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun SearchBox(
modifier: Modifier = Modifier,
searchBoxState: SearchBoxState = SearchBoxState(),
onValueChange: (String) -> Unit = {}
) {
TextField(
modifier = modifier.padding(bottom = 12.dp),
singleLine = true,
value = searchBoxState.query,
onValueChange = {
searchBoxState.setText(it)
onValueChange(it)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = { searchBoxState.setText(searchBoxState.query, true) }
)
)
}
@Composable
fun Search(
modifier: Modifier = Modifier,
searchBoxState: SearchBoxState,
paginator: Paginator
) {
val scope = rememberCoroutineScope()
val pagingHits = paginator.flow.collectAsLazyPagingItems()
val listState = rememberLazyListState()
Column(modifier) {
Row(Modifier.fillMaxWidth()) {
SearchBox(
modifier = Modifier
.weight(1f)
.padding(top = 12.dp, start = 12.dp, end = 12.dp),
searchBoxState = searchBoxState,
onValueChange = { scope.launch { listState.scrollToItem(0) } }
)
}
ProductsList(
modifier = Modifier.fillMaxSize(),
pagingHits = pagingHits,
listState = listState
)
}
}
```
Add the `Search` composable into the `setContent` section in `MainActivity` and pass it your business logic components from `MainViewModel`:
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SearchAppTheme {
Search(searchBoxState = viewModel.searchBoxState, paginator = viewModel.hitsPaginator)
}
}
}
}
```
Launch your app to see the basic search experience in action.
You'll see that the results are changing on each key stroke.
### Displaying metadata: `Stats`
To make the search experience more user-friendly, you can give more context about the search results to your users.
You can do this with different InstantSearch modules.
First, add a statistics component.
This component shows the hit count and the request processing time.
This gives users a complete understanding of their search, without the need for extra interaction.
The `StatsConnector` extracts the metadata from the search response, and provides an interface to present it to users.
Add the `StatsConnector` to the `MainViewModel` and connect it to the `Searcher`.
```kotlin Kotlin icon=code theme={"system"}
class MainViewModel : ViewModel() {
//...
val statsText = StatsTextState()
val statsConnector = StatsConnector(searcher)
val connections = ConnectionHandler(searchBoxConnector, statsConnector)
init {
//...
connections += statsConnector.connectView(statsText, DefaultStatsPresenter())
}
}
```
The `StatsConnector` receives the search statistics now, but doesn't display it yet.
Create a new composable `Stats`:
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun Stats(modifier: Modifier = Modifier, stats: String) {
Text(
modifier = modifier,
text = stats,
style = MaterialTheme.typography.bodySmall,
maxLines = 1
)
}
```
Add the `Stats` composable into the `Column`, in the middle of the `SearchBox` and `ProductsList`:
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun Search(
modifier: Modifier = Modifier,
searchBoxState: SearchBoxState,
paginator: Paginator,
statsText: StatsState,
) {
val scope = rememberCoroutineScope()
val pagingHits = paginator.flow.collectAsLazyPagingItems()
val listState = rememberLazyListState()
Column(modifier) {
Row(Modifier.fillMaxWidth()) {
SearchBox(
modifier = Modifier
.weight(1f)
.padding(top = 12.dp, start = 12.dp, end = 12.dp),
searchBoxState = searchBoxState,
onValueChange = { scope.launch { listState.scrollToItem(0) } }
)
}
Stats(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp, start = 12.dp, end = 12.dp),
stats = statsText.stats
)
ProductsList(
modifier = Modifier.fillMaxSize(),
pagingHits = pagingHits,
listState = listState
)
}
}
```
Update `MainActivity` to pass `StatsState` instance to `Search`:
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SearchAppTheme {
Search(
searchBoxState = viewModel.searchBoxState,
paginator = viewModel.hitsPaginator,
statsText = viewModel.statsText
)
}
}
}
}
```
Rebuild your app.
You'll now see updated results *and* an updated hit count on each keystroke.
### Filter your results: `FacetList`
With your app, you can search more than 10,000 products.
But, you don't want to scroll to the bottom of the list to find the exact product you're looking for.
One can more accurately filter the results by making use of the `FilterListConnector` components.
This section explains how to build a filter that allows to filter products by their category. First, add a `FilterState` component to the `MainViewModel`.
This component provides a convenient way to manage the state of your filters. Add the `manufacturer` refinement attribute.
Next, add the `FilterListConnector`, which stores the list of facets retrieved with search result. Add the connections between `HitsSearcher`, `FilterState` and `FilterListConnector`.
```kotlin Kotlin icon=code theme={"system"}
class MainViewModel : ViewModel() {
// ...
val facetList = FacetListState()
private val filterState = FilterState()
private val categories = "categories"
private val searcherForFacet = FacetsSearcher(
applicationID = "latency",
apiKey = "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName = "instant_search",
attribute = categories
)
private val facetListConnector = FacetListConnector(
searcher = searcherForFacet,
filterState = filterState,
attribute = categories,
selectionMode = SelectionMode.Multiple
)
val connections = ConnectionHandler(searchBoxConnector, statsConnector, facetListConnector)
init {
//...
connections += searcher.connectFilterState(filterState)
connections += facetListConnector.connectView(facetList)
connections += facetListConnector.connectPaginator(hitsPaginator)
searcherForFacet.query = searcherForFacet.query.copy(maxFacetHits = 100)
searcherForFacet.searchAsync()
}
override fun onCleared() {
//...
searcherForFacet.cancel()
}
}
```
Create the `FacetRow` composable to display a facet.
The row represented by a column with two `Text` s for the facet value and count,
plus an `Icon` to display a checkmark for selected facets:
```kotlin Kotlin icon=code theme={"system"}
import com.algolia.client.model.search.FacetHits
@Composable
fun FacetRow(
modifier: Modifier = Modifier,
selectableFacet: SelectableItem
) {
val (facet, isSelected) = selectableFacet
Row(
modifier = modifier.height(56.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.weight(1f)) {
Text(
modifier = Modifier.alignByBaseline(),
text = facet.value,
style = MaterialTheme.typography.bodyLarge
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.alignByBaseline(),
text = facet.count.toString(),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)
)
}
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null,
)
}
}
}
```
Create the `FacetList` composable to display facets list.
Use a `Text` for the attribute and a `LazyColumn` to display `FacetRow` s:
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun FacetList(
modifier: Modifier = Modifier,
facetList: FacetListState
) {
Column(modifier) {
Text(
text = "Categories",
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold),
modifier = Modifier.padding(14.dp)
)
LazyColumn(Modifier.background(MaterialTheme.colorScheme.background)) {
items(count = facetList.items.count(), key = {
index -> facetList.items[index].first.value
}) { index ->
val item = facetList.items[index]
FacetRow(
modifier = Modifier
.clickable { facetList.onSelection?.invoke(item.first) }
.padding(horizontal = 14.dp),
selectableFacet = item,
)
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.width(1.dp)
)
}
}
}
}
```
Put it all together using a `ModalBottomSheetLayout`:
* Inside `content`, put your earlier `Search` content, add `clickable` filter `Icon` to show the facets list
* Create `FacetList` inside `sheetContent` to display your facets list.
```kotlin Kotlin icon=code theme={"system"}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Search(
modifier: Modifier = Modifier,
searchBoxState: SearchBoxState,
paginator: Paginator,
statsText: StatsState,
facetList: FacetListState,
) {
val scope = rememberCoroutineScope()
val pagingHits = paginator.flow.collectAsLazyPagingItems()
val listState = rememberLazyListState()
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var showSheet by remember { mutableStateOf(false) }
if (showSheet) {
ModalBottomSheet(
onDismissRequest = {
scope.launch {
sheetState.hide()
showSheet = false
}
},
sheetState = sheetState
) {
FacetList(facetList = facetList)
}
}
Column(modifier) {
Row(Modifier.fillMaxWidth()) {
SearchBox(
modifier = Modifier
.weight(1f)
.padding(top = 12.dp, start = 12.dp),
searchBoxState = searchBoxState,
onValueChange = { scope.launch { listState.scrollToItem(0) }}
)
Card(Modifier.padding(top = 12.dp, end = 12.dp, start = 8.dp)) {
Icon(
modifier = Modifier
.clickable {
showSheet = true
scope.launch { sheetState.show() }
}
.padding(horizontal = 12.dp)
.height(56.dp),
imageVector = Icons.Default.FilterList,
contentDescription = null
)
}
}
Stats(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp, start = 12.dp, end = 12.dp),
stats = statsText.stats
)
ProductsList(
modifier = Modifier.fillMaxSize(),
pagingHits = pagingHits,
listState = listState
)
}
}
```
Update `Search` in `MainActivity` to include the instance of `FacetListState` from your `MainViewModel`:
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SearchAppTheme {
Search(
searchBoxState = viewModel.searchBoxState,
paginator = viewModel.hitsPaginator,
statsText = viewModel.statsText,
facetList = viewModel.facetList
)
}
}
}
}
```
Rebuild your app.
Now you see a filter button on top right of your screen.
Click it to show the refinements list and select one or more refinements.
Dismiss the refinements list to see the changes happening live to your hits.
### Improve the user experience: `Highlighting`
[Highlighting](/doc/guides/building-search-ui/ui-and-ux-patterns/highlighting-snippeting/js#highlighting) enhances the user experience by putting emphasis on the parts of the result that match the query.
It's a visual indication of *why* a result is relevant to the query.
You can add highlighting by implementing the `Highlightable` interface on `Product`.
Define a `highlightedName` field to retrieve the highlighted value for the `name` attribute.
```kotlin Kotlin icon=code theme={"system"}
import com.algolia.instantsearch.core.Indexable
@Serializable
data class Product(
val name: String,
override val objectID: String,
override val _highlightResult: JsonObject?
) : Indexable, Highlightable {
val highlightedName: HighlightedString?
get() = getHighlight("name")
}
```
Use the `.toAnnotatedString()` extension function to convert an `HighlightedString` into a `AnnotatedString` assignable to a `Text` to display the highlighted names.
```kotlin Kotlin icon=code theme={"system"}
@Composable
fun ProductsList(
modifier: Modifier = Modifier,
pagingHits: LazyPagingItems,
listState: LazyListState
) {
LazyColumn(modifier, listState) {
items(pagingHits) { item ->
if (item == null) return@items
TextAnnotated(
modifier = modifier
.fillMaxWidth()
.padding(14.dp),
annotatedString = item.highlightedName?.toAnnotatedString(),
default = item.name,
style = MaterialTheme.typography.bodyLarge
)
Divider(
modifier = Modifier
.fillMaxWidth()
.width(1.dp)
)
}
}
}
@Composable
fun TextAnnotated(
modifier: Modifier,
annotatedString: AnnotatedString?,
default: String,
style: TextStyle
) {
if (annotatedString != null) {
Text(modifier = modifier, text = annotatedString, style = style)
} else {
Text(modifier = modifier, text = default, style = style)
}
}
```
### Going further
You now have a fully working search experience:
your users can search for products,
refine their results,
and understand how many records are returned and why they're relevant to the query.
Find the full source code [in the GitHub repository](https://github.com/algolia/doc-code-samples/tree/master/instantsearch-android/getting-started).
# Get started with SwiftUI
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/how-to/declarative/ios
Build an InstantSearch iOS page from start to finish using SwiftUI.
This guide describes how to start a SwiftUI project with InstantSearch iOS and create a full search experience from scratch.
This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch.
It includes:
* A search box for users to type queries
* A list to display search results
* A list to results
* Statistics about the search
## Before you begin
To use InstantSearch, you need an Algolia account.
[Sign up](https://dashboard.algolia.com/users/sign_up) for a new account or use the following credentials
(which include a pre-loaded dataset of products appropriate for this guide):
* : `latency`
* Search : `1f6fd3a6fb973cb08419fe7d288fa4db`
- name: `bestbuy`
### Create a new project
In Xcode, create a new Project:
Open Xcode, and select `File -> New -> Project` in the menu bar.
Select `iOS -> App` template and click `Next`.
Give your app a name.
Make sure that you have selected `SwiftUI` option in the `Interface` field and click `Next`.
The `ContentView.swift` file opens with a `Hello World` project and the live preview canvas.
### Add project dependencies
This tutorial uses [Swift Package Manager](https://swift.org/package-manager) to integrate the InstantSearch library.
If you prefer to use another dependency manager (`CocoaPods`), check out the corresponding installation guides for [InstantSearch](https://github.com/algolia/instantsearch-ios#cocoapods).
In the menu bar select `File -> Swift Packages -> Add Package Dependency`.
Paste the GitHub link for the InstantSearch library: [`https://github.com/algolia/instantsearch-ios`](https://github.com/algolia/instantsearch-ios)
Pick the latest library version on the next screen, and select the `InstantSearchSwiftUI` product from the following list:
The InstantSearch dependency is installed and you're all set to work on your app.
## Implementation
Start by creating a classic search interface with search box and results list.
In your Xcode project, open the `ContentView.swift` file and import the `InstantSearch` library.
```swift Swift icon=code theme={"system"}
import InstantSearchSwiftUI
```
### Define your record structure
Define a structure that represent in your index.
For simplicity's sake, the structure only provides the name of the product.
It must conform to the `Codable` protocol to work with InstantSearch.
Add the following structure definition to the `ContentView.swift` file:
```swift Swift icon=code theme={"system"}
struct StockItem: Codable {
let name: String
}
```
### Business logic
Add the `AlgoliaController` class containing the InstantSearch business logic components to the `ContentView.swift` file.
You need three components coupled with the corresponding UI controllers for the basic search experience:
* `HitsSearcher` performs search requests and obtains search results.
* `SearchBoxInteractor` handles a textual input and triggers search requests when needed.
* `HitsInteractor` stores hits and manages the pagination logic.
The `setupConnections` method establishes the connections between these components and their UI controllers to make them work together seamlessly.
```swift Swift icon=code theme={"system"}
class AlgoliaController {
let searcher: HitsSearcher
let searchBoxInteractor: SearchBoxInteractor
let searchBoxController: SearchBoxObservableController
let hitsInteractor: HitsInteractor
let hitsController: HitsObservableController
init() {
self.searcher = try! HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
self.searchBoxInteractor = .init()
self.searchBoxController = .init()
self.hitsInteractor = .init()
self.hitsController = .init()
setupConnections()
}
func setupConnections() {
searchBoxInteractor.connectSearcher(searcher)
searchBoxInteractor.connectController(searchBoxController)
hitsInteractor.connectSearcher(searcher)
hitsInteractor.connectController(hitsController)
}
}
```
### UI
Add `SearchBoxObservableController` and `HitsObservableController` properties to the `ContentView` structure with an `@ObservedObject` property wrapper, so the view is automatically notified when the state of the search text or the hits list changed.
```swift Swift icon=code theme={"system"}
struct ContentView: View {
@ObservedObject var searchBoxController: SearchBoxObservableController
@ObservedObject var hitsController: HitsObservableController
var body: some View {
Text("Hello, world!")
.padding()
}
}
```
Add the `isEditing` property that binds the editing state of the search box.
```swift Swift icon=code theme={"system"}
struct ContentView: View {
@ObservedObject var searchBoxController: SearchBoxObservableController
@ObservedObject var hitsController: HitsObservableController
@State private var isEditing = false
var body: some View {
Text("Hello, world!")
.padding()
}
}
```
Replace the "Hello world!" text view with a vertical stack containing the `SearchBar` configured with `SearchBoxObservableController` properties and `isEditing` state binding.
```swift Swift icon=code theme={"system"}
var body: some View {
VStack(spacing: 7) {
SearchBar(text: $searchBoxController.query,
isEditing: $isEditing,
onSubmit: searchBoxController.submit)
}
}
```
From iOS 15, you can use the [`searchable`](https://developer.apple.com/documentation/swiftui/view-search) modifier to add a system search field.
Embed your search view into the `NavigationView` to ensure the search input field appears.
The body of your view will look as follows:
```swift Swift icon=code theme={"system"}
var body: some View {
VStack(spacing: 7) {
}.searchable(text: $searchBoxController.query)
}
```
The following steps rely on an explicit `SearchBar` view (as a more versatile approach).
Insert the `HitsList` component configured with `HitsObservableController` and a closure constructing the hit row.
The hit row is represented by a vertical stack with a text block presenting the name of the item and a `Divider`.
```swift Swift icon=code theme={"system"}
var body: some View {
VStack(spacing: 7) {
SearchBar(text: $searchBoxController.query,
isEditing: $isEditing,
onSubmit: searchBoxController.submit)
HitsList(hitsController) { hit, _ in
VStack(alignment: .leading, spacing: 10) {
Text(hit?.name ?? "")
.padding(.all, 10)
Divider()
}
}
}
}
```
Complete your search experience with a `noResults` trailing closure in the `HitsList`, that constructs the view presented in case of an empty result set, and add the `navigationBarTitle` string to show the navigation header on top of you search screen.
```swift Swift icon=code theme={"system"}
var body: some View {
VStack(spacing: 7) {
SearchBar(text: $searchBoxController.query,
isEditing: $isEditing,
onSubmit: searchBoxController.submit)
HitsList(hitsController) { (hit, _) in
VStack(alignment: .leading, spacing: 10) {
Text(hit?.name ?? "")
.padding(.all, 10)
Divider()
}
} noResults: {
Text("No Results")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarTitle("Algolia & SwiftUI")
}
```
Try out the search experience in the live preview.
Add a static instance of the `AlgoliaController` in the `PreviewProvider`.
Then, in the `previews` declaration, instantiate the `ContentView` with the UI controller references in the `AlgoliaController` class, and embed it in the `NavigationView`.
Launch the initial search inside the `onAppear` closure of the `NavigationView`.
The resulting `ContentView_Previews` structure content should look as follows:
```swift Swift icon=code theme={"system"}
struct ContentView_Previews: PreviewProvider {
static let algoliaController = AlgoliaController()
static var previews: some View {
NavigationView {
ContentView(searchBoxController: algoliaController.searchBoxController,
hitsController: algoliaController.hitsController)
}.onAppear {
algoliaController.searcher.search()
}
}
}
```
### Preview
Launch your preview to see the basic search experience in action.
The results change on each key stroke.
### Add statistics
To make the search experience more user-friendly, you can give more context about the search results to your users.
You can do this with different InstantSearch modules.
This component shows the hit count and the request processing time.
This gives users a complete understanding of their search, without the need for extra interaction.
Then create a `StatsInteractor`, which extracts the metadata from the search response, and provides an interface to present it to users.
Add `StatsInteractor` and `StatsTextObservableController` to `AlgoliaController` and connect the `StatsInteractor` to the `Searcher` in the `setupConnections` method.
Complete the `setupConnections` method of the `PreviewProvider` with the connection between the `StatsInteractor` and the `StatsTextObservableController`.
```swift Swift icon=code theme={"system"}
class AlgoliaController {
let searcher: HitsSearcher
let searchBoxInteractor: SearchBoxInteractor
let searchBoxController: SearchBoxObservableController
let hitsInteractor: HitsInteractor
let hitsController: HitsObservableController
let statsInteractor: StatsInteractor
let statsController: StatsTextObservableController
init() {
self.searcher = try! HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
self.searchBoxInteractor = .init()
self.hitsInteractor = .init()
self.statsInteractor = .init()
self.searchBoxController = .init()
self.hitsController = .init()
self.statsController = .init()
setupConnections()
}
func setupConnections() {
searchBoxInteractor.connectSearcher(searcher)
searchBoxInteractor.connectController(searchBoxController)
hitsInteractor.connectSearcher(searcher)
hitsInteractor.connectController(hitsController)
statsInteractor.connectSearcher(searcher)
statsInteractor.connectController(statsController)
}
}
```
The `StatsInteractor` receives the search statistics now, but doesn't display it yet.
Add `StatsTextObservableController` property to the `ContentView` and add the `Text` with its `stats` property into the stack in the middle of the `SearchBar` and the `HitsList`.
```swift Swift icon=code theme={"system"}
struct ContentView: View {
@ObservedObject var searchBoxController: SearchBoxObservableController
@ObservedObject var hitsController: HitsObservableController
@ObservedObject var statsController: StatsTextObservableController
@State private var isEditing = false
var body: some View {
VStack(spacing: 7) {
SearchBar(text: $searchBoxController.query,
isEditing: $isEditing,
onSubmit: searchBoxController.submit)
Text(statsController.stats)
.fontWeight(.medium)
HitsList(hitsController) { (hit, _) in
VStack(alignment: .leading, spacing: 10) {
Text(hit?.name ?? "")
.padding(.all, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
Divider()
}
} noResults: {
Text("No Results")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarTitle("Algolia & SwiftUI")
}
}
```
Alter the `PreviewProvider` structure by adding a `StatsTextObservableController` in the `ContentView` initializer.
```swift Swift icon=code theme={"system"}
struct ContentView_Previews: PreviewProvider {
static let algoliaController = AlgoliaController()
static var previews: some View {
NavigationView {
ContentView(searchBoxController: algoliaController.searchBoxController,
hitsController: algoliaController.hitsController,
statsController: algoliaController.statsController)
}.onAppear {
algoliaController.searcher.search()
}
}
}
```
Update your live preview.
Updated results *and* an updated hit count appear on each keystroke.
### Filter your results
With your app, you can search more than 10,000 products.
However, you don't want to scroll to the bottom of the list to find the exact product you're looking for.
One can more accurately filter the results by making use of the `RefinementList` components.
This section explains how to build a filter that allows to filter products by their category.
1. Add a `FilterState` component to the `AlgoliaController`. This component provides a convenient way to manage the state of your filters. Add the `manufacturer` refinement attribute.
2. Add the `FacetListInteractor`, which stores the list of facets retrieved with search results.
3. Add the connections between `HitsSearcher`, `FilterState` and `FacetListInteractor` in the `setupConnections` method.
Complete the `setupConnections` method of the `PreviewProvider` with the connection between the `FacetListInteractor` and the `FacetListObservableController`. To improve the user experience, the connection includes the `FacetListPresenter` parameter that pins the selected facets to the top of the list and uses facet count value as the second ranking criteria, so that the facets with the most hits show up higher.
```swift Swift icon=code expandable theme={"system"}
class AlgoliaController {
let searcher: HitsSearcher
let searchBoxInteractor: SearchBoxInteractor
let searchBoxController: SearchBoxObservableController
let hitsInteractor: HitsInteractor
let hitsController: HitsObservableController
let statsInteractor: StatsInteractor
let statsController: StatsTextObservableController
let filterState: FilterState
let facetListInteractor: FacetListInteractor
let facetListController: FacetListObservableController
init() {
self.searcher = try! HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
self.searchBoxInteractor = .init()
self.searchBoxController = .init()
self.hitsInteractor = .init()
self.hitsController = .init()
self.statsInteractor = .init()
self.statsController = .init()
self.filterState = .init()
self.facetListInteractor = .init()
self.facetListController = .init()
setupConnections()
}
func setupConnections() {
searchBoxInteractor.connectSearcher(searcher)
searchBoxInteractor.connectController(searchBoxController)
hitsInteractor.connectSearcher(searcher)
hitsInteractor.connectController(hitsController)
statsInteractor.connectSearcher(searcher)
statsInteractor.connectController(statsController)
searcher.connectFilterState(filterState)
facetListInteractor.connectSearcher(searcher, with: "manufacturer")
facetListInteractor.connectFilterState(filterState, with: "manufacturer", operator: .or)
facetListInteractor.connectController(facetListController, with: FacetListPresenter(sortBy: [.isRefined, .count(order: .descending)]))
}
}
```
In the `ContentView` add a new state flag `isPresentingFacets`, which defines if the facet list is presented.
```swift Swift icon=code theme={"system"}
@State private var isPresentingFacets = false
```
Then declare a function that constructs the button which triggers the appearance of the facet list by toggling the `isPresentingFacets` flag.
```swift Swift icon=code theme={"system"}
private func facetsButton() -> some View {
Button(action: {
isPresentingFacets.toggle()
},
label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.font(.title)
})
}
```
Add the `FacetListObservableController` to the `ContentView` and a `facets` function constructing the facet list view.
```swift Swift icon=code theme={"system"}
@ObservedObject var facetListController: FacetListObservableController
@ViewBuilder
private func facets() -> some View {
NavigationView {
FacetList(facetListController) { facet, isSelected in
VStack {
FacetRow(facet: facet, isSelected: isSelected)
Divider()
}
} noResults: {
Text("No facet found")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationBarTitle("Brand")
}
}
```
The resulting `ContentView` code should look as follows:
```swift Swift icon=code theme={"system"}
struct ContentView: View {
@ObservedObject var searchBoxController: SearchBoxObservableController
@ObservedObject var hitsController: HitsObservableController
@ObservedObject var statsController: StatsTextObservableController
@ObservedObject var facetListController: FacetListObservableController
@State private var isEditing = false
@State private var isPresentingFacets = false
var body: some View {
VStack(spacing: 7) {
SearchBar(text: $searchBoxController.query,
isEditing: $isEditing,
onSubmit: searchBoxController.submit)
Text(statsController.stats)
.fontWeight(.medium)
HitsList(hitsController) { (hit, _) in
VStack(alignment: .leading, spacing: 10) {
Text(hit?.name ?? "")
.padding(.all, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)
Divider()
}
} noResults: {
Text("No Results")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationBarTitle("Algolia & SwiftUI")
.navigationBarItems(trailing: facetsButton())
.sheet(isPresented: $isPresentingFacets, content: facets)
}
@ViewBuilder
private func facets() -> some View {
NavigationView {
FacetList(facetListController) { facet, isSelected in
VStack {
FacetRow(facet: facet, isSelected: isSelected)
Divider()
}
} noResults: {
Text("No facet found")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationBarTitle("Brand")
}
}
private func facetsButton() -> some View {
Button(action: {
isPresentingFacets.toggle()
},
label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.font(.title)
})
}
}
```
Complete the initializer of the `ContentView` with the `FacetListObservableController`.
```swift Swift icon=code theme={"system"}
struct ContentView_Previews: PreviewProvider {
static let algoliaController = AlgoliaController()
static var previews: some View {
NavigationView {
ContentView(searchBoxController: algoliaController.searchBoxController,
hitsController: algoliaController.hitsController,
statsController: algoliaController.statsController,
facetListController: algoliaController.facetListController)
}.onAppear {
algoliaController.searcher.search()
}
}
}
```
Update the live preview.
Now you see a filter button on top right of your screen.
Click it to present the refinements list, select one or more refinements,
and then dismiss the refinements list by swiping it down to see the filtered result.
## Going further
Your users can enter a query, and your app shows them results as they type.
It also provides a possibility to filter the results even further using `RefinementList`.
This is a great start, but you can go even further and improve on that.
* Have a look at more [complex examples of apps built with InstantSearch](/doc/guides/building-search-ui/resources/demos/ios).
* Head to the InstantSearch [components page](/doc/api-reference/widgets/ios) to see other components that you could use.
# Get started with imperative UI
Source: https://www.algolia.com/doc/guides/building-search-ui/getting-started/how-to/programmatically/android
Build an InstantSearch Android page from an empty Android project.
This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch.
This guide walks you through the steps needed to create a full InstantSearch Android experience from scratch.
It includes:
* A search box for users to type queries
* A list to display search results
* A list to results
* Statistics about the search
## Before you begin
To use InstantSearch, you need an Algolia account.
[Sign up](https://dashboard.algolia.com/users/sign_up) for a new account or use the following credentials
(which include a pre-loaded dataset of products appropriate for this guide):
* : `latency`
* Search : `1f6fd3a6fb973cb08419fe7d288fa4db`
- name: `instant_search`
### Create a new project and add InstantSearch Android
In Android Studio, create a new Project:
* On the Target screen, select **Phone and Tablet**
* On the Add an Activity screen, select **Empty Activity**
In your app's `build.gradle`, add the following dependency:
```groovy build.gradle icon=braces theme={"system"}
implementation 'com.algolia:instantsearch-android:4.+'
implementation 'com.algolia:instantsearch-android-paging3:4.+'
```
This guide uses InstantSearch Android with [Android Architecture Components](https://developer.android.com/topic/libraries/architecture), so you also need to add the following dependencies:
```groovy build.gradle icon=braces theme={"system"}
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.+'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.+'
```
To perform network operations in your app, `AndroidManifest.xml` must include the following permissions:
```xml XML icon=code-xml theme={"system"}
```
Setup [`kotlinx.serialization`](https://github.com/Kotlin/kotlinx.serialization#setup) by adding the serialization plugin to your `build.gradle`:
```groovy build.gradle icon=braces theme={"system"}
plugins {
// ...
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.0'
}
```
## Implementation
### Architecture overview
* `MainActivity`: This activity controls the displayed fragment.
* `MyViewModel`: A `ViewModel` from Android Architecture Components. The business logic lives here.
* `ProductFragment`: This fragment displays a list of search results in a `RecyclerView`, a `SearchView` input, and a `Stats` indicator.
* `FacetFragment`: This fragment displays a list of facets to filter your search results.
### Initialize a searcher
The central part of your search experience is the `Searcher`.
The `Searcher` performs a and obtains search results.
Most InstantSearch components connected to the `Searcher`.
In this guide, you'll only target one index, so instantiate a `HitsSearcher` with the proper credentials.
Go to `MyViewModel.kt` file and add the following code:
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
val searcher = HitsSearcher(
applicationID = "latency",
apiKey = "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName = "instant_search"
)
override fun onCleared() {
super.onCleared()
searcher.cancel()
}
}
```
A `ViewModel` is a good place to put your data sources.
This way, the data persists during orientation changes and you can share it across multiple fragments.
### Display results: `Hits`
Suppose you want to display search results in a `RecyclerView`.
To simultaneously provide a good user experience and display thousands of products,
you can implement an infinite scrolling mechanism using the [Paging Library](https://developer.android.com/topic/libraries/architecture/paging) from Android Architecture Component.
To display your results:
Create a `LiveData` object, which holds a `PagedList` of `Product`.
Create the `Product` data class which contains a single `name` field.
```kotlin Kotlin icon=code theme={"system"}
@Serializable
data class Product(
val name: String
)
```
Create the `product_item.xml` file.
```xml XML icon=code-xml theme={"system"}
```
Create the `ProductViewHolder` to bind a `Product` item to a `RecyclerView.ViewHolder`.
```kotlin Kotlin icon=code theme={"system"}
class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val itemName = view.findViewById(R.id.itemName)
fun bind(product: Product) {
itemName.text = product.name
}
}
```
Create a `ProductAdapter` by extending `PagingDataAdapter`.
The `ProductAdapter` binds products to the `ViewHolder`.
```kotlin Kotlin icon=code theme={"system"}
class ProductAdapter : PagingDataAdapter(ProductDiffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
return ProductViewHolder(parent.inflate(R.layout.list_item_small))
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
getItem(position)?.let { holder.bind(it) }
}
object ProductDiffUtil : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Product, newItem: Product) = oldItem.objectID == newItem.objectID
override fun areContentsTheSame(oldItem: Product, newItem: Product) = oldItem == newItem
}
}
```
In your `ViewModel` use a `Paginator` with your searcher:
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
// Searcher initialization
// ...
val paginator = Paginator(
searcher = searcher,
pagingConfig = PagingConfig(pageSize = 50, enablePlaceholders = false),
transformer = { hit -> hit.deserialize(Product.serializer()) }
)
override fun onCleared() {
super.onCleared()
searcher.cancel()
}
}
```
Now that your `ViewModel` has some data, you can create a simple `product_fragment.xml` with a `Toolbar` and a `RecyclerView` to display the products:
```xml XML icon=code theme={"system"}
```
In the `ProductFragment`, get a reference of `MyViewModel` using `activityViewModels()`.
Then, observe the `LiveData` to update the `ProductAdapter` on every new page of products.
Finally, configure the `RecyclerView` by setting its adapter and `LayoutManager`.
```kotlin Kotlin icon=code theme={"system"}
class ProductFragment : Fragment(R.layout.fragment_product) {
private val viewModel: MyViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapterProduct = ProductAdapter()
viewModel.paginator.liveData.observe(viewLifecycleOwner) { pagingData ->
adapterProduct.submitData(lifecycle, pagingData)
}
view.findViewById(R.id.productList).let {
it.itemAnimator = null
it.adapter = adapterProduct
it.layoutManager = LinearLayoutManager(requireContext())
it.autoScrollToStart(adapterProduct)
}
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
```
To display the `ProductFragment`, update `activity_main.xml` to have a container for the fragments:
```xml XML icon=code theme={"system"}
```
Update `MainActivity` to display `ProductFragment`:
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showProductFragment()
}
fun showProductFragment() {
supportFragmentManager.commit {
replace(R.id.container)
}
}
}
```
You have now learned how to display search results in an infinite scrolling `RecyclerView`.
### Searching your data: `SearchBox`
To search your data, users need an input field.
Any change in this field should trigger a new request, and then update the search results.
To achieve this, use a `SearchBoxConnector`.
This takes a `Searcher` and connected to `Paginator` using `connectPaginator`.
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
// Searcher initialization
// Hits initialization
// ...
val searchBox = SearchBoxConnector(searcher)
val connection = ConnectionHandler(searchBox)
init {
connection += searchBox.connectPaginator(paginator)
}
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.clear()
}
}
```
Most InstantSearch components should be connected and disconnected in accordance to the [Android Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) to avoid memory leaks.
A `ConnectionHandler` handles a set of `Connection` s for you: Each `+=` call with a component implementing the `Connection` interface will connect it and make it active.
Whenever you want to free resources or deactivate a component, call the `disconnect` method.
You can now add a `SearchView` in your `Toolbar`:
```xml XML icon=code-xml theme={"system"}
```
Connect the `SearchBoxViewAppCompat` to the `SearchBoxConnectorPagedList` stored in `MyViewModel` using a new `ConnectionHandler` that conforms to the `ProductFragment` lifecycle:
```kotlin Kotlin icon=code theme={"system"}
class ProductFragment : Fragment(R.layout.fragment_product) {
private val viewModel: MyViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Hits
// ...
val searchBoxView = SearchBoxViewAppCompat(searchView)
connection += viewModel.searchBox.connectView(searchBoxView)
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
```
You can now build and run your app, and see a basic search experience.
The results change with each key stroke.
### Displaying metadata: `Stats`
It's a good practice to show the number of hits that were returned for a search.
You can use the `Stats` components to do this with minimal code.
Add a `StatsConnector` to your `MyViewModel`, and connect it with a `ConnectionHandler`.
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
// Searcher initialization
// Hits initialization
// SearchBox initialization
// ...
val stats = StatsConnector(searcher)
val connection = ConnectionHandler(searchBox, stats)
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.clear()
}
}
```
Add a `TextView` to your `product_fragment.xml` file to display the stats.
```xml XML icon=code-xml theme={"system"}
```
Next, connect the `StatsConnector` to a `StatsTextView` in your `ProductFragment`.
```kotlin Kotlin icon=code theme={"system"}
class ProductFragment : Fragment(R.layout.fragment_product) {
private val viewModel: MyViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Hits
// SearchBox
// ...
val statsView = StatsTextView(view.findViewById(R.id.stats))
connection += viewModel.stats.connectView(statsView, DefaultStatsPresenter())
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
```
You can now build and run your app and see your new search experience in action.
### Filter your data: `FacetList`
Filtering search results helps your users find what they want.
You can create a `FacetList` to filter products by category.
Create a drawable resource `ic_check.xml`.
This resource displays checked filters.
```xml XML icon=code-xml theme={"system"}
```
Create a `facet_item.xml` file.
This is the layout for a `RecyclerView.ViewHolder`.
```xml XML icon=code-xml theme={"system"}
```
Implement the `FacetListViewHolder` and its `Factory`, so that later on it works with a `FacetListAdapter`.
```kotlin Kotlin icon=code theme={"system"}
import com.algolia.client.model.search.FacetHits
class MyFacetListViewHolder(view: View) : FacetListViewHolder(view) {
override fun bind(facet: FacetHits, selected: Boolean, onClickListener: View.OnClickListener) {
view.setOnClickListener(onClickListener)
val facetCount = view.findViewById(R.id.facetCount)
facetCount.text = facet.count.toString()
facetCount.visibility = View.VISIBLE
view.findViewById(R.id.icon).visibility = if (selected) View.VISIBLE else View.INVISIBLE
view.findViewById(R.id.facetName).text = facet.value
}
object Factory : FacetListViewHolder.Factory {
override fun createViewHolder(parent: ViewGroup): FacetListViewHolder {
return MyFacetListViewHolder(parent.inflate(R.layout.list_facet_selectable))
}
}
}
```
You can use a new component to handle the filtering logic: the [`FilterState`](/doc/api-reference/widgets/filter-state/android).
Pass the `FilterState` to your `FacetListConnector`.
The `FacetListConnector` needs an attribute: use `"categories"`.
Inject `MyFacetListViewHolder.Factory` into your `FacetListAdapter`. The `FacetListAdapter` is an out of the box `RecyclerView.Adapter` for a `FacetList`.
Connect the different parts together:
* The `Searcher` connects itself to the `FilterState`, and applies its filters with every search.
* The `FilterState` connects to your products `Paginator` to invalidate search results when new filter are applied.
* The `facetList` connects to its adapter.
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
// Client, Searcher...
// Products
// Stats
// ...
val filterState = FilterState()
val facetList = FacetListConnector(
searcher = searcher,
filterState = filterState,
attribute = "categories",
selectionMode = SelectionMode.Single
)
val facetPresenter = DefaultFacetListPresenter(
sortBy = listOf(FacetSortCriterion.CountDescending, FacetSortCriterion.IsRefined),
limit = 100
)
val connection = ConnectionHandler(searchBox, stats, facetList)
init {
// SearchBox
// ..
connection += searcher.connectFilterState(filterState)
connection += filterState.connectPaginator(paginator)
}
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.clear()
}
}
```
To display your facets, create a `facet_fragment.xml` layout with a `RecyclerView`.
```xml XML icon=code-xml theme={"system"}
```
Create a `FacetFragment` and configure your `RecyclerView` with its adapter and `LayoutManager`.
```kotlin Kotlin icon=code theme={"system"}
class FacetFragment : Fragment(R.layout.fragment_facet) {
private val viewModel: MyViewModel by activityViewModels()
private val connection = ConnectionHandler()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapterFacet = FacetListAdapter(MyFacetListViewHolder.Factory)
val facetList = view.findViewById(R.id.facetList)
connection += viewModel.facetList.connectView(adapterFacet, viewModel.facetPresenter)
facetList.let {
it.adapter = adapterFacet
it.layoutManager = LinearLayoutManager(requireContext())
it.autoScrollToStart(adapterFacet)
}
}
override fun onDestroyView() {
super.onDestroyView()
connection.clear()
}
}
```
Add the code to switch to `FacetFragment` in `MainActivity` with "navigation up" support.
```kotlin Kotlin icon=code theme={"system"}
class MainActivity : AppCompatActivity() {
val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_getting_started)
showProductFragment()
}
private fun showFacetFragment() {
supportFragmentManager.commit {
add(R.id.container)
addToBackStack("facet")
}
}
private fun showProductFragment() {
supportFragmentManager.commit {
replace(R.id.container)
}
}
override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) return true
return super.onSupportNavigateUp()
}
}
```
Add a filters button in `product_fragment.xml`
```xml XML icon=code-xml theme={"system"}
```
Add `displayFilters` and `navigateToFilters()` to `MyViewModel` to trigger filters display:
```kotlin Kotlin icon=code theme={"system"}
class MyViewModel : ViewModel() {
// Client, Searcher...
// Products
// Stats
// FilterState
// ...
private val _displayFilters = MutableLiveData()
val displayFilters: LiveData get() = _displayFilters
fun navigateToFilters() {
_displayFilters.value = Unit
}
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.clear()
}
}
```
In `ProductFragment`, add a listener to the filters button to switch to `FacetFragment`:
```kotlin Kotlin icon=code theme={"system"}
class ProductFragment : Fragment() {
private val connection = ConnectionHandler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.product_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProvider(requireActivity())[MyViewModel::class.java]
// Hits
// SearchBox
// Stats
// ...
view.findViewById