SearchCop расширяет ваши модели ActiveRecord для поддержки системы полнотекстового поиска, например, запросов с помощью простых строк запроса и запросов на основе хеша. Предположим, у вас есть модель Book
с различными атрибутами, такими как title
, author
, stock
, price
, available
. Используя SearchCop, вы можете выполнить:
Book . search ( "Joanne Rowling Harry Potter" )
Book . search ( "author: Rowling title:'Harry Potter'" )
Book . search ( "price > 10 AND price < 20 -stock:0 (Potter OR Rowling)" )
# ...
Таким образом, вы можете передать строку поискового запроса своим моделям, и вы, администраторы вашего приложения и/или пользователи получат мощные функции запросов без необходимости интеграции дополнительных сторонних поисковых серверов, поскольку SearchCop может использовать возможности полнотекстового индексирования вашей СУБД в независимый от базы данных способ (в настоящее время поддерживаются полнотекстовые индексы MySQL и PostgreSQL) и оптимизирует запросы для их оптимального использования. Подробнее читайте ниже.
Также поддерживаются сложные запросы на основе хеша:
Book . search ( author : "Rowling" , title : "Harry Potter" )
Book . search ( or : [ { author : "Rowling" } , { author : "Tolkien" } ] )
Book . search ( and : [ { price : { gt : 10 } } , { not : { stock : 0 } } , or : [ { title : "Potter" } , { author : "Rowling" } ] ] )
Book . search ( or : [ { query : "Rowling -Potter" } , { query : "Tolkien -Rings" } ] )
Book . search ( title : { my_custom_sql_query : "Rowl" } } )
# ...
Добавьте эту строку в Gemfile вашего приложения:
gem 'search_cop'
И затем выполните:
$ bundle
Или установите его самостоятельно как:
$ gem install search_cop
Чтобы включить SearchCop для модели, include SearchCop
и укажите атрибуты, которые вы хотите предоставлять поисковым запросам в search_scope
:
class Book < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :title , :description , :stock , :price , :created_at , :available
attributes comment : [ "comments.title" , "comments.message" ]
attributes author : "author.name"
# ...
end
has_many :comments
belongs_to :author
end
Конечно, вы также можете указать несколько блоков search_scope
по своему усмотрению:
search_scope :admin_search do
attributes :title , :description , :stock , :price , :created_at , :available
# ...
end
search_scope :user_search do
attributes :title , :description
# ...
end
SearchCop анализирует запрос и сопоставляет его с запросом SQL независимо от базы данных. Таким образом, SearchCop не привязан к конкретной СУБД.
Book . search ( "stock > 0" )
# ... WHERE books.stock > 0
Book . search ( "price > 10 stock > 0" )
# ... WHERE books.price > 10 AND books.stock > 0
Book . search ( "Harry Potter" )
# ... WHERE (books.title LIKE '%Harry%' OR books.description LIKE '%Harry%' OR ...) AND (books.title LIKE '%Potter%' OR books.description LIKE '%Potter%' ...)
Book . search ( "available:yes OR created_at:2014" )
# ... WHERE books.available = 1 OR (books.created_at >= '2014-01-01 00:00:00.00000' and books.created_at <= '2014-12-31 23:59:59.99999')
SearchCop использует методы Begin_of_year и End_of_year ActiveSupport для значений, используемых при построении SQL-запроса для этого случая.
Конечно, эти запросы LIKE '%...%'
не обеспечат оптимальную производительность, но ознакомьтесь с разделом ниже, посвященным полнотекстовым возможностям SearchCop, чтобы понять, как можно оптимизировать полученные запросы.
Поскольку Book.search(...)
возвращает ActiveRecord::Relation
, вы можете выполнять предварительную или постобработку результатов поиска любым возможным способом:
Book . where ( available : true ) . search ( "Harry Potter" ) . order ( "books.id desc" ) . paginate ( page : params [ :page ] )
Когда вы передаете строку запроса в SearchCop, она анализируется, анализируется и сопоставляется для окончательного создания SQL-запроса. Точнее, когда SearchCop анализирует запрос, он создает объекты (узлы), которые представляют выражения запроса (узлы «И», «Или», «Не», «Строка», «Дата» и т. д.). Для построения запроса SQL SearchCop использует концепцию посетителей, например, используемую в Arel, так что для каждого узла должен быть посетитель, который преобразует узел в SQL. Если посетитель отсутствует, возникает исключение, когда построитель запросов пытается «посетить» узел. Посетители несут ответственность за очистку введенных пользователем данных. В основном это делается с помощью кавычек (строки, имени таблицы, кавычек столбца и т. д.). SearchCop использует методы, предоставляемые адаптером подключения ActiveRecord, для очистки/цитирования во избежание SQL-инъекций. Хотя мы никогда не можем быть на 100% застрахованы от проблем безопасности, SearchCop серьезно относится к вопросам безопасности. Пожалуйста, ответственно сообщайте через службу безопасности на сайте flakks dot com, если вы обнаружите какие-либо проблемы, связанные с безопасностью.
SearchCop поддерживает поля json для MySQL, а также поля json, jsonb и hstore для postgres. В настоящее время ожидается, что значения полей всегда будут строками, а массивы не поддерживаются. Вы можете указать атрибуты json с помощью:
search_scope :search do
attributes user_agent : "context->browser->user_agent"
# ...
end
где context
— это столбец json/jsonb, который, например, содержит:
{
"browser" : {
"user_agent" : " Firefox ... "
}
}
По умолчанию, т. е. если вы не сообщите SearchCop о своих полнотекстовых индексах, SearchCop будет использовать запросы LIKE '%...%'
. К сожалению, если вы не создадите индекс триграммы (только postgres), эти запросы не смогут использовать индексы SQL, поэтому каждая строка должна сканироваться вашей СУБД при поиске Book.search("Harry Potter")
или аналогичного. Чтобы избежать штрафов, связанных с запросами LIKE
, SearchCop может использовать возможности полнотекстового индексирования MySQL и PostgreSQL. Чтобы использовать уже существующие полнотекстовые индексы, просто укажите SearchCop использовать их с помощью:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes :title , :author
options :title , :type => :fulltext
options :author , :type => :fulltext
end
# ...
end
SearchCop затем прозрачно изменит свои SQL-запросы для атрибутов, имеющих полнотекстовые индексы, на:
Book . search ( "Harry Potter" )
# MySQL: ... WHERE (MATCH(books.title) AGAINST('+Harry' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Harry' IN BOOLEAN MODE)) AND (MATCH(books.title) AGAINST ('+Potter' IN BOOLEAN MODE) OR MATCH(books.author) AGAINST('+Potter' IN BOOLEAN MODE))
# PostgreSQL: ... WHERE (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Harry') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Harry')) AND (to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Potter') OR to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Potter'))
Очевидно, что эти запросы не всегда будут возвращать те же результаты, что и запросы LIKE
с подстановочными знаками, поскольку мы ищем слова, а не подстроки. Однако полнотекстовые индексы обычно, конечно, обеспечивают более высокую производительность.
Более того, приведенный выше запрос еще не идеален. Чтобы еще больше улучшить его, SearchCop пытается оптимизировать запросы, чтобы оптимально использовать полнотекстовые индексы, в то же время позволяя смешивать их с неполнотекстовыми атрибутами. Чтобы еще больше улучшить запросы, вы можете сгруппировать атрибуты и указать поле по умолчанию для поиска, например, SearchCop больше не должен выполнять поиск во всех полях:
search_scope :search do
attributes all : [ :author , :title ]
options :all , :type => :fulltext , default : true
# Use default: true to explicitly enable fields as default fields (whitelist approach)
# Use default: false to explicitly disable fields as default fields (blacklist approach)
end
Теперь SearchCop может оптимизировать следующий, еще не оптимальный запрос:
Book . search ( "Rowling OR Tolkien stock > 1" )
# MySQL: ... WHERE ((MATCH(books.author) AGAINST('+Rowling' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Rowling' IN BOOLEAN MODE)) OR (MATCH(books.author) AGAINST('+Tolkien' IN BOOLEAN MODE) OR MATCH(books.title) AGAINST('+Tolkien' IN BOOLEAN MODE))) AND books.stock > 1
# PostgreSQL: ... WHERE ((to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Rowling') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Rowling')) OR (to_tsvector('simple', books.author) @@ to_tsquery('simple', 'Tolkien') OR to_tsvector('simple', books.title) @@ to_tsquery('simple', 'Tolkien'))) AND books.stock > 1
на следующий, более производительный запрос:
Book . search ( "Rowling OR Tolkien stock > 1" )
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('Rowling Tolkien' IN BOOLEAN MODE) AND books.stock > 1
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', 'Rowling | Tokien') and books.stock > 1
Что здесь происходит? Итак, мы указали all
как имя группы атрибутов, состоящей из author
и title
. Поскольку мы, кроме того, указали all
как полнотекстовый атрибут, SearchCop предполагает наличие составного полнотекстового индекса для author
и title
, поэтому запрос оптимизируется соответствующим образом. Наконец, мы указали all
как атрибут по умолчанию для поиска, так что SearchCop может игнорировать другие атрибуты, например, stock
, если они не указаны в запросах напрямую (например, stock > 0
).
Другие запросы будут оптимизированы аналогичным образом, например, SearchCop пытается минимизировать полнотекстовые ограничения в запросе, а именно MATCH() AGAINST()
для MySQL и to_tsvector() @@ to_tsquery()
для PostgreSQL.
Book . search ( "(Rowling -Potter) OR Tolkien" )
# MySQL: ... WHERE MATCH(books.author, books.title) AGAINST('(+Rowling -Potter) Tolkien' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector('simple', books.author || ' ' || books.title) @@ to_tsquery('simple', '(Rowling & !Potter) | Tolkien')
Чтобы создать полнотекстовый индекс для books.title
в MySQL, просто используйте:
add_index :books , :title , :type => :fulltext
Что касается составных индексов, которые, например, будут использоваться для поля по умолчанию, all
мы уже указали выше, используйте:
add_index :books , [ :author , :title ] , :type => :fulltext
Обратите внимание, что MySQL поддерживает полнотекстовые индексы для MyISAM, а начиная с версии MySQL 5.6+, и для InnoDB. Для получения более подробной информации о полнотекстовых индексах MySQL посетите http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html.
Что касается PostgreSQL, существует больше способов создания полнотекстового индекса. Однако один из самых простых способов:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', title))"
Более того, для PostgreSQL вам следует изменить формат схемы в config/application.rb
:
config . active_record . schema_format = :sql
Что касается составных индексов для PostgreSQL, используйте:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', author || ' ' || title))"
Чтобы правильно обрабатывать значения NULL в PostgreSQL, используйте COALESCE как во время создания индекса, так и при указании search_scope
:
ActiveRecord :: Base . connection . execute "CREATE INDEX fulltext_index_books_on_title ON books USING GIN(to_tsvector('simple', COALESCE(author, '') || ' ' || COALESCE(title, '')))"
плюс:
search_scope :search do
attributes :title
options :title , :type => :fulltext , coalesce : true
end
Чтобы использовать другой словарь PostgreSQL, кроме simple
, вам необходимо соответствующим образом создать индекс и сообщить об этом SearchCop, например:
search_scope :search do
attributes :title
options :title , :type => :fulltext , dictionary : "english"
end
Для получения более подробной информации о полнотекстовых индексах PostgreSQL посетите http://www.postgresql.org/docs/9.3/static/textsearch.html.
Если вы предоставляете неполнотекстовые атрибуты для поисковых запросов (цена, акции и т. д.), соответствующие запросы, такие как Book.search("stock > 0")
, будут получать прибыль от обычных неполнотекстовых индексов. Таким образом, вам следует добавить обычный индекс для каждого столбца, который вы предоставляете поисковым запросам, а также полнотекстовый индекс для каждого полнотекстового атрибута.
Если вы не можете использовать полнотекстовые индексы, потому что вы, например, все еще используете MySQL 5.5 при использовании InnoDB или другой СУБД без полнотекстовой поддержки, вы можете заставить свою СУБД использовать обычные неполнотекстовые индексы для строковых столбцов, если вам не нужны оставил подстановочный знак в запросах LIKE
. Просто укажите следующую опцию:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username
options :username , left_wildcard : false
end
# ...
так что SearchCop будет опускать самый левый подстановочный знак.
User . search ( "admin" )
# ... WHERE users.username LIKE 'admin%'
Аналогичным образом вы также можете отключить правый подстановочный знак:
search_scope :search do
attributes :username
options :username , right_wildcard : false
end
Когда вы определяете несколько полей в области поиска, SearcCop по умолчанию будет использовать оператор AND для объединения условий, например:
class User < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :username , :fullname
end
# ...
end
Таким образом, поиск типа User.search("something")
сгенерирует запрос со следующими условиями:
... WHERE username LIKE ' %something% ' AND fullname LIKE ' %something% '
Однако бывают случаи, когда использование AND в качестве оператора по умолчанию нежелательно, поэтому SearchCop позволяет переопределить его и вместо этого использовать OR в качестве оператора по умолчанию. Запрос типа User.search("something", default_operator: :or)
сгенерирует запрос с использованием OR для объединения условий.
... WHERE username LIKE ' %something% ' OR fullname LIKE ' %something% '
Наконец, обратите внимание, что вы также можете применить его к полнотекстовым индексам/запросам.
Если вы укажете доступные для поиска атрибуты из другой модели, например
class Book < ActiveRecord :: Base
# ...
belongs_to :author
search_scope :search do
attributes author : "author.name"
end
# ...
end
SearchCop по умолчанию будет eager_load
указанные ассоциации при выполнении Book.search(...)
. Если вам не нужна автоматическая eager_load
или вам нужно выполнять специальные операции, укажите scope
:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
scope { joins ( :author ) . eager_load ( :comments ) } # etc.
end
# ...
end
SearchCop пропустит автоматическую загрузку ассоциаций и вместо этого будет использовать область действия. Вы также можете использовать scope
вместе с aliases
для выполнения произвольно сложных соединений и поиска в объединенных моделях/таблицах:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
attributes similar : [ "similar_books.title" , "similar_books.description" ]
scope do
joins "left outer join books similar_books on ..."
end
aliases similar_books : Book # Tell SearchCop how to map SQL aliases to models
end
# ...
end
На ассоциации ассоциаций также можно ссылаться и использовать:
class Book < ActiveRecord :: Base
# ...
has_many :comments
has_many :users , :through => :comments
search_scope :search do
attributes user : "users.username"
end
# ...
end
SearchCop пытается вывести имя класса модели и псевдоним SQL из указанных атрибутов для автоматического определения определений типов данных и т. д. Обычно это работает вполне нормально. Если вы используете собственные имена таблиц через self.table_name = ...
или если модель связана несколько раз, SearchCop, однако, не может вывести имена классов и псевдонимов SQL, например
class Book < ActiveRecord :: Base
# ...
has_many :users , :through => :comments
belongs_to :user
search_scope :search do
attributes user : [ "user.username" , "users_books.username" ]
end
# ...
end
Здесь для работы запросов вам необходимо использовать users_books.username
, поскольку ActiveRecord назначает разные псевдонимы SQL для пользователей в своих SQL-запросах, поскольку модель пользователя связана несколько раз. Однако, поскольку SearchCop теперь не может вывести модель User
из users_books
, вам необходимо добавить:
class Book < ActiveRecord :: Base
# ...
search_scope :search do
# ...
aliases :users_books => :users
end
# ...
end
чтобы сообщить SearchCop о пользовательском псевдониме SQL и сопоставлении. Кроме того, вы всегда можете выполнить соединения самостоятельно с помощью блока scope {}
плюс aliases
и использовать свои собственные псевдонимы sql, чтобы стать независимыми от имен, автоматически назначенных ActiveRecord.
Запросы строки запроса поддерживают AND/and
, OR/or
, :
, =
, !=
, <
, <=
, >
, >=
, NOT/not/-
, ()
, "..."
и '...'
. Операторы по умолчанию — AND
и matches
, OR
имеет приоритет над AND
. NOT
может использоваться только как инфиксный оператор для одного атрибута.
Поддержка запросов на основе хеша and: [...]
и or: [...]
, которые принимают массив not: {...}
, matches: {...}
, eq: {...}
, not_eq: {...}
, lt: {...}
, lteq: {...}
, gt: {...}
, gteq: {...}
и аргументы query: "..."
. Более того, query: "..."
позволяет создавать подзапросы. Другие правила для запросов строки запроса также применимы к запросам на основе хеша.
SearchCop также предоставляет возможность определять собственные операторы, определяя generator
в search_scope
. Затем их можно использовать при поиске по запросу на основе хеша. Это полезно, если вы хотите использовать операторы базы данных, которые не поддерживаются SearchCop.
Обратите внимание: при использовании генераторов вы несете ответственность за очистку/цитирование значений (см. пример ниже). В противном случае ваш генератор позволит инъекцию SQL. Поэтому, пожалуйста, используйте генераторы только в том случае, если вы знаете, что делаете.
Например, если вы хотите выполнить запрос LIKE
, где название книги начинается со строки, вы можете определить область поиска следующим образом:
search_scope :search do
attributes :title
generator :starts_with do | column_name , raw_value |
pattern = " #{ raw_value } %"
" #{ column_name } LIKE #{ quote pattern } "
end
end
Если вы хотите выполнить поиск, вы используете его следующим образом:
Book . search ( title : { starts_with : "The Great" } )
Примечание по безопасности. Запрос, возвращаемый генератором, будет интерполирован непосредственно в запрос, который поступает в вашу базу данных. Это открывает потенциальную точку внедрения SQL в вашем приложении. Если вы используете эту функцию, вам нужно убедиться, что возвращаемый запрос безопасен для выполнения.
При поиске в логических полях, полях даты и времени, метке времени и т. д. SearchCop выполняет некоторое сопоставление. Следующие запросы эквивалентны:
Book . search ( "available:true" )
Book . search ( "available:1" )
Book . search ( "available:yes" )
а также
Book . search ( "available:false" )
Book . search ( "available:0" )
Book . search ( "available:no" )
Для полей даты и времени SearchCop расширяет определенные значения до диапазонов:
Book . search ( "created_at:2014" )
# ... WHERE created_at >= '2014-01-01 00:00:00' AND created_at <= '2014-12-31 23:59:59'
Book . search ( "created_at:2014-06" )
# ... WHERE created_at >= '2014-06-01 00:00:00' AND created_at <= '2014-06-30 23:59:59'
Book . search ( "created_at:2014-06-15" )
# ... WHERE created_at >= '2014-06-15 00:00:00' AND created_at <= '2014-06-15 23:59:59'
Возможно объединение поисков. Однако цепочка в настоящее время не позволяет SearchCop оптимизировать отдельные запросы для полнотекстовых индексов.
Book . search ( "Harry" ) . search ( "Potter" )
будет генерировать
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry' IN BOOLEAN MODE) AND MATCH(...) AGAINST('+Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry') AND to_tsvector(...) @@ to_tsquery('simple', 'Potter')
вместо
# MySQL: ... WHERE MATCH(...) AGAINST('+Harry +Potter' IN BOOLEAN MODE)
# PostgreSQL: ... WHERE to_tsvector(...) @@ to_tsquery('simple', 'Harry & Potter')
Таким образом, если вы используете полнотекстовые индексы, вам лучше избегать цепочек.
При использовании Model#search
SearchCop удобно предотвращает возникновение определенных исключений в случае, если переданная ему строка запроса недействительна (ошибки анализа, ошибки несовместимого типа данных и т. д.). Вместо этого Model#search
возвращает пустое отношение. Однако, если вам нужно отладить определенные случаи, используйте Model#unsafe_search
, который их вызовет.
Book . unsafe_search ( "stock: None" ) # => raise SearchCop::IncompatibleDatatype
SearchCop предоставляет отражающие методы, а именно #attributes
, #default_attributes
, #options
и #aliases
. Вы можете использовать эти методы, например, чтобы предоставить индивидуальный виджет помощи по поиску для ваших моделей, в котором перечислены атрибуты для поиска, а также атрибуты по умолчанию и т. д.
class Product < ActiveRecord :: Base
include SearchCop
search_scope :search do
attributes :title , :description
options :title , default : true
end
end
Product . search_reflection ( :search ) . attributes
# {"title" => ["products.title"], "description" => ["products.description"]}
Product . search_reflection ( :search ) . default_attributes
# {"title" => ["products.title"]}
# ...
Начиная с версии 1.0.0, SearchCop использует семантическое управление версиями: SemVer.
git checkout -b my-new-feature
)git commit -am 'Add some feature'
)git push origin my-new-feature
)