Поговорим о поиске в Ruby on Rails?
Я решил разбить повествование на две части: в первой скучная настройка проекта и простой поиск по одному полю одной модели. Во второй подробнее остановимся на тонкостях и я постараюсь рассказать про все, что может плагин. Кстати в исходниках (ссылка в тексте) проект уже немного изменен для второй части, но проблем это не вызовет.
Устанавливаем Rails не ниже 2.0.2
Скачиваем сфинкс 0.9.8: www.sphinxsearch.com/downloads.html и собираем самостоятельно, или используем порты/портажи/<вставить нужное>
Sphinx поддерживает две субд — MySQL и PostgreSQL, но достаточно легко можно добиться поддержки любой базы данных.
Проверка после установки:
Путь до searchd и indexer должен быть в переменной окружения path.
Сфинкс состоит из нескольких утилит, некоторые из них:
searchd — поисковый демон
search — консольный аналог searchd для отладки/теста поиска.
indexer — индексатор.
Создаем проект:
Незабудьте отредактировать config/database.yml
Для удобства для работы со сфинксом будем использовать плагин.
Я считаю, что адекватных плагина для Rails два — ultrasphinx и Thinking Sphinx (кстати пока писал статью вышел RailsCast про него). Так как последний из-за внутренних именований второй конфликтует с другим плагином «redhill on rails», то я использую первый. Но возможно второй лучше — выбирайте сами. :)
Установка плагина:
default.base — заготовка для конфигурационного файла sphinx-а. В первой части просто настроим пути до логов/пидов/индексов:
Для простоты сделаем один контроллер с формой, которая с помощью ajax-а, будет искать скажем… Артистов по имени. Модель артиста будет состоять из одного поля — title:
Код миграции, пусть у артиста будет только одно поле title(db/migrate/..._create_artists.rb):
Теперь скажем сфинксу, что искать мы будем по одному полю (app/models/artist.rb):
Запись «is_indexed :fields => ['title']» означает, что индексирование будет происходить по одному полю.
Ну и создаем базы и выполняем миграции:
Так же стоит настроить роуты в файле config/routes.rb:
Код контроллера(app/controllers/home_controller.rb):
Первым регулярным выражением мы разбираем поисковый запрос, разбивая слова по пробелам, игнорируем пустые слова( например ,,) и добавляем ко всем слова кавычки. Операция && означает лишь набор слов, ну например запросу
«Bleed it out» => 'Bleed' && 'it' && 'out' будет соответствовать и запись «Sell it out» (два слова из трех совпали), т.е. && не диктует список обязательных слов, а лишь перечисляет их (если вам необходимо обязательное наличие всех слов, то нужно использовать AND, но об этом во второй части).
Коротко пробежимся по параметрам:
:query — поисковый запрос
:sort_mode — тип сортировки результатов
:class_names — массив имен классов моделей которые будут созданы в результате поиска. Sphinx внутри себя хранит каждый документ как набор полей и их значений. В Rails с таким представлением работать не удобно, а куда удобнее с готовым объектом модели. Ultrasphinx сам определит к какой модели относиться найденный документ и создаст его экземпляр, таким образом сам поиск ничем не отличается от Artist.find(...) или Artist.paginate (да, результаты поискового запроса совместимы с will_paginate-ом).
Команда @artists.run выполняет запрос. Запросы выполняются очень быстро. На семимилионной базе — тысячные секунды.
Представления(шаблоны) можно посмотреть в готовом проекте
теперь можно что нибудь добавить в базу:
Выполним необходимые приготавления для работы плагина (это нужно делать каждый раз когда вы что либо меняете в моделях в описании is_indexed):
Запускаемся и тестируемся)
Индексы отдельно данные в базе отдельно. Когда мы удаляем/изменяем/добавляем в базу индексы не изменяются. Чтобы изменения базы отразились на индексах полную реиндексацию базы:
Конечно это не очень хорошо. Но могу заверить, что решение проблемы существует и называется delta-индексированием. Об этом в следующей части.
Sphinx — очень крутая штука :). Open source, бесплатна, шустро ищет и индексирует. Must have!
Из аналогов могу отметить acts_as_ferret, для маленьких проектов он подойдет идеально (например мы использовали его на Хакфесте Рамблера), но для больших объемов данных он ведет себя мягко говоря неважно — очень долго индексирует.
Для постгресткого tsearch2 есть вроде бы не плохой плагин: Acts as tsearch, в бою не применял, не знаю. Еще есть acts_as_solr
Я решил разбить повествование на две части: в первой скучная настройка проекта и простой поиск по одному полю одной модели. Во второй подробнее остановимся на тонкостях и я постараюсь рассказать про все, что может плагин. Кстати в исходниках (ссылка в тексте) проект уже немного изменен для второй части, но проблем это не вызовет.
Установка
Устанавливаем Rails не ниже 2.0.2
Скачиваем сфинкс 0.9.8: www.sphinxsearch.com/downloads.html и собираем самостоятельно, или используем порты/портажи/<вставить нужное>
$ sudo port install sphinx
Sphinx поддерживает две субд — MySQL и PostgreSQL, но достаточно легко можно добиться поддержки любой базы данных.
Проверка после установки:
Macintosh:sphinx-0.9.8 kronos$ searchd -h
Sphinx 0.9.8-release (r1371)
Copyright (c) 2001-2008, Andrew Aksyonoff
...
Путь до searchd и indexer должен быть в переменной окружения path.
Сфинкс состоит из нескольких утилит, некоторые из них:
searchd — поисковый демон
search — консольный аналог searchd для отладки/теста поиска.
indexer — индексатор.
Создаем проект:
$ rails sphinxtest -d mysql
$ cd sphinxtest/
Незабудьте отредактировать config/database.yml
Для удобства для работы со сфинксом будем использовать плагин.
Я считаю, что адекватных плагина для Rails два — ultrasphinx и Thinking Sphinx (кстати пока писал статью вышел RailsCast про него). Так как последний из-за внутренних именований второй конфликтует с другим плагином «redhill on rails», то я использую первый. Но возможно второй лучше — выбирайте сами. :)
Установка плагина:
$ script/plugin install git://github.com/fauna/ultrasphinx.git
Настройка
$ mkdir config/ultrasphinx
cp vendor/plugins/ultrasphinx/examples/default.base config/ultrasphinx/
default.base — заготовка для конфигурационного файла sphinx-а. В первой части просто настроим пути до логов/пидов/индексов:
# ...
searchd
{
# ...
log = /opt/local/var/db/sphinx/log/searchd.log
query_log = /opt/local/var/db/sphinx/log/query.log
pid_file = /opt/local/var/db/sphinx/log/searchd.pid
# ...
}
# ...
index
{
# путь где будут лежать индексы
path = /opt/local/var/db/sphinx/
# ...
}
# ...
Пишем код
Для простоты сделаем один контроллер с формой, которая с помощью ajax-а, будет искать скажем… Артистов по имени. Модель артиста будет состоять из одного поля — title:
$ script/generate controller home index search
$ script/generate model artist
Код миграции, пусть у артиста будет только одно поле title(db/migrate/..._create_artists.rb):
class CreateArtists < ActiveRecord::Migration def self.up create_table :artists do |t| t.string :title, :null => false t.timestamps end end def self.down drop_table :artists end end
Теперь скажем сфинксу, что искать мы будем по одному полю (app/models/artist.rb):
class Artist < ActiveRecord::Base
is_indexed :fields => ['title']
end
Запись «is_indexed :fields => ['title']» означает, что индексирование будет происходить по одному полю.
Ну и создаем базы и выполняем миграции:
$ rake db:create
$ rake db:migrate
Так же стоит настроить роуты в файле config/routes.rb:
map.root :controller => 'home'
map.search 'search', :conditions => {:method => :get}, :controller => 'home', :action => 'search'
Код контроллера(app/controllers/home_controller.rb):
class HomeController < ApplicationController def index end def search query = params[:query].split(/'([^']+)'|"([^"]+)"|\s+|\+/).reject{|x| x.empty?}.map{|x| x.inspect }*' && ' @artists = Ultrasphinx::Search.new(:query => query, :sort_mode => 'relevance', :class_names => ["Artist"]) @artists.run respond_to do |format| format.js #search.js.erb end end end
Первым регулярным выражением мы разбираем поисковый запрос, разбивая слова по пробелам, игнорируем пустые слова( например ,,) и добавляем ко всем слова кавычки. Операция && означает лишь набор слов, ну например запросу
«Bleed it out» => 'Bleed' && 'it' && 'out' будет соответствовать и запись «Sell it out» (два слова из трех совпали), т.е. && не диктует список обязательных слов, а лишь перечисляет их (если вам необходимо обязательное наличие всех слов, то нужно использовать AND, но об этом во второй части).
Коротко пробежимся по параметрам:
:query — поисковый запрос
:sort_mode — тип сортировки результатов
:class_names — массив имен классов моделей которые будут созданы в результате поиска. Sphinx внутри себя хранит каждый документ как набор полей и их значений. В Rails с таким представлением работать не удобно, а куда удобнее с готовым объектом модели. Ultrasphinx сам определит к какой модели относиться найденный документ и создаст его экземпляр, таким образом сам поиск ничем не отличается от Artist.find(...) или Artist.paginate (да, результаты поискового запроса совместимы с will_paginate-ом).
Команда @artists.run выполняет запрос. Запросы выполняются очень быстро. На семимилионной базе — тысячные секунды.
Представления(шаблоны) можно посмотреть в готовом проекте
теперь можно что нибудь добавить в базу:
$ script/console
>> Artist.create(:title => 'Tiesto')
>> Artist.create(:title => 'Armin')
>> Artist.create(:title => 'ATB')
>> exit
Выполним необходимые приготавления для работы плагина (это нужно делать каждый раз когда вы что либо меняете в моделях в описании is_indexed):
$ rake ultrasphinx:configure
$ rake ultrasphinx:index
$ rake ultrasphinx:daemon:start (либо restart если уже запущен)
Запускаемся и тестируемся)
$ mongrel_rails -p 3001 -d
Одно маленькое но
Индексы отдельно данные в базе отдельно. Когда мы удаляем/изменяем/добавляем в базу индексы не изменяются. Чтобы изменения базы отразились на индексах полную реиндексацию базы:
$ rake ultrasphinx:index
Конечно это не очень хорошо. Но могу заверить, что решение проблемы существует и называется delta-индексированием. Об этом в следующей части.
Резюме
Sphinx — очень крутая штука :). Open source, бесплатна, шустро ищет и индексирует. Must have!
Аналоги
Из аналогов могу отметить acts_as_ferret, для маленьких проектов он подойдет идеально (например мы использовали его на Хакфесте Рамблера), но для больших объемов данных он ведет себя мягко говоря неважно — очень долго индексирует.
Для постгресткого tsearch2 есть вроде бы не плохой плагин: Acts as tsearch, в бою не применял, не знаю. Еще есть acts_as_solr