Autocomplete
Last updated:
LumoSearch autocomplete provides instant search-as-you-type suggestions using prefix-aware indexes. It returns relevance-ranked results as the user types, handling partial words and typos via the same BM25F scoring pipeline as full search — with sub-millisecond response times for up to 100K documents.
Basic Usage
import { LumoSearch } from '@lumosearch/search'
const docs = [
{ title: 'JavaScript Fundamentals' },
{ title: 'Java Programming' },
{ title: 'Python Basics' }
]
const search = new LumoSearch(docs, {
keys: ['title']
})
// As user types "jav"
const suggestions = search.autocomplete('jav', { limit: 5 })
// Returns: ["JavaScript Fundamentals", "Java Programming"]How It Works
Autocomplete uses the same scoring pipeline as search(), but optimized for prefix matching:
- Prefix indexes enable fast retrieval of terms starting with the query
- Token indexes catch exact matches
- Trigram indexes handle fuzzy prefix matching
- Results are ranked by relevance, not just alphabetical order
React Integration
import { useState, useMemo } from 'react'
import { LumoSearch } from '@lumosearch/search'
function SearchBox({ docs }) {
const [query, setQuery] = useState('')
const search = useMemo(
() => new LumoSearch(docs, {
keys: [{ name: 'title', weight: 3 }]
}),
[docs]
)
const suggestions = query.length > 0
? search.autocomplete(query, { limit: 10 })
: []
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{suggestions.length > 0 && (
<ul>
{suggestions.map((result) => (
<li key={result.refIndex}>
{result.item.title}
</li>
))}
</ul>
)}
</div>
)
}Debouncing
For better performance, debounce autocomplete queries:
import { useState, useMemo, useEffect } from 'react'
import { LumoSearch } from '@lumosearch/search'
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
function SearchBox({ docs }) {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 150)
const search = useMemo(
() => new LumoSearch(docs, { keys: ['title'] }),
[docs]
)
const suggestions = debouncedQuery.length > 0
? search.autocomplete(debouncedQuery, { limit: 10 })
: []
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{suggestions.length > 0 && (
<ul>
{suggestions.map((result) => (
<li key={result.refIndex}>
{result.item.title}
</li>
))}
</ul>
)}
</div>
)
}Highlighting Matches
function HighlightedSuggestion({ result }) {
const highlight = result.highlights[0]
if (!highlight) return <span>{result.item.title}</span>
const { value, indices } = highlight
const parts = []
let lastIndex = 0
indices.forEach(([start, end]) => {
// Text before match
if (start > lastIndex) {
parts.push(
<span key={lastIndex}>{value.slice(lastIndex, start)}</span>
)
}
// Matched text
parts.push(
<strong key={start}>{value.slice(start, end)}</strong>
)
lastIndex = end
})
// Text after last match
if (lastIndex < value.length) {
parts.push(<span key={lastIndex}>{value.slice(lastIndex)}</span>)
}
return <span>{parts}</span>
}
function SearchBox({ docs }) {
const [query, setQuery] = useState('')
const search = useMemo(
() => new LumoSearch(docs, { keys: ['title'] }),
[docs]
)
const suggestions = query.length > 0
? search.autocomplete(query, { limit: 10 })
: []
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{suggestions.length > 0 && (
<ul>
{suggestions.map((result) => (
<li key={result.refIndex}>
<HighlightedSuggestion result={result} />
</li>
))}
</ul>
)}
</div>
)
}Keyboard Navigation
function SearchBox({ docs, onSelect }) {
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(-1)
const search = useMemo(
() => new LumoSearch(docs, { keys: ['title'] }),
[docs]
)
const suggestions = query.length > 0
? search.autocomplete(query, { limit: 10 })
: []
const handleKeyDown = (e) => {
if (suggestions.length === 0) return
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex((i) =>
i < suggestions.length - 1 ? i + 1 : i
)
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex((i) => (i > 0 ? i - 1 : -1))
break
case 'Enter':
e.preventDefault()
if (selectedIndex >= 0) {
onSelect(suggestions[selectedIndex].item)
setQuery('')
setSelectedIndex(-1)
}
break
case 'Escape':
setQuery('')
setSelectedIndex(-1)
break
}
}
return (
<div>
<input
type="search"
value={query}
onChange={(e) => {
setQuery(e.target.value)
setSelectedIndex(-1)
}}
onKeyDown={handleKeyDown}
placeholder="Search..."
/>
{suggestions.length > 0 && (
<ul>
{suggestions.map((result, index) => (
<li
key={result.refIndex}
onClick={() => onSelect(result.item)}
className={selectedIndex === index ? 'selected' : ''}
>
{result.item.title}
</li>
))}
</ul>
)}
</div>
)
}Grouped Suggestions
Group autocomplete results by category:
function SearchBox({ docs }) {
const [query, setQuery] = useState('')
const search = useMemo(
() => new LumoSearch(docs, {
keys: ['title', 'category']
}),
[docs]
)
const suggestions = query.length > 0
? search.autocomplete(query, { limit: 20 })
: []
// Group by category
const grouped = suggestions.reduce((acc, result) => {
const cat = result.item.category
if (!acc[cat]) acc[cat] = []
acc[cat].push(result)
return acc
}, {})
return (
<div>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{Object.keys(grouped).length > 0 && (
<div>
{Object.entries(grouped).map(([category, results]) => (
<div key={category}>
<h3>{category}</h3>
<ul>
{results.map((result) => (
<li key={result.refIndex}>
{result.item.title}
</li>
))}
</ul>
</div>
))}
</div>
)}
</div>
)
}Performance Tips
- Always debounce user input (150-300ms is a good range)
- Cache the LumoSearch instance with useMemo in React
- Keep
limitlow (5-10 suggestions) - Consider showing suggestions only after 2-3 characters
- Use a web worker for large datasets
Minimum Query Length
const MIN_QUERY_LENGTH = 2
const suggestions = query.length >= MIN_QUERY_LENGTH
? search.autocomplete(query, { limit: 10 })
: []Frequently Asked Questions
How does LumoSearch autocomplete work?
LumoSearch autocomplete uses prefix indexes to match partial words as the user types. Typing 'jav' instantly returns 'JavaScript Fundamentals' and 'Java Programming'. Results are ranked by BM25F relevance, not alphabetically, so the most relevant suggestions appear first.
Should I debounce autocomplete queries?
Yes. Debouncing with 150-300ms delay prevents unnecessary computation on every keystroke. In React, combine useDebounce with useMemo to cache the search instance and only recompute suggestions after the user pauses typing.
How many autocomplete suggestions should I show?
Show 5-10 suggestions for the best user experience. More suggestions increase cognitive load without helping users find what they need faster. Set limit: 5 or limit: 10 in the autocomplete options.