<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sheikh Abdullah</title>
    <description>The latest articles on DEV Community by Sheikh Abdullah (@sheikh566).</description>
    <link>https://dev.to/sheikh566</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F961257%2Fe7b0b9cc-f8ad-430a-9c9e-ac9a03056be5.png</url>
      <title>DEV Community: Sheikh Abdullah</title>
      <link>https://dev.to/sheikh566</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sheikh566"/>
    <language>en</language>
    <item>
      <title>Beyond LIKE: Mastering PostgreSQL's Built-in Full-Text Search</title>
      <dc:creator>Sheikh Abdullah</dc:creator>
      <pubDate>Sat, 17 May 2025 17:56:57 +0000</pubDate>
      <link>https://dev.to/sheikh566/beyond-like-mastering-postgresqls-built-in-full-text-search-4h35</link>
      <guid>https://dev.to/sheikh566/beyond-like-mastering-postgresqls-built-in-full-text-search-4h35</guid>
      <description>&lt;p&gt;A few months ago, my manager assigned me to a project that required improving its search functionality. The team initially considered Elasticsearch as the primary solution, though they were open to exploring alternatives. I researched the topic and discovered PostgreSQL's full-text search. Since our data was already stored in PostgreSQL, I compared the two tools. The application I worked on served private users and had strict privacy regulations, with complex conditions determining who could view or edit specific data within the company. &lt;/p&gt;

&lt;p&gt;These intricate queries proved challenging for Elasticsearch to handle effectively, while PostgreSQL's full-text search, being built on top of SQL syntax, required no extra work to handle this. It also integrates seamlessly with our existing database, requires no additional infrastructure, and has no licensing costs, unlike Elasticsearch. Additionally, PostgreSQL efficiently manages relational data, while Elasticsearch can be less flexible outside its ecosystem and demands more system resources.&lt;/p&gt;

&lt;p&gt;But what the heck is full-text search? It’s a way to dig through text data fast, finding matches based on words or phrases, not just exact strings. &lt;br&gt;
Think of it like googling something, but for your own database. &lt;br&gt;
Instead of saying "find me this exact sentence," you can ask "show me anything with ‘book’ and ‘read’ in it, "and it'll pull up stuff like "I read a book" or "books for reading." It ignores stop words like "the", "and", "I", etc, to pull more relevant results. In postgres, it uses tricks like turning text into searchable chunks and matching them up, so you’re not stuck with slow, basic searches that miss the point. It’s all about making search feel natural, not robotic.&lt;/p&gt;

&lt;p&gt;PostgreSQL’s full-text search is powered by two core concepts: tsvector and tsquery. A &lt;code&gt;tsvector&lt;/code&gt; is a data type that stores pre-processed, searchable data. Think of it as a transformed, indexed version of your text. When you run a search, PostgreSQL compares your search terms with these indexed values rather than the raw text. This allows for faster and more efficient lookups. A &lt;code&gt;tsquery&lt;/code&gt;, on the other hand, is essentially a search query. It represents the words and phrases you're looking for, possibly enriched with operators to define how terms should be combined or modified. Examples include using &lt;code&gt;&amp;amp;&lt;/code&gt; for logical AND, &lt;code&gt;|&lt;/code&gt; for OR, and &lt;code&gt;!&lt;/code&gt; for NOT, giving you the power to craft complex search conditions.&lt;br&gt;
Here’s a quick example to illustrate how these work together in practice: &lt;br&gt;
Say you have a table &lt;code&gt;documents&lt;/code&gt; with a column named &lt;code&gt;content&lt;/code&gt;, and you want to know which documents contain both "book" and "read" in the content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'book &amp;amp; read'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, so there is a lot of stuff going on after the &lt;code&gt;WHERE&lt;/code&gt; keyword. Let's break it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;to_tsvector('english', content)&lt;/code&gt;: This function converts the &lt;code&gt;content&lt;/code&gt; column into a tsvector. The &lt;code&gt;english&lt;/code&gt; argument specifies the language of the text, which is important for language-specific stemming and stopword lists.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;to_tsquery('english', 'book &amp;amp; read')&lt;/code&gt;: This function converts the search query into a tsquery. Again, the &lt;code&gt;english&lt;/code&gt; argument specifies the language of the text.&lt;/li&gt;
&lt;li&gt;Finally, the &lt;code&gt;@@&lt;/code&gt; operator checks if the tsvector matches the tsquery and return TRUE or FALSE.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But what if we want to search for documents containing either "book" or "read" in the content? Then we can use the &lt;code&gt;|&lt;/code&gt; operator like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'book | read'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to search for documents containing "book" and "read" but not "fantasy", you can use the &lt;code&gt;!&lt;/code&gt; operator like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'book &amp;amp; read &amp;amp; !fantasy'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can craft more complex queries by grouping terms with parentheses. For example, to find documents which must contain "book" and either "read" or "fantasy", you can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'book &amp;amp; (read | fantasy)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While all these operators are great for building "Advanced Search" features (like Gmail or Wikipedia) in your application, but most of the time, users just want a simple search.&lt;br&gt;
For that, you can use the &lt;code&gt;plainto_tsquery&lt;/code&gt; function. It's similar to &lt;code&gt;to_tsquery&lt;/code&gt; but doesn't interpret special characters as operators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;plainto_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'book read'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;plainto_tsquery&lt;/code&gt;, each space is treated as an &lt;code&gt;&amp;amp;&lt;/code&gt; operator. So, the query above will return documents containing both "book" and "read".&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching Across Multiple Columns
&lt;/h2&gt;

