Song search application built using Typesense.
Tags: javascript, typesense, beginners, webdev
Published: 2022-04-29 08:39:44

Crafting search engines from absolute scratch which are typo-tolerant, effective, and efficient is really very difficult. A typographical error could cause a search to return nothing, even if the requested item is in the database. By obviating the need to develop a search engine, Typesense could save a lot of time and effort. Your users will also be able to use the search feature in your app effectively, resulting in a positive user experience. Typesense is an open-source typo-tolerant search engine for developers, designed to cut down on the time it takes to find information quickly. To learn more about typesense, go to the following link.
- [What is Typesense?](https://typesense.org/docs/overview/what-is-typesense.html)

This article will walk you through installing Typesense, creating a new application from scratch, preconfiguring the client, and more. This post will also show you how to construct a Typesense Collection. Finally, we’ll start our app and initialize it, then add an item to our collection and start searching.

## Building a Song Search Application from scratch !

### Typesense Configuration

Typesense can be used by installing its prebuilt docker image which is already provided to us by Typesense or by using the [Typesense cloud](https://cloud.typesense.org/) hosting solution, which is the most straightforward way to get started. To get started, go to the Typesense cloud website and sign up with your Github account, or use the docker method easily and directly. We’ll use the Docker method for the purposes of this tutorial.To follow along with this tutorial, go to the [Typesense Dockerhub](https://hub.docker.com/r/typesense/typesense/) and download the prebuilt docker image, then follow the instructions 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=songsearch --enable-cors --listen-port 8108
```

![Docker run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03mc7s6dgt2i0826etgs.png)
 
![Docker desktop](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hq1pe90tbqx1s9a6r7w4.png)

### Building Search UIs from scratch.

Using the open source InstantSearch.js library library or its React, Vue, and Angularjs, as well as the Typesense-InstantSearch-Adapter, you can create a plug-and-play full-featured search interface with just a few lines of code.

Finally, lets’s actually build a search UI for our Application .

### Step-by-step instructions for installing

Let’s begin with a basic template:

```
npx create-instantsearch-app typesense-songsearch
```

```
? The name of the application or widget typesense-songsearch
? InstantSearch template InstantSearch.js

Creating a new InstantSearch app in typesense-songsearch.

? InstantSearch.js version 4.38.0
? Application ID latency
? Search API key 6be0576ff61c053d5f9a3225e2a90f76
? Index name instant_search
? Attributes to display
  Used to generate the default result template
? Attributes to display
  Used to filter the search interface Dynamic widgets

📦  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 173.57s.

🎉  Created typesense-songsearch at typesense-songsearch.

Begin by typing:

  cd typesense-songsearch
  yarn start

⚡️  Start building something awesome!
```

![Instantsearch app setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3nui2zl5q01ownb6yr8.png)

![Instantsearch app setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w5sdntps1ijvbp9rwz75.png)

This is how your folder structure should look after you’ve finished installing your Instantsearch app (UI).

![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxaa8ze7ajsb1u2rp85z.png)

For the `npx create-instantsearch-app` command, here are some setup suggestions: Any of the web libraries supported by typesense can be used, including `InstantSearch.js`, `React`, `Vue`, and `Angular`. The default version of `InstantSearch.js` can be used. The application ID can be any string; we’ll be changing it later in the article. Search API key: any string - later in the guide, we’ll replace this with the Typesense Search-only API Key and Our index name is the name of the collection in Typesense, and lastly , leave the Displaying attributes as it is.

Next, we’ll need to install the Typesense InstantSearch adapter to use InstantSearch with a Typesense backend:

```
npm install typesense-instantsearch-adapter
```

![typesense instantsearch adapter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nebyy9na9hrvex6t2hqc.png)

### Installing Typesense

```
npm install typesense
```

![Install typesense](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jxzcb0q1t1qt7i09bir5.png)

We’re ready to create a Typesense collection, Now that everything is installed and running, we can create a Typesense collection, index some documents in it, and try searching for them.

Your `package.json` file should look like this after you’ve installed all of the necessary dependencies.

```json
{
  "name": "typesense-songsearch",
  "version": "1.0.0",
  "private": true,
  "main": "src/app.js",
  "scripts": {
    "start": "parcel index.html --port 3000",
    "build": "parcel build index.html",
    "lint": "eslint .",
    "lint:fix": "npm run lint -- --fix"
  },
  "devDependencies": {
    "babel-eslint": "10.0.3",
    "eslint": "5.7.0",
    "eslint-config-algolia": "13.2.3",
    "eslint-config-prettier": "3.6.0",
    "eslint-plugin-import": "2.19.1",
    "eslint-plugin-prettier": "3.1.2",
    "parcel-bundler": "1.12.5",
    "prettier": "1.19.1"
  },
  "dependencies": {
    "algoliasearch": "4",
    "instantsearch.js": "4.38.0",
    "typesense": "^0.14.0",
    "typesense-instantsearch-adapter": "^2.2.0"
  }
}
```

As you can see, it’s a very basic app with only two important files: `app.js` and `index.html`, which contains the structure of our entire app.

In the `index.html` file, you can see an id. We can use those ids to attach UI components such as a `searchbox`, `results`, and `facets` if you want to include them, as well as `pagination` widgets.

```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-songsearch</title>
  </head>

  <body>
    <header class="header">
      <h1 class="header-title">
        <a href="/">typesense-songsearch</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="dynamic-widgets"></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>
```

As you can see below, the `app.js` file is configured for algolia, and we’ll need to change that for typesense.

```jsx
// app.js
const { algoliasearch, instantsearch } = window;

const searchClient = algoliasearch("latency", "songsearch");

const search = instantsearch({
  indexName: "instant_search",
  searchClient,
});
search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
  instantsearch.widgets.hits({
    container: "#hits",
  }),
  instantsearch.widgets.configure({
    facets: ["*"],
    maxValuesPerFacet: 20,
  }),
  instantsearch.widgets.dynamicWidgets({
    container: "#dynamic-widgets",
    fallbackWidget({ container, attribute }) {
      return instantsearch.widgets.refinementList({
        container,
        attribute,
      });
    },
    widgets: [],
  }),
  instantsearch.widgets.pagination({
    container: "#pagination",
  }),
]);

