Guides

Search API

Last updated by
Harlan Wilton
in chore: sync.

Overview

The Search API provides a simple HTTP endpoint for searching your indexed content. It supports both full-text (keyword) and semantic (meaning-based) search.

Endpoint: GET /api/search

Basic Usage

Search by keywords using Fuse.js (always available):

curl "https://your-site.com/api/search?q=nuxt+modules&type=fulltext"

Search by meaning using embeddings (requires embeddings enabled):

curl "https://your-site.com/api/search?q=how+to+build+modules&type=semantic"

Query Parameters

q (required)

The search query string.

# Single word
?q=nuxt

# Multiple words (URL encoded)
?q=nuxt+modules

# Phrase
?q="getting started"

type

Search type: fulltext or semantic

  • Default: Configured via search.defaultType (default: fulltext)
  • fulltext: Keyword-based search using Fuse.js
  • semantic: Meaning-based search using embeddings
# Keyword search
?q=modules&type=fulltext

# Semantic search
?q=modules&type=semantic

limit

Number of results to return (1-100).

  • Default: Configured via search.defaultLimit (default: 10)
# Return top 5 results
?q=nuxt&limit=5

# Return maximum results
?q=nuxt&limit=100

Response Format

Success Response

{
  "query": "nuxt modules",
  "type": "semantic",
  "provider": "transformers",
  "limit": 10,
  "results": [
    {
      "id": "doc-0",
      "route": "/guide/module-development",
      "title": "Module Development Guide",
      "description": "Learn how to build Nuxt modules",
      "score": 0.89,
      "excerpt": "...relevant excerpt from content..."
    }
  ],
  "total": 1
}

Response Fields

  • query: The search query
  • type: Search type used (fulltext or semantic)
  • provider: Search provider (fuse, transformers, or ollama)
  • limit: Number of results requested
  • results: Array of search results
  • total: Total number of results

Result Fields

  • id: Unique document identifier
  • route: Page route/URL
  • title: Page title
  • description: Page description
  • score: Relevance score (0-1, higher is better)
  • excerpt: Relevant excerpt from the content

Search Types

Full-Text Search

Uses Fuse.js for fuzzy keyword matching.

Features:

  • Fuzzy matching (handles typos)
  • Multi-field search (title, description, content, chunks)
  • Weighted scoring (title > description > content)
  • Always available (no embeddings needed)

Example:

curl "https://example.com/api/search?q=modul+devlopment&type=fulltext"

Even with typos ("modul", "devlopment"), it finds "Module Development"!

Configuration:

Fuse.js uses these settings:

  • threshold: 0.4 (lower = stricter matching)
  • ignoreLocation: true (match anywhere in text)
  • minMatchCharLength: 2

Semantic Search

Uses embeddings to understand meaning and context.

Features:

  • Finds semantically similar content
  • No exact keyword match needed
  • Understands synonyms and related concepts
  • Searches both document-level and chunk-level embeddings

Example:

curl "https://example.com/api/search?q=how+to+extend+nuxt&type=semantic"

Finds:

  • "Plugin Development" (related concept)
  • "Module Creation Guide" (similar meaning)
  • "Extending Nuxt" (exact match)

How It Works:

  1. Generate embedding for query
  2. Calculate cosine similarity with all documents
  3. Calculate cosine similarity with all chunks
  4. Use best score (max of document and chunk scores)
  5. Sort by score and return top results

Provider Selection

The API automatically selects the search provider:

// Semantic search requested
if (type === 'semantic') {
  if (embeddings.provider === 'transformers.js') {
    // Use Transformers.js
  }
  else if (embeddings.provider === 'ollama') {
    // Use Ollama API
  }
  else {
    // Fallback to Fuse.js
  }
}

// Full-text search
if (type === 'fulltext') {
  // Use Fuse.js
}

Excerpt Generation

The API automatically extracts relevant excerpts:

Full-Text (Fuse.js)

Extracts context around the matched text:

{
  "excerpt": "...to build Nuxt modules, you need to understand the module..."
}

Semantic (Transformers/Ollama)

Returns the most relevant chunk:

{
  "excerpt": "Module development involves creating reusable functionality..."
}

Error Responses

Missing Query

{
  "statusCode": 400,
  "message": "Query parameter 'q' is required"
}

Invalid Limit

{
  "statusCode": 400,
  "message": "Limit must be between 1 and 100"
}

Index Not Found