&lt;p&gt;Often, you'll want to search across multiple columns in a table. For example, you might want to search both the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; of documents. PostgreSQL makes this easy through vector concatenation.&lt;/p&gt;

&lt;p&gt;When searching across multiple columns, you can concatenate tsvectors using the &lt;code&gt;||&lt;/code&gt; operator. This combines the lexemes from different columns into a single tsvector that can be searched with a tsquery.&lt;/p&gt;

&lt;p&gt;Here's how you might search across both the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; columns of a &lt;code&gt;documents&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, &lt;code&gt;test&lt;/code&gt; is the phrase you're searching for. The &lt;code&gt;||&lt;/code&gt; operator concatenates the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt; columns into a single tsvector, which is then searched using the &lt;code&gt;@@&lt;/code&gt; operator.&lt;br&gt;
You might be thinking, why not just do the string concatenation in the query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="err"&gt;❌&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="err"&gt;❌&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;❌&lt;/span&gt;   &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is not recommended because the parser treats the title and content as one continuous string, which can lead to unexpected results. &lt;br&gt;
Moreover, column-level weighting (which we will discuss later) couldn't be applied.&lt;/p&gt;
&lt;h2&gt;
  
  
  Speeding Up The Searches
&lt;/h2&gt;

&lt;p&gt;While these examples work, calling to_tsvector() on every query is inefficient. &lt;br&gt;
Each time you run the query, PostgreSQL has to process and transform the entire text content into a tsvector. &lt;br&gt;
For large tables or frequent searches, this can significantly impact performance.&lt;br&gt;
A better approach is to store the tsvector in a separate column and keep it updated &lt;br&gt;
using database triggers or your backend code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"vector"&lt;/span&gt; &lt;span class="n"&gt;tsvector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; 
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="nv"&gt;"vector"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To further speed up the searches, you can create a GIN index on the &lt;code&gt;vector&lt;/code&gt; column.&lt;br&gt;
GIN index is a specialized index type that efficiently handles queries on composite data types, such as arrays, jsonb and tsvector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_vector&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;GIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to search on columns spread across multiple tables, you can a materialized view to store the concatenated tsvector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;documents_search&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also create a GIN index on the &lt;code&gt;vector&lt;/code&gt; column of the materialized view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Weights
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's Full-Text Search allows you to assign different weights to different parts of your document,&lt;br&gt;
influencing how search results are ranked. This is achieved using the &lt;code&gt;setweight&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;setweight&lt;/code&gt; function takes two arguments: a &lt;code&gt;tsvector&lt;/code&gt; and a weight label ('A', 'B', 'C', or 'D'). &lt;br&gt;
'A' is the highest weight, and 'D' is the lowest. By default, lexemes are assigned weight 'D'.&lt;/p&gt;

&lt;p&gt;Here's how you can use &lt;code&gt;setweight&lt;/code&gt; to prioritize matches in the &lt;code&gt;title&lt;/code&gt; over matches in the &lt;code&gt;content&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="nv"&gt;"vector"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;setweight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_tsvector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'B'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting weights is useless if you don't need to rank the results. There are two ranking functions in PostgreSQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ts_rank&lt;/code&gt;: Basic ranking that considers the number of matching lexemes and their weights&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ts_rank_cd&lt;/code&gt;: Also considers the distance between matching lexemes (coverage density)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how to use them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts_rank_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query will return documents that match the query "test" and rank them by how closely they match the query.&lt;/p&gt;