search.start();
```

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.

> If you’ve already installed it, you can skip this step.

```
npm install --save typesense-instantsearch-adapter
```

![Typesense adapter installation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2gcokj4zgpcx902shn2.png)

Finally, the project can be adjusted to use Typesense. In the background, we’ve got our typesense instance up and running. Open src/app.js and change the way InstantSearch is initialized to get InstantSearch.js to use the Typesense adapter:

```jsx
const searchClient = algoliasearch("latency", "songsearch");

const search = instantsearch({
  indexName: "instant_search",
  searchClient,
});
```

to this

```jsx
const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "songsearch",
    nodes: [
      {
        host: "localhost",
        port: "8108",
        protocol: "http",
      },
    ],
  },
  additionalSearchParameters: {
    queryBy: "title,primary_artist_name,album_name", //quering by
  },
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: "songs",
});
```

Now we need to import our songs data, so create a dataset folder and inside it, create your own songs json file, filling it with all of the necessary song information, or download the songs dataset from here. Finally, your folder structure should look something like this.

![Folder structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aelk0xig71ptjvmxvj6o.png)
 
![loaddata](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4thblmtycfuqfvaiqurx.png)

Let’s get to work on the data-importing scripts. We’ll start by creating a file called `loadData.js` and we will export and initialize the typesense client inside but before we do that we need to install a few packages `lodash` and `fast-json-stringify`.

```
npm i lodash
```

![lodash](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qyv5doksyexg62kwmwr.png)

and

```
npm i fast-json-stringify
```
![JSON Stringify](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/foxbz3vhkz9cvp7xls3t.png)

So, at the very top, import all of the dependencies that this script requires to run, as shown below.

```jsx
// loadData.js
const _ = require("lodash");
const fastJson = require("fast-json-stringify");
const fs = require("fs");
const readline = require("readline");
const Typesense = require("typesense");
```

After that, Create a stringify based on the schema of the documents that need to be stringified.

```jsx
// loadData.js
const _ = require("lodash");
const fastJson = require("fast-json-stringify");
const fs = require("fs");
const readline = require("readline");
const Typesense = require("typesense");

