AI-Powered Search for Vue and Nuxt Applications
Add AI-powered search to Vue/Nuxt apps in minutes. One HTTP request, a small composable you own, Nuxt server routes for SSR.
The Vue ecosystem lacks a cohesive search solution. You're forced to choose between abandoned libraries, vendor lock-in, or building everything yourself.
The Vue ecosystem lacks a first-party search solution. You're left stitching together community libraries or building from scratch.
Vue InstantSearch ties your search UI to Algolia's ecosystem, with no standalone option.
Manual API integration with Meilisearch requires writing your own reactive composables, state management, and error handling.
No SDK needed — a reactive composable you own and Nuxt 3 server routes for SSR, rendered with your own components.
A ~30-line useSearchJet composable (ref + watch + fetch) with full Vue 3 reactivity. Results update automatically when the query changes.
A server/api/search.get.ts route keeps your key in runtimeConfig. Pair it with useAsyncData to pre-fetch results server-side for SEO-critical pages.
Results are plain JSON (id, title, content, url, excerpt), so you render them with your own components, slots, and styles.
No SDK types to learn. Describe the response (results, total, limit, offset) with a small interface and Volar infers it in templates.
Works with your state management. Call the search API from Pinia actions or Vuex modules, or use your composable inside a setup store.
Works with @nuxtjs/i18n and vue-i18n. Build a multi-language search UI with your own translated strings.
Two approaches — Nuxt 3 server route + page, or a reusable composable
// server/api/search.get.ts — runs on the server, so the key stays private
// nuxt.config.ts: runtimeConfig: { searchjetSearchKey: '' } (env: NUXT_SEARCHJET_SEARCH_KEY)
export default defineEventHandler(async (event) => {
const { q } = getQuery(event);
if (!q) return { results: [], total: 0 };
const { searchjetSearchKey } = useRuntimeConfig(event);
return $fetch('https://app.searchjetengine.com/api/v1/search', {
query: { q, limit: 10 },
headers: { Authorization: `Bearer ${searchjetSearchKey}` },
});
});
<!-- pages/search.vue -->
<script setup>
const route = useRoute();
const query = ref(String(route.query.q ?? ''));
// Rendered on the server on first load, re-fetched when query changes
const { data, status } = await useAsyncData(
'search',
() => $fetch('/api/search', { query: { q: query.value } }),
{ watch: [query] }
);
</script>
<template>
<div class="max-w-4xl mx-auto py-12 px-6">
<!-- .lazy = search on Enter/blur; use a debounced ref for search-as-you-type -->
<input
v-model.lazy="query"
type="search"
placeholder="Search docs..."
class="w-full p-4 border-2 border-gray-200 rounded-xl text-lg focus:border-blue-500 focus:outline-none"
/>
<div v-if="status === 'pending'" class="mt-8 text-center text-gray-500">
Searching...
</div>
<div v-else class="mt-8 space-y-4">
<p v-if="query">{{ data?.total ?? 0 }} results</p>
<article
v-for="hit in data?.results ?? []"
:key="hit.id"
class="p-6 border border-gray-200 rounded-xl hover:border-blue-300"
>
<h3 class="text-xl font-semibold"><a :href="hit.url">{{ hit.title }}</a></h3>
<p class="mt-2 text-gray-600">{{ hit.excerpt }}</p>
</article>
</div>
</div>
</template>// composables/useSearchJet.js — ~30 lines you write yourself (not a package)
import { ref, watch, toValue } from 'vue';
const API = 'https://app.searchjetengine.com/api/v1/search';
const SEARCH_KEY = 'your_search_api_key'; // search-only key; never the private key
export function useSearchJet(query, filters) {
const hits = ref([]);
const total = ref(0);
const isSearching = ref(false);
let controller, timer;
watch(() => [toValue(query), toValue(filters)], ([q, f]) => {
clearTimeout(timer);
controller?.abort();
if (!q) { hits.value = []; total.value = 0; return; }
timer = setTimeout(async () => {
controller = new AbortController();
isSearching.value = true;
const params = new URLSearchParams({ q, limit: '20' });
if (f) params.set('filters', f);
try {
const res = await fetch(`${API}?${params}`, {
headers: { Authorization: `Bearer ${SEARCH_KEY}` },
signal: controller.signal,
});
const data = await res.json();
hits.value = data.results ?? []; // results is the array of hits
total.value = data.total ?? 0;
} catch {} // aborted or network error
isSearching.value = false;
}, 200); // debounce
}, { immediate: true });
return { hits, total, isSearching };
}
// composables/useProductSearch.js
import { ref, computed } from 'vue';
import { useSearchJet } from './useSearchJet';
export function useProductSearch() {
const query = ref('');
const category = ref('');
// "filters" only works on attributes marked filterable in your index
// settings; otherwise the API returns error code INVALID_FILTER.
const filters = computed(() =>
category.value ? `category = "${category.value}"` : ''
);
const { hits, total, isSearching } = useSearchJet(query, filters);
// price and category are custom fields you indexed
const products = computed(() =>
hits.value.map((hit) => ({
...hit,
formattedPrice: new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(hit.price),
}))
);
return { query, category, products, total, isSearching };
}Need a complete starter project?
View GitHub ExamplesIllustrative example of a Vue or Nuxt app adding hosted search. Your results will vary.
| Metric | Before | After | Impact |
|---|---|---|---|
| Development Time | Weeks | Days | Much faster to launch |
| Bundle Size | 45KB+ (Algolia) | None — built-in fetch | No SDK to ship |
| Search Latency | Slower on large datasets | <150ms | Faster results |
| Monthly Cost | Usage-based (Algolia) | $0-19 (Free or Pro) | A fraction of the cost |
Everything you need to know about SearchJet for Vue/Nuxt
Join developers building exceptional search experiences with SearchJet
Already have an account? Log in