Building an AI-Powered Search Bar - A Guide with OpenAI, Supabase, and NuxtContent - Part 1

Building an AI-Powered Search Bar - A Guide with OpenAI, Supabase, and NuxtContent - Part 1

ByTobias Reich

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.

shell
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

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.

.env
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.

index.vue
<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>
index.vue
<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.

[article
<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>
[article
<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.

sql
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 👋

Weitere Artikel

Building a Chatbot with OpenAI API in TypeScript
Tutorial

Building a Chatbot with OpenAI API in TypeScript

Explore the creation of an AI chatbot using TypeScript and OpenAI's powerful API in this streamlined tutorial. Perfect for developers at any level, this guide covers setting up a TypeScript project, integrating the OpenAI API, and crafting a basic interactive chatbot. Step into the world of conversational AI with clear instructions, practical examples, and a focus on type-safe development, setting a solid foundation for more advanced applications.

Martin Kogut

Co-Founder / CEO

Machen wir Ihre digitale Zukunft möglich

Unsere Standorte

  • Berlin
    Bornstraße 32, 12163 Berlin, Germany
  • Szczecin
    ul. Herbowa 14, 71-427 Szczecin, Poland