const stringify = fastJson({
  title: "Song Schema",
  type: "object",
  properties: {
    track_id: {
      type: "string",
    },
    title: {
      type: "string",
    },
    album_name: {
      type: "string",
      nullable: true,
    },
    primary_artist_name: {
      type: "string",
    },
    genres: {
      type: "array",
      items: {
        type: "string",
      },
    },
    country: {
      type: "string",
    },
    release_date: {
      type: "integer",
    },
    release_decade: {
      type: "string",
    },
    release_group_types: {
      type: "array",
      items: {
        type: "string",
      },
    },
    urls: {
      type: "array",
      items: {
        type: "object",
        properties: {
          type: {
            type: "string",
          },
          url: {
            type: "string",
          },
        },
      },
    },
  },
});
```

Create a new `extractUrls` function.

```js
function extractUrls(parsedRecord) {
  return parsedRecord["relations"]
    .filter((r) =>
      [
        "amazon asin",
        "streaming",
        "free streaming",
        "download for free",
        "purchase for download",
      ].includes(r["type"])
    )
    .map((r) => {
      return { type: r["type"], url: r["url"]["resource"] };
    });
}
```

Now create an asynchronous `addSongsToTypesense` function and copy the code exactly as it is shown below. This function will simply index all of the songs into Typesense, and if an error occurs during the process, it will simply log an error.


```js
async function addSongsToTypesense(songs, typesense, songsCollection) {
  try {
    const returnDataChunks = await Promise.all(
      _.chunk(songs, Math.ceil(songs.length / CHUNK_SIZE)).map((songsChunk) => {
        const jsonlString = songsChunk
          .map((song) => stringify(song))
          .join("\n");

        return typesense
          .collections(songsCollection)
          .documents()
          .import(jsonlString);
      })
    );

    const failedItems = returnDataChunks
      .map((returnData) =>
        returnData
          .split("\n")
          .map((r) => JSON.parse(r))
          .filter((item) => item.success === false)
      )
      .flat();
    if (failedItems.length > 0) {
      throw new Error(
        `Error indexing items ${JSON.stringify(failedItems, null, 2)}`
      );
    }
  } catch (error) {
    console.log(error);
  }
}
```

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 add the following changes to the code.

```js
module.exports = (async () => {
  const typesense = new Typesense.Client({
    nodes: [
      {
        host: "localhost",
        port: "8108",
        protocol: "http",
      },
    ],
    apiKey: "songsearch",
  });

  const songsCollection = "songs";
  const schema = {
    name: songsCollection,
    fields: [
      { name: "track_id", type: "string" },
      { name: "title", type: "string" },
      { name: "album_name", type: "string", optional: true },
      { name: "primary_artist_name", type: "string", facet: true },
      { name: "genres", type: "string[]", facet: true },
      { name: "country", type: "string", facet: true },
      { name: "release_date", type: "int64" },
      { name: "release_decade", type: "string", facet: true },
      { name: "release_group_types", type: "string[]", facet: true },
    ],
    default_sorting_field: "release_date",
  };

  console.log(`Populating data in Typesense "${songsCollection}" collection`);

  console.log(`Creating schema...`);
  await typesense.collections().create(schema);

  console.log(`Songs records...`);

  const fileStream = fs.createReadStream(DATA_FILE);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  let songs = [];
  let currentLine = 0;
  for await (const line of rl) {
    currentLine += 1;
    const parsedRecord = JSON.parse(line);
    try {
      songs.push(
        ...parsedRecord["media"]
          .map((media) => media["tracks"])
          .flat()
          .filter((track) => track) // To remove nulls
          .map((track) => {
            const releaseDate =
              Math.round(
                Date.parse(
                  parsedRecord["release-group"]["first-release-date"]
                ) / 1000
              ) || 0;

            // Be sure to update the schema passed to stringify when updating this structure
            const song = {
              track_id: track["id"],
              title: track["title"],
              album_name: parsedRecord["title"],
              primary_artist_name:
                parsedRecord["artist-credit"][0]["artist"]["name"],
              genres: [
                ...track["recording"]["genres"].map((g) => g.name),
                ...parsedRecord["genres"].map((g) => g.name),
                ...parsedRecord["release-group"]["genres"].map((g) => g.name),
              ].map(
                ([firstChar, ...rest]) =>
                  firstChar.toUpperCase() + rest.join("").toLowerCase()
              ),
              country: parsedRecord["country"] || "Unknown",
              release_date: releaseDate,
              release_decade: `${
                Math.round(new Date(releaseDate * 1000).getUTCFullYear() / 10) *
                10
              }s`,
              release_group_types: [
                parsedRecord["release-group"]["primary-type"] || "Unknown",
                parsedRecord["release-group"]["secondary-types"] || null,
              ]
                .flat()
                .filter((e) => e),
              urls: extractUrls(parsedRecord),
            };
            process.stdout.write("-");

            return song;
          })
      );
    } catch (e) {
      console.error(e);
      console.error(parsedRecord);
      throw e;
    }

    if (currentLine >= MAX_LINES) {
      break;
    }
  }

  if (songs.length > 0) {
    await addSongsToTypesense(songs, typesense, songsCollection);
    console.log("ALL songs INDEXED ✅ !");
  }
})();
```

If an error occurs while loading the data, simply add the following snippet of code to the `loadData.js` file before creating `songSchema`.

![loadData](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3cixue8xgg9yxrc4ify3.png)
 
```js
await typesense.collections("songs").delete();
```

This is how your final code should look like.

```js
// loadData.js
const _ = require("lodash");
const fastJson = require("fast-json-stringify");
const fs = require("fs");
const readline = require("readline");
const Typesense = require("typesense");

