Использование поискового движка Sphinx в Ruby on Rails

(MySQL, Ruby & Rails) · English (39,725 views)

Почти любому Веб-приложению необходима логика поиска данных, и зачастую это должен быть полнотекстовый поиск. Если вы используете базу данных 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. Я собрал его, и добавил в архив рабочий файл конфигурации. Вы можете забрать сборку здесь.

37 Responses to this entry

Subscribe to comments with RSS

DEkart @
said on 26.11.2006 at 15.02 · Permalink

Шикарная штука, спасибо! Давно уже искал как сделать поиск и по русским, и по английским текстам. Буду рекомендовать всем знакомым рельсовикам. Надеюсь, с UTF-8 никаких проблем у поисковика нет?

said on 26.11.2006 at 18.45 · Permalink

Вообще никаких проблем :-) Специально проверил еще раз. Более того, в движке реализована такая штука, как морфологический поиск для русского языка. К тому же, автор открыт для предложений, потому если есть какие-то замечания или предложения по усовершенствованию – велкам на форум. Огромная вероятность того, что запрошенные фичи будут включены в последующие релизы.

DEkart @
said on 27.11.2006 at 9.22 · Permalink

:) вообще кайф! проаннонсирую в русскоязычной ror-группе.

а как с производительностью? относительно ferret

said on 27.11.2006 at 14.19 · Permalink

Эх, еще бы оно с постгресом работало…

Roman Semenenko
said on 27.11.2006 at 16.09 · Permalink

Не могли бы вы описать преимущества этого движка над Ferret ?

DEkart @
said on 27.11.2006 at 17.16 · Permalink

В документации по HyperEstraier я не нашел ни слова о русскоязычной морфологии. Он, может быть, конечно, и лучше, но как его локализовать под нужды русскоязычного проекта?

said on 14.12.2006 at 23.18 · Permalink

Bregor,
Sphinx успешно работает с PostgreSQL.

guest,
а чем конкретно HyperEstraier лучше?

Sam
said on 21.12.2006 at 17.27 · Permalink

в чем преимущества перед ferret?

said on 25.12.2006 at 14.59 · Permalink

Честно говоря, нигде не видел сравнения это движков. Если будет время – проведу…

said on 25.12.2006 at 15.25 · Permalink

Как минимум, в Ferret я с ходу не нашел поддержки распределенного поиска – те. есть вопрос с масштабируемостью.

Любопытно сравнивать скорость, но это надо делать аккуратно – надо помнить, что Sphinx по умолчанию (MATCH_ALL) считает степень совпадения фразы -это заметно более трудоемкая операция, чем просто сосчитать частоты слов в документе.

John
said on 12.01.2007 at 7.33 · Permalink

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 :)

Dmitry
said on 15.01.2007 at 12.30 · Permalink

xmlpipe источник позволяет индексировать локальные файлы на высокой скорости с любой предварительной обработкой.
В моем случае это было примерно так – 50 Гб данных, запакованных в zip в формате doc обрабатывались последовательно (unzip, rtf2txt), затем приводились к формату xml.
При этом поиск работает в среднем от 0.001 до 0.005 секунд на стандартном сервере ( 3Ghz, 1Gb, RAID SATA)

В следующей версии (она уже есть на cvs) Андрей обещал “практически” wildcard search.

shawn
said on 03.02.2007 at 6.07 · Permalink

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

shawn
said on 03.02.2007 at 6.08 · Permalink

while nfields > 0 and p 0 && p 0 and p

shawn
said on 03.02.2007 at 6.10 · Permalink

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:

1
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
    fields = []
    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 &amp;&amp; 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.

said on 07.02.2007 at 0.16 · Permalink

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

1
2
-    result[:time] = '%.3f' % (result[:time] / 1000)
+    result[:time] = '%.3f' % (result[:time] / 1000.0)
said on 07.02.2007 at 0.25 · Permalink

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:

1
2
- result[:time] = '%.3f' % (result[:time] / 1000)
+ result[:time] = '%.3f' % (result[:time] / 10000.0)
Danila @
said on 09.02.2007 at 18.20 · Permalink

Как установить Сфинкс под Windows, Если я использую пакет разработчика DENWER?

said on 22.02.2007 at 1.03 · Permalink

Thanks for the comment! I will review it shortly and post update. Thanks again

said on 22.02.2007 at 1.14 · Permalink

Danila, sphinx ставится как отдельное приложение. Просто возьми билд под Windows (мой или с официального сайта), настрой конфиг и запусти searchd.

Nikolay Karev
said on 03.03.2007 at 10.12 · Permalink

вот этот кусок кода потенциально проблеммный:

1
posts = Post.find :all, :conditions => "id IN (#{ids})"

Если ids содержит несколько тысяч результатов, то есть вероятность что сдохнет парсер запросов в СУБД. И всё очень мрачно упадёт.
Так что имхо лучше сделать или ограничение на количество результатов от sphinx или разбивать их на блоки и уже поблочно вытаскивать из СУБД.

said on 20.03.2007 at 14.37 · Permalink

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. :)

joost @
said on 27.03.2007 at 16.32 · Permalink

thx! :)

joost @
said on 27.03.2007 at 16.36 · Permalink

BTW, is there already an update?? Which includes the above fixes? This would be great!

said on 27.03.2007 at 17.51 · Permalink

Update would be published tomorrow or the day after tomorrow. Currently I’m finishing RSpec tests which would cover whole functionality.

joost @
said on 03.04.2007 at 18.35 · Permalink

Currently I get the following error using the plugin (with v0.9.7 of Sphinx). All database fields are MySQL INT(11).

1
2
3
4
5
6
../config/../vendor/plugins/sphinx/lib/sphinx.rb:256:in 'pack': bignum too big to convert into 'unsigned long' (RangeError)
        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!! :)

said on 03.04.2007 at 19.04 · Permalink

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.

said on 10.09.2007 at 5.12 · Permalink
1
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
Index: vendor/plugins/sphinx/lib/client.rb
===================================================================
--- 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)
tolya @
said on 11.09.2008 at 12.54 · Permalink

Привет, Всем!

У меня появился вопрос по Sphinx, помогите пожалуйста найти решение.

У меня есть следующая структура в конфигурационном файле:

sphinx.conf:

1
2
3
4
5
6
7
8
source sphinx_users_main
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

1
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
Sphinx 0.9.8-release (r1371)
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

1
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
Sphinx 0.9.8-release (r1371)
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

Подскажите мне пожалуйста, как это можно организовать?

Спасибо

said on 11.09.2008 at 14.51 · Permalink

Второй параметр метода Query – название индекса, по которому искать:

1
sphinx.Query('test', 'spaces_main');
tolya @
said on 12.09.2008 at 14.27 · Permalink

Спасибо большое за ответ.

Подскажите пожалуйста, как я могу в Sphinx изменить шаблон, по которому мне возвращается результат запроса?
Например в результате запроса: sphinx.Query(‘test’)
я хотел бы, чтоб в результате я мог бы получить кроме всего прочего: test16, test_12, hello@test.com.

Спасибо

Anatoliy @
said on 24.09.2008 at 17.23 · Permalink

Привет, всем!!!

Подскажите пожалуйста, как в sphinx реализовать такой же поиск, какой бы например был бы при ‘…LIKE %name%…’

Спасибо

Comments are closed

Comments for this entry are closed for a while. If you have anything to say – use a contact form. Thank you for your patience.