The need for a search feature in an application cannot be overstated. It could make the life of users easier and also make them excited to use an application. The ease of finding a particular resource or a collection of resources on an application greatly affects the user experience of an application (web or mobile).
There are several ways to achieve search in an application; however, in this article, we’ll be exploring the use of Elasticsearch to build a search engine for our Strapi application by adding a search feature to the Strapi Foodadvisor application.
Prerequisites
Before continuing in this article, you should have the following:
Introduction to Strapi
Strapi is the leading open-source, customizable, headless CMS that gives developers the freedom to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content easily.
Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences by making the admin panel and API extensible through a plugin system.
Scaffolding a Strapi Project
To install Strapi, head over to the documentation We’ll be using the SQLite database for this project. Run the following commands:
yarn create strapi-app my-project # using yarn
npx create-strapi-app@latest my-project # using npx
Replace my-project
with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi.
If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following commands to start the Strapi development server:
yarn develop # using yarn
npm run develop # using npm
The development server starts the app on http://localhost:1337/admin.
What is Elasticsearch?
Elasticsearch “Helps everyone find what they need faster—from employees who need documents from your intranet to customers browsing online for the perfect pair of shoes”.
Basically, Elasticsearch provides a way for you to integrate a full-blown search engine into your application and it’s helpful.
Why Should You Use Elasticsearch?
Traditional relational databases do not really do well when searching through a lot of text. Speed is a huge factor for search, i.e. the speed at which results are returned, and accuracy. Below are some benefits of using Elasticsearch:
- Elasticsearch is great for searching large datasets because of it’s incredible speed. According to the documentation, Elasticsearch is almost real-time.
- Elasticsearch is highly customizable; it provides auto-complete and logging features. The ability to track what users search and tailor search suggestions according to their needs.
What to Consider Before Using Elasticsearch
There are a couple of options for running Elasticsearch:
- Running hosted Elasticsearch service using Elastic Cloud: the easy way to get started with Elasticsearch
- Running a self-managed Elasticsearch:
- Running Elasticsearch on a local machine
- Running Elasticsearch in a Docker container
- Running Elasticsearch cloud on Kubernetes
If you want to hit the ground running quick, you should consider using the Elastic Cloud service. It offers all features but a subscription fees is required, Although you can sign up for a free trial
Running a self-managed Elasticsearch instance means that you get all the features of Elasticsearch for free, but you have to go through the hassles of setting up the instance, i.e. making sure the host machine has enough memory to run the Elasticsearch.
Getting Started with Elasticsearch
In this tutorial, we’ll be running a self-managed Elasticsearch instance in a Docker container. If you prefer to use Elasticsearch, register for free trial here. Follow the steps below to run Strapi using Docker compose:
- Create an
Elastic_Deployment
directory. - Create a
.env
file that will store credentials that Elasticsearch requires.
1 STACK_VERSION=8.4.2
2 ELASTIC_PASSWORD=Elastic_password
3 KIBANA_PASSWORD=Kibana_password
4 ES_PORT=9200
5 CLUSTER_NAME=es-cluster
6 LICENSE=basic
7 MEM_LIMIT=1073741824
8 KIBANA_PORT=5601
9 ENTERPRISE_SEARCH_PORT=3002
10 ENCRYPTION_KEYS=secret
Replace the Elastic_password
and kibana_password
values above with whatever passwords you like.
- Create a
docker-compose.yaml
file and paste the following configurations:
1 version: "2.2"
2
3 services:
4 setup:
5 image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
6 volumes:
7 - certs:/usr/share/elasticsearch/config/certs
8 user: "0"
9 command: >
10 bash -c '
11 if [ x${ELASTIC_PASSWORD} == x ]; then
12 echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
13 exit 1;
14 elif [ x${KIBANA_PASSWORD} == x ]; then
15 echo "Set the KIBANA_PASSWORD environment variable in the .env file";
16 exit 1;
17 fi;
18 if [ ! -f certs/ca.zip ]; then
19 echo "Creating CA";
20 bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
21 unzip config/certs/ca.zip -d config/certs;
22 fi;
23 if [ ! -f certs/certs.zip ]; then
24 echo "Creating certs";
25 echo -ne \
26 "instances:\n"\
27 " - name: es01\n"\
28 " dns:\n"\
29 " - es01\n"\
30 " - localhost\n"\
31 " ip:\n"\
32 " - 127.0.0.1\n"\
33 > config/certs/instances.yml;
34 bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key;
35 unzip config/certs/certs.zip -d config/certs;
36 fi;
37 echo "Setting file permissions"
38 chown -R root:root config/certs;
39 find . -type d -exec chmod 750 \{\} \;;
40 find . -type f -exec chmod 640 \{\} \;;
41 echo "Waiting for Elasticsearch availability";
42 until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | grep -q "missing authentication credentials"; do sleep 30; done;
43 echo "Setting kibana_system password";
44 until curl -s -X POST --cacert config/certs/ca/ca.crt -u elastic:${ELASTIC_PASSWORD} -H "Content-Type: application/json" https://es01:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
45 echo "All done!";
46 '
47 healthcheck:
48 test: ["CMD-SHELL", "[ -f config/certs/es01/es01.crt ]"]
49 interval: 1s
50 timeout: 5s
51 retries: 120
52
53 es01:
54 depends_on:
55 setup:
56 condition: service_healthy
57 image: docker.elastic.co/elasticsearch/elasticsearch:${STACK_VERSION}
58 volumes:
59 - certs:/usr/share/elasticsearch/config/certs
60 - esdata01:/usr/share/elasticsearch/data
61 ports:
62 - ${ES_PORT}:9200
63 environment:
64 - node.name=es01
65 - cluster.name=${CLUSTER_NAME}
66 - cluster.initial_master_nodes=es01
67 - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
68 - bootstrap.memory_lock=true
69 - xpack.security.enabled=true
70 - xpack.security.http.ssl.enabled=true
71 - xpack.security.http.ssl.key=certs/es01/es01.key
72 - xpack.security.http.ssl.certificate=certs/es01/es01.crt
73 - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
74 - xpack.security.http.ssl.verification_mode=certificate
75 - xpack.security.transport.ssl.enabled=true
76 - xpack.security.transport.ssl.key=certs/es01/es01.key
77 - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
78 - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
79 - xpack.security.transport.ssl.verification_mode=certificate
80 - xpack.license.self_generated.type=${LICENSE}
81 mem_limit: ${MEM_LIMIT}
82 ulimits:
83 memlock:
84 soft: -1
85 hard: -1
86 healthcheck:
87 test:
88 [
89 "CMD-SHELL",
90 "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'",
91 ]
92 interval: 10s
93 timeout: 10s
94 retries: 120
95
96 kibana:
97 depends_on:
98 es01:
99 condition: service_healthy
100 image: docker.elastic.co/kibana/kibana:${STACK_VERSION}
101 volumes:
102 - certs:/usr/share/kibana/config/certs
103 - kibanadata:/usr/share/kibana/data
104 ports:
105 - ${KIBANA_PORT}:5601
106 environment:
107 - SERVERNAME=kibana
108 - ELASTICSEARCH_HOSTS=https://es01:9200
109 - ELASTICSEARCH_USERNAME=kibana_system
110 - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
111 - ELASTICSEARCH_SSL_CERTIFICATEAUTHORITIES=config/certs/ca/ca.crt
112 - ENTERPRISESEARCH_HOST=http://enterprisesearch:${ENTERPRISE_SEARCH_PORT}
113 mem_limit: ${MEM_LIMIT}
114 healthcheck:
115 test:
116 [
117 "CMD-SHELL",
118 "curl -s -I http://localhost:5601 | grep -q 'HTTP/1.1 302 Found'",
119 ]
120 interval: 10s
121 timeout: 10s
122 retries: 120
123
124 enterprisesearch:
125 depends_on:
126 es01:
127 condition: service_healthy
128 kibana:
129 condition: service_healthy
130 image: docker.elastic.co/enterprise-search/enterprise-search:${STACK_VERSION}
131 volumes:
132 - certs:/usr/share/enterprise-search/config/certs
133 - enterprisesearchdata:/usr/share/enterprise-search/config
134 ports:
135 - ${ENTERPRISE_SEARCH_PORT}:3002
136 environment:
137 - SERVERNAME=enterprisesearch
138 - secret_management.encryption_keys=[${ENCRYPTION_KEYS}]
139 - allow_es_settings_modification=true
140 - elasticsearch.host=https://es01:9200
141 - elasticsearch.username=elastic
142 - elasticsearch.password=${ELASTIC_PASSWORD}
143 - elasticsearch.ssl.enabled=true
144 - elasticsearch.ssl.certificate_authority=/usr/share/enterprise-search/config/certs/ca/ca.crt
145 - kibana.external_url=http://kibana:5601
146 mem_limit: ${MEM_LIMIT}
147 healthcheck:
148 test:
149 [
150 "CMD-SHELL",
151 "curl -s -I http://localhost:3002 | grep -q 'HTTP/1.1 302 Found'",
152 ]
153 interval: 10s
154 timeout: 10s
155 retries: 120
156
157 volumes:
158 certs:
159 driver: local
160 enterprisesearchdata:
161 driver: local
162 esdata01:
163 driver: local
164 kibanadata:
165 driver: local
To start the Elasticsearch instance, run:
docker-compose up --remove-orphans
If you get an error relating to vm.max_map_count
, then refer to this documentation on how to solve it for your particular OS.
The Strapi FoodAdvisor Application
FoodAdvisor is the official Strapi demo application; you can learn a lot about Strapi by studying the repo.
To clone the repository, run the following command in your terminal:
git clone https://github.com/strapi/foodadvisor.git
Follow the instructions below to start the foodadvisor application:
Server:
- Navigate to the
api
directory of the foodadvisor application, by runnngcd api
in your terminal. - In the
foodAdvisor/api
directory, copy the contents of theenv.example
file into a.env
file. - Run the following commands:
yarn && yarn seed && yarn develop
The Strapi server should be up and running on http://localhost:1337.
Client:
- Navigate to the
client
directory of the foodadvisor application by runnngcd client
in your terminal. - Run the following commands:
yarn && yarn dev
The next.js client should be running on http://localhost:3000.
Integrating Elasticsearch into the Strapi Server
To connect Elasticsearch to our Strapi server, we need to install a client that allows our server talk to Elasticsearch. Run the command below:
yarn add @elastic/elasticsearch
or
npm i @elastic/elasticsearch
In the foodAdvisor/api
directory, follow the instructions below.
1. Create a helpers
directory - mkdir helpers
.
2. In the helpers directory, create an elastic_client.js
file by running the command below:
cd helpers
touch elastic_client.js
- Update the content of
elastic_client.js
with the following:
1 const { Client } = require('@elastic/elasticsearch')
2 const host = process.env.ELASTIC_HOST
3
4 const fs = require('fs')
5
6 const connector = () => {
7
8 return new Client({
9 node: host,
10 auth: {
11 username: process.env.ELASTIC_USERNAME,
12 password: process.env.ELASTIC_PASSWORD
13 },
14 tls: {
15 ca: fs.readFileSync('./http_ca.crt'),
16 rejectUnauthorized: false
17 }
18 })
19 }
20
21 const testConn = (client) => {
22 client.info()
23 .then(response => console.log(response))
24 .catch(error => console.error(error))
25 }
26
27 module.exports = {
28 connector,
29 testConn
30 }
To have this connection working properly:
1. Update the contents of the .env
file in the foodadvisor/api
directory. Open up the .env
file and add the following credentials to it:
1 ELASTIC_HOST=https://localhost:9200
2 ELASTIC_USERNAME=elastic
3 ELASTIC_PASSWORD=ELASTIC_PASSWORD_FROM_YOUR_ELASTIC_DEPLOYMENT
Elasticsearch is always running on https://localhost:9200
and the username for the free version elasticsearch is elastic
. Elastic_password
should be the same password you set in you elastic_deployment/.env
file.
1 tls: {
2 ca: fs.readFileSync('./http_ca.crt'),
3 rejectUnauthorized: false
4 }
To get the ./http_ca.crt
file, we have to go into the Elasticsearch container. In order to do that, run:
1 docker exec --it <CONTAINER_ID> /bin/bash
2 cd config/certs/ca
3 cat ca.crt
CONTAINER_ID
is the id of the container exposed on :9200
. Copy the results of the crt command, then create a file in the foodadvisor/api
directory called http_ca.crt
and paste the results in it.
- Create an
elastic_index.js
file in thefoodadvisor/api/scripts
directory and update its contents with the following lines of code:
1 const strapi_url = 'http://localhost:1337/'
2 const axios = require('axios')
3 require('array.prototype.flatmap').shim()
4
5 const { connector, testConn } = require('../helpers/elastic_client')
6
7 const client = connector()
8 testConn(client)
9
10 const run = async () => {
11
12 const response = await axios.get(`${strapi_url}api/search/restaurants`)
13
14 const dataset = response.data
15
16 await client.indices.create({
17 index: 'foodadvisor-restaurant',
18 operations: {
19 mappings: {
20 properties: {
21 id: { type: 'integer' },
22 name: { type: 'text' },
23 slug: { type: 'keyword' },
24 location: { type: 'text' },
25 description: { type: 'text' },
26 url: { type: 'text' }
27 }
28 }
29 }
30 }, { ignore: [400] })
31
32 const operations = dataset.flatMap(doc => [{ index: { _index: 'foodadvisor-restaurant' } }, doc])
33
34 const bulkResponse = await client.bulk({ refresh: true, operations })
35
36 if (bulkResponse.errors) {
37 const erroredDocuments = []
38 // The items array has the same order of the dataset we just indexed.
39 // The presence of the `error` key indicates that the operation
40 // that we did for the document has failed.
41 bulkResponse.items.forEach((action, i) => {
42 const operation = Object.keys(action)[0]
43 if (action[operation].error) {
44 erroredDocuments.push({
45 // If the status is 429 it means that you can retry the document,
46 // otherwise it's very likely a mapping error, and you should
47 // fix the document before to try it again.
48 status: action[operation].status,
49 error: action[operation].error,
50 operation: body[i * 2],
51 document: body[i * 2 + 1]
52 })
53 }
54 })
55 }
56
57 const count = await client.count({ index: 'foodadvisor-restaurant' })
58
59 console.log(count)
60
61 }
62
63 run().catch(console.log)
In the code snippet above, we’re creating an index programmatically. An index is an optimized collection of documents and each document is a collection of fields, which are the key-value pairs that contain your data. Next, we’re creating a run()
function which performs a bulk insert of data into the created index.
Run npm i array.prototype.flatmap
to install the array.prototype.flatmap
package.
Make sure your Strapi server is up and running, then run the following command to populate your foodadvisor-restaurant
Elasticsearch index:
node /api/script/elastic_index.js
Generating a Strapi API
To generate a Strapi API, run the following:
cd api && yarn strapi generate api
- Name the generated API
search
. - When asked if the API is for a plugin, select "no" as the answer.
The generated API should be located in foodadvisor/api/src/api/search
. The content of the directory includes:
- Routes Directory: Update the contents of its
search.js
file:
1 module.exports = {
2 routes: [
3 {
4 method: 'GET',
5 path: '/search/restaurants',
6 handler: 'search.restaurants',
7 config: {
8 policies: [],
9 middlewares: [],
10 auth: false
11 },
12 },
13 {
14 method: 'POST',
15 path: '/search/restaurants',
16 handler: 'search.search_restaurants',
17 config: {
18 policies: [],
19 middlewares: [],
20 auth: false
21 },
22 },
23 ],
24 };
- Controllers Directory: Update the contents of its
search.js
file:
1 'use strict';
2
3 /**
4 * A set of functions called "actions" for `search`
5 */
6
7 module.exports = {
8 restaurants: async (ctx, next) => {
9 try {
10 const data = await strapi.service('api::search.search').restaurants()
11 // console.log('here', data)
12 ctx.body = data
13 } catch (err) {
14 ctx.body = err;
15 }
16 },
17
18 search_restaurants: async(ctx, next) => {
19 try {
20 const data = await strapi.service('api::search.search').search_restaurants(ctx.query)
21 // console.log('here', ctx.query)
22 ctx.body = data
23 } catch (err) {
24 ctx.body = err;
25 }
26 }
27
28 };
- Services Directory: Update the contents of its
search.js
file:
1 'use strict';
2
3 const { connector, testConn } = require('../../../../helpers/elastic_client')
4
5 const client = connector()
6
7 /**
8 * search service
9 */
10
11 module.exports = ({ strapi }) => ({
12
13 restaurants: async () => {
14
15 const data = await strapi.entityService.findMany('api::restaurant.restaurant', {
16 populate: { information: true, place: true, images: true }
17 })
18
19 const mappedData = data.map((el, i) => {
20 return { id: el.id, slug: el.slug, name: el.name, description: el.information.description, location: el.place.name, image: el.images[0].url }
21 })
22
23 return mappedData
24 },
25
26 search_restaurants: async (data) => {
27
28 //test client's connection to elastic search
29 testConn(client)
30
31 async function read() {
32
33 const search = data.s
34 const field = data.field || 'name'
35
36 const body = await client.search({
37 index: 'foodadvisor-restaurant',
38 body: {
39 query: {
40 regexp: {
41 [field]: {
42 value: `${search}.*`,
43 flags: "ALL",
44 case_insensitive: true,
45 },
46 }
47 }
48 }
49 })
50
51 const mappedData = body.hits.hits
52
53 await Promise.all(mappedData.map(async(el, i) => {
54 mappedData[i] = await strapi.entityService.findOne('api::restaurant.restaurant', el._source.id, {
55 populate: { information: true, place: true, images: true, category: true }
56 })
57 }))
58
59 mappedData.map((el, i) => {
60 const images = el.images
61 const place = el.place
62 const category = el.category
63 delete el.images
64 delete el.place
65 delete el.category
66 const imageData = []
67
68 images.forEach(el => {
69 imageData.push({ id: el.id, attributes: el })
70 })
71 el.images = {
72 data: imageData
73 }
74 el.place = {
75 data: {
76 attributes: place
77 }
78 }
79 el.category = {
80 data: {
81 attributes: category
82 }
83 }
84 })
85
86 return mappedData
87 }
88
89 return read().catch(console.log)
90
91 },
92
93 populate_restaurants: async(data) => {
94 console.log('data')
95 await Promise.all(data.map(async(el, i) => {
96 data[i] = await strapi.entityService.findOne('api::restaurant.restaurant', el.id, {
97 populate: { information: true, place: true, images: true, category: true }
98 })
99 }))
100
101 data.map((el, i) => {
102 const images = el.images
103 const place = el.place
104 const category = el.category
105 delete el.images
106 delete el.place
107 delete el.category
108 const imageData = []
109 images.forEach(el => {
110 imageData.push({ id: el.id, attributes: el })
111 })
112 el.images = {
113 data: imageData
114 }
115 el.place = {
116 data: {
117 attributes: place
118 }
119 }
120 el.category = {
121 data: {
122 attributes: category
123 }
124 }
125 })
126 }
127 });
Updating the Next.js Frontend
- Update the API request to include a POST request to the restaurant search route. In the
client/utils/index.js
file add the following lines of code to its content:
1 export async function search(searchText) {
2 console.log('searching', searchText)
3 const resRestaurants = await fetch(
4 getStrapiURL(`/search/restaurants?s=${searchText}`),
5 {
6 method: "POST"
7 }
8 )
9
10 const restaurants = await resRestaurants.json()
11
12 return { restaurants: restaurants, count: restaurants.length }
13 }
- Next, in
client/pages/restaurants/index.js
, update the following code appropriately:
1 //other imports
2 import { getData, getRestaurants, getStrapiURL, search } from "../../utils";
3
4 //other useState hooks
5 const [searchText, setSearchText] = useState('')
6 const [searchData, setSearchData] = useState('')
7
8 //replace the <container> tag and it's children with the following
9 <Container>
10 <Header {...header} />
11 <div className="flex flex-col content-end items-center md:flex-row gap-2 my-24 px-4">
12 <div>
13 {/* categories */}
14 <select
15 className="block w-52 py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
16 onChange={(value) => {
17 setCategoryId(delve(value, "target.value"))
18 setSearchData('')
19 }}
20 >
21 <option value="">
22 {categoryId
23 ? "Clear filter"
24 : categoryText || "Select a category"}
25 </option>
26 {categories &&
27 categories.map((category, index) => (
28 <option
29 key={`categoryOption-${index}`}
30 value={delve(category, "attributes.id")}
31 >
32 {delve(category, "attributes.name")}
33 </option>
34 ))}
35 </select>
36 </div>
37 <div>
38 {/* location */}
39 <select
40 className="block w-52 py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500"
41 onChange={(value) => {
42 setPlaceId(delve(value, "target.value"))
43 setSearchData('')
44 }}
45 >
46 <option value="">
47 {placeId ? "Clear filter" : placeText || "Select a place"}
48 </option>
49 {places &&
50 places.map((place, index) => (
51 <option
52 key={`placeOption-${index}`}
53 value={delve(place, "attributes.id")}
54 >
55 {delve(place, "attributes.name")}
56 </option>
57 ))}
58 </select>
59 </div>
60 {/* search */}
61 <div className="flex flex-col md:flex-row justify-items-end gap-2 px-2">
62 <input className="block w-80 right-0 py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500" placeholder="Search Restaurants" onChange={(event) => {
63 setSearchText(event.target.value)
64 }}/>
65 <button
66 type="button"
67 className={`${
68 searchText.length <= 2 ? "cursor-not-allowed opacity-50" : ""
69 } w-1/4 p-4 border rounded-full bg-primary hover:bg-primary-darker text-white hover:bg-gray-100 focus:outline-none`} disabled={searchText.length <= 2} onClick={async () => {
70 const res = await search(searchText)
71 setSearchData(res)
72 setCategoryId(null)
73 setPlaceId(null)
74 // console.log(data.restaurants)
75 }}
76 >
77 Search
78 </button>
79 </div>
80 </div>
81
82 <NoResults status={status || (searchData != '' && searchData.length == 0)} length={ searchData != '' ? searchData.restaurants.length : delve(data, "restaurants").length} />
83
84 {/* render initial data || search results */}
85 {searchData.length <= 0 ? <div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-16 mt-24 px-4">
86 {status === "success" &&
87 delve(data, "restaurants") &&
88 data.restaurants.map((restaurant, index) => (
89 <RestaurantCard
90 {...restaurant.attributes}
91 locale={locale}
92 key={index}
93 />
94 ))}
95 </div> : <div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-16 mt-24 px-4">
96 {status === "success" &&
97 delve(data, "restaurants") &&
98 searchData.restaurants.map((restaurant, index) => (
99 <RestaurantCard
100 {...restaurant}
101 locale={locale}
102 key={index}
103 />
104 ))}
105 </div>
106 }
107
108
109 {delve(data, "count") > 0 && (
110 <div className="grid grid-cols-3 gap-4 my-24">
111 <div className="col-start-2 col-end-3">
112 {searchData.length <= 0 ? <div className="flex items-center">
113 <button
114 type="button"
115 className={`${
116 pageNumber <= 1 ? "cursor-not-allowed opacity-50" : ""
117 } w-full p-4 border text-base rounded-l-xl text-gray-600 bg-white hover:bg-gray-100 focus:outline-none`}
118 onClick={() => setPageNumber(pageNumber - 1)}
119 disabled={pageNumber <= 1}
120 >
121 Previous
122 </button>
123
124 <button
125 type="button"
126 className={`${
127 pageNumber >= lastPage
128 ? "cursor-not-allowed opacity-50"
129 : ""
130 } w-full p-4 border-t border-b border-r text-base rounded-r-xl text-gray-600 bg-white hover:bg-gray-100 focus:outline-none`}
131 onClick={() => setPageNumber(pageNumber + 1)}
132 disabled={pageNumber >= lastPage}
133 >
134 Next
135 </button>
136 </div>: ''}
137 </div>
138 </div>
139 )}
140 </Container>
Testrunning the Search Feature
Navigate to the FoodAdvisor restaurant page located at http://localhost:3000/restaurants?lang=en
.
Here’s how the page should look like:
Type any text into the search-box, then click the search button. Depending on the text you entered, you could have a collection of results returned to you or the no results component is rendered.
Valid search results:
Invalid search results:
Conclusion
In this article, we discussed what Elasticsearch is and its benefits. We also saw how to create Elastic indices programmatically and how to integrate Elasticsearch into a Strapi application. This is just the tip of the iceberg of what Elasticsearch can do. I surely do hope that now you have some basics nailed down you’re ready to explore more features of Elasticsearch.
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)