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 highlights

Search 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 candidateLimit if missing relevant results
  • Decrease candidateLimit if 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.

Related