Are you struggling with slow and inefficient searches in your database? Do you want to provide your users with lightning-fast, relevant search results? Look no further! In this two-part tutorial, we’ll dive into the world of full-text search using PostgreSQL, and then integrate it with Next.js and Prisma.
In Part 1, we’ll cover:
The basics of full-text search and its advantages over traditional LIKE queries
How to set up and use PostgreSQL’s native full-text search capabilities
Integrating full-text search with Next.js
Using Prisma with PostgreSQL full-text search
By the end of this part, you’ll have a solid foundation in implementing powerful search features in your applications.
1. What is Full-Text Search?
Imagine you have a huge library of books, and you want to find all the books that mention “time travel.” You could flip through each book, page by page, but that would take forever! Full-text search is like having a super-smart assistant who can instantly find all the relevant books for you.
In database terms, full-text search allows you to search through large amounts of text quickly and efficiently. It goes beyond simple pattern matching (like the LIKE operator in SQL) and understands the meaning of words, their variations, and even their importance in the text.
Comparison with Traditional LIKE Queries
Traditional LIKE queries in SQL are limited in several ways:
Performance: LIKE queries with wildcards (e.g., WHERE title LIKE '%time travel%') can’t use indexes effectively, leading to full table scans. This becomes increasingly slow as your dataset grows.
Flexibility: LIKE queries don’t handle word variations or synonyms. Searching for “run” won’t find “running” or “ran”.
Relevance: LIKE queries can’t rank results based on relevance. They simply return all matches in no particular order.
Full-text search addresses these limitations:
Performance: Full-text search uses specialized indexes, allowing for fast searches even on large datasets.
Flexibility: It understands word variations and can be configured to understand synonyms.
Relevance: Results can be ranked based on how well they match the search query.
Performance Advantages for Large Datasets
For large datasets, the performance difference between LIKE queries and full-text search can be dramatic:
A LIKE query might take seconds or even minutes to search through millions of rows.
A properly indexed full-text search can return results in milliseconds, regardless of the dataset size.
This performance advantage becomes crucial for applications with large amounts of text data or those requiring real-time search capabilities.
PostgreSQL, a powerful open-source database, comes with built-in full-text search capabilities. It can:
Break down text into words (called tokens)
Remove common words that don’t help in searching (like “the” or “and”)
Understand different forms of words (e.g., “run,” “running,” and “ran” are treated as the same word)
Rank results based on how well they match the search query
Now that we know what full-text search is, let’s see how we can set it up using Prisma, a modern database toolkit.
2. Native PostgreSQL Full-Text Search
Quick Note on PostgreSQL Basics:
To connect to PostgreSQL, open your terminal and type:
psql postgres
To create a new database, use:
CREATEDATABASEyour_database_name;
To connect to your new database:
\c your_database_name
or exit psql and reconnect:
psql your_database_name
PostgreSQL provides robust full-text search capabilities out of the box. Let’s explore how to use these features directly with PostgreSQL before we integrate them into our application.
2.1 Setting Up Full-Text Search Indexes
First, let’s create a table and add some sample data:
-- Create a table
CREATETABLEarticles (
id SERIALPRIMARY KEY,
title TEXT,
content TEXT
);
-- Insert some sample data
INSERT INTO articles (title, content) VALUES
('PostgreSQL Full-Text Search', 'Learn how to implement powerful full-text search capabilities in PostgreSQL...'),
('Next.js and Database Integration', 'Explore efficient ways to connect Next.js applications with databases...'),
('Prisma ORM Overview', 'Discover the features and benefits of using Prisma ORM for database operations...');
-- Create a GIN index for full-text search
CREATEINDEXarticles_fts_idxON articles USING gin(to_tsvector('english', title || ' ' || content));
Here, we’ve created a GIN (Generalized Inverted Index) on a combination of the title and content fields. The to_tsvector function converts the text into a searchable format.
Note: GIN indexes are best for static data that doesn’t change often. They’re faster for lookups but slower to build and take more space. For frequently changing data, consider using a GiST (Generalized Search Tree) index instead.
2.2 Basic Full-Text Search Queries
Now that we have our index set up, let’s explore various ways to perform full-text searches:
-- Basic search for articles containing both "postgresql" and "search"
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', 'postgresql & search');
-- Search for articles containing either "database" or "sql"
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', 'database | sql');
-- Search for articles containing "full" followed by "text" (in that order)
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', 'full <-> text');
-- Search for articles containing "database" but not "nosql"
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', 'database & !nosql');
Let’s break down these queries and their results:
Basic AND search:
title
-----------------------------
PostgreSQL Full-Text Search
(1 row)
This query returns articles containing both “postgresql” and “search”.
OR search:
title
----------------------------------
Next.js and Database Integration
Prisma ORM Overview
(2 rows)
This query returns articles containing either “database” or “sql”.
Phrase search:
title
-----------------------------
PostgreSQL Full-Text Search
(1 row)
This query looks for “full” immediately followed by “text”.
Exclusion search:
title
----------------------------------
Next.js and Database Integration
Prisma ORM Overview
(2 rows)
This query returns articles containing “database” but not “nosql”.
In these queries:
to_tsvector converts our text to a searchable format called a tsvector.
@@ is the text search match operator, which returns true if the tsvector matches the tsquery.
to_tsquery converts our search term into a tsquery format, which can include operators:
& for AND
| for OR
! for NOT
<-> for phrase search (words must be adjacent and in order)
Use these queries when you need precise control over the search logic. They’re particularly useful for:
Implementing advanced search features in applications
Searching across multiple fields (like title and content)
Creating complex search conditions (e.g., must contain X and Y, but not Z)
2.3 Advanced Query Techniques
PostgreSQL offers several functions for more user-friendly and flexible searches:
plainto_tsquery: Converts plain text to a tsquery, treating the input as a phrase
phraseto_tsquery: Similar to plainto_tsquery, but it also creates phrase searches
websearch_to_tsquery: Supports a syntax similar to what web search engines use
Let’s look at examples and use cases for each:
-- plainto_tsquery: Searches for articles containing all words in any order
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 'postgresql database');
-- phraseto_tsquery: Searches for articles containing the exact phrase
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ phraseto_tsquery('english', 'full text search');
-- websearch_to_tsquery: Supports a more intuitive search syntax
SELECT title
FROM articles
WHERE to_tsvector('english', title || ' ' || content) @@ websearch_to_tsquery('english', 'postgresql -prisma "full text search"');
The websearch_to_tsquery function supports the following syntax:
Unquoted words are treated as optional but preferred
"quoted phrases" must appear exactly as written
- excludes words (e.g., -prisma excludes articles containing “prisma”)
OR can be used to specify alternatives
Use these functions when:
You want to provide a more user-friendly search interface
You need to handle user input that might include quotes or special characters
You want to mimic the behavior of popular web search engines
2.4 Ranking and Highlighting Results
To improve the usefulness of our search results, we can rank them and highlight the matching terms:
SELECT
title,
ts_rank(to_tsvector('english', title || ' ' || content), query) AS rank,
to_tsvector('english', title || ' ' || content) @@ query
ORDER BY
rank DESC;
Let’s analyze the results of each query:
plainto_tsquery:
title
-------
(0 rows)
This query returned no results. It searched for articles containing both “postgresql” and “database” in any order. The lack of results suggests that no single article in our sample data contains both these terms.
phraseto_tsquery:
title
-----------------------------
PostgreSQL Full-Text Search
(1 row)
This query successfully found an article with the exact phrase “full text search”. It’s more strict than plainto_tsquery as it requires the words to appear in the specified order.
websearch_to_tsquery:
title
-----------------------------
PostgreSQL Full-Text Search
(1 row)
This query demonstrates a more complex search: it looks for articles containing “postgresql” and the phrase “full text search”, but excludes any containing “prisma”. The result shows it successfully found a matching article.
Let’s break this query down:
ts_rank calculates a relevance score based on how well the document matches the query. It considers factors like:
How many times the search terms appear
How close the search terms are to each other
The importance of the field where the match occurred (e.g., title vs. content)
ts_headline generates an excerpt of the content with matching terms highlighted. We can customize its behavior:
StartSel and StopSel define the HTML tags used for highlighting
MaxWords and MinWords control the length of the excerpt
Other options include ShortWord (minimum word length to highlight) and HighlightAll (highlight all words)
We join the articles table with the query to avoid repeating the to_tsquery call
The results are ordered by rank, showing the most relevant results first
Use this approach when you want to:
Present search results in order of relevance
Show users why a particular result matched their search
Provide context around the matching terms in the search results
For even more control over ranking, you can use ts_rank_cd, which considers the proximity of matching lexemes:
SELECT
title,
ts_rank_cd(to_tsvector('english', title || ' ' || content), query) AS rank,
to_tsvector('english', title || ' ' || content) @@ query
ORDER BY
rank DESC;
Let’s break down the results:
Title: PostgreSQL Full-Text Search
Rank: 0.058333334
Excerpt: Learn how to implement powerful full-text <mark>search</mark> capabilities in <mark>PostgreSQL</mark>...
(1 row)
This query demonstrates several powerful features:
Matching: It finds articles that contain both “postgresql” and “search”.
Ranking: The ts_rank_cd function assigns a relevance score to each result. Higher scores indicate more relevant matches.
Highlighting: The ts_headline function creates an excerpt of the content with the matching terms highlighted (in this case, wrapped in <mark> tags).
Ordering: The results are ordered by rank, showing the most relevant results first.
Use this approach when you want to:
Present search results in order of relevance
Show users why a particular result matched their search
Provide context around the matching terms in the search results
ts_rank_cd is particularly useful when you want to give higher scores to documents where the matching terms appear closer together.
By understanding and utilizing these PostgreSQL full-text search features, you can create powerful and flexible search functionality in your applications, providing users with fast, relevant, and highlighted search results.
3. Integrating PostgreSQL Full-Text Search with Next.js
Now that we understand how to use PostgreSQL’s full-text search capabilities, let’s integrate them into a Next.js application.
3.1 Setting Up PostgreSQL and Creating Credentials
First, let’s set up PostgreSQL using psql:
Open a terminal and start psql:
Terminal window
psqlpostgres
I’m going to use the db I created earlier full_text_search_db:
WHERE to_tsvector('english', title || ' ' || content) @@ query
ORDER BY rank DESC
`;
res.status(200).json(results);
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'An error occurred while searching' });
}
}
4.4 Conclusion: Integrating Full-Text Search with Next.js and Prisma
By combining PostgreSQL’s powerful full-text search capabilities with Next.js and Prisma, we’ve created a robust and efficient search solution. While Prisma doesn’t natively support full-text search, using raw SQL queries allows us to leverage the best of both worlds: Prisma’s excellent ORM features for most database operations, and PostgreSQL’s specialized full-text search for advanced querying.
This approach provides several benefits:
Type-safe and intuitive database operations with Prisma for most of your application.
Powerful and flexible full-text search capabilities using PostgreSQL’s built-in features.
Seamless integration with Next.js, allowing for server-side search processing and efficient client-side rendering.
As we move forward, we’ll explore even more advanced search techniques using pgvector, which can provide semantic search capabilities beyond traditional full-text search.
5. Conclusion and Next Steps
Congratulations! You’ve now mastered the fundamentals of implementing full-text search with PostgreSQL and Prisma. Let’s recap what we’ve learned:
We explored the concept of full-text search and its advantages over traditional LIKE queries.
We set up full-text search indexes in PostgreSQL and learned how to perform basic and advanced search queries.
We integrated PostgreSQL’s full-text search capabilities with a Next.js application.
We used Prisma alongside PostgreSQL’s full-text search features, combining the best of both worlds.
This knowledge equips you to create fast, efficient, and relevant search functionality in your applications. But the journey doesn’t end here! In Part 2, we’ll dive into even more advanced search techniques using pgvector, which will allow us to perform semantic searches and work with high-dimensional data.
As you continue to develop your search functionality, consider exploring these topics:
Multilingual support for non-English content
Fuzzy searching for typo tolerance
Faceted search to enhance user experience
Real-time search suggestions
Remember, while PostgreSQL’s full-text search is powerful, very large-scale applications might benefit from dedicated search engines like Elasticsearch or Algolia. However, for most applications, the techniques we’ve covered here will provide excellent performance and flexibility.
In Part 2, we’ll take your search capabilities to the next level with pgvector!
Ready to Build with LLMs?
The concepts in this post are just the start. My free 11-page cheat sheet gives you copy-paste prompts and patterns to get reliable, structured output from any model.
Get Your Free LLM Cheat Sheet
🎁 Get the 11-page LLM Prompting Cheat Sheet (copy-paste prompts + patterns)
Join 100+ other developers and get my 11-page PDF guide to writing production-ready prompts, sent to you instantly.