Get Started
In this walkthrough we are going to get started building a search experience with Next.js.
You don't need to use Next.js to use Searchkit, but it is the easiest way to get started.
In this walkthrough, we will:
- Setup an api route to fetch results from Elasticsearch
- Use React InstantSearch to display the results 🎉
Download an Example Project
You can check out a Next.js project with Searchkit here:
curl https://codeload.github.com/searchkit/searchkit/tar.gz/main | \
tar -xz --strip=2 searchkit-main/examples/with-ui-nextjs-reactor view the example codebase on github here (opens in a new tab)
Code Sandbox Example
You can also check out the code sandbox example here:
Create a Next.js app
This tutorial will use the new Next.js App Router. If you're using pages, keep this in mind when following along.
First, we need to create a Next.js app. We can do this by running the following command:
npx create-next-app@latestand follow the instructions.
Navigate to the newly created directory.
Install Dependencies
Next we need to install the dependencies for this project:
npm install @searchkit/instantsearch-client @searchkit/api react-instantsearchSetup the Node API
Create a new file in the app/api/search directory called route.ts and add the following code:
import Client from "@searchkit/api";
import { NextRequest, NextResponse } from 'next/server'
const apiConfig = {
connection: {
host: "<replace-with-your-elasticsearch-host>",
// if you are authenticating with an api key
// https://www.searchkit.co/docs/guides/setup-elasticsearch#connecting-with-api-key
// apiKey: '###'
// if you are authenticating with a username/password combo
// https://www.searchkit.co/docs/guides/setup-elasticsearch#connecting-with-usernamepassword
// auth: {
// username: "elastic",
// password: "changeme"
// },
},
search_settings: {
highlight_attributes: ["title", "actors"],
search_attributes: ["title", "actors"],
result_attributes: ["title", "actors"],
facet_attributes: ["type", "rated"],
},
};
const apiClient = Client(apiConfig);
export async function POST(req: NextRequest, res: NextResponse) {
const data = await req.json()
const results = await apiClient.handleRequest(data)
return NextResponse.json(results)
}Replace the host and apiKey with your Elasticsearch host and API key. The apiKey is optional, but recommended for production environments. You can find more information about the API key here (opens in a new tab).
This will setup a new Next.js route handler (opens in a new tab) under the /api/search path. This route will handle the search requests and use the InstantSearch Elasticsearch Adapter to handle the requests. The response is then returned back to the client.
For more information on API configuration, see the API Configuration docs.
Setup the Frontend
Now that we have the API setup, we can start building the frontend. We will use react-instantsearch (opens in a new tab) to build the search experience.
First, we need to create a new file (if it doesn't already exist) in the app directory called page.tsx and add the following code:
import { InstantSearch, SearchBox, Hits } from "react-instantsearch";
import createClient from "@searchkit/instantsearch-client";
const searchClient = createClient({
url: "/api/search",
});
export default function Search() {
return (
<InstantSearch
searchClient={searchClient}
indexName="<elasticsearch index or alias name>"
>
<SearchBox />
<Hits />
</InstantSearch>
);
}Instantsearch will use the searchClient to make requests to the API we created earlier. The indexName is the name of the index we want to search.
Run the app
Now that we have everything setup, we can run the app and see the search experience in action.
npm run devIMAGE1
Searchable Attributes
Now that we have the search experience setup, we can add additional search functionality.
Adjusting the search fields
we can adjust the search fields by updating the search_attributes in the apiConfig object in the app/api/search/route.ts file.
search_attributes: ["title^3", "actors", "plot"],Above we have boosted title by 3 times. This means that the title will have a higher weight than the other fields. This will make sure that the title has a higher importance in the search results.
Overriding the Default Query
We can optionally override the default search query by implementing the getQuery function in the handleRequest method called in the app/api/search/route.ts file.
This function will receive the query and the function will return the Elasticsearch query that will be used to search the index.
const results = await apiClient.handleRequest(body, {
getQuery: (query, search_attributes) => {
return [
{
combined_fields: {
query,
fields: search_attributes,
},
},
];
},
});Customizing the Results Hit
We can add a custom hit component to display the results. We can create a new file called Hit.ts in the components directory and add the following code:
Below we are using the Highlight component from react-instantsearch to highlight the search term in the title and actors fields.
import { Highlight } from "react-instantsearch";
const hitView = (props) => {
return (
<div>
<h2>
<Highlight hit={props.hit} attribute="title" />
</h2>
<br />
<Highlight hit={props.hit} attribute="actors" />
</div>
);
};We need to pass the attribute prop to the highlight_attributes config to tell which fields to bring highlight options for.
highlight_attributes: ["title", "actors"],Then we can import the Hit component in the app/page.tsx file and pass it to the parent Hits component.
import Hit from "../components/Hit";
export default function Search() {
return (
<InstantSearch searchClient={searchClient} indexName="movies">
<SearchBox />
<Hits hitComponent={Hit} />
</InstantSearch>
);
}Facets
Adding a Refinement List Facet
Start by updating the apiConfig object in the app/api/search/route.ts file to add the type facet.
facet_attributes: [{ attribute: "type", "type": "string" }],This assumes there is a type field in the index that is a keyword type field.
If the field is a text type field, you can define and use the type.keyword subfield instead.
facet_attributes: [{ attribute: "type", field: "type.keyword", type: "string" }],Then we can add the RefinementList component to the pages/search.js file.
import {
InstantSearch,
SearchBox,
Hits,
RefinementList,
} from "react-instantsearch";
export default function Search() {
return (
<InstantSearch searchClient={searchClient} indexName="movies">
<SearchBox />
<RefinementList attribute="type" />
<Hits hitComponent={Hit} />
</InstantSearch>
);
}Make it searchable
By default, the RefinementList component will show all the values for the facet. We can make it searchable by adding the searchable prop.
<RefinementList attribute="type" searchable />Adding a Numeric Range based Facet
Start by updating the apiConfig object in the app/page.tsx file to add the imdbrating facet.
This requires the imdbrating field to be a numeric type field like a float in the Elasticsearch index.
facet_attributes: [
{ attribute: "imdbrating", type: "numeric" },
{ attribute: "type", field: "type.keyword", type: "string" }
],Then we can add the RangeInput component to the app/page.tsx file.
import {
InstantSearch,
SearchBox,
Hits,
RangeInput,
} from "react-instantsearch";
<RangeInput
attribute="imdbrating"
/>;Server Side Rendering
Below we add the following additional imports:
- The
getServerStatefunction fromreact-instantsearch - The
renderToStringfunction fromreact-dom/server - The
InstantSearchServerStateandInstantSearchSSRProvidercomponents fromreact-instantsearch - The
createInstantSearchRouterNextfunction fromreact-instantsearch-router-nextjs - The
singletonRouterfromnext/router
Then we wrap the InstantSearch component with the InstantSearchSSRProvider component and pass the serverState prop to it.
This allows us to render the search experience on the server and send the initial state to the client. This will make the search experience load faster and also improve SEO.
import {
InstantSearch, SearchBox, Hits, RefinementList, RangeInput,
InstantSearchServerState, InstantSearchSSRProvider, getServerState
} from 'react-instantsearch';
import { renderToString } from 'react-dom/server';
import Client from '@searchkit/instantsearch-client'
import { GetServerSideProps } from 'next';
import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';
import singletonRouter from 'next/router';
type WebProps = {
serverState?: InstantSearchServerState;
url?: string;
serverUrl?: string;
};
export default function Web({ serverState, url, serverUrl }: WebProps) {
const searchClient = Client({
url: serverUrl + '/api/product-search',
});
return (
<InstantSearchSSRProvider {...serverState}>
<div className="ais-InstantSearch">
<InstantSearch searchClient={searchClient} indexName="movies">
<SearchBox />
<RefinementList attribute="type" searchable />
<RangeInput attribute="imdbrating" />
<Hits hitComponent={Hit} />
</InstantSearch>
</div>
</InstantSearchSSRProvider>
);
}
export const getServerSideProps: GetServerSideProps<WebProps> =
async function getServerSideProps({ req }) {
const protocol = req.headers.referer?.split('://')[0] || 'http';
const serverUrl = `${protocol}://${req.headers.host}`;
const url = `${protocol}://${req.headers.host}${req.url}`;
const serverState = await getServerState(<Web url={url} serverUrl={serverUrl} />, {
renderToString,
});
return {
props: {
serverState,
url,
serverUrl
},
};
};Summary
We have quickly built a really nice search experience from scratch using Elasticsearch and Algolia InstantSearch. We have also learned how to customize the search experience by adjusting the search fields and overriding the default Elasticsearch query.