Почти любому Веб-приложению необходима логика поиска данных, и зачастую это должен быть полнотекстовый поиск. Если вы используете базу данных MySQL, можно воспользоваться поиском FULLTEXT, но это не самое эффективное решение, особенно если объем данных велик. В этом случае используются сторонние поисковые движки, и один из них (и, на мой взгляд, самый эффективный из них) – это Sphinx. В данной заметке я представлю свой порт клиентской библиотеки Sphinx на Ruby и покажу, как его использовать.
Для начала, что такое Sphinx вообще? Sphinx – это полнотекстовый поисковый движок, которые предоставляет функции быстрого, эффективного и релевантного полнотекстового поиска другим приложениям. Sphinx был разработан специально для лучшей интеграции с базами данных SQL и скриптовыми языками. На сегодняшний момент встроенные источники данных поддерживают выборку либо напрямую из MySQL, либо через канал XML.
Текущий дистрибутив Sphinx включает следующие части:
- indexer: утилита для создания полнотекстовых индексов;
- search: простая (тестовая) утилита для запросов к полнотекстовым индексам из командной строки;
- searchd: демон для поиска в полнотекстовых индексах из стороннего программного обеспечения (например, Веб-скриптов);
- sphinxapi: набор библиотек API для популярных скриптовых языков для Веб (в данный момент только PHP);
Я не буду рассказывать, как установить этот движок. Если Вы впервые слышите о нем, посмотрите официальную документацию (но если Вы хотите получить эту информацию от меня, всегда можно попросить в комментариях, и я расскажу об установке в одной из последующих заметок). Вместо этого, представлю свой порт клиентской библиотеки Sphinx на Ruby и покажу, как ее использовать (обратите внимание, что Вам необходим Sphinx 0.9.7-RC2).
Для начала скачайте плагин с RubyForge, или с этого сайта:
Скачать Sphinx-0.2.0.zip
Это плагин Ruby on Rails, потому распакуйте его в каталог <app>/vendor/plugins (библиотека может использоваться и вне контекста Rails-приложения). Теперь Вы можете написать что-то вроде этого в Вашем коде:
1 2 3 4 5 6 7 8 9 10 11 | sphinx = Sphinx.new sphinx.set_match_mode(Sphinx::SPH_MATCH_ANY) result = sphinx.query('term1 term2') # Получить соответствующие объекты модели ids = result[:matches].map { |id, value| id }.join(',') posts = Post.find :all, :conditions => "id IN (#{ids})" # Получить выдержки docs = posts.map { |post| post.body } excerpts = sphinx.build_excerpts(docs, 'index', 'term1 term2') |
Довольно просто, не правда ли? Существует несколько опций, которые Вы можете использовать для получения более релевантных результатов поиска:
- set_limits(offset, limit) – индекс первого документа и количество документов для выборки.
- set_match_mode(mode) – режим поиска (может быть SPH_MATCH_ALL – поиск по всем словам, SPH_MATCH_ANY – поиск по любому из слов, SPH_MATCH_PHRASE – поиск по точной фразе, SPH_MATCH_BOOLEAN – поиск по логическому выражению).
- set_sort_mode(mode) – режим сортировки (can be SPH_SORT_RELEVANCE – сортировать по релевантности документа по убыванию, затем по дате, SPH_SORT_ATTR_DESC – сортировать по дате документа по убыванию, затем по релевантности по убыванию, SPH_SORT_ATTR_ASC – сортировать документы по дате по возрастанию, затем по релевантности по убыванию, SPH_SORT_TIME_SEGMENTS – сортировать по сегментам времени (час/день/неделя/что-то еще) по убыванию, затем по релевантности по убыванию).
Другие опции можно найти в документации API.
Если Вас заинтересовала эта библиотека, если Вы нашли ошибки или знаете, как ее можно улучшить – пожалуйста, отпишитесь в комментариях.
Обновление: К сожалению, нет скомпилированной версии последнего Sphinx 0.9.7-rc2 для Windows. Я собрал его, и добавил в архив рабочий файл конфигурации. Вы можете забрать сборку здесь.

