Building a search UI
This example demonstrates how to build a basic search UI using Boox. We'll create a simple HTML page with a search box and display the search results dynamically.
Project setup
Create a new Vanilla project using Vite with TypeScript template:
npm create vite@latest boox-vanilla -- --template vanilla-ts
BASH
Now run:
cd boox-vanilla && npm install
BASH
Install Boox and other dependencies:
npm install -D boox debounce metaphone stemmer stopword @types/stopword
BASH
File structure
boox-vanilla
├── src
│ └── main.tsx
├── index.html
└── vite.config.ts
TEXT
Did you know?
You can remove unnecessary files from the default Vite installation.
Code snippets
Kindly replicate the code snippets provided below:
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Basic usage - Boox</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
</head>
<body class="vstack min-vh-100 bg-body-secondary">
<main
class="mx-auto my-4 bg-body w-100 rounded-3 shadow"
style="max-width: 30rem"
>
<form id="search-form" class="p-3" role="search">
<div class="form-group">
<input
type="search"
class="form-control form-control-lg"
id="search-query"
placeholder="Search (e.g. pikachu, water pwer)"
aria-label="Search pokemons"
autofocus
/>
<div class="form-text">
<strong>Note:</strong>
the
<code class="bg-body-secondary">pwer</code>
demonstrates a typing error for
<code class="bg-body-secondary">power</code>
.
</div>
</div>
</form>
<p id="results-length" class="d-none px-3 text-muted"></p>
<div
id="results"
class="list-group list-group-flush overflow-auto rounded-3 d-none"
></div>
</main>
<footer class="container mt-auto py-3">
<p class="d-flex justify-content-center text-muted">
Search by
<a
class="icon-link ms-2"
href="https://github.com/bent10/boox"
target="_blank"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 180 180"
height="28"
aria-hidden="true"
>
<g fill="none" fill-rule="nonzero">
<path
fill="currentColor"
d="M111.223 75.983 91.937 66.34a3.214 3.214 0 0 0-2.877 0l-19.286 9.643A3.214 3.214 0 0 0 68 78.859v22.5c0 1.219.688 2.333 1.778 2.877l19.285 9.643a3.214 3.214 0 0 0 2.874 0l19.286-9.643A3.214 3.214 0 0 0 113 101.36v-22.5a3.214 3.214 0 0 0-1.777-2.876ZM90.5 72.81l12.099 6.05L90.5 84.908l-12.099-6.05L90.5 72.81ZM74.429 84.06l12.857 6.429v15.313l-12.857-6.429V84.06Zm19.285 21.742V90.489l12.857-6.429v15.313l-12.857 6.429Z"
></path>
<path
fill="var(--bs-secondary-color)"
d="M180 90c0-19.343-18-36.203-44.692-45.307C126.203 18 109.343 0 90 0 61.005 0 37.5 40.297 37.5 90c-.01 9.195.825 18.371 2.498 27.412C24.323 110.063 15 99.75 15 90a23.678 23.678 0 0 1 8.288-16.335A154.035 154.035 0 0 1 27 52.552C10.343 62.079 0 75.33 0 90c0 19.343 18 36.203 44.693 45.308C53.797 162 70.657 180 90 180c14.67 0 27.922-10.343 37.448-27a154.035 154.035 0 0 1-21.113 3.683A23.677 23.677 0 0 1 90 165c-9.75 0-20.063-9.322-27.413-24.998A149.798 149.798 0 0 0 90 142.5c49.702 0 90-23.505 90-52.5ZM90 15c9.75 0 20.063 9.323 27.412 24.998A149.798 149.798 0 0 0 90 37.5c-3.06 0-6.08.09-9.06.27a64.89 64.89 0 0 0-7.77 15.75A140.557 140.557 0 0 1 90 52.5a131.4 131.4 0 0 1 33.345 4.155A131.4 131.4 0 0 1 127.5 90c0 5.625-.339 11.245-1.013 16.83a64.89 64.89 0 0 0 15.75-7.77c.175-2.985.263-6.005.263-9.06.01-9.195-.825-18.371-2.498-27.413C155.678 69.938 165 80.25 165 90c0 17.737-30.795 37.5-75 37.5a131.4 131.4 0 0 1-33.345-4.155A131.4 131.4 0 0 1 52.5 90c0-44.205 19.763-75 37.5-75Z"
></path>
</g>
</svg>
<span class="fw-bold text-body">Boox</span>
</a>
</p>
</footer>
<script type="module" src="src/main.ts"></script>
</body>
</html>
HTML
import Boox, { type SearchResult } from 'boox'
import debounce from 'debounce'
import { metaphone } from 'metaphone'
import { stemmer } from 'stemmer'
import { removeStopwords } from 'stopword'
/**
* Interface representing a Pokemon data object.
*/
interface Pokemon {
id: string
image_url: string
caption: string
name: string
hp: number
set_name: string
}
const searchQueryInput =
document.querySelector<HTMLInputElement>('#search-query')
const resultsList = document.querySelector<HTMLDivElement>('#results')
const resultsLength =
document.querySelector<HTMLParagraphElement>('#results-length')
/**
* The Boox instance for searching Pokemon data.
*/
const boox = new Boox<Pokemon>({
features: ['name', 'caption', 'set_name'],
attributes: ['hp', 'image_url'],
modelOptions: {
tokenizer(input) {
// Split into words and remove stopwords
return removeStopwords(Array.from(input.match(/\b\w+\b/g) || []))
},
// Use the Porter stemmer
stemmer: stemmer,
// Use Double Metaphone for phonetic encoding
phonetic: metaphone
}
})
/**
* Fetches Pokemon data from the specified URL.
*
* @returns A promise that resolves to an array of Pokemon objects.
*/
async function fetchData(): Promise<Pokemon[]> {
try {
const response = await fetch(
'https://stilearning.com/boox/demo/datasets/pokemon-100r.json',
{ cache: 'default' }
)
return await response.json()
} catch (error) {
throw error
}
}
fetchData().then(async pokemons => {
await boox.addDocuments(pokemons)
})
/**
* Performs a search with the current query and updates the UI.
*/
const performSearch = debounce(async () => {
const query = searchQueryInput?.value || ''
const results = await boox.search(query)
updateUI(results)
}, 300)
/**
* Updates the UI with the given search results.
*
* @param results - The array of SearchResult objects.
*/
function updateUI(results: SearchResult<Pokemon>[]) {
resultsList?.classList.toggle('d-none', !results.length)
if (resultsLength) {
resultsLength.classList.toggle('d-none', !results.length)
resultsLength.textContent = results.length
? `Found ${results.length} of ${Object.keys(boox.currentState.documents).length} pokemons`
: ''
}
displayResults(results)
}
/**
* Displays the search results in the results list.
*
* @param results - The array of SearchResult objects.
*/
function displayResults(results: SearchResult<Pokemon>[]) {
if (!resultsList) return
resultsList.innerHTML = ''
results.forEach(result => {
const { image_url, name: image_alt, hp } = result.attributes
const { text: name } = result.context('name')
const { text: set_name } = result.context('set_name')
const { text: caption } = result.context('caption', 80)
const resultElement = document.createElement('a')
resultElement.classList.add(
'list-group-item',
'list-group-item-action',
'd-flex',
'gap-3',
'py-3'
)
resultElement.href = String(image_url)
resultElement.target = '_blank'
resultElement.innerHTML = `<img loading='lazy' src="${image_url}" alt="${image_alt}" width="32" height="32" class="rounded-circle flex-shrink-0">
<div class="d-flex gap-2 w-100 justify-content-between">
<div>
<h6 class="mb-1">${name} – ${set_name}</h6>
<p class="mb-0 opacity-75">${caption}...</p>
</div>
<small class="opacity-50 text-nowrap">HP ${hp}</small>
</div>`
resultsList.appendChild(resultElement)
})
}
searchQueryInput?.addEventListener('input', performSearch)
TS
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
external: ['os']
}
}
})
TS
Running the app
1. Start the development server:
npm run dev
BASH
Open http://localhost:5173 in your browser to see the search app in action.
2. Build for Production:
npm run build
BASH
Heads up
Remember to consult the documentation for Boox and other libraries for more advanced usage and configuration options.