Let's dive into the world of AI-powered web development. We're combining Nuxt Content, Supabase, and OpenAI's embeddings to craft an advanced AI content search system. Together, we'll step through a process that not only enhances search functionality but also brings a new level of intelligence to user interactions. By the end of this tutorial, you'll have a fully functional AI search bar integrated that you can integrate into your own application. In part 1 we will set up our environment together, so you can follow along easily. Part 2 will then contain the walk through of the search functionality.
But first: What are embeddings?
Embeddings are a fascinating and powerful concept in machine learning and natural language processing. At their core, embeddings are a way to convert complex, high-dimensional data like words, sentences, or even entire documents into a lower-dimensional space. This transformation makes it easier to process and analyze the data.
In simpler terms, embeddings transform the text of our blog articles and search queries into a format that a computer can understand and compare.
Set Up the Project:
Staring off, we set up our project environment with Nuxt and add key dependencies like OpenAI and Supabase.
npx nuxi@latest init blog-search-article.nosync -t content
cd blog-search-article.nosync
yarn add openai --dev @nuxtjs/supabase
yarn dev
Add supabase to the Nuxt modules and disable the default redirecting in nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
modules: ["@nuxt/content", "@nuxtjs/supabase"],
supabase: {
redirect: false,
},
});
Now, we turn to ensuring our project is both secure and well-organized. This includes adding a .env
file for API keys and tidying up the pages directory.
Securing API Keys:
We use a .env
file to keep our API keys confidential, safeguarding our project's integrity.
SUPABASE_URL="*****"
SUPABASE_KEY="*****"
OPENAI_API_KEY="*****“
Organizing Our Pages:
Let's set up the two main pages for our project. In the pages
folder delete any auto-generated files and create index.vue
as well as [article].vue
.
Index.vue
Index.vue will be the page where an overview of all blogs will be present. Later on, we will add a search bar to filter the blog articles.
<template>
<div>
<h1>Articles</h1>
<div v-if="articles">
<div v-for="(article, index) in filteredArticles" :key="index">
<article>
<h2 class="font-display text-2xl font-semibold text-neutral-950">
<NuxtLink :to="article._path">{{ article.title }}</NuxtLink>
</h2>
<p class="mt-6 max-w-2xl text-base text-neutral-600">
{{ article.description }}
</p>
</article>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const route = useRoute();
interface Article {
title: string;
description: string;
}
// fetch the articles from the content folder
const { data: article } = await useAsyncData(() => {
return queryContent<Article>(`${route.params.article}`).findOne();
});
// preparation for later
filteredArticles.value = articles.value || [];
</script>
[article].vue
The [article].vue
component is where individual articles come to life, dynamically rendering content based on URL parameters.
<template>
<div v-if="article">
<h1>{{ article.title }}</h1>
<ContentRenderer :value="article">
<div
class="[&>*]:mx-auto [&>*]:max-w-3xl [&>:first-child]:!mt-0 [&>:last-child]:!mb-0"
>
<ContentRendererMarkdown
class="prose prose-xl prose-h2:text-2xl prose-a:no-underline prose-headings:no-underline prose-h2:no-underline prose-a:font-semibold"
:value="article"
/>
</div>
</ContentRenderer>
</div>
<div v-else>
<h1>Article not found</h1>
</div>
<NuxtLink :to="localePath('/')">Go back to Home</NuxtLink>
</template>
<script setup lang="ts">
const route = useRoute();
interface Article {
title: string;
description: string;
}
const { data: article } = await useAsyncData(() => {
return queryContent<Article>(`/blog/${route.params.article}`).findOne();
});
</script>
Setting Up Supabase
Now we will set up our Supabase backend, so we can later implement the search functionality here. Enable the vector extension and create a new Table called „blog_chunk_embeddings“. This will be the table where we will store the embeddings for the blog articles.
create extension if not exists vector with schema public;
create table
public.blog_chunk_embeddings (
id bigint generated by default as identity,
embedding public.vector not null,
article_title text not null,
constraint blog_chunk_vectors_pkey primary key (id)
) tablespace pg_default;
For the simplicity of this guide I won’t explain row Leven security. You can add rules if you want to of course but if you don’t, make sure it is turned off.
Part 2
That's it for the setup. See you in Part 2 👋