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

Nov 26

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

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

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

Подписаться на комментарии по RSS или TrackBack на 'Использование поискового движка Sphinx в Ruby on Rails'.

1
сказал 26.11.2006 в 15.02

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

2
сказал 26.11.2006 в 18.45

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

3
сказал 27.11.2006 в 9.22

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

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

4
сказал 27.11.2006 в 14.19

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

5
Roman Semenenko
сказал 27.11.2006 в 16.09

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

6
сказал 27.11.2006 в 16.57

хоть бы один тест написал в плугин.
http://hyperestraier.sourceforge.net/ намного лучше

7
сказал 27.11.2006 в 17.16

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

8
сказал 14.12.2006 в 23.18

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

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

9
сказал 20.12.2006 в 8.33

[...] I have updated Sphinx Client Library along with Sphinx 0.9.7-RC2 Windows build. [...]

10
Sam
сказал 21.12.2006 в 17.27

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

11
сказал 25.12.2006 в 14.59

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

12
сказал 25.12.2006 в 15.25

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

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

13
John
сказал 12.01.2007 в 7.33

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

14
Dmitry
сказал 15.01.2007 в 12.30

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

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

15
shawn
сказал 03.02.2007 в 6.07

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

16
shawn
сказал 03.02.2007 в 6.08

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

17
shawn
сказал 03.02.2007 в 6.10

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:

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.

18
сказал 07.02.2007 в 0.16

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

-    result[:time] = '%.3f' % (result[:time] / 1000)
+    result[:time] = '%.3f' % (result[:time] / 1000.0)
19
сказал 07.02.2007 в 0.25

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:

- result[:time] = '%.3f' % (result[:time] / 1000)
+ result[:time] = '%.3f' % (result[:time] / 10000.0)
20
Danila
сказал 09.02.2007 в 18.20

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

21
сказал 22.02.2007 в 1.03

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

22
сказал 22.02.2007 в 1.14

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

23
Nikolay Karev
сказал 03.03.2007 в 10.12

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

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

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

24
сказал 20.03.2007 в 14.37

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

25
сказал 20.03.2007 в 14.40

Thanks for the tip, Akhil

26
joost
сказал 27.03.2007 в 16.32

thx! :)

27
joost
сказал 27.03.2007 в 16.36

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

28
сказал 27.03.2007 в 17.51

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

29
joost
сказал 03.04.2007 в 18.35

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

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

30
сказал 03.04.2007 в 19.04

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.

31
сказал 19.05.2007 в 5.53

[...] ActiveSearch Spinx indexed_search_engine SearchGenerator 一整個generator 另外,也可以做成Ajax real-time [...]

32
сказал 10.09.2007 в 5.12
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)

Оставить отзыв

Вы можете использовать простые теги форматирования HTML (вроде <a>, <ul> and others). Чтобы вставить пример код, используйте <code lang="php">$a = "hello";</code> (поддерживаемые языки: ruby, php, yaml, html, csharp, javascript). Также Вы можете использовать <code>$a = "hello";</code>, синтаксис не будет подсвечен. Если вы не хотите использовать тег <code>, замените символ < на &lt;.

Отправить

 
Copyright © 2005 - 2008, Dmytro Shteflyuk