Implementing and Improving PostgreSQL Full-Text Search in Ruby Apps

PostgreSQL is a great relational database with a lot of features. This blog post describes how to implement PostgreSQL full-text search and add it to your Ruby-on-Rails app. I will demonstrate how it works on a test app that searches articles by a title, content, the author’s name, and comments. In addition, the article explains how to speed up search by eliminating excessive join queries that greatly slow down the system’s performance.
Preparation
There already exist ready-made solutions for implementing PostgreSQL full-text search. We will be using this one. So, you need to add the following line to your Gemfile.
1 | <span class="n">gem</span> <span class="s1">'pg_search'</span> |
Then, add the following line to your application.rb
.
1 | <span class="n">config</span><span class="p">.</span><span class="nf">active_record</span><span class="p">.</span><span class="nf">schema_format</span> <span class="o">=</span> <span class="ss">:sql</span> |
A sample app
My sample app will have three models.
- 1<span class="k">class</span> <span class="nc">Article</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><br data-jekyll-commonmark-ghpages=""> <span class="n">belongs_to</span> <span class="ss">:author</span><br data-jekyll-commonmark-ghpages=""> <span class="n">has_many</span> <span class="ss">:comments</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="n">attr_accessible</span> <span class="ss">:content</span><span class="p">,</span> <span class="ss">:title</span><br data-jekyll-commonmark-ghpages=""><span class="k">end</span>
- 1<span class="k">class</span> <span class="nc">Author</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><br data-jekyll-commonmark-ghpages=""> <span class="n">attr_accessible</span> <span class="ss">:name</span><br data-jekyll-commonmark-ghpages=""> <span class="n">has_many</span> <span class="ss">:articles</span><br data-jekyll-commonmark-ghpages=""><span class="k">end</span>
- 1<span class="k">class</span> <span class="nc">Comment</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><br data-jekyll-commonmark-ghpages=""> <span class="n">belongs_to</span> <span class="ss">:article</span><br data-jekyll-commonmark-ghpages=""> <span class="n">attr_accessible</span> <span class="ss">:content</span><br data-jekyll-commonmark-ghpages=""><span class="k">end</span>
With these models, search will be implemented for articles. It will become possible to search an article by its title, content, comments, and the author’s name. In addition, search will have other settings about which you can read at gem’s page. So, to enable search for articles you should add the followong to the article
model.
1 | <span class="kp">include</span> <span class="no">PgSearch</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""><span class="n">pg_search_scope</span> <span class="ss">:search</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">against: </span><span class="p">[</span><span class="ss">:title</span><span class="p">,</span> <span class="ss">:content</span><span class="p">],</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">associated_against: </span><span class="p">{</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">author: :name</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">comments: :content</span><br data-jekyll-commonmark-ghpages=""> <span class="p">},</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">using: </span><span class="p">{</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">tsearch: </span><span class="p">{</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">dictionary: </span><span class="s1">'english'</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">any_word: </span><span class="kp">true</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">prefix: </span><span class="kp">true</span><br data-jekyll-commonmark-ghpages=""> <span class="p">}</span><br data-jekyll-commonmark-ghpages=""> <span class="p">}</span> |
Then, you can perform search using the Article.search
method.
1 | <span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">011</span><span class="p">:</span><span class="mi">0</span><span class="o">></span> <span class="no">Article</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s1">'title'</span><span class="p">)</span><br data-jekyll-commonmark-ghpages=""><span class="o">=></span> <span class="p">[</span><span class="c1">#<Article id: 3, author_id: 1, title: "title", content: "this is content", created_at: "2013-08-26 10:09:01", updated_at: "2013-08-27 07:00:18">]</span> |
Hooray, it found an article by the word title! However, look at the following query.
1 | <span class="no">Article</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">1.9</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"articles"</span><span class="p">.</span><span class="nf">*</span><span class="p">,</span> <span class="p">((</span><span class="n">ts_rank</span><span class="p">((</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"title"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"content"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">pg_search_1eb533ea18bbbe0846ef24</span><span class="p">.</span><span class="nf">pg_search_a612c20e7f822205b5b540</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">pg_search_121ea89914a721445aee70</span><span class="p">.</span><span class="nf">pg_search_6e76a7a40d9cb3861e7fb2</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))),</span> <span class="p">(</span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="s1">''' '</span> <span class="o">||</span> <span class="s1">'title'</span> <span class="o">||</span> <span class="s1">' '''</span><span class="p">)),</span> <span class="mi">0</span><span class="p">)))</span> <span class="no">AS</span> <span class="n">pg_search_rank</span> <span class="no">FROM</span> <span class="s2">"articles"</span> <span class="no">LEFT</span> <span class="no">OUTER</span> <span class="no">JOIN</span> <span class="p">(</span><span class="no">SELECT</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">AS</span> <span class="nb">id</span><span class="p">,</span> <span class="n">string_agg</span><span class="p">(</span><span class="s2">"authors"</span><span class="o">.</span><span class="s2">"name"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">' '</span><span class="p">)</span> <span class="no">AS</span> <span class="n">pg_search_a612c20e7f822205b5b540</span> <span class="no">FROM</span> <span class="s2">"articles"</span> <span class="no">INNER</span> <span class="no">JOIN</span> <span class="s2">"authors"</span> <span class="no">ON</span> <span class="s2">"authors"</span><span class="o">.</span><span class="s2">"id"</span> <span class="o">=</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"author_id"</span> <span class="no">GROUP</span> <span class="no">BY</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span><span class="p">)</span> <span class="n">pg_search_1eb533ea18bbbe0846ef24</span> <span class="no">ON</span> <span class="n">pg_search_1eb533ea18bbbe0846ef24</span><span class="p">.</span><span class="nf">id</span> <span class="o">=</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">LEFT</span> <span class="no">OUTER</span> <span class="no">JOIN</span> <span class="p">(</span><span class="no">SELECT</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">AS</span> <span class="nb">id</span><span class="p">,</span> <span class="n">string_agg</span><span class="p">(</span><span class="s2">"comments"</span><span class="o">.</span><span class="s2">"content"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">' '</span><span class="p">)</span> <span class="no">AS</span> <span class="n">pg_search_6e76a7a40d9cb3861e7fb2</span> <span class="no">FROM</span> <span class="s2">"articles"</span> <span class="no">INNER</span> <span class="no">JOIN</span> <span class="s2">"comments"</span> <span class="no">ON</span> <span class="s2">"comments"</span><span class="o">.</span><span class="s2">"article_id"</span> <span class="o">=</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">GROUP</span> <span class="no">BY</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span><span class="p">)</span> <span class="n">pg_search_121ea89914a721445aee70</span> <span class="no">ON</span> <span class="n">pg_search_121ea89914a721445aee70</span><span class="p">.</span><span class="nf">id</span> <span class="o">=</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">WHERE</span> <span class="p">(((</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"title"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"content"</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">pg_search_1eb533ea18bbbe0846ef24</span><span class="p">.</span><span class="nf">pg_search_a612c20e7f822205b5b540</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">))</span> <span class="o">||</span> <span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">pg_search_121ea89914a721445aee70</span><span class="p">.</span><span class="nf">pg_search_6e76a7a40d9cb3861e7fb2</span><span class="o">::</span><span class="n">text</span><span class="p">,</span> <span class="s1">''</span><span class="p">)))</span> <span class="err">@@</span> <span class="p">(</span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">'simple'</span><span class="p">,</span> <span class="s1">''' '</span> <span class="o">||</span> <span class="s1">'title'</span> <span class="o">||</span> <span class="s1">' '''</span><span class="p">))))</span> <span class="no">ORDER</span> <span class="no">BY</span> <span class="n">pg_search_rank</span> <span class="no">DESC</span><span class="p">,</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">ASC</span> |
Above, you can see that the query has several joins, which is not good, because it greatly slows performance. I will show you how to avoid this behavior and improve search performance.
Improving performance
I am going to add a new column to the tables with articles. The new column will have the
tsvector type and store all the words by which an article can be searched. I will also add the GIN index to speed up search. So, we need to generate migration.
1 | <span class="k">class</span> <span class="nc">AddTsvectorColumnToArticles</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><br data-jekyll-commonmark-ghpages=""> <span class="k">def</span> <span class="nf">up</span><br data-jekyll-commonmark-ghpages=""> <span class="n">add_column</span> <span class="ss">:articles</span><span class="p">,</span> <span class="ss">:search_vector</span><span class="p">,</span> <span class="ss">:tsvector</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="n">execute</span> <span class="o"><<-</span><span class="no">EOS</span><span class="sh"><br data-jekyll-commonmark-ghpages=""> CREATE INDEX articles_search_vector_idx ON articles USING gin(search_vector);<br data-jekyll-commonmark-ghpages=""></span><span class="no"> EOS</span><br data-jekyll-commonmark-ghpages=""> <span class="k">end</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="k">def</span> <span class="nf">down</span><br data-jekyll-commonmark-ghpages=""> <span class="n">remove_column</span> <span class="ss">:articles</span><span class="p">,</span> <span class="ss">:search_vector</span><br data-jekyll-commonmark-ghpages=""> <span class="k">end</span><br data-jekyll-commonmark-ghpages=""><span class="k">end</span> |
I’ve added the tsvector
column to the articles named search_vector
. Now, we need to fill up this vector. For the purpose, I am going to write the postgresql
function for filling up the vector and a trigger that will fill up this vector when inserting or updating an article. Below is an example of migration for creating a trigger and a function.
1 | <span class="k">class</span> <span class="nc">CreateFunctionAndTriggerForFillingSearchVectorOfArticles</span> <span class="o"><</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><br data-jekyll-commonmark-ghpages=""> <span class="k">def</span> <span class="nf">up</span><br data-jekyll-commonmark-ghpages=""> <span class="n">execute</span> <span class="o"><<-</span><span class="no">EOS</span><span class="sh"><br data-jekyll-commonmark-ghpages=""> CREATE OR REPLACE FUNCTION fill_search_vector_for_acticle() RETURNS trigger LANGUAGE plpgsql AS $$<br data-jekyll-commonmark-ghpages=""> declare<br data-jekyll-commonmark-ghpages=""> article_author record;<br data-jekyll-commonmark-ghpages=""> article_comments record;<br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> begin<br data-jekyll-commonmark-ghpages=""> select name into article_author from authors where id = new.author_id;<br data-jekyll-commonmark-ghpages=""> select string_agg(content, ' ') as content into article_comments from comments where article_id = new.id;<br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> new.search_vector :=<br data-jekyll-commonmark-ghpages=""> setweight(to_tsvector('pg_catalog.english', coalesce(new.title, '')), 'A') ||<br data-jekyll-commonmark-ghpages=""> setweight(to_tsvector('pg_catalog.english', coalesce(new.content, '')), 'B') ||<br data-jekyll-commonmark-ghpages=""> setweight(to_tsvector('pg_catalog.english', coalesce(article_author.name, '')), 'B') ||<br data-jekyll-commonmark-ghpages=""> setweight(to_tsvector('pg_catalog.english', coalesce(article_comments.content, '')), 'B');<br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> return new;<br data-jekyll-commonmark-ghpages=""> end<br data-jekyll-commonmark-ghpages=""> $$;<br data-jekyll-commonmark-ghpages=""></span><span class="no"> EOS</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="n">execute</span> <span class="o"><<-</span><span class="no">EOS</span><span class="sh"><br data-jekyll-commonmark-ghpages=""> CREATE TRIGGER articles_search_content_trigger BEFORE INSERT OR UPDATE<br data-jekyll-commonmark-ghpages=""> ON articles FOR EACH ROW EXECUTE PROCEDURE fill_search_vector_for_acticle();<br data-jekyll-commonmark-ghpages=""></span><span class="no"> EOS</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="no">Article</span><span class="p">.</span><span class="nf">find_each</span><span class="p">(</span><span class="o">&</span><span class="ss">:touch</span><span class="p">)</span><br data-jekyll-commonmark-ghpages=""> <span class="k">end</span><br data-jekyll-commonmark-ghpages=""><br data-jekyll-commonmark-ghpages=""> <span class="k">def</span> <span class="nf">down</span><br data-jekyll-commonmark-ghpages=""> <span class="n">execute</span> <span class="o"><<-</span><span class="no">EOS</span><span class="sh"><br data-jekyll-commonmark-ghpages=""> DROP FUNCTION fill_search_vector_for_acticle();<br data-jekyll-commonmark-ghpages=""> DROP TRIGGER articles_search_content_trigger ON articles;<br data-jekyll-commonmark-ghpages=""></span><span class="no"> EOS</span><br data-jekyll-commonmark-ghpages=""> <span class="k">end</span><br data-jekyll-commonmark-ghpages=""><span class="k">end</span> |
Each time, when an article is created or updated, a new vector will be built for the article. Look at the following example.
1 | <span class="nb">select</span> <span class="nb">name</span> <span class="n">into</span> <span class="n">article_author</span> <span class="n">from</span> <span class="n">authors</span> <span class="n">where</span> <span class="nb">id</span> <span class="o">=</span> <span class="n">new</span><span class="p">.</span><span class="nf">author_id</span><span class="p">;</span><br data-jekyll-commonmark-ghpages=""> <span class="nb">select</span> <span class="n">string_agg</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="s1">' '</span><span class="p">)</span> <span class="n">as</span> <span class="n">content</span> <span class="n">into</span> <span class="n">article_comments</span> <span class="n">from</span> <span class="n">comments</span> <span class="n">where</span> <span class="n">article_id</span> <span class="o">=</span> <span class="n">new</span><span class="p">.</span><span class="nf">id</span><span class="p">;</span> |
I am fetching a name from an author and concatenated content of an article. Then, I create a new vector.
1 | <span class="n">new</span><span class="p">.</span><span class="nf">search_vector</span> <span class="p">:</span><span class="o">=</span><br data-jekyll-commonmark-ghpages=""> <span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'pg_catalog.english'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">new</span><span class="p">.</span><span class="nf">title</span><span class="p">,</span> <span class="s1">''</span><span class="p">)),</span> <span class="s1">'A'</span><span class="p">)</span> <span class="o">||</span><br data-jekyll-commonmark-ghpages=""> <span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'pg_catalog.english'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">new</span><span class="p">.</span><span class="nf">content</span><span class="p">,</span> <span class="s1">''</span><span class="p">)),</span> <span class="s1">'B'</span><span class="p">)</span> <span class="o">||</span><br data-jekyll-commonmark-ghpages=""> <span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'pg_catalog.english'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">article_author</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span> <span class="s1">''</span><span class="p">)),</span> <span class="s1">'B'</span><span class="p">)</span> <span class="o">||</span><br data-jekyll-commonmark-ghpages=""> <span class="n">setweight</span><span class="p">(</span><span class="n">to_tsvector</span><span class="p">(</span><span class="s1">'pg_catalog.english'</span><span class="p">,</span> <span class="n">coalesce</span><span class="p">(</span><span class="n">article_comments</span><span class="p">.</span><span class="nf">content</span><span class="p">,</span> <span class="s1">''</span><span class="p">)),</span> <span class="s1">'B'</span><span class="p">);</span> |
More details about syntax of each function can be found in this documentation.
We also need to update an article each time when a comment is updated. For this purpose, we will add touch true
for association at the comments
model.
1 | <span class="n">belongs_to</span> <span class="ss">:article</span><span class="p">,</span> <span class="ss">touch: </span><span class="kp">true</span> |
Now, every time when comments are updated, an article will be updated true
. This will call a trigger that will update the articles’ search_vector
column.
The last step is to make search use the new search_vector
column. For the purpose, we need to change pg_search_scope
to the article
model.
1 | <span class="n">pg_search_scope</span> <span class="ss">:search</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">against: :search_vector</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">using: </span><span class="p">{</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">tsearch: </span><span class="p">{</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">dictionary: </span><span class="s1">'english'</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">any_word: </span><span class="kp">true</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">prefix: </span><span class="kp">true</span><span class="p">,</span><br data-jekyll-commonmark-ghpages=""> <span class="ss">tsvector_column: </span><span class="s1">'search_vector'</span><br data-jekyll-commonmark-ghpages=""> <span class="p">}</span><br data-jekyll-commonmark-ghpages=""> <span class="p">}</span> |
Results
Now, our articles have the tsvector
column that stores searches by text.
1 | <span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">051</span><span class="p">:</span><span class="mi">0</span><span class="o">></span> <span class="no">Article</span><span class="p">.</span><span class="nf">last</span><span class="p">.</span><span class="nf">search_vector</span><br data-jekyll-commonmark-ghpages=""><span class="o">=></span> <span class="s2">"'astraukh':6B 'comment':9B 'content':4B,10B 'pavel':5B 'titl':1A"</span><br data-jekyll-commonmark-ghpages=""><span class="n">irb</span><span class="p">(</span><span class="n">main</span><span class="p">):</span><span class="mo">052</span><span class="p">:</span><span class="mi">0</span><span class="o">></span> <span class="no">Article</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s1">'title'</span><span class="p">)</span><br data-jekyll-commonmark-ghpages=""><span class="o">=></span> <span class="p">[</span><span class="c1">#<Article id: 3, author_id: 1, title: "title", content: "this is content", created_at: "2013-08-27 08:05:55", updated_at: "2013-08-27 08:06:18", search_vector: "'astraukh':6B 'comment':9B 'content':4B,10B 'pavel'...">]</span> |
A query looks like this.
1 | <span class="no">Article</span> <span class="no">Load</span> <span class="p">(</span><span class="mf">0.4</span><span class="n">ms</span><span class="p">)</span> <span class="no">SELECT</span> <span class="s2">"articles"</span><span class="p">.</span><span class="nf">*</span><span class="p">,</span> <span class="p">((</span><span class="n">ts_rank</span><span class="p">((</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"search_vector"</span><span class="p">),</span> <span class="p">(</span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">'english'</span><span class="p">,</span> <span class="s1">''' '</span> <span class="o">||</span> <span class="s1">'title'</span> <span class="o">||</span> <span class="s1">' '''</span> <span class="o">||</span> <span class="s1">':*'</span><span class="p">)),</span> <span class="mi">0</span><span class="p">)))</span> <span class="no">AS</span> <span class="n">pg_search_rank</span> <span class="no">FROM</span> <span class="s2">"articles"</span> <span class="no">WHERE</span> <span class="p">(((</span><span class="s2">"articles"</span><span class="o">.</span><span class="s2">"search_vector"</span><span class="p">)</span> <span class="err">@@</span> <span class="p">(</span><span class="n">to_tsquery</span><span class="p">(</span><span class="s1">'english'</span><span class="p">,</span> <span class="s1">''' '</span> <span class="o">||</span> <span class="s1">'title'</span> <span class="o">||</span> <span class="s1">' '''</span> <span class="o">||</span> <span class="s1">':*'</span><span class="p">))))</span> <span class="no">ORDER</span> <span class="no">BY</span> <span class="n">pg_search_rank</span> <span class="no">DESC</span><span class="p">,</span> <span class="s2">"articles"</span><span class="o">.</span><span class="s2">"id"</span> <span class="no">ASC</span> |
As you see, the query doesn’t have any joins, and this behavior greatly improves search performance. Now, you know how to implement and improve your search using PostgreSQL full-text search.
Further reading
- Deploying a Rails 5 App with MongoDB, Redis, and CarrierWave to IBM Bluemix
- Accessing an External Database Storage from a Bluemix-Based Ruby App
- Using the PostgreSQL Service from an ASP.NET App on GE’s Predix
About the author
Pavel Astraukh developed a number of projects using Ruby on Rails and other technologies. He worked in small and large teams utilizing the Agile methodology. Find him on GitHub.