Search API
Last updated:
The LumoSearch search() method accepts a query string and returns BM25F-ranked results with relevance scores, matched fields, and character-level highlight ranges. It supports exact-match filters, custom predicates, and configurable result limits — all executing in under 5ms on datasets up to 100K documents.
Basic Usage
import { LumoSearch } from '@lumosearch/search'
const docs = [
{ title: 'JavaScript Guide', category: 'tutorials' },
{ title: 'TypeScript Handbook', category: 'docs' },
{ title: 'React Patterns', category: 'tutorials' }
]
const search = new LumoSearch(docs, {
keys: [{ name: 'title', weight: 3 }]
})
const results = search.search('javascript')
// Returns ranked results with scores and highlightsSearch Options
Limit Results
Control how many results to return:
// Return top 5 results
const results = search.search('javascript', { limit: 5 })
// Return top 20 results
const results = search.search('javascript', { limit: 20 })Exact-Match Filters
Filter results by exact field values:
// Only return documents where category = 'tutorials'
const results = search.search('javascript', {
filters: {
category: 'tutorials'
}
})
// Multiple filters
const results = search.search('javascript', {
filters: {
category: 'tutorials',
published: true,
author: 'Jane Doe'
}
})Custom Predicates
Use custom filter functions for complex filtering logic:
// Filter by rating
const results = search.search('javascript', {
predicate: (doc) => doc.rating > 4.0
})
// Complex predicate
const results = search.search('javascript', {
predicate: (doc) => {
const isRecent = new Date(doc.publishedAt) > new Date('2024-01-01')
const isHighQuality = doc.rating > 4.5
const hasEnoughReviews = doc.reviewCount > 10
return isRecent && isHighQuality && hasEnoughReviews
}
})Combining Options
const results = search.search('javascript patterns', {
limit: 10,
filters: {
category: 'tutorials',
published: true
},
predicate: (doc) => doc.rating > 4.0 && doc.price < 50
})Result Structure
Each search result contains:
{
item: {
// Original document
title: 'JavaScript Patterns',
body: 'Design patterns for JavaScript...',
category: 'tutorials'
},
refIndex: 0, // Position in original collection
score: 0.94, // Relevance score (0-1)
matchedFields: ['title'], // Fields with matches
highlights: [
{
field: 'title',
value: 'JavaScript Patterns',
indices: [[0, 10]] // Character ranges
}
]
}Scoring and Relevance
Results are ranked by relevance score (0 to 1):
- 1.0 — Perfect match
- 0.8-0.9 — High relevance
- 0.5-0.7 — Medium relevance
- < 0.5 — Lower relevance
Scores incorporate BM25F ranking, field weights, exact match boosts, prefix boosts, and proximity scoring.
Working with Highlights
Use highlight ranges to show users where matches occurred:
const results = search.search('javascript patterns')
results.forEach(result => {
result.highlights.forEach(highlight => {
const { field, value, indices } = highlight
// Build HTML with <mark> tags
let html = value
let offset = 0
indices.forEach(([start, end]) => {
const before = html.slice(0, start + offset)
const match = html.slice(start + offset, end + offset)
const after = html.slice(end + offset)
html = before + '<mark>' + match + '</mark>' + after
offset += 13 // length of '<mark></mark>'
})
console.log(field, ':', html)
// title : <mark>JavaScript</mark> <mark>Patterns</mark>
})
})Common Patterns
Paginated Results
const PAGE_SIZE = 10
function searchWithPagination(query, page = 1) {
// Get more results than needed
const allResults = search.search(query, {
limit: page * PAGE_SIZE
})
// Slice for current page
const start = (page - 1) * PAGE_SIZE
const end = start + PAGE_SIZE
return allResults.slice(start, end)
}
const page1 = searchWithPagination('javascript', 1)
const page2 = searchWithPagination('javascript', 2)Search with Category Facets
function searchWithFacets(query, selectedCategory = null) {
// Get all results first
const allResults = search.search(query, { limit: 1000 })
// Build facet counts
const facets = {}
allResults.forEach(result => {
const cat = result.item.category
facets[cat] = (facets[cat] || 0) + 1
})
// Apply category filter if selected
const filtered = selectedCategory
? allResults.filter(r => r.item.category === selectedCategory)
: allResults
return {
results: filtered.slice(0, 10),
facets
}
}Minimum Score Threshold
const results = search.search('javascript')
.filter(result => result.score > 0.5)
// Or with predicate
const results = search.search('javascript', {
limit: 100
}).filter(result => result.score > 0.7)Performance Tips
- Use filters before predicates when possible (filters are faster)
- Increase
candidateLimitif missing relevant results - Decrease
candidateLimitif queries are too slow - Cache search instances — indexing is expensive
- Use web workers for large datasets
Frequently Asked Questions
How do I filter search results in LumoSearch?
Use the filters option to match exact field values (e.g., { filters: { category: 'tutorials' } }), or use the predicate option for custom logic (e.g., predicate: (doc) => doc.rating > 4.0). Filters are faster than predicates and should be preferred when possible.
What does the search result score mean?
Each result includes a score from 0 to 1 representing relevance. A score of 1.0 is a perfect match. Scores incorporate BM25F ranking, field weights, exact match boosts (2x), prefix boosts (1.25x), and phrase proximity. Results are returned sorted by score descending.
How do I implement pagination with LumoSearch?
LumoSearch doesn't have built-in offset pagination. Instead, request more results via the limit option (e.g., limit: page * pageSize) and slice the array for the current page. For most use cases, showing top-10 ranked results is more effective than pagination.
How do I highlight matched text in search results?
Each result includes a highlights array with field name, full value, and character-level index ranges. Use these ranges to wrap matched text in <mark> tags or any other styling. LumoSearch provides character-precise ranges, not word-level highlights.