pg_search
pg_search builds ActiveRecord named scopes that take advantage of PostgreSQL’s full text search
Top Related Projects
Intelligent search made easy
Elasticsearch integrations for ActiveModel/Record and Ruby on Rails
Object-based searching.
Sphinx/Manticore plugin for ActiveRecord/Rails
Quick Overview
pg_search is a Ruby gem that extends ActiveRecord models with full-text search capabilities using PostgreSQL's built-in full-text search features. It provides a simple and flexible way to add powerful search functionality to Rails applications without the need for external search engines.
Pros
- Easy integration with existing Rails projects
- Utilizes PostgreSQL's native full-text search for efficiency
- Supports multiple search configurations and weighting
- Offers various search options like trigram similarity and dmetaphone
Cons
- Limited to PostgreSQL databases only
- May require additional configuration for complex search scenarios
- Performance can be impacted for large datasets without proper indexing
- Learning curve for advanced features and customizations
Code Examples
- Basic search configuration:
class Article < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_by_title, against: :title
end
Article.search_by_title("Ruby on Rails")
- Multi-column search with weights:
class Book < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_content, against: {
title: 'A',
author: 'B',
description: 'C'
}
end
Book.search_content("programming")
- Using trigram similarity:
class User < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_by_name,
against: [:first_name, :last_name],
using: {
trigram: {
threshold: 0.1
}
}
end
User.search_by_name("John")
Getting Started
- Add pg_search to your Gemfile:
gem 'pg_search'
- Run bundle install:
bundle install
- Include PgSearch in your model:
class Product < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_by_name, against: :name
end
- Use the search method in your controller or elsewhere:
@results = Product.search_by_name(params[:query])
Competitor Comparisons
Intelligent search made easy
Pros of Searchkick
- Supports multiple search backends (Elasticsearch, OpenSearch, Algolia)
- Offers advanced features like autocomplete, faceted search, and geospatial search
- Provides a simple, Rails-like syntax for complex search operations
Cons of Searchkick
- Requires additional setup and maintenance of external search engines
- May have a steeper learning curve for developers unfamiliar with Elasticsearch or similar systems
- Can be overkill for simple search needs in smaller applications
Code Comparison
pg_search:
class Product < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_by_name, against: :name
end
Product.search_by_name("example")
Searchkick:
class Product < ApplicationRecord
searchkick
end
Product.search("example")
Summary
pg_search is a lightweight, PostgreSQL-specific search solution that's easy to set up and use for basic search needs. It's ideal for smaller applications or those already using PostgreSQL.
Searchkick offers more advanced search capabilities and flexibility in terms of backend choices. It's better suited for larger applications or those requiring complex search functionality, but comes with additional setup and maintenance overhead.
The choice between the two depends on the specific needs of your project, the desired search features, and the willingness to manage external search engines.
Elasticsearch integrations for ActiveModel/Record and Ruby on Rails
Pros of elasticsearch-rails
- Offers advanced full-text search capabilities with complex queries and relevance scoring
- Provides distributed search and analytics across large datasets
- Supports real-time indexing and near-instantaneous search results
Cons of elasticsearch-rails
- Requires additional infrastructure setup and maintenance (Elasticsearch cluster)
- Higher complexity and learning curve compared to pg_search
- Potential performance overhead for smaller datasets or simple search requirements
Code Comparison
pg_search:
class Product < ApplicationRecord
include PgSearch::Model
pg_search_scope :search_by_name, against: :name
end
Product.search_by_name("example")
elasticsearch-rails:
class Product < ApplicationRecord
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
settings index: { number_of_shards: 1 } do
mappings dynamic: 'false' do
indexes :name, analyzer: 'english'
end
end
end
Product.__elasticsearch__.search("example")
Both libraries provide search functionality for Rails applications, but elasticsearch-rails offers more advanced features at the cost of increased complexity. pg_search is simpler to set up and use, making it suitable for basic search needs, while elasticsearch-rails excels in scenarios requiring sophisticated full-text search and analytics capabilities.
Object-based searching.
Pros of Ransack
- More flexible and powerful querying capabilities, allowing complex searches across multiple models and associations
- Provides a user-friendly DSL for building advanced search forms
- Supports various ORMs, not limited to PostgreSQL
Cons of Ransack
- Can be more complex to set up and use for simple search scenarios
- May have a steeper learning curve for developers new to the gem
- Potentially slower performance for large datasets compared to pg_search's native PostgreSQL full-text search
Code Comparison
Ransack:
@q = Person.ransack(params[:q])
@people = @q.result(distinct: true)
pg_search:
@people = Person.pg_search_scope :search_by_full_name,
against: [:first_name, :last_name],
using: {
tsearch: { prefix: true }
}
Summary
Ransack offers more versatile search capabilities across different ORMs, making it suitable for complex search requirements. It provides a powerful DSL for building advanced search forms but may be overkill for simpler search needs. pg_search, on the other hand, is specifically designed for PostgreSQL and leverages its full-text search capabilities, potentially offering better performance for large datasets. The choice between the two depends on the specific project requirements, database used, and the complexity of the search functionality needed.
Sphinx/Manticore plugin for ActiveRecord/Rails
Pros of thinking-sphinx
- Supports multiple search backends (Sphinx and Elasticsearch)
- Offers more advanced features like faceted search and geospatial search
- Provides better integration with Rails, including ActiveRecord callbacks
Cons of thinking-sphinx
- More complex setup and configuration compared to pg_search
- Requires external search engines, increasing infrastructure complexity
- May have a steeper learning curve for developers new to full-text search
Code Comparison
thinking-sphinx:
class Article < ActiveRecord::Base
define_index do
indexes title, sortable: true
indexes content
has author_id, created_at, updated_at
end
end
pg_search:
class Article < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_by_title_and_content,
against: [:title, :content],
using: {
tsearch: { prefix: true }
}
end
Both libraries provide ways to define searchable fields, but thinking-sphinx offers more granular control over indexing and searching options, while pg_search has a simpler, more straightforward approach leveraging PostgreSQL's built-in full-text search capabilities.
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual CopilotREADME
pg_search
DESCRIPTION
PgSearch builds named scopes that take advantage of PostgreSQL's full text search.
Read the blog post introducing PgSearch at https://tanzu.vmware.com/content/blog/pg-search-how-i-learned-to-stop-worrying-and-love-postgresql-full-text-search
REQUIREMENTS
- Ruby 3.0+
- Active Record 6.1+
- PostgreSQL 9.2+
- PostgreSQL extensions for certain features
INSTALL
$ gem install pg_search
or add this line to your Gemfile:
gem 'pg_search'
Non-Rails projects
In addition to installing and requiring the gem, you may want to include the PgSearch rake tasks in your Rakefile. This isn't necessary for Rails projects, which gain the Rake tasks via a Railtie.
load "pg_search/tasks.rb"
USAGE
To add PgSearch to an Active Record model, simply include the PgSearch module.
class Shape < ActiveRecord::Base
include PgSearch::Model
end
Contents
- Multi-search vs. search scopes
- Multi-search
pg_search_scope
- Searching using different search features
- Limiting Fields When Combining Features
- Ignoring accent marks
- Using tsvector columns
- Configuring ranking and ordering
Multi-search vs. search scopes
pg_search supports two different techniques for searching, multi-search and search scopes.
The first technique is multi-search, in which records of many different Active Record classes can be mixed together into one global search index across your entire application. Most sites that want to support a generic search page will want to use this feature.
The other technique is search scopes, which allow you to do more advanced searching against only one Active Record class. This is more useful for building things like autocompleters or filtering a list of items in a faceted search.
Multi-search
Setup
Before using multi-search, you must generate and run a migration to create the pg_search_documents database table.
$ rails g pg_search:migration:multisearch
$ rake db:migrate
multisearchable
To add a model to the global search index for your application, call multisearchable in its class definition.
class EpicPoem < ActiveRecord::Base
include PgSearch::Model
multisearchable against: [:title, :author]
end
class Flower < ActiveRecord::Base
include PgSearch::Model
multisearchable against: :color
end
If this model already has existing records, you will need to reindex this model to get existing records into the pg_search_documents table. See the rebuild task below.
Whenever a record is created, updated, or destroyed, an Active Record callback will fire, leading to the creation of a corresponding PgSearch::Document record in the pg_search_documents table. The :against option can be one or several methods which will be called on the record to generate its search text.
You can also pass a Proc or method name to call to determine whether or not a particular record should be included.
class Convertible < ActiveRecord::Base
include PgSearch::Model
multisearchable against: [:make, :model],
if: :available_in_red?
end
class Jalopy < ActiveRecord::Base
include PgSearch::Model
multisearchable against: [:make, :model],
if: lambda { |record| record.model_year > 1970 }
end
Note that the Proc or method name is called in an after_save hook. This means that you should be careful when using Time or other objects. In the following example, if the record was last saved before the published_at timestamp, it won't get listed in global search at all until it is touched again after the timestamp.
class AntipatternExample
include PgSearch::Model
multisearchable against: [:contents],
if: :published?
def published?
published_at < Time.now
end
end
problematic_record = AntipatternExample.create!(
contents: "Using :if with a timestamp",
published_at: 10.minutes.from_now
)
problematic_record.published? # => false
PgSearch.multisearch("timestamp") # => No results
sleep 20.minutes
problematic_record.published? # => true
PgSearch.multisearch("timestamp") # => No results
problematic_record.save!
problematic_record.published? # => true
PgSearch.multisearch("timestamp") # => Includes problematic_record
More Options
Conditionally update pg_search_documents
You can also use the :update_if
option to pass a Proc or method name to call
to determine whether or not a particular record should be updated.
Note that the Proc or method name is called in an after_save
hook, so if you
are relying on ActiveRecord dirty flags use *_previously_changed?
.
class Message < ActiveRecord::Base
include PgSearch::Model
multisearchable against: [:body],
update_if: :body_previously_changed?
end
Specify additional attributes to be saved on the pg_search_documents table
You can specify :additional_attributes
to be saved within the pg_search_documents
table. For example, perhaps you are indexing a book model and an article model and wanted to include the author_id.
First, we need to add a reference to author to the migration creating our pg_search_documents
table.
create_table :pg_search_documents do |t|
t.text :content
t.references :author, index: true
t.belongs_to :searchable, polymorphic: true, index: true
t.timestamps null: false
end
Then, we can send in this additional attribute in a lambda
multisearchable(
against: [:title, :body],
additional_attributes: -> (article) { { author_id: article.author_id } }
)
This allows much faster searches without joins later on by doing something like:
PgSearch.multisearch(params['search']).where(author_id: 2)
NOTE: You must currently manually call record.update_pg_search_document
for
the additional attribute to be included in the pg_search_documents table
Multi-search associations
Two associations are built automatically. On the original record, there is a has_one :pg_search_document association pointing to the PgSearch::Document record, and on the PgSearch::Document record there is a belongs_to :searchable polymorphic association pointing back to the original record.
odyssey = EpicPoem.create!(title: "Odyssey", author: "Homer")
search_document = odyssey.pg_search_document #=> PgSearch::Document instance
search_document.searchable #=> #<EpicPoem id: 1, title: "Odyssey", author: "Homer">
Searching in the global search index
To fetch the PgSearch::Document entries for all of the records that match a given query, use PgSearch.multisearch.
odyssey = EpicPoem.create!(title: "Odyssey", author: "Homer")
rose = Flower.create!(color: "Red")
PgSearch.multisearch("Homer") #=> [#<PgSearch::Document searchable: odyssey>]
PgSearch.multisearch("Red") #=> [#<PgSearch::Document searchable: rose>]
Chaining method calls onto the results
PgSearch.multisearch returns an ActiveRecord::Relation, just like scopes do, so you can chain scope calls to the end. This works with gems like Kaminari that add scope methods. Just like with regular scopes, the database will only receive SQL requests when necessary.
PgSearch.multisearch("Bertha").limit(10)
PgSearch.multisearch("Juggler").where(searchable_type: "Occupation")
PgSearch.multisearch("Alamo").page(3).per(30)
PgSearch.multisearch("Diagonal").find_each do |document|
puts document.searchable.updated_at
end
PgSearch.multisearch("Moro").reorder("").group(:searchable_type).count(:all)
PgSearch.multisearch("Square").includes(:searchable)
Configuring multi-search
PgSearch.multisearch can be configured using the same options as
pg_search_scope
(explained in more detail below). Just set the
PgSearch.multisearch_options in an initializer:
PgSearch.multisearch_options = {
using: [:tsearch, :trigram],
ignoring: :accents
}
Rebuilding search documents for a given class
If you change the :against option on a class, add multisearchable to a class that already has records in the database, or remove multisearchable from a class in order to remove it from the index, you will find that the pg_search_documents table could become out-of-sync with the actual records in your other tables.
The index can also become out-of-sync if you ever modify records in a way that does not trigger Active Record callbacks. For instance, the #update_attribute instance method and the .update_all class method both skip callbacks and directly modify the database.
To remove all of the documents for a given class, you can simply delete all of the PgSearch::Document records.
PgSearch::Document.delete_by(searchable_type: "Animal")
To regenerate the documents for a given class, run:
PgSearch::Multisearch.rebuild(Product)
The rebuild
method will delete all the documents for the given class
before regenerating them. In some situations this may not be desirable,
such as when you're using single-table inheritance and searchable_type
is your base class. You can prevent rebuild
from deleting your records
like so:
PgSearch::Multisearch.rebuild(Product, clean_up: false)
rebuild
runs inside a single transaction. To run outside of a transaction,
you can pass transactional: false
like so:
PgSearch::Multisearch.rebuild(Product, transactional: false)
Rebuild is also available as a Rake task, for convenience.
$ rake pg_search:multisearch:rebuild[BlogPost]
A second optional argument can be passed to specify the PostgreSQL schema search path to use, for multi-tenant databases that have multiple pg_search_documents tables. The following will set the schema search path to "my_schema" before reindexing.
$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]
For models that are multisearchable :against
methods that directly map to
Active Record attributes, an efficient single SQL statement is run to update
the pg_search_documents
table all at once. However, if you call any dynamic
methods in :against
then update_pg_search_document
will be called on the
individual records being indexed in batches.
You can also provide a custom implementation for rebuilding the documents by
adding a class method called rebuild_pg_search_documents
to your model.
class Movie < ActiveRecord::Base
belongs_to :director
def director_name
director.name
end
multisearchable against: [:name, :director_name]
# Naive approach
def self.rebuild_pg_search_documents
find_each { |record| record.update_pg_search_document }
end
# More sophisticated approach
def self.rebuild_pg_search_documents
connection.execute <<~SQL.squish
INSERT INTO pg_search_documents (searchable_type, searchable_id, content, created_at, updated_at)
SELECT 'Movie' AS searchable_type,
movies.id AS searchable_id,
CONCAT_WS(' ', movies.name, directors.name) AS content,
now() AS created_at,
now() AS updated_at
FROM movies
LEFT JOIN directors
ON directors.id = movies.director_id
SQL
end
end
Note: If using PostgreSQL before 9.1, replace the CONCAT_WS()
function call with double-pipe concatenation, eg. (movies.name || ' ' || directors.name)
. However, now be aware that if any of the joined values is NULL then the final content
value will also be NULL, whereas CONCAT_WS()
will selectively ignore NULL values.
Disabling multi-search indexing temporarily
If you have a large bulk operation to perform, such as importing a lot of records from an external source, you might want to speed things up by turning off indexing temporarily. You could then use one of the techniques above to rebuild the search documents off-line.
PgSearch.disable_multisearch do
Movie.import_from_xml_file(File.open("movies.xml"))
end
pg_search_scope
You can use pg_search_scope to build a search scope. The first parameter is a scope name, and the second parameter is an options hash. The only required option is :against, which tells pg_search_scope which column or columns to search against.
Searching against one column
To search against a column, pass a symbol as the :against option.
class BlogPost < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_by_title, against: :title
end
We now have an ActiveRecord scope named search_by_title on our BlogPost model. It takes one parameter, a search query string.
BlogPost.create!(title: "Recent Developments in the World of Pastrami")
BlogPost.create!(title: "Prosciutto and You: A Retrospective")
BlogPost.search_by_title("pastrami") # => [#<BlogPost id: 2, title: "Recent Developments in the World of Pastrami">]
Searching against multiple columns
Just pass an Array if you'd like to search more than one column.
class Person < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_by_full_name, against: [:first_name, :last_name]
end
Now our search query can match either or both of the columns.
person_1 = Person.create!(first_name: "Grant", last_name: "Hill")
person_2 = Person.create!(first_name: "Hugh", last_name: "Grant")
Person.search_by_full_name("Grant") # => [person_1, person_2]
Person.search_by_full_name("Grant Hill") # => [person_1]
Dynamic search scopes
Just like with Active Record named scopes, you can pass in a Proc object that returns a hash of options. For instance, the following scope takes a parameter that dynamically chooses which column to search against.
Important: The returned hash must include a :query key. Its value does not necessary have to be dynamic. You could choose to hard-code it to a specific value if you wanted.
class Person < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_by_name, lambda { |name_part, query|
raise ArgumentError unless [:first, :last].include?(name_part)
{
against: name_part,
query: query
}
}
end
person_1 = Person.create!(first_name: "Grant", last_name: "Hill")
person_2 = Person.create!(first_name: "Hugh", last_name: "Grant")
Person.search_by_name :first, "Grant" # => [person_1]
Person.search_by_name :last, "Grant" # => [person_2]
Searching through associations
It is possible to search columns on associated models. Note that if you do this, it will be impossible to speed up searches with database indexes. However, it is supported as a quick way to try out cross-model searching.
You can pass a Hash into the :associated_against option to set up searching through associations. The keys are the names of the associations and the value works just like an :against option for the other model. Right now, searching deeper than one association away is not supported. You can work around this by setting up a series of :through associations to point all the way through.
class Cracker < ActiveRecord::Base
has_many :cheeses
end
class Cheese < ActiveRecord::Base
end
class Salami < ActiveRecord::Base
include PgSearch::Model
belongs_to :cracker
has_many :cheeses, through: :cracker
pg_search_scope :tasty_search, associated_against: {
cheeses: [:kind, :brand],
cracker: :kind
}
end
salami_1 = Salami.create!
salami_2 = Salami.create!
salami_3 = Salami.create!
limburger = Cheese.create!(kind: "Limburger")
brie = Cheese.create!(kind: "Brie")
pepper_jack = Cheese.create!(kind: "Pepper Jack")
Cracker.create!(kind: "Black Pepper", cheeses: [brie], salami: salami_1)
Cracker.create!(kind: "Ritz", cheeses: [limburger, pepper_jack], salami: salami_2)
Cracker.create!(kind: "Graham", cheeses: [limburger], salami: salami_3)
Salami.tasty_search("pepper") # => [salami_1, salami_2]
Searching using different search features
By default, pg_search_scope uses the built-in PostgreSQL text search. If you pass the :using option to pg_search_scope, you can choose alternative search techniques.
class Beer < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_name, against: :name, using: [:tsearch, :trigram, :dmetaphone]
end
Here's an example if you pass multiple :using options with additional configurations.
class Beer < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_name,
against: :name,
using: {
:trigram => {},
:dmetaphone => {},
:tsearch => { :prefix => true }
}
end
The currently implemented features are
- :tsearch - Full text search, which is built-in to PostgreSQL
- :trigram - Trigram search, which requires the trigram extension
- :dmetaphone - Double Metaphone search, which requires the fuzzystrmatch extension
:tsearch (Full Text Search)
PostgreSQL's built-in full text search supports weighting, prefix searches, and stemming in multiple languages.
Weighting
Each searchable column can be given a weight of "A", "B", "C", or "D". Columns with earlier letters are weighted higher than those with later letters. So, in the following example, the title is the most important, followed by the subtitle, and finally the content.
class NewsArticle < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_full_text, against: {
title: 'A',
subtitle: 'B',
content: 'C'
}
end
You can also pass the weights in as an array of arrays, or any other structure that responds to #each and yields either a single symbol or a symbol and a weight. If you omit the weight, a default will be used.
class NewsArticle < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_full_text, against: [
[:title, 'A'],
[:subtitle, 'B'],
[:content, 'C']
]
end
class NewsArticle < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_full_text, against: [
[:title, 'A'],
{subtitle: 'B'},
:content
]
end
:prefix (PostgreSQL 8.4 and newer only)
PostgreSQL's full text search matches on whole words by default. If you want to search for partial words, however, you can set :prefix to true. Since this is a :tsearch-specific option, you should pass it to :tsearch directly, as shown in the following example.
class Superhero < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :whose_name_starts_with,
against: :name,
using: {
tsearch: { prefix: true }
}
end
batman = Superhero.create name: 'Batman'
batgirl = Superhero.create name: 'Batgirl'
robin = Superhero.create name: 'Robin'
Superhero.whose_name_starts_with("Bat") # => [batman, batgirl]
:negation
PostgreSQL's full text search matches all search terms by default. If you want
to exclude certain words, you can set :negation to true. Then any term that begins with
an exclamation point !
will be excluded from the results. Since this
is a :tsearch-specific option, you should pass it to :tsearch directly, as
shown in the following example.
Note that combining this with other search features can have unexpected results. For example, :trigram searches don't have a concept of excluded terms, and thus if you use both :tsearch and :trigram in tandem, you may still find results that contain the term that you were trying to exclude.
class Animal < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :with_name_matching,
against: :name,
using: {
tsearch: {negation: true}
}
end
one_fish = Animal.create(name: "one fish")
two_fish = Animal.create(name: "two fish")
red_fish = Animal.create(name: "red fish")
blue_fish = Animal.create(name: "blue fish")
Animal.with_name_matching("fish !red !blue") # => [one_fish, two_fish]
:dictionary
PostgreSQL full text search also support multiple dictionaries for stemming. You can learn more about how dictionaries work by reading the PostgreSQL documention. If you use one of the language dictionaries, such as "english", then variants of words (e.g. "jumping" and "jumped") will match each other. If you don't want stemming, you should pick the "simple" dictionary which does not do any stemming. If you don't specify a dictionary, the "simple" dictionary will be used.
class BoringTweet < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :kinda_matching,
against: :text,
using: {
tsearch: {dictionary: "english"}
}
pg_search_scope :literally_matching,
against: :text,
using: {
tsearch: {dictionary: "simple"}
}
end
sleep = BoringTweet.create! text: "I snoozed my alarm for fourteen hours today. I bet I can beat that tomorrow! #sleep"
sleeping = BoringTweet.create! text: "You know what I like? Sleeping. That's what. #enjoyment"
sleeps = BoringTweet.create! text: "In the jungle, the mighty jungle, the lion sleeps #tonight"
BoringTweet.kinda_matching("sleeping") # => [sleep, sleeping, sleeps]
BoringTweet.literally_matching("sleeps") # => [sleeps]
:normalization
PostgreSQL supports multiple algorithms for ranking results against queries. For instance, you might want to consider overall document size or the distance between multiple search terms in the original text. This option takes an integer, which is passed directly to PostgreSQL. According to the latest PostgreSQL documentation, the supported algorithms are:
0 (the default) ignores the document length
1 divides the rank by 1 + the logarithm of the document length
2 divides the rank by the document length
4 divides the rank by the mean harmonic distance between extents
8 divides the rank by the number of unique words in document
16 divides the rank by 1 + the logarithm of the number of unique words in document
32 divides the rank by itself + 1
This integer is a bitmask, so if you want to combine algorithms, you can add their numbers together. (e.g. to use algorithms 1, 8, and 32, you would pass 1 + 8 + 32 = 41)
class BigLongDocument < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :regular_search,
against: :text
pg_search_scope :short_search,
against: :text,
using: {
tsearch: {normalization: 2}
}
long = BigLongDocument.create!(text: "Four score and twenty years ago")
short = BigLongDocument.create!(text: "Four score")
BigLongDocument.regular_search("four score") #=> [long, short]
BigLongDocument.short_search("four score") #=> [short, long]
:any_word
Setting this attribute to true will perform a search which will return all models containing any word in the search terms.
class Number < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search_any_word,
against: :text,
using: {
tsearch: {any_word: true}
}
pg_search_scope :search_all_words,
against: :text
end
one = Number.create! text: 'one'
two = Number.create! text: 'two'
three = Number.create! text: 'three'
Number.search_any_word('one two three') # => [one, two, three]
Number.search_all_words('one two three') # => []
:sort_only
Setting this attribute to true will make this feature available for sorting, but will not include it in the query's WHERE condition.
class Person < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search,
against: :name,
using: {
tsearch: {any_word: true},
dmetaphone: {any_word: true, sort_only: true}
}
end
exact = Person.create!(name: 'ash hines')
one_exact_one_close = Person.create!(name: 'ash heinz')
one_exact = Person.create!(name: 'ash smith')
one_close = Person.create!(name: 'leigh heinz')
Person.search('ash hines') # => [exact, one_exact_one_close, one_exact]
:highlight
Adding .with_pg_search_highlight after the pg_search_scope you can access to
pg_highlight
attribute for each object.
class Person < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :search,
against: :bio,
using: {
tsearch: {
highlight: {
StartSel: '<b>',
StopSel: '</b>',
MaxWords: 123,
MinWords: 456,
ShortWord: 4,
HighlightAll: true,
MaxFragments: 3,
FragmentDelimiter: '…'
}
}
}
end
Person.create!(:bio => "Born in rural Alberta, where the buffalo roam.")
first_match = Person.search("Alberta").with_pg_search_highlight.first
first_match.pg_search_highlight # => "Born in rural <b>Alberta</b>, where the buffalo roam."
The highlight option accepts all options supported by ts_headline, and uses PostgreSQL's defaults.
See the documentation for details on the meaning of each option.
:dmetaphone (Double Metaphone soundalike search)
Double Metaphone is an algorithm for matching words that sound alike even if they are spelled very differently. For example, "Geoff" and "Jeff" sound identical and thus match. Currently, this is not a true double-metaphone, as only the first metaphone is used for searching.
Double Metaphone support is currently available as part of the fuzzystrmatch extension that must be installed before this feature can be used. In addition to the extension, you must install a utility function into your database. To generate and run a migration for this, run:
$ rails g pg_search:migration:dmetaphone
$ rake db:migrate
The following example shows how to use :dmetaphone.
class Word < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :that_sounds_like,
against: :spelling,
using: :dmetaphone
end
four = Word.create! spelling: 'four'
far = Word.create! spelling: 'far'
fur = Word.create! spelling: 'fur'
five = Word.create! spelling: 'five'
Word.that_sounds_like("fir") # => [four, far, fur]
:trigram (Trigram search)
Trigram search works by counting how many three-letter substrings (or "trigrams") match between the query and the text. For example, the string "Lorem ipsum" can be split into the following trigrams:
[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
Trigram search has some ability to work even with typos and misspellings in the query or text.
Trigram support is currently available as part of the pg_trgm extension that must be installed before this feature can be used.
class Website < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :kinda_spelled_like,
against: :name,
using: :trigram
end
yahooo = Website.create! name: "Yahooo!"
yohoo = Website.create! name: "Yohoo!"
gogle = Website.create! name: "Gogle"
facebook = Website.create! name: "Facebook"
Website.kinda_spelled_like("Yahoo!") # => [yahooo, yohoo]
:threshold
By default, trigram searches find records which have a similarity of at least 0.3
using pg_trgm's calculations. You may specify a custom threshold if you prefer.
Higher numbers match more strictly, and thus return fewer results. Lower numbers
match more permissively, letting in more results. Please note that setting a trigram
threshold will force a table scan as the derived query uses the
similarity()
function instead of the %
operator.
class Vegetable < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :strictly_spelled_like,
against: :name,
using: {
trigram: {
threshold: 0.5
}
}
pg_search_scope :roughly_spelled_like,
against: :name,
using: {
trigram: {
threshold: 0.1
}
}
end
cauliflower = Vegetable.create! name: "cauliflower"
Vegetable.roughly_spelled_like("couliflower") # => [cauliflower]
Vegetable.strictly_spelled_like("couliflower") # => [cauliflower]
Vegetable.roughly_spelled_like("collyflower") # => [cauliflower]
Vegetable.strictly_spelled_like("collyflower") # => []
:word_similarity
Allows you to match words in longer strings.
By default, trigram searches use %
or similarity()
as a similarity value.
Set word_similarity
to true
to opt for <%
and word_similarity
instead.
This causes the trigram search to use the similarity of the query term
and the word with greatest similarity.
class Sentence < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :similarity_like,
against: :name,
using: {
trigram: {
word_similarity: true
}
}
pg_search_scope :word_similarity_like,
against: :name,
using: [:trigram]
end
sentence = Sentence.create! name: "Those are two words."
Sentence.similarity_like("word") # => []
Sentence.word_similarity_like("word") # => [sentence]
Limiting Fields When Combining Features
Sometimes when doing queries combining different features you might want to search against only some of the fields with certain features. For example perhaps you want to only do a trigram search against the shorter fields so that you don't need to reduce the threshold excessively. You can specify which fields using the 'only' option:
class Image < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :combined_search,
against: [:file_name, :short_description, :long_description]
using: {
tsearch: { dictionary: 'english' },
trigram: {
only: [:file_name, :short_description]
}
}
end
Now you can succesfully retrieve an Image with a file_name: 'image_foo.jpg' and long_description: 'This description is so long that it would make a trigram search fail any reasonable threshold limit' with:
Image.combined_search('reasonable') # found with tsearch
Image.combined_search('foo') # found with trigram
Ignoring accent marks
Most of the time you will want to ignore accent marks when searching. This makes it possible to find words like "piñata" when searching with the query "pinata". If you set a pg_search_scope to ignore accents, it will ignore accents in both the searchable text and the query terms.
Ignoring accents uses the unaccent extension that must be installed before this feature can be used.
class SpanishQuestion < ActiveRecord::Base
include PgSearch::Model
pg_search_scope :gringo_search,
against: :word,
ignoring: :accents
end
what = SpanishQuestion.create(word: "Qué")
how_many = SpanishQuestion.create(word: "Cuánto")
how = SpanishQuestion.create(word: "Cómo")
SpanishQuestion.gringo_search("Que") # => [what]
SpanishQuestion.gringo_search("Cüåñtô") # => [how_many]
Advanced users may wish to add indexes for the expressions that pg_search generates. Unfortunately, the unaccent function supplied by this extension is not indexable (as of PostgreSQL 9.1). Thus, you may want to write your own wrapper function and use it instead. This can be configured by calling the following code, perhaps in an initializer.
PgSearch.unaccent_function = "my_unaccent"
Using tsvector columns
PostgreSQL allows you the ability to search against a column with type tsvector instead of using an expression; this speeds up searching dramatically as it offloads creation of the tsvector that the tsquery is evaluated against.
To use this functionality you'll need to do a few things:
-
Create a column of type tsvector that you'd like to search against. If you want to search using multiple search methods, for example tsearch and dmetaphone, you'll need a column for each.
-
Create a trigger function that will update the column(s) using the expression appropriate for that type of search. See: the PostgreSQL documentation for text search triggers
-
Should you have any pre-existing data in the table, update the newly-created tsvector columns with the expression that your trigger function uses.
-
Add the option to pg_search_scope, e.g:
pg_search_scope :fast_content_search, against: :content, using: { dmetaphone: { tsvector_column: 'tsvector_content_dmetaphone' }, tsearch: { dictionary: 'english', tsvector_column: 'tsvector_content_tsearch' }, trigram: {} # trigram does not use tsvectors }
Please note that the :against column is only used when the tsvector_column is not present for the search type.
Combining multiple tsvectors
It's possible to search against more than one tsvector at a time. This could be useful if you want to maintain multiple search scopes but do not want to maintain separate tsvectors for each scope. For example:
pg_search_scope :search_title,
against: :title,
using: {
tsearch: {
tsvector_column: "title_tsvector"
}
}
pg_search_scope :search_body,
against: :body,
using: {
tsearch: {
tsvector_column: "body_tsvector"
}
}
pg_search_scope :search_title_and_body,
against: [:title, :body],
using: {
tsearch: {
tsvector_column: ["title_tsvector", "body_tsvector"]
}
}
Configuring ranking and ordering
:ranked_by (Choosing a ranking algorithm)
By default, pg_search ranks results based on the :tsearch similarity between the searchable text and the query. To use a different ranking algorithm, you can pass a :ranked_by option to pg_search_scope.
pg_search_scope :search_by_tsearch_but_rank_by_trigram,
against: :title,
using: [:tsearch],
ranked_by: ":trigram"
Note that :ranked_by using a String to represent the ranking expression. This allows for more complex possibilities. Strings like ":tsearch", ":trigram", and ":dmetaphone" are automatically expanded into the appropriate SQL expressions.
# Weighted ranking to balance multiple approaches
ranked_by: ":dmetaphone + (0.25 * :trigram)"
# A more complex example, where books.num_pages is an integer column in the table itself
ranked_by: "(books.num_pages * :trigram) + (:tsearch / 2.0)"
:order_within_rank (Breaking ties)
PostgreSQL does not guarantee a consistent order when multiple records have the same value in the ORDER BY clause. This can cause trouble with pagination. Imagine a case where 12 records all have the same ranking value. If you use a pagination library such as kaminari or will_paginate to return results in pages of 10, then you would expect to see 10 of the records on page 1, and the remaining 2 records at the top of the next page, ahead of lower-ranked results.
But since there is no consistent ordering, PostgreSQL might choose to rearrange the order of those 12 records between different SQL statements. You might end up getting some of the same records from page 1 on page 2 as well, and likewise there may be records that don't show up at all.
pg_search fixes this problem by adding a second expression to the ORDER BY clause, after the :ranked_by expression explained above. By default, the tiebreaker order is ascending by id.
ORDER BY [complicated :ranked_by expression...], id ASC
This might not be desirable for your application, especially if you do not want old records to outrank new records. By passing an :order_within_rank, you can specify an alternate tiebreaker expression. A common example would be descending by updated_at, to rank the most recently updated records first.
pg_search_scope :search_and_break_ties_by_latest_update,
against: [:title, :content],
order_within_rank: "blog_posts.updated_at DESC"
PgSearch#pg_search_rank (Reading a record's rank as a Float)
It may be useful or interesting to see the rank of a particular record. This can be helpful for debugging why one record outranks another. You could also use it to show some sort of relevancy value to end users of an application.
To retrieve the rank, call .with_pg_search_rank
on a scope, and then call
.pg_search_rank
on a returned record.
shirt_brands = ShirtBrand.search_by_name("Penguin").with_pg_search_rank
shirt_brands[0].pg_search_rank #=> 0.0759909
shirt_brands[1].pg_search_rank #=> 0.0607927
Search rank and chained scopes
Each PgSearch scope generates a named subquery for the search rank. If you
chain multiple scopes then PgSearch will generate a ranking query for each
scope, so the ranking queries must have unique names. If you need to reference
the ranking query (e.g. in a GROUP BY clause) you can regenerate the subquery
name with the PgScope::Configuration.alias
method by passing the name of the
queried table.
shirt_brands = ShirtBrand.search_by_name("Penguin")
.joins(:shirt_sizes)
.group("shirt_brands.id, #{PgSearch::Configuration.alias('shirt_brands')}.rank")
ATTRIBUTIONS
PgSearch would not have been possible without inspiration from texticle (now renamed textacular). Thanks to Aaron Patterson for the original version and to Casebook PBC (https://www.casebook.net) for gifting the community with it!
CONTRIBUTIONS AND FEEDBACK
Please read our CONTRIBUTING guide.
We also have a Google Group for discussing pg_search and other Casebook PBC open source projects.
LICENSE
Copyright © 2010â2022 Casebook PBC. Licensed under the MIT license, see LICENSE file.
Top Related Projects
Intelligent search made easy
Elasticsearch integrations for ActiveModel/Record and Ruby on Rails
Object-based searching.
Sphinx/Manticore plugin for ActiveRecord/Rails
Convert designs to code with AI
Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.
Try Visual Copilot