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
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"
q (required)The search query string.
# Single word
?q=nuxt
# Multiple words (URL encoded)
?q=nuxt+modules
# Phrase
?q="getting started"
typeSearch type: fulltext or semantic
search.defaultType (default: fulltext)# Keyword search
?q=modules&type=fulltext
# Semantic search
?q=modules&type=semantic
limitNumber of results to return (1-100).
search.defaultLimit (default: 10)# Return top 5 results
?q=nuxt&limit=5
# Return maximum results
?q=nuxt&limit=100
{
"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
}
query: The search querytype: Search type used (fulltext or semantic)provider: Search provider (fuse, transformers, or ollama)limit: Number of results requestedresults: Array of search resultstotal: Total number of resultsid: Unique document identifierroute: Page route/URLtitle: Page titledescription: Page descriptionscore: Relevance score (0-1, higher is better)excerpt: Relevant excerpt from the contentUses Fuse.js for fuzzy keyword matching.
Features:
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: 2Uses embeddings to understand meaning and context.
Features:
Example:
curl "https://example.com/api/search?q=how+to+extend+nuxt&type=semantic"
Finds:
How It Works:
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
}
The API automatically extracts relevant excerpts:
Extracts context around the matched text:
{
"excerpt": "...to build Nuxt modules, you need to understand the module..."
}
Returns the most relevant chunk:
{
"excerpt": "Module development involves creating reusable functionality..."
}
{
"statusCode": 400,
"message": "Query parameter 'q' is required"
}
{
"statusCode": 400,
"message": "Limit must be between 1 and 100"
}
{
"statusCode": 500,
"message": "Search index not found. Make sure the site has been built."
}
{
"statusCode": 400,
"message": "Semantic search not available. Use type=fulltext instead."
}
First request initializes the search provider:
Subsequent requests are much faster:
const response = await fetch(
`/api/search?${new URLSearchParams({
q: 'nuxt modules',
type: 'semantic',
limit: '10'
})}`
)
const data = await response.json()
console.log(data.results)
import axios from 'axios'
const { data } = await axios.get('/api/search', {
params: {
q: 'nuxt modules',
type: 'semantic',
limit: 10
}
})
console.log(data.results)
<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>
Create a reusable search component:
<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)
}
Configure search defaults in nuxt.config.ts:
export default defineNuxtConfig({
aiSearch: {
search: {
defaultType: 'semantic', // Default search type
defaultLimit: 10, // Default result limit
}
}
})