&lt;p&gt;Earlier we talked about why you shouldn't concatenate columns (string concatenation) before creating the tsvector. Because if you do that, &lt;br&gt;
you won't be able to set weights to individual columns.&lt;/p&gt;
&lt;h2&gt;
  
  
  Highlighting Matches
&lt;/h2&gt;

&lt;p&gt;PostgreSQL provides the &lt;code&gt;ts_headline&lt;/code&gt; function to highlight matching terms in the search results. &lt;br&gt;
This function takes the original text and a tsquery as input, and returns the text with matching terms highlighted using &lt;code&gt;&amp;lt;b&amp;gt;&amp;lt;/b&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;Here's the basic syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;cte&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;to_tsquery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;ts_headline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;title_match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ts_headline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;content_match&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cte&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result will look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;title_match&lt;/th&gt;
&lt;th&gt;content_match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; document&lt;/td&gt;
&lt;td&gt;This is a &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; document that contains the word &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; multiple times to &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; the highlighting feature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A title&lt;/td&gt;
&lt;td&gt;The second document also contains a &amp;lt;b&amp;gt;test&amp;lt;/b&amp;gt; word&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want replace the default &lt;code&gt;&amp;lt;b&amp;gt;&amp;lt;/b&amp;gt;&lt;/code&gt; tags with something else, you can define them in the fourth argument of the &lt;code&gt;ts_headline&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ts_headline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'english'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'StartSel={, StopSel=}'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we replaced &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; with &lt;code&gt;{&lt;/code&gt; and &lt;code&gt;&amp;lt;/b&amp;gt;&lt;/code&gt; with &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;PostgreSQL's full-text search feature is simple, flexible and powerful. It can fulfill most of your search needs.&lt;br&gt;
Though every tool has limitations, PostgreSQL's FTS is no exception. There are scenarios where it might not be the optimal choice, and dedicated search engines like Elasticsearch, OpenSearch, or Solr could be more suitable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Extremely Large Datasets and High Query Throughput:&lt;/strong&gt; For applications dealing with terabytes or petabytes of data, or requiring tens of thousands of queries per second, distributed search engines are designed for horizontal scalability and can offer better performance. PostgreSQL FTS operates within the context of the database server, which might become a bottleneck at such scales for search-heavy workloads.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Complex Relevancy Tuning and Machine Learning:&lt;/strong&gt; If your application demands highly sophisticated ranking algorithms, personalized search results, "learning to rank" (LTR) models, or advanced features like "more like this" that go beyond PostgreSQL's &lt;code&gt;ts_rank&lt;/code&gt; and &lt;code&gt;ts_rank_cd&lt;/code&gt; capabilities, dedicated search platforms provide more extensive tools and flexibility.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Requirement for a Decoupled Search Service:&lt;/strong&gt; Architecturally, you might prefer to decouple your search infrastructure from your primary database. This allows for independent scaling, resource allocation, different maintenance windows, and potentially different technology choices for the search component. Using a dedicated search service facilitates this separation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these situations, the additional complexity and operational overhead of managing a separate search system can be justified by the advanced capabilities and scalability they provide. However, for a vast majority of applications, PostgreSQL's built-in FTS offers an excellent balance of power, simplicity, and integration directly within your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Application
&lt;/h2&gt;

&lt;p&gt;Wikipedia also uses a full-text search engine to power its Advanced Search feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38hbz65f066pppaftrxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38hbz65f066pppaftrxl.png" alt="Wikipedia's Advanced Search feature" width="800" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created a simpler version to showcase PostgreSQL's full-text search capabilities in a real-life application. Check out the deployed demo: &lt;a href="https://tg1r1rgrpd.execute-api.ap-south-1.amazonaws.com/dev/search" rel="noopener noreferrer"&gt;Wikipedia Advanced Search&lt;/a&gt;. You can view the source code on GitHub: &lt;a href="https://github.com/Sheikh566/wikipedia-advanced-search" rel="noopener noreferrer"&gt;Sheikh566/wikipedia-advanced-search&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backend</category>
      <category>elasticsearch</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
