Add vector search to a Lovable app with Qdrant

LovableQdrantVectorRAGAIDatabases

Lovable is a good fit for AI apps because the UI generation is fast and the iteration loop is tight. The point you hit eventually is that the app needs to store embeddings: for RAG over a knowledge base, for semantic search across user content, or for recommendations. Supabase has pgvector and that works at small scale. Past a few hundred thousand vectors it stops working well, and the answer is a dedicated vector database. This post wires Qdrant into a Lovable app in the same shape.

Contents

pgvector or Qdrant?

If your app already has a Postgres database (it probably does) and you have fewer than 100,000 vectors, use pgvector. The setup is just CREATE EXTENSION vector; and you query embeddings with the same SQL connection your app already uses. One database to back up, one connection pool, one bill.

Switch to Qdrant when one of these is true:

  • You have more than a few hundred thousand vectors and pgvector queries start taking over 200ms.
  • You want filtered vector search (find similar items where category = 'tech' and price < 50). pgvector technically supports this but the planner often picks the wrong index.
  • You want to update vectors at high rate without write contention against your transactional Postgres.
  • You want to keep your transactional Postgres small and predictable, and the vector data is large.

A typical Lovable AI app starts with pgvector and grows into Qdrant. The migration is straightforward because both store the same shape of data, just under different APIs. Vector databases compared in 2026 has the full breakdown if you want it.

The rest of this post assumes you have decided on Qdrant.

Create the database

Visit layerbase.com/create/qdrant, name it (something like lovable-rag), and click Sign in and create. Provisioning is about ten seconds.

The dashboard gives you two things you need:

text
QDRANT_URL=https://your-host.cloud.layerbase.dev
QDRANT_API_KEY=<long random string>

Qdrant on Layerbase is HTTPS-only and the API key goes in the api-key header. The official client handles both for you.

Add it to Lovable

Set both env vars in Lovable's settings panel:

text
QDRANT_URL=https://your-host.cloud.layerbase.dev
QDRANT_API_KEY=<your key>

Install the official client:

bash
pnpm add @qdrant/js-client-rest

Create a shared client in lib/qdrant.ts:

ts
import { QdrantClient } from '@qdrant/js-client-rest'

export const qdrant = new QdrantClient({
  url: process.env.QDRANT_URL!,
  apiKey: process.env.QDRANT_API_KEY!,
})

Insert and search

Before you can insert vectors you need a collection. Collections in Qdrant are like tables. Run this once during setup:

ts
import { qdrant } from '@/lib/qdrant'

await qdrant.createCollection('documents', {
  vectors: {
    size: 1536, // OpenAI text-embedding-3-small dimension
    distance: 'Cosine',
  },
})

size: 1536 matches OpenAI's text-embedding-3-small. If you use a different embedding model, set the size to whatever that model outputs.

Insert a document:

ts
import { qdrant } from '@/lib/qdrant'
import OpenAI from 'openai'

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

async function indexDocument(id: string, text: string) {
  const embeddingResponse = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  })
  const vector = embeddingResponse.data[0].embedding

  await qdrant.upsert('documents', {
    points: [
      {
        id,
        vector,
        payload: { text }, // store the source text alongside the vector
      },
    ],
  })
}

Search for similar documents:

ts
async function search(query: string, limit = 5) {
  const embeddingResponse = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: query,
  })
  const vector = embeddingResponse.data[0].embedding

  const results = await qdrant.search('documents', {
    vector,
    limit,
    with_payload: true,
  })

  return results.map((r) => ({
    id: r.id,
    score: r.score,
    text: r.payload?.text,
  }))
}

That is the whole core. Embed, upsert, embed query, search.

Use it for RAG

The standard retrieval-augmented-generation flow is: user asks a question, embed the question, find the top N similar documents, pass those documents to the LLM as context, return the answer.

ts
async function answerQuestion(question: string) {
  const docs = await search(question, 5)

  const context = docs.map((d) => d.text).join('\n\n')

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: `Answer the user's question based only on this context:\n\n${context}`,
      },
      { role: 'user', content: question },
    ],
  })

  return completion.choices[0].message.content
}

There are a hundred refinements you can make (re-ranking, hybrid search, chunk size tuning, query expansion). They all build on top of this shape. Start here, see if it answers your questions, then add complexity only where the answers are bad.

Filtered search

The thing pgvector struggles with that Qdrant handles cleanly is filtered vector search. If your documents have categories or user IDs and you want "find similar docs that belong to this user," Qdrant indexes the filter alongside the vector:

ts
const results = await qdrant.search('documents', {
  vector,
  limit: 5,
  filter: {
    must: [
      { key: 'user_id', match: { value: userId } },
    ],
  },
  with_payload: true,
})

Add user_id to the payload when you insert, and the filter just works. This is the most common reason teams move from pgvector to Qdrant in practice.

Local development with SpinDB

SpinDB runs Qdrant locally with one command:

bash
npm i -g spindb
spindb create lovable-rag-dev --engine qdrant --start
spindb url lovable-rag-dev

Put the printed URL in .env.local:

text
QDRANT_URL=http://localhost:6333
QDRANT_API_KEY=

Local Qdrant does not require an API key, so the env var can be empty. What is SpinDB? covers the rest, including how to copy collection schemas between local and cloud.

Wrapping up

The short version:

  1. Create a Qdrant at layerbase.com/create/qdrant
  2. Set QDRANT_URL and QDRANT_API_KEY in Lovable's env settings
  3. pnpm add @qdrant/js-client-rest and create a shared client
  4. Embed with OpenAI, upsert into Qdrant, search by query embedding
  5. Local dev with SpinDB

For most Lovable AI apps this is the whole vector layer. The collection grows, you stay on the same connection string, you do not pay per query.

Something not working?