PgSearch создает именованные области, использующие преимущества полнотекстового поиска PostgreSQL.
Прочтите сообщение в блоге, посвященное PgSearch, по адресу https://tanzu.vmware.com/content/blog/pg-search-how-i-learned-to-stop-worrying-and-love-postgresql-full-text-search.
$ gem install pg_search
или добавьте эту строку в свой Gemfile:
gem 'pg_search'
Помимо установки и запроса драгоценного камня, вы можете включить в свой Rakefile задачи PgSearch rake. Это не обязательно для проектов Rails, которые получают задачи Rake через Railtie.
load "pg_search/tasks.rb"
Чтобы добавить PgSearch в модель Active Record, просто включите модуль PgSearch.
class Shape < ActiveRecord :: Base
include PgSearch :: Model
end
multisearchable
pg_search_scope
:tsearch
(полнотекстовый поиск):prefix
(только PostgreSQL 8.4 и новее):negation
:dictionary
:normalization
:any_word
:sort_only
:highlight
:dmetaphone
(поиск двойного звука Метафона):trigram
(поиск триграмм):threshold
:word_similarity
:ranked_by
(Выбор алгоритма ранжирования):order_within_rank
(Разрыв связей)PgSearch#pg_search_rank
(Чтение ранга записи как числа с плавающей запятой)pg_search поддерживает два разных метода поиска: множественный поиск и области поиска.
Первый метод — это множественный поиск, при котором записи множества различных классов Active Record можно смешивать в один глобальный индекс поиска по всему приложению. Большинство сайтов, которые хотят поддерживать общую страницу поиска, захотят использовать эту функцию.
Другой метод — это области поиска, которые позволяют выполнять более расширенный поиск только по одному классу Active Record. Это более полезно для создания таких вещей, как автозаполнение или фильтрация списка элементов в фасетном поиске.
Прежде чем использовать множественный поиск, необходимо сгенерировать и запустить миграцию для создания таблицы базы данных pg_search_documents.
$ rails g pg_search:migration:multisearch
$ rake db:migrate
Чтобы добавить модель в глобальный поисковый индекс вашего приложения, вызовите multisearchable в определении его класса.
class EpicPoem < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :title , :author ]
end
class Flower < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : :color
end
Если в этой модели уже есть записи, вам потребуется переиндексировать эту модель, чтобы добавить существующие записи в таблицу pg_search_documents. См. задачу восстановления ниже.
Всякий раз, когда запись создается, обновляется или уничтожается, срабатывает обратный вызов Active Record, что приводит к созданию соответствующей записи PgSearch::Document в таблице pg_search_documents. Опция :against может обозначать один или несколько методов, которые будут вызываться для записи для генерации текста поиска.
Вы также можете передать имя процедуры или метода для вызова, чтобы определить, следует ли включать конкретную запись.
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
Обратите внимание, что имя процедуры или метода вызывается в ловушке after_save. Это означает, что вам следует быть осторожными при использовании времени или других объектов. В следующем примере, если запись была последний раз сохранена до отметки времениPublish_at, она вообще не будет отображаться в глобальном поиске, пока к ней не будут применены еще раз после отметки времени.
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
Условно обновить pg_search_documents
Вы также можете использовать опцию :update_if
для передачи имени процедуры или метода для вызова, чтобы определить, следует ли обновлять конкретную запись.
Обратите внимание, что имя процедуры или метода вызывается в хуке after_save
, поэтому, если вы полагаетесь на грязные флаги ActiveRecord, используйте *_previously_changed?
.
class Message < ActiveRecord :: Base
include PgSearch :: Model
multisearchable against : [ :body ] ,
update_if : :body_previously_changed?
end
Укажите дополнительные атрибуты, которые будут сохранены в таблице pg_search_documents.
Вы можете указать :additional_attributes
для сохранения в таблице pg_search_documents
. Например, возможно, вы индексируете модель книги и модель статьи и хотите включить в нее атрибутauthor_id.
Во-первых, нам нужно добавить ссылку на автора в миграцию, создающую нашу таблицу pg_search_documents
.
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
Затем мы можем отправить этот дополнительный атрибут в лямбда-выражении.
multisearchable (
against : [ :title , :body ] ,
additional_attributes : -> ( article ) { { author_id : article . author_id } }
)
Это позволяет значительно ускорить поиск без последующих объединений, выполнив что-то вроде:
PgSearch . multisearch ( params [ 'search' ] ) . where ( author_id : 2 )
ПРИМЕЧАНИЕ. В настоящее время вам необходимо вручную вызвать record.update_pg_search_document
, чтобы дополнительный атрибут был включен в таблицу pg_search_documents.
Две ассоциации создаются автоматически. В исходной записи имеется ассоциация has_one :pg_search_document, указывающая на запись PgSearch::Document, а в записи PgSearch::Document — полиморфная ассоциация own_to :searchable, указывающая на исходную запись.
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">
Чтобы получить записи PgSearch::Document для всех записей, соответствующих данному запросу, используйте 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>]
PgSearch.multisearch возвращает ActiveRecord::Relation, как и области видимости, поэтому вы можете связывать вызовы областей до конца. Это работает с такими драгоценными камнями, как Каминари, которые добавляют методы области действия. Как и в случае с обычными областями, база данных будет получать SQL-запросы только при необходимости.
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 )
PgSearch.multisearch можно настроить с использованием тех же параметров, что и pg_search_scope
(более подробно описано ниже). Просто установите PgSearch.multisearch_options в инициализаторе:
PgSearch . multisearch_options = {
using : [ :tsearch , :trigram ] ,
ignoring : :accents
}
Если вы измените параметр :against класса, добавите multisearchable к классу, который уже имеет записи в базе данных, или удалите multisearchable из класса, чтобы удалить его из индекса, вы обнаружите, что таблица pg_search_documents может стать недействительной. синхронизации с фактическими записями в других таблицах.
Индекс также может рассинхронизироваться, если вы когда-либо изменяете записи таким образом, чтобы не вызывать обратные вызовы Active Record. Например, метод экземпляра #update_attribute и метод класса .update_all пропускают обратные вызовы и напрямую изменяют базу данных.
Чтобы удалить все документы для данного класса, вы можете просто удалить все записи PgSearch::Document.
PgSearch :: Document . delete_by ( searchable_type : "Animal" )
Чтобы восстановить документы для данного класса, запустите:
PgSearch :: Multisearch . rebuild ( Product )
Метод rebuild
удалит все документы данного класса перед их повторной генерацией. В некоторых ситуациях это может быть нежелательно, например, когда вы используете наследование одной таблицы, а ваш базовый класс — searchable_type
. Вы можете предотвратить удаление ваших записей при rebuild
следующим образом:
PgSearch :: Multisearch . rebuild ( Product , clean_up : false )
rebuild
выполняется внутри одной транзакции. Чтобы работать вне транзакции, вы можете передать transactional: false
следующим образом:
PgSearch :: Multisearch . rebuild ( Product , transactional : false )
Для удобства перестроение также доступно как задача Rake.
$ rake pg_search:multisearch:rebuild[BlogPost]
Можно передать второй необязательный аргумент, чтобы указать путь поиска схемы PostgreSQL, который будет использоваться для мультитенантных баз данных, имеющих несколько таблиц pg_search_documents. Ниже перед переиндексацией в качестве пути поиска схемы будет установлено значение «my_schema».
$ rake pg_search:multisearch:rebuild[BlogPost,my_schema]
Для моделей, поддерживающих множественный поиск :against
методами, которые напрямую сопоставляются с атрибутами Active Record, запускается один эффективный оператор SQL для одновременного обновления всей таблицы pg_search_documents
. Однако если вы вызываете какие-либо динамические методы в :against
, то update_pg_search_document
будет вызываться для отдельных записей, индексируемых в пакетном режиме.
Вы также можете предоставить собственную реализацию для восстановления документов, добавив в свою модель метод класса rebuild_pg_search_documents
.
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
Примечание. Если вы используете PostgreSQL до версии 9.1, замените вызов функции CONCAT_WS()
двухконвейерной конкатенацией, например. (movies.name || ' ' || directors.name)
. Однако теперь имейте в виду, что если какое-либо из объединенных значений равно NULL, то окончательное значение content
также будет NULL, тогда как CONCAT_WS()
будет выборочно игнорировать значения NULL.
Если вам предстоит выполнить объемную операцию, например импорт большого количества записей из внешнего источника, возможно, вы захотите ускорить процесс, временно отключив индексирование. Затем вы можете использовать один из описанных выше методов для восстановления документов поиска в автономном режиме.
PgSearch . disable_multisearch do
Movie . import_from_xml_file ( File . open ( "movies.xml" ) )
end
Вы можете использовать pg_search_scope для создания области поиска. Первый параметр — это имя области, а второй параметр — хеш опций. Единственная обязательная опция — :against, которая сообщает pg_search_scope, по какому столбцу или столбцам выполнять поиск.
Для поиска по столбцу передайте символ в качестве опции : Against.
class BlogPost < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_title , against : :title
end
Теперь у нас есть область ActiveRecord с именем search_by_title в нашей модели BlogPost. Он принимает один параметр — строку поискового запроса.
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">]
Просто передайте массив, если вы хотите выполнить поиск по нескольким столбцам.
class Person < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_by_full_name , against : [ :first_name , :last_name ]
end
Теперь наш поисковый запрос может соответствовать одному или обоим столбцам.
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]
Как и в случае с именованными областями Active Record, вы можете передать объект Proc, который возвращает хеш параметров. Например, следующая область принимает параметр, который динамически выбирает столбец для поиска.
Важно: возвращаемый хэш должен включать ключ :query. Его значение не обязательно должно быть динамическим. Если хотите, вы можете жестко запрограммировать для него определенное значение.
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]
Возможен поиск столбцов по связанным моделям. Обратите внимание: если вы это сделаете, ускорить поиск по индексам базы данных будет невозможно. Однако он поддерживается как быстрый способ опробовать поиск между моделями.
Вы можете передать хеш в параметр :associated_against, чтобы настроить поиск по ассоциациям. Ключи — это имена ассоциаций, а значение работает так же, как опция : Against для другой модели. В настоящее время поиск глубже одной ассоциации не поддерживается. Вы можете обойти эту проблему, установив ряд ассоциаций :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]
По умолчанию pg_search_scope использует встроенный текстовый поиск PostgreSQL. Если вы передадите параметр :using функции pg_search_scope, вы сможете выбрать альтернативные методы поиска.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name , against : :name , using : [ :tsearch , :trigram , :dmetaphone ]
end
Вот пример, если вы передаете несколько параметров :using с дополнительными конфигурациями.
class Beer < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_name ,
against : :name ,
using : {
:trigram => { } ,
:dmetaphone => { } ,
:tsearch => { :prefix => true }
}
end
Реализованные на данный момент функции:
Встроенный полнотекстовый поиск PostgreSQL поддерживает взвешивание, поиск по префиксам и стемминг на нескольких языках.
Каждому столбцу, доступному для поиска, может быть присвоен вес «A», «B», «C» или «D». Столбцы с более ранними буквами имеют больший вес, чем столбцы с более поздними буквами. Итак, в следующем примере заголовок является наиболее важным, за ним следует подзаголовок и, наконец, содержание.
class NewsArticle < ActiveRecord :: Base
include PgSearch :: Model
pg_search_scope :search_full_text , against : {
title : 'A' ,
subtitle : 'B' ,
content : 'C'
}
end
Вы также можете передать веса в виде массива массивов или любой другой структуры, которая реагирует на #each и возвращает либо один символ, либо символ и вес. Если вы опустите вес, будет использовано значение по умолчанию.
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
Полнотекстовый поиск PostgreSQL по умолчанию находит целые слова. Однако если вы хотите искать части слов, вы можете установить для :prefix значение true. Поскольку это опция, специфичная для :tsearch, вам следует передать ее непосредственно в :tsearch, как показано в следующем примере.
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]
Полнотекстовый поиск PostgreSQL по умолчанию соответствует всем критериям поиска. Если вы хотите исключить определенные слова, вы можете установить для :negation значение true. Тогда любой термин, начинающийся с восклицательного знака !
будут исключены из результатов. Поскольку это опция, специфичная для :tsearch, вам следует передать ее непосредственно в :tsearch, как показано в следующем примере.
Обратите внимание, что объединение этой функции с другими функциями поиска может привести к неожиданным результатам. Например, в поиске :trigram нет концепции исключенных терминов, и поэтому, если вы используете :tsearch и :trigram одновременно, вы все равно можете найти результаты, содержащие термин, который вы пытались исключить.
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]
Полнотекстовый поиск PostgreSQL также поддерживает несколько словарей для стемминга. Вы можете узнать больше о том, как работают словари, прочитав документацию PostgreSQL. Если вы используете один из языковых словарей, например «английский», то варианты слов (например, «прыгал» и «прыгнул») будут соответствовать друг другу. Если вы не хотите использовать стемминг, вам следует выбрать «простой» словарь, который не выполняет стемминг. Если вы не укажете словарь, будет использоваться «простой» словарь.
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]
PostgreSQL поддерживает несколько алгоритмов ранжирования результатов по запросам. Например, вы можете учитывать общий размер документа или расстояние между несколькими поисковыми запросами в исходном тексте. Эта опция принимает целое число, которое передается непосредственно в PostgreSQL. Согласно последней документации PostgreSQL, поддерживаемые алгоритмы:
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
Это целое число является битовой маской, поэтому, если вы хотите объединить алгоритмы, вы можете сложить их числа. (например, чтобы использовать алгоритмы 1, 8 и 32, вы должны передать 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]
Если для этого атрибута установлено значение true, будет выполнен поиск, который вернет все модели, содержащие любое слово в условиях поиска.
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' ) # => []
Установка для этого атрибута значения true сделает эту функцию доступной для сортировки, но не будет включать ее в условие WHERE запроса.
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]
Добавив .with_pg_search_highlight после pg_search_scope, вы можете получить доступ к атрибуту pg_highlight
для каждого объекта.
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."
Опция выделения принимает все параметры, поддерживаемые ts_headline, и использует значения по умолчанию PostgreSQL.
Подробную информацию о значении каждой опции смотрите в документации.
Double Metaphone — это алгоритм сопоставления слов, которые звучат одинаково, даже если они пишутся по-разному. Например, «Джефф» и «Джефф» звучат одинаково и, следовательно, совпадают. В настоящее время это не настоящий двойной метафон, поскольку для поиска используется только первый метафон.
Поддержка Double Metaphone в настоящее время доступна как часть расширения fuzzystrmatch, которое необходимо установить перед использованием этой функции. Помимо расширения, в вашу базу данных необходимо установить служебную функцию. Чтобы сгенерировать и запустить миграцию, выполните:
$ rails g pg_search:migration:dmetaphone
$ rake db:migrate
В следующем примере показано, как использовать :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]
Поиск по триграммам работает путем подсчета количества трехбуквенных подстрок (или «триграмм»), соответствующих запросу и тексту. Например, строку «Lorem ipsum» можно разбить на следующие триграммы:
[" Lo", "Lor", "ore", "rem", "em ", "m i", " ip", "ips", "psu", "sum", "um ", "m "]
Триграммный поиск имеет некоторую возможность работать даже с опечатками и орфографическими ошибками в запросе или тексте.
Поддержка Trigram в настоящее время доступна как часть расширения pg_trgm, которое необходимо установить, прежде чем можно будет использовать эту функцию.
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]
По умолчанию поиск по триграммам находит записи, которые имеют сходство не менее 0,3, используя вычисления pg_trgm. При желании вы можете указать собственный порог. Более высокие числа соответствуют более строго и, следовательно, возвращают меньше результатов. Меньшие числа соответствуют более разрешающе, позволяя получить больше результатов. Обратите внимание, что установка порога триграммы приведет к принудительному сканированию таблицы, поскольку производный запрос использует функцию similarity()
вместо оператора %
.
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" ) # => []
Позволяет сопоставлять слова в более длинных строках. По умолчанию при поиске триграмм в качестве значения сходства используется %
или similarity()
. Установите для word_similarity
значение true
, чтобы вместо этого выбрать <%
и word_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]
Иногда при выполнении запросов, сочетающих различные функции, вам может потребоваться выполнить поиск только по некоторым полям с определенными функциями. Например, возможно, вы хотите выполнять поиск по триграммам только по более коротким полям, чтобы вам не приходилось чрезмерно уменьшать порог. Вы можете указать, какие поля используют опцию «только»:
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
Теперь вы можете успешно получить изображение с именем файла: 'image_foo.jpg' и long_description: 'Это описание настолько длинное, что оно приведет к тому, что поиск по триграмме не сможет выполнить любой разумный пороговый предел' с помощью:
Image . combined_search ( 'reasonable' ) # found with tsearch
Image . combined_search ( 'foo' ) # found with trigram
В большинстве случаев вам захочется игнорировать знаки ударения при поиске. Это позволяет находить такие слова, как «пиньята», при поиске по запросу «пината». Если вы настроите pg_search_scope на игнорирование диакритических знаков, они будут игнорироваться как в тексте, доступном для поиска, так и в терминах запроса.
Для игнорирования акцентов используется расширение unaccent, которое необходимо установить, прежде чем можно будет использовать эту функцию.
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]
Опытные пользователи могут захотеть добавить индексы для выражений, генерируемых pg_search. К сожалению, функция unaccent, предоставляемая этим расширением, не является индексируемой (начиная с PostgreSQL 9.1). Таким образом, вы можете написать свою собственную функцию-обертку и использовать ее. Это можно настроить, вызвав следующий код, возможно, в инициализаторе.
PgSearch . unaccent_function = "my_unaccent"
PostgreSQL позволяет вам выполнять поиск по столбцу типа tsvector вместо использования выражения; это значительно ускоряет поиск, поскольку снимает нагрузку с создания tsvector, по которому оценивается tsquery.
Чтобы использовать эту функцию, вам необходимо сделать несколько вещей:
Создайте столбец типа tsvector, по которому вы хотите выполнить поиск. Если вы хотите выполнить поиск, используя несколько методов поиска, например tsearch и dmetaphone, вам понадобится столбец для каждого.
Создайте триггерную функцию, которая будет обновлять столбцы, используя выражение, подходящее для этого типа поиска. См. документацию PostgreSQL по триггерам текстового поиска.
Если у вас есть какие-либо ранее существовавшие данные в таблице, обновите вновь созданные столбцы tsvector выражением, которое использует ваша триггерная функция.
Добавьте опцию в pg_search_scope, например:
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
}
Обратите внимание, что столбец :against используется только в том случае, если для типа поиска отсутствует tsvector_column.
Можно искать более чем по одному tsvector одновременно. Это может быть полезно, если вы хотите поддерживать несколько областей поиска, но не хотите поддерживать отдельные tsvector для каждой области. Например:
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" ]
}
}
По умолчанию pg_search ранжирует результаты на основе сходства :tsearch между текстом, доступным для поиска, и запросом. Чтобы использовать другой алгоритм ранжирования, вы можете передать параметр :ranked_by в pg_search_scope.
pg_search_scope :search_by_tsearch_but_rank_by_trigram ,
against : :title ,
using : [ :tsearch ] ,
ranked_by : ":trigram"
Обратите внимание, что :ranked_by использует строку для представления выражения ранжирования. Это позволяет реализовать более сложные возможности. Строки типа «:tsearch», «:trigram» и «:dmetaphone» автоматически преобразуются в соответствующие выражения SQL.
# 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)"
PostgreSQL не гарантирует согласованный порядок, если несколько записей имеют одно и то же значение в предложении ORDER BY. Это может вызвать проблемы с нумерацией страниц. Представьте себе случай, когда все 12 записей имеют одинаковое значение ранжирования. Если вы используете библиотеку нумерации страниц, такую как kaminari или will_paginate, для возврата результатов на страницах по 10, то вы ожидаете увидеть 10 записей на странице 1, а оставшиеся 2 записи — вверху следующей страницы, перед нижними. ранжированные результаты.
Но поскольку последовательного порядка нет, PostgreSQL может изменить порядок этих 12 записей между различными операторами SQL. В конечном итоге вы можете получить некоторые из тех же записей со страницы 1 на странице 2, а также могут быть записи, которые вообще не отображаются.
pg_search решает эту проблему, добавляя второе выражение в предложение ORDER BY после выражения :ranked_by, описанного выше. По умолчанию порядок тай-брейка увеличивается по идентификатору.
ORDER BY [complicated :ranked_by expression...], id ASC
Это может быть нежелательно для вашего приложения, особенно если вы не хотите, чтобы старые записи превосходили новые записи. Передавая :order_within_rank, вы можете указать альтернативное выражение разрешения конфликтов. Типичным примером может быть нисходящий по обновленному_at, чтобы ранжировать в первую очередь самые последние обновленные записи.
pg_search_scope :search_and_break_ties_by_latest_update ,
against : [ :title , :content ] ,
order_within_rank : "blog_posts.updated_at DESC"
Может быть полезно или интересно узнать рейтинг конкретной записи. Это может быть полезно для устранения причин, по которым одна запись превосходит другую. Вы также можете использовать его, чтобы показать конечным пользователям приложения какую-то ценность релевантности.
Чтобы получить рейтинг, вызовите .with_pg_search_rank
для области, а затем вызовите .pg_search_rank
для возвращаемой записи.
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
Каждая область PgSearch генерирует именованный подзапрос для ранга поиска. Если вы объединяете несколько областей, PgSearch сгенерирует запрос ранжирования для каждой области, поэтому запросы ранжирования должны иметь уникальные имена. Если вам нужно сослаться на запрос ранжирования (например, в предложении GROUP BY), вы можете повторно создать имя подзапроса с помощью метода PgScope::Configuration.alias
, передав имя запрашиваемой таблицы.
shirt_brands = ShirtBrand . search_by_name ( "Penguin" )
. joins ( :shirt_sizes )
. group ( "shirt_brands.id, #{ PgSearch :: Configuration . alias ( 'shirt_brands' ) } .rank" )
PgSearch был бы невозможен без вдохновения от Texticle (теперь переименованного в Textacular). Спасибо Аарону Паттерсону за оригинальную версию и Casebook PBC (https://www.casebook.net) за то, что подарили ее сообществу!
Пожалуйста, прочтите наше руководство по ВКЛАДУ.
У нас также есть группа Google для обсуждения pg_search и других проектов с открытым исходным кодом Casebook PBC.
Авторские права © 2010–2022 Casebook PBC. Лицензия MIT, см. файл LICENSE.