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-react
or 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@latest
and 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-instantsearch
Setup 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 dev
IMAGE1
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
getServerState
function fromreact-instantsearch
- The
renderToString
function fromreact-dom/server
- The
InstantSearchServerState
andInstantSearchSSRProvider
components fromreact-instantsearch
- The
createInstantSearchRouterNext
function fromreact-instantsearch-router-nextjs
- The
singletonRouter
fromnext/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.