Books Search application built using Typesense
Outline: [Article Title]
Keyword: [Enter Targeted Keyword]
Keyword MSV: [Enter Targeted Keyword’s Monthly Search Volume]
Author: [Enter Author Name]
Due Date: [Enter Due Date]
Publish Date: [Enter Desired Publish Date]
User Persona: [Enter Targeted Reader and/or User Persona]
Building search engines that are typo-tolerant, effective, and efficient is difficult. Even if the requested item is in the database, a typographical error could cause a search to return nothing.Typesense could save a lot of time and effort by eliminating the need to build a search engine. Additionally, your users will be able to effectively use the search feature in your application, resulting in a positive user experience.Typesense is an open-source typo-tolerant search engine for developers, designed to speed up the time to search effectively and efficiently.Visit the link below to learn more about typesense.
This article will cover how to set up Typesense , initialize a nodejs application from the ground up, preconfigure the client, and more. Not only that, but this article will also guide you on how to make a Typesense Collection. Finally, we will start and initialize our express app, add an item to our collection, and then search our collection.
Building a Book Search Application
Typesense Setup
Typesense can be used by installing its docker image or by using the Typesense cloud hosting solution that it provides, which is the simplest way to get started with it. To get started, go to Typesense cloud official website and sign up with your Github account or you can use it directly by docker method.For the purposes of this tutorial, we’ll use the Docker method.
To do so, simply go to the Typesense Dockerhub and download the prebuilt image, then follow the instructions stated below.
docker pull typesense/typesense
and
mkdir /tmp/typesense-data
docker run -p 8108:8108 -v/tmp/data:/data typesense/typesense:0.22.2 --data-dir /data --api-key=6be0576ff61c053d5f9a3225e2a90f76 --enable-cors
We’re ready to create a Typesense collection, index some documents in it, and try searching for them now that Typesense is installed and running.
Now lets actually build a search UI fro our Application .
Building Search UIs from scratch.
With just a few lines of code, you can create a plug-and-play full-featured search interface using the open source InstantSearch.js library or its React, Vue, and Angularjs, as well as the Typesense-InstantSearch-Adapter .
Step by step installation guide
Let’s begin with a basic template:
$ npx create-instantsearch-app typesense-booksearch
Creating a new InstantSearch app in typesense-booksearch.
? InstantSearch template InstantSearch.js
? InstantSearch.js version 4.38.0
? Application ID typesense
? Search API key typesense_search_only_api_key
? Index name books
? Attributes to display
Used to generate the default result template
📦 Installing dependencies...
yarn install v1.22.0
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
✨ Done in 24.73s.
🎉 Created typesense-instantsearch-demo at typesense-instantsearch-demo.
Begin by typing:
cd typesense-instantsearch-demo
yarn start
⚡️ Start building something awesome!
After you’ve finished installing your UI, the folder structure should look like this.
Here are some setup suggestions for the npx create-instantsearch-app
command:
- You can use any of the web libraries typesense supports, including InstantSearch.js, React, Vue, and Angular.
- You can use the default version of InstantSearch.js.
- Any string can be used as the application ID; we’ll be changing this later in the article.
- Search API key: any string - we’ll replace this with your Typesense Search-only API Key later in the guide.
- The name of your collection in Typesense is the index name.
- Displaying attributes: leave it as is.
To use InstantSearch with a Typesense backend, we’ll need to install the Typesense InstantSearch adapter:
$ npm install typesense-instantsearch-adapter
As you can see, it’s a very simple application with two important files in it: app.js
and index.html
, which contains the structure of our entire application.
You can see an id in the ‘index.html’ file in the screenshot below. We can use those ids to attach pieces of UI, for example, a searchbox, results, and then facets if you want to add them, and finally there is pagination as well.
As you can see in the image below, the app.js file is configured for algolia, and we’ll need to change that for typesense.
To use InstantSearch with a Typesense backend, we’ll need to install the Typesense InstantSearch adapter. To do so, simply open a terminal window and type the following command into it.
npm install --save typesense-instantsearch-adapter
We can now finally adjust the project to use Typesense.We’ve got our typesense instance up and running in the background.To get InstantSearch.js to use the Typesense adapter, open src/app.js and change the way InstantSearch is initialized, from this:
const { algoliasearch, instantsearch } = window;
const searchClient = algoliasearch(
"latency",
"6be0576ff61c053d5f9a3225e2a90f76"
);
const search = instantsearch({
indexName: "instant_search",
searchClient,
});
to this
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,authors", //quering by
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const search = instantsearch({
searchClient,
indexName: "books",
});
Now, we need to import our books data, so make a dataset folder and inside it make your own books json file and fill it with all the necessary book information or you can download the books dataset from here.Finally, your folder structure should resemble the following.
Let’s get started writing the scripts that import the data. First, we’ll create a file called loadData.js
and initialize the typesense client inside it.
//loadData.js
const Typesense = require("typesense");
const runClient = () => {
let client = new Typesense.Client({
nodes: [
{
host: "localhost", // For Typesense Cloud use xxx.a1.typesense.net
port: "8108", // For Typesense Cloud use 443
protocol: "http", // For Typesense Cloud use https
},
],
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
connectionTimeoutSeconds: 2,
});
};
runClient();
Creating a collection named books
A Collection in Typesense is a set of related Documents that functions similarly to a table in a relational database. We give a collection a name and describe the fields that will be indexed when a document is added to the collection when we create it.
Now go to the loadData.js
file and make the following changes to the code.
const Typesense = require("typesense");
const runClient = async () => {
let client = new Typesense.Client({
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
connectionTimeoutSeconds: 2,
});
const booksSchema = {
name: "books",
fields: [
{ name: "title", type: "string" },
{ name: "authors", type: "string[]", facet: true },
{ name: "publication_year", type: "int32", facet: true },
{ name: "ratings_count", type: "int32" },
{ name: "average_rating", type: "float" },
],
default_sorting_field: "ratings_count",
};
const loadData = await client.collections().create(booksSchema);
console.log(loadData);
};
runClient();
If an error occurs while loading the data, simply add the following snippet of code to the loadData.js
file before creating bookschema
.
await client.collections("books").delete();
This is how your final code should look.
//loadData.js
const Typesense = require("typesense");
const runClient = async () => {
let client = new Typesense.Client({
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
connectionTimeoutSeconds: 2,
});
const booksSchema = {
name: "books",
fields: [
{ name: "title", type: "string" },
{ name: "authors", type: "string[]", facet: true },
{ name: "publication_year", type: "int32", facet: true },
{ name: "ratings_count", type: "int32" },
{ name: "average_rating", type: "float" },
],
default_sorting_field: "ratings_count",
};
await client.collections("books").delete();
const loadData = await client.collections().create(booksSchema);
console.log(loadData);
};
runClient();
Finally, type node loadData.js
into the terminal of that same project directory, and you should see something similar to the image below.
Now that we have the schema, we must add the documents. We have our documents saved as books.jsonl
in our dataset folder. All you have to do now is import and load the document into our loadData.js
file.
const fs = require("fs/promises");
const documents = await fs.readFile("./dataset/books.jsonl");
client.collections("books").documents().import(documents, { batch_size: 200 });
Now we can specify the batch file size, and for now, let’s insert 200 elements. Your final code inside loadData.js
file should look like this.
const Typesense = require("typesense");
const fs = require("fs/promises");
const runClient = async () => {
let client = new Typesense.Client({
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
connectionTimeoutSeconds: 2,
});
const booksSchema = {
name: "books",
fields: [
{ name: "title", type: "string" },
{ name: "authors", type: "string[]", facet: true },
{ name: "publication_year", type: "int32", facet: true },
{ name: "ratings_count", type: "int32" },
{ name: "average_rating", type: "float" },
],
default_sorting_field: "ratings_count",
};
await client.collections("books").delete();
const loadData = await client.collections().create(booksSchema);
const documents = await fs.readFile("./dataset/books.jsonl");
client
.collections("books")
.documents()
.import(documents, { batch_size: 200 });
console.log(loadData);
};
runClient();
Finally, type node loadData.js
into the terminal of that same project directory, and you have successfully imported 10 thousand documents into our search index.
Now,you can start your application by typing npm start
in the terminal of that project directory.
We need to fix the fact that the application works flawlessly but that nothing is displayed.
So, to fix it, go to our app.js
file and remove a few of the widgets that we don’t need, so the app.js
file should look something like this.
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const search = instantsearch({
searchClient,
indexName: "books",
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
}),
instantsearch.widgets.pagination({
container: "#pagination",
}),
]);
search.start();
Simply re-run the application after you’ve fixed it, and your application should now look like this.
You can now build a search interface using any of the InstantSearch widgets, and we’ll be adding a template to present the data in a pleasing manner.Simplycopy the template code below and add it inside the #hits
widget present inside app.js
file.
templates: {
item: `
<div>
<img src="" align="left" alt="" />
<div class="hit-name">
{ "attribute": "title" }
</div>
<div class="hit-description">
{ "attribute": "authors" }
</div>
<div class="hit-price">\$</div>
<div class="hit-rating">Rating: </div>
</div>
`,
},
app.js
file should resemble something like this.
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const search = instantsearch({
searchClient,
indexName: "books",
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: `
<div>
<img src="" align="left" alt="" />
<div class="hit-name">
{ "attribute": "title" }
</div>
<div class="hit-description">
{ "attribute": "authors" }
</div>
<div class="hit-price">\$</div>
<div class="hit-rating">Rating: </div>
</div>
`,
},
}),
instantsearch.widgets.pagination({
container: "#pagination",
}),
]);
search.start();
Again, re-run the application after you’ve fixed it, and your application should now look like this.
To configure the 12 hits per page or you can configure to your own requirements, simply add the following code above the #hits
widgets inside the app.js
file.
instantsearch.widgets.configure({
hitsPerPage: 12,
}),
So Now let’s try to add the facets ,to do that so we’ll add a instant search widgets and in instanctsearch library this is called refinementlist
and then we have to specify the container so let’s call it a #filterList
and then the attribute we want to take so in our case let’s call it authors
we reference that if we do this and then we add the id name as a refinements
on our index.html
and run our application it should probaly work. As a result, this is how our final code should look.
//app.js
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const search = instantsearch({
searchClient,
indexName: "books",
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.configure({
hitsPerPage: 12,
}),
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: `
<div>
<img src="" align="left" alt="" />
<div class="hit-name">
{ "attribute": "title" }
</div>
<div class="hit-description">
{ "attribute": "authors" }
</div>
<div class="hit-price">\$</div>
<div class="hit-rating">Rating: </div>
</div>
`,
},
}),
instantsearch.widgets.refinementList({
container: "#filterList",
attribute: "authors",
}),
instantsearch.widgets.pagination({
container: "#pagination",
}),
]);
search.start();
and our index.html
file
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="shortcut icon" href="./favicon.png" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/algolia-min.css"
/>
<link rel="stylesheet" href="./src/index.css" />
<link rel="stylesheet" href="./src/app.css" />
<title>typesense-booksearch</title>
</head>
<body>
<header class="header">
<h1 class="header-title">
<a href="/">typesense-booksearch</a>
</h1>
<p class="header-subtitle">
using
<a href="https://github.com/algolia/instantsearch.js">
InstantSearch.js
</a>
</p>
</header>
<div class="container">
<div class="search-panel">
<div class="search-panel__filters">
<div id="filterList"></div>
</div>
<div class="search-panel__results">
<div id="searchbox"></div>
<div id="hits"></div>
</div>
</div>
<div id="pagination"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4.10.5/dist/algoliasearch-lite.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.38.0"></script>
<script src="./src/app.js"></script>
</body>
</html>
Here’s how your application should appear.
Finally, we can add sorting functionality to the application by following the same steps as before: add the widget called sortBy
, specify the container with #sort-books
, and specify the items
with the label default with the value books
, and then create another label called Publication year
with the value books/sort/publication_year:asc
, and again another label called Ratings
with the value books/sort/average_rating:asc
.
instantsearch.widgets.sortBy({
container: '#sort',
items: [
{ label: 'Default', value: 'books' },
{ label: 'Ratings', value: 'books/sort/average_rating:asc' },
{ label: 'Publication year', value: 'books/sort/publication_year:asc' },
],
}),
Finally, update and add sort-books
id before the searchbox
inside index.html
file.
Ultimately, as a final step, let’s update the styles and template for the information that we want to display in our app, so here’s how your code should look.
/* app.css */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600&display=swap");
body {
font-family: "Inter", sans-serif;
}
.header {
display: flex;
align-items: center;
min-height: 70px;
padding: 0.5rem 1rem;
background-color: #8661d1;
color: #fff;
margin-bottom: 2rem;
}
.header a {
color: #fff;
text-decoration: none;
}
.header-title {
font-size: 1.2rem;
font-weight: normal;
}
.header-title::after {
content: " ▸ ";
padding: 0 0.5rem;
}
.header-subtitle {
font-size: 1.2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.search-panel {
display: flex;
}
.search-panel__filters {
flex: 1;
}
.search-panel__results {
flex: 3;
}
.ais-Highlight-highlighted {
color: inherit;
font-size: inherit;
}
#searchbox {
margin-bottom: 2rem;
}
#pagination {
margin: 2rem auto;
text-align: center;
}
your app.js
file should look like this
// app.js
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: "6be0576ff61c053d5f9a3225e2a90f76",
nodes: [
{
host: "localhost",
port: "8108",
protocol: "http",
},
],
},
additionalSearchParameters: {
queryBy: "title,authors",
},
});
const searchClient = typesenseInstantsearchAdapter.searchClient;
const search = instantsearch({
searchClient,
indexName: "books",
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#searchbox",
}),
instantsearch.widgets.configure({
hitsPerPage: 12,
}),
instantsearch.widgets.hits({
container: "#hits",
templates: {
item: `
<div>
<img src="" align="left" alt="" />
<div class="hit-name">
{ "attribute": "title" }
</div>
<div class="hit-description">
{ "attribute": "authors" }
</div>
<div class="hit-price"></div>
<div class="hit-rating">Rating: </div>
</div>
`,
},
}),
instantsearch.widgets.refinementList({
container: "#filterList",
attribute: "authors",
}),
instantsearch.widgets.sortBy({
container: "#sort-books",
items: [
{ label: "Default", value: "books" },
{ label: "Ratings", value: "books/sort/average_rating:asc" },
{ label: "Publication year", value: "books/sort/publication_year:asc" },
],
}),
instantsearch.widgets.pagination({
container: "#pagination",
}),
]);
search.start();
Finally, your ‘index.html’ file should appear as follows:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.webmanifest" />
<link rel="shortcut icon" href="./favicon.png" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/instantsearch.css@7/themes/algolia-min.css"
/>
<link rel="stylesheet" href="./src/index.css" />
<link rel="stylesheet" href="./src/app.css" />
<title>typesense-booksearch</title>
</head>
<body>
<header class="header">
<h1 class="header-title">
<a href="/">typesense-booksearch</a>
</h1>
<p class="header-subtitle">
using
<a href="https://github.com/algolia/instantsearch.js">
InstantSearch.js
</a>
</p>
</header>
<div class="container">
<div class="search-panel">
<div class="search-panel__filters">
<div id="filterList"></div>
</div>
<div class="search-panel__results">
<div id="sort-books"></div>
<div id="searchbox"></div>
<div id="hits"></div>
</div>
</div>
<div id="pagination"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4.10.5/dist/algoliasearch-lite.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.38.0"></script>
<script src="./src/app.js"></script>
</body>
</html>
Let’s take a look at the final version of our typesense-integrated application.
Closing
Typesense was built with several distinctive features primarily aimed at making the developer’s job easier while also giving customer as well as user the ability to provide a better search experience as possible.This article may have been entertaining as well as instructive in terms of how to install typesense from the ground up on a variety of platforms. Join Aviyel’s community to learn more about the open source project, get tips on how to contribute, and join active dev groups.
Call-to-Action
Aviyel is a collaborative platform that assists open source project communities in monetizing and long-term sustainability. To know more visit Aviyel.com and find great blogs and events, just like this one! Sign up now for early access, and don’t forget to follow us on our socials!