const stringify = fastJson({
  title: "Song Schema",
  type: "object",
  properties: {
    track_id: {
      type: "string",
    },
    title: {
      type: "string",
    },
    album_name: {
      type: "string",
      nullable: true,
    },
    primary_artist_name: {
      type: "string",
    },
    genres: {
      type: "array",
      items: {
        type: "string",
      },
    },
    country: {
      type: "string",
    },
    release_date: {
      type: "integer",
    },
    release_decade: {
      type: "string",
    },
    release_group_types: {
      type: "array",
      items: {
        type: "string",
      },
    },
    urls: {
      type: "array",
      items: {
        type: "object",
        properties: {
          type: {
            type: "string",
          },
          url: {
            type: "string",
          },
        },
      },
    },
  },
});

const BATCH_SIZE = 200;
const CHUNK_SIZE = 3;
const MAX_LINES = Infinity;
const DATA_FILE = "./dataset/songs.json";

function extractUrls(parsedRecord) {
  return parsedRecord["relations"]
    .filter((r) =>
      [
        "amazon asin",
        "streaming",
        "free streaming",
        "download for free",
        "purchase for download",
      ].includes(r["type"])
    )
    .map((r) => {
      return { type: r["type"], url: r["url"]["resource"] };
    });
}

async function addSongsToTypesense(songs, typesense, songsCollection) {
  try {
    const returnDataChunks = await Promise.all(
      _.chunk(songs, Math.ceil(songs.length / CHUNK_SIZE)).map((songsChunk) => {
        const jsonlString = songsChunk
          .map((song) => stringify(song))
          .join("\n");

        return typesense
          .collections(songsCollection)
          .documents()
          .import(jsonlString);
      })
    );

    const failedItems = returnDataChunks
      .map((returnData) =>
        returnData
          .split("\n")
          .map((r) => JSON.parse(r))
          .filter((item) => item.success === false)
      )
      .flat();
    if (failedItems.length > 0) {
      throw new Error(
        `Error indexing items ${JSON.stringify(failedItems, null, 2)}`
      );
    }
  } catch (error) {
    console.log(error);
  }
}

