Building Type-Safe APIs with Nuxt 3 and Prisma
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
You don't always need a dedicated search engine. Implement performant full-text search with PostgreSQL's built-in capabilities.
Mbeah Essilfie
April 26, 2026 at 06:04 AM
Elasticsearch is powerful but adds operational complexity. For most applications under 10M documents, PostgreSQL's built-in full-text search is more than sufficient.
-- Add a search vector column
ALTER TABLE posts ADD COLUMN search_vector tsvector;
-- Populate it with weighted content
UPDATE posts SET search_vector =
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(excerpt, '')), 'B') ||
setweight(to_tsvector('english', coalesce(content, '')), 'C');
-- Create a GIN index for fast lookups
CREATE INDEX idx_posts_search ON posts USING GIN (search_vector);
Weights (A > B > C > D) control relevance ranking — title matches score higher than body matches.
CREATE OR REPLACE FUNCTION update_post_search_vector()
RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(NEW.excerpt, '')), 'B') ||
setweight(to_tsvector('english', coalesce(NEW.content, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER posts_search_vector_update
BEFORE INSERT OR UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_post_search_vector();
-- Basic search
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('english', 'typescript & vue') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20;
-- With prefix matching (for autocomplete)
SELECT title
FROM posts
WHERE search_vector @@ to_tsquery('english', 'type:*')
LIMIT 5;
// server/api/search.get.ts
export default defineEventHandler(async (event) => {
const { q, page = 1, limit = 20 } = getQuery(event)
if (!q || typeof q !== 'string') return { data: [] }
// Convert user query to tsquery format
const searchTerms = q.trim().split(/\s+/).join(' & ')
const posts = await prisma.$queryRaw`
SELECT id, title, slug, excerpt,
ts_rank(search_vector, to_tsquery('english', ${searchTerms})) as rank,
ts_headline('english', content, to_tsquery('english', ${searchTerms}),
'StartSel=<mark>, StopSel=</mark>, MaxWords=50') as highlight
FROM posts
WHERE search_vector @@ to_tsquery('english', ${searchTerms})
AND status = 'published'
ORDER BY rank DESC
LIMIT ${limit}
OFFSET ${(page - 1) * limit}
`
return { data: posts }
})
ts_headline generates highlighted snippets showing where the match occurred — perfect for search result previews.For typo tolerance, add trigram similarity:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_posts_title_trgm ON posts USING GIN (title gin_trgm_ops);
-- Find posts with similar titles (typo-tolerant)
SELECT title, similarity(title, 'javscript tutoral') AS sim
FROM posts
WHERE similarity(title, 'javscript tutoral') > 0.3
ORDER BY sim DESC;
On a table with 1M rows:
For everything else, PostgreSQL's built-in search is surprisingly capable and zero extra infrastructure.
Fullstack Software Developer
Learn how to build end-to-end type-safe APIs using Nuxt 3 server routes and Prisma ORM for a seamless developer experience.
Go beyond basic utility classes and learn how to build cohesive, maintainable design systems with Tailwind CSS.
Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.
