Add vector search to a Lovable app with Qdrant
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?
- Create the database
- Add it to Lovable
- Insert and search
- Use it for RAG
- Local development with SpinDB
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:
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:
QDRANT_URL=https://your-host.cloud.layerbase.dev
QDRANT_API_KEY=<your key>Install the official client:
pnpm add @qdrant/js-client-restCreate a shared client in lib/qdrant.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:
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:
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:
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.
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:
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:
npm i -g spindb
spindb create lovable-rag-dev --engine qdrant --start
spindb url lovable-rag-devPut the printed URL in .env.local:
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:
- Create a Qdrant at layerbase.com/create/qdrant
- Set
QDRANT_URLandQDRANT_API_KEYin Lovable's env settings pnpm add @qdrant/js-client-restand create a shared client- Embed with OpenAI, upsert into Qdrant, search by query embedding
- 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.