module.exports = (async () => {
  const typesense = new Typesense.Client({
    nodes: [
      {
        host: "localhost",
        port: "8108",
        protocol: "http",
      },
    ],
    apiKey: "songsearch",
  });

  await typesense.collections("songs").delete();

  const songsCollection = "songs";
  const schema = {
    name: songsCollection,
    fields: [
      { name: "track_id", type: "string" },
      { name: "title", type: "string" },
      { name: "album_name", type: "string", optional: true },
      { name: "primary_artist_name", type: "string", facet: true },
      { name: "genres", type: "string[]", facet: true },
      { name: "country", type: "string", facet: true },
      { name: "release_date", type: "int64" },
      { name: "release_decade", type: "string", facet: true },
      { name: "release_group_types", type: "string[]", facet: true },
    ],
    default_sorting_field: "release_date",
  };

  console.log(`Populating data in Typesense "${songsCollection}" collection`);

  console.log(`Creating schema...`);
  await typesense.collections().create(schema);

  console.log(`Songs records...`);

  const fileStream = fs.createReadStream(DATA_FILE);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity,
  });

  let songs = [];
  let currentLine = 0;
  for await (const line of rl) {
    currentLine += 1;
    const parsedRecord = JSON.parse(line);
    try {
      songs.push(
        ...parsedRecord["media"]
          .map((media) => media["tracks"])
          .flat()
          .filter((track) => track) // To remove nulls
          .map((track) => {
            const releaseDate =
              Math.round(
                Date.parse(
                  parsedRecord["release-group"]["first-release-date"]
                ) / 1000
              ) || 0;

            // Be sure to update the schema passed to stringify when updating this structure
            const song = {
              track_id: track["id"],
              title: track["title"],
              album_name: parsedRecord["title"],
              primary_artist_name:
                parsedRecord["artist-credit"][0]["artist"]["name"],
              genres: [
                ...track["recording"]["genres"].map((g) => g.name),
                ...parsedRecord["genres"].map((g) => g.name),
                ...parsedRecord["release-group"]["genres"].map((g) => g.name),
              ].map(
                ([firstChar, ...rest]) =>
                  firstChar.toUpperCase() + rest.join("").toLowerCase()
              ),
              country: parsedRecord["country"] || "Unknown",
              release_date: releaseDate,
              release_decade: `${
                Math.round(new Date(releaseDate * 1000).getUTCFullYear() / 10) *
                10
              }s`,
              release_group_types: [
                parsedRecord["release-group"]["primary-type"] || "Unknown",
                parsedRecord["release-group"]["secondary-types"] || null,
              ]
                .flat()
                .filter((e) => e),
              urls: extractUrls(parsedRecord),
            };
            process.stdout.write("-");

            return song;
          })
      );
    } catch (e) {
      console.error(e);
      console.error(parsedRecord);
      throw e;
    }

    if (currentLine >= MAX_LINES) {
      break;
    }
  }

  if (songs.length > 0) {
    await addSongsToTypesense(songs, typesense, songsCollection);
    console.log("ALL songs INDEXED ✅ !");
  }
})();
```

Finally, type `node loadData.js` into the terminal of that same project directory, and you have successfully imported 1 thousand songs documents into the search index.

![LoadData](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7g3qqkqc5veb7mkjk02s.png)
 
Now, you can start your application by typing `npm start` in the terminal of that project directory.

![npm start](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v4ftpt08kj99pmfi15tz.png)
 
We need to fix the fact that the application functions perfectly but no information, as well as data, is displayed in it.

![Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pst64o6j8mmsiwz1yxlm.png)

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.

```js
// app.js
const { instantsearch } = window;

import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "songsearch",
    nodes: [
      {
        host: "localhost",
        port: "8108",
        protocol: "http",
      },
    ],
  },
  additionalSearchParameters: {
    queryBy: "title,primary_artist_name,album_name", //quering by
  },
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: "songs",
});

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.

![Start Server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ingddm581pztolc6mx70.png)

![Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/huf9icp5s17vffvlkmy8.png)

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.

```jsx
templates: {
item: `
<h2>
{ "attribute": "title" }
</h2>
<div>
by
<a style="color:#FF6F82">{ "attribute": "primary_artist_name" }</a>
</div>
<div>
Album: <a style="color:#8661d1" >{ "attribute": "album_name" }</a>
</div>
<div class="text-muted small mb-2">
</div> `,
},
```

`app.js` file should resemble something like this.
 
```jsx
// App.js
const { instantsearch } = window;

import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: 'songsearch',
    nodes: [
      {
        host: 'localhost',
        port: '8108',
        protocol: 'http',
      },
    ],
  },
  additionalSearchParameters: {
    queryBy: 'title,primary_artist_name,album_name', //quering by
  },
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: 'songs',
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: '#searchbox',
  }),
  instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      item: `
      <h2>
        { "attribute": "title" }
      </h2>
      <div>
        by
        <a style="color:#FF6F82">{ "attribute": "primary_artist_name" }</a>
      </div>
      <div>
        Album: <a style="color:#8661d1" >{ "attribute": "album_name" }</a>
      </div>
      <div class="text-muted small mb-2">
        
      </div>
      `,
    },
  }),
  instantsearch.widgets.pagination({
    container: '#pagination',
  }),
]);

search.start();
```

