Full-Text Search
Built-in FTS5 Search
Content module provides useSearchCollection, a zero-dependency composable powered by SQLite FTS5. It builds an inverted index from your content sections and provides instant ranked search with prefix matching and snippets.
<script setup lang="ts">
const { status, search } = useSearchCollection('docs')
const query = ref('')
const results = ref([])
watch(query, async (value) => {
results.value = value
? await search(value, { snippet: { columns: ['content'], around: 40 } })
: []
})
</script>
<template>
<UInput v-model="query" :disabled="status !== 'ready'" placeholder="Search..." />
<ul>
<li v-for="result in results" :key="result.id">
<NuxtLink :to="result.id">{{ result.title }}</NuxtLink>
<p v-if="result.snippets?.content" v-html="result.snippets.content" />
</li>
</ul>
</template>
External Libraries
You can also use queryCollectionSearchSections to get raw section data and pass it to search libraries like Fuse.js or MiniSearch. This approach gives you typo-tolerant fuzzy matching at the cost of loading all sections into memory.
MiniSearch
MiniSearch is a lightweight full-text search library with prefix matching, fuzzy search, and field boosting.
<script setup lang="ts">
import MiniSearch from 'minisearch'
const query = ref('')
const { data } = await useAsyncData('search', () => queryCollectionSearchSections('docs'))
const miniSearch = new MiniSearch({
fields: ['title', 'content'],
storeFields: ['title', 'content'],
searchOptions: {
prefix: true,
fuzzy: 0.2,
},
})
// Add data to the MiniSearch instance
miniSearch.addAll(toValue(data.value))
const result = computed(() => miniSearch.search(toValue(query)))
</script>
<template>
<UContainer class="p-4">
<UCard>
<UInput v-model="query" placeholder="Search..." />
<ul>
<li v-for="link of result" :key="link.id" class="mt-2">
<NuxtLink :to="link.id">{{ link.title }}</NuxtLink>
<p class="text-gray-500 text-xs">{{ link.content }}</p>
</li>
</ul>
</UCard>
</UContainer>
</template>
Fuse.js
Fuse.js is a fuzzy search library that uses the Bitap algorithm for typo-tolerant matching with configurable thresholds.
<script setup lang="ts">
import Fuse from 'fuse.js'
const query = ref('')
const { data } = await useAsyncData('search-data', () => queryCollectionSearchSections('docs'))
const fuse = new Fuse(data.value, {
keys: ['title', 'description']
})
const result = computed(() => fuse.search(toValue(query)).slice(0, 10))
</script>
<template>
<UContainer class="p-4">
<UCard>
<UInput v-model="query" placeholder="Search..." class="w-full" />
<ul>
<li v-for="link of result" :key="link.item.id" class="mt-2">
<UButton variant="ghost" class="w-full" :to="link.item.id">
{{ link.item.title }}
<span class="text-gray-500 text-xs">
{{ link.item.content?.slice(0, 100) }}...
</span>
</UButton>
</li>
</ul>
</UCard>
</UContainer>
</template>
Nuxt UI
Nuxt UI provides a ContentSearch component that is powered by fuse.js and accepts the output of queryCollectionSearchSections directly.
<script setup lang="ts">
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
const { data: files } = await useAsyncData('search', () => queryCollectionSearchSections('docs'))
const searchTerm = ref('')
</script>
<template>
<UContentSearch
v-model:search-term="searchTerm"
:files="files"
:navigation="navigation"
:fuse="{ resultLimit: 42 }"
/>
</template>