{
  "statusCode": 500,
  "message": "Search index not found. Make sure the site has been built."
}

Semantic Search Not Available

{
  "statusCode": 400,
  "message": "Semantic search not available. Use type=fulltext instead."
}

Performance

Cold Start

First request initializes the search provider:

  • Fuse.js: ~500ms (load index)
  • Transformers.js: ~2s (load index + model)
  • Ollama: ~1s (load index + test connection)

Warm Requests

Subsequent requests are much faster:

  • Full-text: <100ms
  • Semantic: <500ms

Memory Usage

  • Fuse.js: ~50MB
  • Transformers.js: ~200MB (model + index)
  • Ollama: ~50MB (index only)

JavaScript/TypeScript Usage

Fetch API

const response = await fetch(
  `/api/search?${new URLSearchParams({
    q: 'nuxt modules',
    type: 'semantic',
    limit: '10'
  })}`
)

const data = await response.json()
console.log(data.results)

Axios

import axios from 'axios'

const { data } = await axios.get('/api/search', {
  params: {
    q: 'nuxt modules',
    type: 'semantic',
    limit: 10
  }
})

console.log(data.results)

Composable (Nuxt)

<script setup>
const query = ref('')
const results = ref([])

async function search() {
  const response = await $fetch('/api/search', {
    params: {
      q: query.value,
      type: 'semantic',
      limit: 10
    }
  })
  results.value = response.results
}
</script>

<template>
  <div>
    <input v-model="query" @input="search">
    <div v-for="result in results" :key="result.id">
      <h3>{{ result.title }}</h3>
      <p>{{ result.excerpt }}</p>
    </div>
  </div>
</template>

Advanced Usage

Search UI Component

Create a reusable search component:

components/SiteSearch.vue
<script setup>
const query = ref('')
const results = ref([])
const loading = ref(false)
const searchType = ref('semantic')

const debouncedSearch = useDebounceFn(async () => {
  if (!query.value) {
    results.value = []
    return
  }

  loading.value = true
  try {
    const response = await $fetch('/api/search', {
      params: {
        q: query.value,
        type: searchType.value,
        limit: 10
      }
    })
    results.value = response.results
  }
  finally {
    loading.value = false
  }
}, 300)

watch(query, debouncedSearch)
</script>

<template>
  <div>
    <input
      v-model="query"
      placeholder="Search..."
      class="search-input"
    >

    <select v-model="searchType">
      <option value="semantic">
        Semantic
      </option>
      <option value="fulltext">
        Full-text
      </option>
    </select>

    <div v-if="loading">
      Searching...
    </div>

    <div v-for="result in results" :key="result.id" class="result">
      <NuxtLink :to="result.route">
        <h3>{{ result.title }}</h3>
        <p>{{ result.description }}</p>
        <p class="excerpt">
          {{ result.excerpt }}
        </p>
        <span class="score">Score: {{ result.score.toFixed(2) }}</span>
      </NuxtLink>
    </div>
  </div>
</template>

Combine full-text and semantic results:

async function hybridSearch(query: string) {
  const [fulltext, semantic] = await Promise.all([
    $fetch('/api/search', {
      params: { q: query, type: 'fulltext', limit: 20 }
    }),
    $fetch('/api/search', {
      params: { q: query, type: 'semantic', limit: 20 }
    })
  ])

  // Combine and deduplicate
  const seen = new Set()
  const combined = []

  for (const result of [...semantic.results, ...fulltext.results]) {
    if (!seen.has(result.id)) {
      seen.add(result.id)
      combined.push(result)
    }
  }

  // Sort by score
  combined.sort((a, b) => b.score - a.score)

  return combined.slice(0, 10)
}

Configuration

Configure search defaults in nuxt.config.ts:

export default defineNuxtConfig({
  aiSearch: {
    search: {
      defaultType: 'semantic', // Default search type
      defaultLimit: 10, // Default result limit
    }
  }
})

Best Practices

Query Optimization

  1. Keep queries focused: Shorter queries often work better
  2. Use semantic for concepts: "how to build modules" vs "module build"
  3. Use fulltext for exact matches: Product names, error codes

Result Display

  1. Show excerpts: Help users understand why a result matched
  2. Highlight search terms: In full-text results (Fuse.js provides indices)
  3. Display scores: Show relevance to users

Performance

  1. Debounce input: Wait 300ms before searching
  2. Limit results: Start with 10, load more on demand
  3. Cache results: Store recent searches client-side

Next Steps

Did this page help you?