Шикарная штука, спасибо! Давно уже искал как сделать поиск и по русским, и по английским текстам. Буду рекомендовать всем знакомым рельсовикам. Надеюсь, с UTF-8 никаких проблем у поисковика нет?
Вообще никаких проблем :-) Специально проверил еще раз. Более того, в движке реализована такая штука, как морфологический поиск для русского языка. К тому же, автор открыт для предложений, потому если есть какие-то замечания или предложения по усовершенствованию – велкам на форум. Огромная вероятность того, что запрошенные фичи будут включены в последующие релизы.
:) вообще кайф! проаннонсирую в русскоязычной ror-группе.
а как с производительностью? относительно ferret
Эх, еще бы оно с постгресом работало…
Не могли бы вы описать преимущества этого движка над Ferret ?
хоть бы один тест написал в плугин.
http://hyperestraier.sourceforge.net/ намного лучше
В документации по HyperEstraier я не нашел ни слова о русскоязычной морфологии. Он, может быть, конечно, и лучше, но как его локализовать под нужды русскоязычного проекта?
Bregor,
Sphinx успешно работает с PostgreSQL.
guest,
а чем конкретно HyperEstraier лучше?
[...] I have updated Sphinx Client Library along with Sphinx 0.9.7-RC2 Windows build. [...]
в чем преимущества перед ferret?
Честно говоря, нигде не видел сравнения это движков. Если будет время – проведу…
Как минимум, в Ferret я с ходу не нашел поддержки распределенного поиска – те. есть вопрос с масштабируемостью.
Любопытно сравнивать скорость, но это надо делать аккуратно – надо помнить, что Sphinx по умолчанию (MATCH_ALL) считает степень совпадения фразы -это заметно более трудоемкая операция, чем просто сосчитать частоты слов в документе.
Why Sphinx? You know there is already a port of Apache Lucene to Ruby called Ferret, and its supposed to be even faster. The “Acts_as_ferret” plugin for Rails builds the functioanlity right into your models :)
xmlpipe источник позволяет индексировать локальные файлы на высокой скорости с любой предварительной обработкой.
В моем случае это было примерно так – 50 Гб данных, запакованных в zip в формате doc обрабатывались последовательно (unzip, rtf2txt), затем приводились к формату xml.
При этом поиск работает в среднем от 0.001 до 0.005 секунд на стандартном сервере ( 3Ghz, 1Gb, RAID SATA)
В следующей версии (она уже есть на cvs) Андрей обещал “практически” wildcard search.
Hi, I found a bug in the plugin code. When you read the attrs, they are put them in a hash, which isn’t guaranteed to be in a specific order. Then they are used to unpack the data in order. This was resulting in some attrs being mixed up when doing a grouping query (@count was switched with @groupby, etc).
Here is a fix that worked for me. I just tracked the attr names in an array so that we are guaranteed they stay in the same order, then use those to unpack the attrs in order. The only lines that are changed are the ones where the new attrs_names_in_order variable is used:
fields = []
attrs = {}
attrs_names_in_order = []
nfields = response[p, 4].unpack(‘N*’).first
p += 4
while nfields > 0 and p 0 && p 0 and p
while nfields > 0 and p 0 && p 0 and p
Hmmm, looks like it doesn’t like the brackets in the code. Let’s try this again:
Hi, I found a bug in the plugin code. When you read the attrs, they are put them in a hash, which isn’t guaranteed to be in a specific order. Then they are used to unpack the data in order. This was resulting in some attrs being mixed up when doing a grouping query (@count was switched with @groupby, etc).
Here is a fix that worked for me. I just tracked the attr names in an array so that we are guaranteed they stay in the same order, then use those to unpack the attrs in order. The only lines that are changed are the ones where the new attrs_names_in_order variable is used:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
attrs = {}
attrs_names_in_order = []
nfields = response[p, 4].unpack('N*').first
p += 4
while nfields > 0 and p < max
nfields -= 1
len = response[p, 4].unpack('N*').first
p += 4
fields << response[p, len]
p += len
end
result[:fields] = fields
nattrs = response[p, 4].unpack('N*').first
p += 4
while nattrs > 0 && p < max
nattrs -= 1
len = response[p, 4].unpack('N*').first
p += 4
attr = response[p, len]
p += len
type = response[p, 4].unpack('N*').first
p += 4
attrs[attr.to_sym] = type;
attrs_names_in_order << attr.to_sym
end
result[:attrs] = attrs
# read match count
count = response[p, 4].unpack('N*').first
p += 4
# read matches
result[:matches] = {}
while count > 0 and p < max
count -= 1
doc, weight = response[p, 8].unpack('N*N*')
p += 8
result[:matches][doc] ||= {}
result[:matches][doc][:weight] = weight
for attr in attrs_names_in_order
val = response[p, 4].unpack('N*').first
p += 4
result[:matches][doc][:attrs] ||= {}
result[:matches][doc][:attrs][attr] = val
end
end
Hopefully you can add the fix in and maybe get the updated ruby api distributed with sphinx 9.7 when it gets released.
I think you may may have a minor error on line 339 in sphinx.rb one of the values in the devision should be a float so that it returns a float value
2
+ result[:time] = '%.3f' % (result[:time] / 1000.0)
update .. actually I think sphinx returns things with 1 = 1/10,000 of second not 1=1/1000th … let me know if you find otherwise:
2
+ result[:time] = '%.3f' % (result[:time] / 10000.0)
Как установить Сфинкс под Windows, Если я использую пакет разработчика DENWER?
Thanks for the comment! I will review it shortly and post update. Thanks again
Danila, sphinx ставится как отдельное приложение. Просто возьми билд под Windows (мой или с официального сайта), настрой конфиг и запусти searchd.
вот этот кусок кода потенциально проблеммный:
Если ids содержит несколько тысяч результатов, то есть вероятность что сдохнет парсер запросов в СУБД. И всё очень мрачно упадёт.
Так что имхо лучше сделать или ограничение на количество результатов от sphinx или разбивать их на блоки и уже поблочно вытаскивать из СУБД.
Hi,
I have used fullsearch feature in many of my projects. I have used ferret and hyperestraier. You can use acts_as_ferret for ferret searching and acts_as_searchable for hyperestraier. Ferret provides multiple model search and other does’nt. I prefer hyperestraier for fulltext search. :)
Thanks for the tip, Akhil
thx! :)
BTW, is there already an update?? Which includes the above fixes? This would be great!
Update would be published tomorrow or the day after tomorrow. Currently I’m finishing RSpec tests which would cover whole functionality.
Currently I get the following error using the plugin (with v0.9.7 of Sphinx). All database fields are MySQL INT(11).
2
3
4
5
6
from ../config/../vendor/plugins/sphinx/lib/sphinx.rb:256:in 'query'
from ../config/../vendor/plugins/sphinx/lib/sphinx.rb:253:in 'each'
from ../config/../vendor/plugins/sphinx/lib/sphinx.rb:253:in 'query'
from ./test.rb:136:in 'search_entry'
from ./test.rb:149
Any idea? Please let me know.. Also about an update!! :)
joost, do you use set_filter_range in your code? Could you show me values you have sent to this method? Also it would be great, if you contact me directly to fix it quickly.
I’m updating API now and will upload it in next few days.
[...] ActiveSearch Spinx indexed_search_engine SearchGenerator 一整個generator 另外,也可以做成Ajax real-time [...]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
===================================================================
--- vendor/plugins/sphinx/lib/client.rb (revision 5885)
+++ vendor/plugins/sphinx/lib/client.rb (working copy)
@@ -391,18 +391,20 @@
count = response[p, 4].unpack('N*').first; p += 4
# read matches
- result['matches'] = {}
+ result['matches'] = []
while count > 0 and p < max
count -= 1
doc, weight = response[p, 8].unpack('N*N*'); p += 8
- result['matches'][doc] ||= {}
- result['matches'][doc]['weight'] = weight
+ doc_data = {}
+ doc_data['weight'] = weight
attrs_names_in_order.each do |attr|
val = response[p, 4].unpack('N*').first; p += 4
- result['matches'][doc]['attrs'] ||= {}
- result['matches'][doc]['attrs'][attr] = val
+ doc_data['attrs'] ||= {}
+ doc_data['attrs'][attr] = val
end
+
+ result['matches'] << [doc, doc_data]
end
result['total'], result['total_found'], msecs, words = response[p, 16].unpack('N*N*N*N*'); p += 16
result['time'] = '%.3f' % (msecs / 1000.0)
[...] to embed into Ruboss (Heroku?) – Create Flex MXML templates for a Blog, Comments, Search (with Sphinx) Forum, Profile, Wiki, PhotoGallery, and Podcast Viewer. Bookmark It [...]
Привет, Всем!
У меня появился вопрос по Sphinx, помогите пожалуйста найти решение.
У меня есть следующая структура в конфигурационном файле:
sphinx.conf:
2
3
4
5
6
7
8
source sphinx_users_delta : sphinx_users_main
source sphinx_spaces_main
source sphinx_spaces_delta : sphinx_spaces_main
index users_main
index users_delta : users_main
index spaces_main
index spaces_delta : spaces_main
Такая структура была придумана мной для того, чтоб можно было при поиске получать ID по отдельной таблицы(указав по какому индексу с конфигурационного файла производить поиск).
Все, вроде как, корректно работает:
search -a test
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Copyright (c) 2001-2008, Andrew Aksyonoff
using config file '/usr/local/etc/sphinx.conf'...
index 'users_main': query 'test ': returned 14 matches of 14 total in 0.000 sec
displaying matches:
1. document=3592, weight=2
2. document=4178, weight=2
3. document=4179, weight=2
4. document=4181, weight=2
5. document=6192, weight=2
6. document=2807, weight=1
7. document=3593, weight=1
8. document=4717, weight=1
9. document=4740, weight=1
10. document=6090, weight=1
11. document=6196, weight=1
12. document=6218, weight=1
13. document=6219, weight=1
14. document=6220, weight=1
words:
1. 'test': 14 documents, 19 hits
index 'users_delta': query 'test ': returned 0 matches of 0 total in 0.000 sec
words:
1. 'test': 0 documents, 0 hits
index 'spaces_main': query 'test ': returned 17 matches of 17 total in 0.000 sec
displaying matches:
1. document=937, weight=1
2. document=940, weight=1
3. document=942, weight=1
4. document=943, weight=1
5. document=944, weight=1
6. document=945, weight=1
7. document=964, weight=1
8. document=983, weight=1
9. document=984, weight=1
10. document=985, weight=1
11. document=986, weight=1
12. document=987, weight=1
13. document=988, weight=1
14. document=989, weight=1
15. document=990, weight=1
16. document=991, weight=1
17. document=992, weight=1
words:
1. 'test': 17 documents, 17 hits
index 'spaces_delta': query 'test ': returned 0 matches of 0 total in 0.000 sec
words:
1. 'test': 0 documents, 0 hits
Но вот не могу понять, как с помощью Sphinx организовать поиск по указанному мной индексу, как например я это делаю с консоли:
search -i spaces_main -a test
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Copyright (c) 2001-2008, Andrew Aksyonoff
using config file '/usr/local/etc/sphinx.conf'...
index 'spaces_main': query 'test ': returned 17 matches of 17 total in 0.000 sec
displaying matches:
1. document=937, weight=1
2. document=940, weight=1
3. document=942, weight=1
4. document=943, weight=1
5. document=944, weight=1
6. document=945, weight=1
7. document=964, weight=1
8. document=983, weight=1
9. document=984, weight=1
10. document=985, weight=1
11. document=986, weight=1
12. document=987, weight=1
13. document=988, weight=1
14. document=989, weight=1
15. document=990, weight=1
16. document=991, weight=1
17. document=992, weight=1
words:
1. 'test': 17 documents, 17 hits
Подскажите мне пожалуйста, как это можно организовать?
Спасибо
Второй параметр метода Query – название индекса, по которому искать:
Спасибо большое за ответ.
Подскажите пожалуйста, как я могу в Sphinx изменить шаблон, по которому мне возвращается результат запроса?
Например в результате запроса: sphinx.Query(‘test’)
я хотел бы, чтоб в результате я мог бы получить кроме всего прочего: test16, test_12, hello@test.com.
Спасибо
Привет, всем!!!
Подскажите пожалуйста, как в sphinx реализовать такой же поиск, какой бы например был бы при ‘…LIKE %name%…’
Спасибо