Steps
1. Install the required packages
First, install FlexSearch and Material-UI if you haven't already.
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled flexsearch2. Set up FlexSearch
Create a FlexSearch index for documents. You can configure this index according to your needs, but here’s a basic example that supports autocompletion.
// utils/articleSearchIndex.ts
import leven from "leven";
import { Article } from "../components/Search";
import FlexSearch from "flexsearch";
// Create a FlexSearch index
const index = new FlexSearch.Document({
document: {
id: "id",
index: ["title", "content"], // Fields to index for search
store: ["id", "title", "content"], // Fields to retrieve in results
},
language: "fr", // Language support for tokenization
tokenize: "forward", // Tokenization for autocompletion
encoder: "simple", // Simplified text encoding for search
threshold: 3, // Fuzzy search tolerance
resolution: 5, // Higher resolution for more accurate results
});
// Add data to the index
export const addDataToIndex = (data: Article[]) => {
data.forEach((item) => {
index.add(item);
});
};
export const searchIndex = async (query: string) => {
try {
// BUG: puck does not exists in flexseach types
const result = await (index as any).searchAsync(query, {
index: ["title", "content"],
suggest: true,
pluck: "title", // we only need the title
enrich: true, // enrich the result format
});
return result;
} catch (error) {
console.error("Error during FlexSearch tokenization:", error);
return [];
}
};
3. Material-UI Autocomplete component
Now, let's create a search bar using the Autocomplete component from Material-UI. The results will be fetched dynamically based on user input from FlexSearch.
// components/Search.tsx
import { useState, useEffect, ChangeEvent } from "react";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import { addDataToIndex, searchIndex } from "../utils/articleSearchIndex";
// Define Article interface
export interface Article {
id: number;
title: string;
content: string;
}
// Sample data to populate the index
const articles: Article[] = [
{
id: 1,
title: "How to Use FlexSearch",
content: "A guide on using FlexSearch for document search.",
},
{
id: 2,
title: "Building Autocomplete",
content: "Steps to implement autocomplete with FlexSearch.",
},
{
id: 3,
title: "React with FlexSearch",
content: "Integrating FlexSearch into a React app.",
},
];
const Search = () => {
const [query, setQuery] = useState<string>("");
const [suggestions, setSuggestions] = useState<Article[]>([]);
// Populate the FlexSearch index on component mount
useEffect(() => {
addDataToIndex(articles);
}, []);
// Handle search input changes
const handleSearch = (_: ChangeEvent<{}>, value: string) => {
setQuery(value);
const searchResults = searchIndex(value);
const formattedResults = searchResults.map((result) => result.doc);
setSuggestions(formattedResults);
};
return (
<Autocomplete
freeSolo
options={suggestions}
filterOptions={x => x}
getOptionLabel={(option: Article | string) =>
typeof option === "string" ? option : option.title
}
inputValue={query}
onInputChange={handleSearch}
renderInput={(params) => (
<TextField {...params} label="Search Articles" variant="outlined" />
)}
/>
);
};
export default Search;
4. Use the Component in Your App
Finally, use the Search component in your main application file:
// App.js
import React from "react";
import Search from "./components/Search";
const App = () => {
return (
<div>
<h1>Flex search</h1>
<Search/>
</div>
);
};
export default App;5. Optional Enhancements
Debouncing Input: To prevent too many search requests as the user types, debounce the search function to trigger only after a short delay:
npm install lodash// components/Search.tsx
import { debounce } from 'lodash';
import { useCallback } from "react";
// Debounced search handler
const handleDebouncedSearch = useCallback(
debounce(async (value: string) => {
const searchResults = await searchIndex(value);
setSuggestions(searchResults);
}, 500),
[]
);
const handleSearch = (_: ChangeEvent<{}>, value: string) => {
setQuery(value);
handleDebouncedSearch(value);
};
For more accurate autocorrection: You could dynamically adjust the allowed Levenshtein distance based on the length of the query, e.g., shorter queries have a smaller allowable distance to avoid excessive false positives.
// utils/articleSearchIndex.ts import leven from "leven"; /** * Suggest corrections using Levenshtein distance * e.g: fexsearch => flexsearch */ export const suggestIndex = (query: string, data: Article[]) => { const suggestions = data.filter((item: Article) => { const maxDistance = item.title.length - leven(query.toLocaleLowerCase(), item.title.toLocaleLowerCase()); return maxDistance >= 6; }); return suggestions; };
If no results are returned from the FlexSearch query, fall back to autocorrection using Levenshtein distance:
const handleDebouncedSearch = useCallback(
debounce(async (value: string) => {
let searchResults = await searchIndex(value);
if (searchResults.length <= 0) {
searchResults = suggestIndex(value, articles);
} else {
searchResults = searchResults.map((result) => result.doc);
}
setSuggestions(searchResults);
}, 500),
[]
);Explanation
FlexSearch Index: We created a FlexSearch index for the documents, indexing both
titleandcontent. You can extend this based on your document structure.Autocomplete Component: The
Autocompletecomponent from Material-UI is used to show search suggestions as the user types.Search Input Handling:
handleSearchfetches search results from FlexSearch for the dropdown.Options Rendering: The search results (matching documents) are passed to the
Autocompletecomponent asoptions. We usegetOptionLabelto define what will be shown in the dropdown.
Links:
You can try this tutorial directly on CodeSandbox via this link: FlexSearch Autocompletion.
Or check the source code on GitHub.