After you’ve fixed it, re-run the application, and your app should now look like this.

![Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gz7q4665gdfrdzzwoghl.png)

Simply add the following code above the `#hits` widgets inside the `app.js` file to configure the `20 hits per page` or to configure to your own requirements.

```
instantsearch.widgets.configure({
    hitsPerPage: 20,
  }),
```

So now let’s try to add the facets, to do that we’ll add an instant search widgets .In the `instantsearch` library this is called `refinementlist` and finally we have to specify the container, in our case let’s call it a `#genreFilter`,`#artistFilter` and then the attribute we want to take, in our case let’s call it genres and `primary_artist_name` respectively. So, our final code should look like this.
 
 ```js
// app.js
const { instantsearch } = window;

import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter";

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: "songsearch",
    nodes: [
      {
        host: "localhost",
        port: "8108",
        protocol: "http",
      },
    ],
  },
  additionalSearchParameters: {
    queryBy: "title,primary_artist_name,album_name", //quering by
  },
});
const searchClient = typesenseInstantsearchAdapter.searchClient;

const search = instantsearch({
  searchClient,
  indexName: "songs",
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: "#searchbox",
  }),
  instantsearch.widgets.configure({
    hitsPerPage: 20,
  }),
  instantsearch.widgets.hits({
    container: "#hits",
    templates: {
      item: `
      <h2>
        { "attribute": "title" }
      </h2>
      <div>
        by
        <a style="color:#FF6F82">{ "attribute": "primary_artist_name" }</a>
      </div>
      <div>
        Album: <a style="color:#8661d1" >{ "attribute": "album_name" }</a>
      </div>
      <div class="text-muted small mb-2">
        
      </div>
      `,
    },
  }),
  instantsearch.widgets.refinementList({
    container: "#genreFilter",
    attribute: "genres",
    searchable: true,
    searchablePlaceholder: "Search genres",
    showMore: true,
  }),
  instantsearch.widgets.refinementList({
    container: "#artistFilter",
    attribute: "primary_artist_name",
    searchable: true,
    searchablePlaceholder: "Search Artist",
    showMore: true,
  }),
  instantsearch.widgets.pagination({
    container: "#pagination",
  }),
]);

search.start();
```

and our `index.html` file should look something like this.
 
```html
<!-- 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-songsearch</title>
  </head>

  <body>
    <header class="header">
      <h1 class="header-title">
        <a href="/">typesense-songsearch</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="genreFilter"></div>
          <br />
          <div id="artistFilter"></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>
```

This is how your application should look like.

![Final Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bg2x9wilhg8zosk0yno4.png)

Finally, we can add sorting functionality to the app by repeating the exact steps above: add the `sortBy` widget, specify the container with `#sort-songs`, specify the items with the label default with the value `songs`, and then create another label called `Recent songs` with the value `songs/sort/release_date:asc`, and another label called `Oldest songs` with the value `songs/sort/release_date:dsc`

```js
 instantsearch.widgets.sortBy({
    container: '#sort-songs',
    items: [
      { label: 'Default', value: `songs` },
      { label: 'Recent songs', value: `songs/sort/release_date:asc` },
      { label: 'Oldest songs', value: `songs/sort/release_date:desc` },
    ],
  }),
```

Finally, update and add `sort-songs` id before the `searchbox` inside `index.html` file.

```html
<!-- 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-songsearch</title>
  </head>

  <body>
    <header class="header">
      <h1 class="header-title">
        <a href="/">typesense-songsearch</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="genreFilter"></div>
          <br />
          <div id="artistFilter"></div>
        </div>

        <div class="search-panel__results">
          <div id="sort-songs"></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 finished product of our typesense-integrated songsearch application.

![Final Demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fw6q8pbqdyk1ip76lrfb.png)

The entire source code of the application can be found [here](https://github.com/aviyelverse/Article-project-demo/tree/main/songs-searchapp-typesense)

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 create a fullstack song search app using typesense from the ground. Join Aviyel’s community to learn more about the open source project, get tips on how to contribute, and join active dev groups.

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!