Sphinx - Распределённый поиск
Как известно, одним из ключевых достоинств Sphinx является очень высокая скорость поиска по очень большим объёмам данных. Как утверждают разработчики, самый большой кластер содержит 3 миллиарда документов и выдерживает 50 миллионов запросов в сутки. Понятно, что чем больше нагрузка на Sphinx, тем мощнее ему нужен сервер, мощность которого невозможно бесконечно увеличивать. К счастью, это и не обязательно, поскольку Sphinx имеет встроенные инструменты для горизонтального масштабирования, позволяющие параллельно выполнять запросы на нескольких серверах, а затем объединять результаты.
В этой статье я на простом примере постараюсь показать, как можно настроить несколько экземпляров Sphinx, а затем объединить их для выполнения распределённого поиска.
Подготовка базы данных
Прежде всего нам потребуются тестовые данные. Снова воспользуемся базой данных AdventureWorks, которую можно скачать с сайта codeplex.com.
В этой базе данных нас будут интересовать три таблицы: Production.Product (Товары), Production.ProductSubcategory (Подкатегории) и Production.ProductCategory (Категории). Мы создадим два распределённых индекса, содержащих товары из категорий «Bikes» и «Clothing», при этом один из индексов будет использовать распределённый поиск, а второй - зеркалирование.
Хранимая процедура для получения товаров
Чтобы сократить количество sql-запросов в файлах конфигурации, создадим в базе данных хранимую процедуру для получения товаров из определённой категории.
create procedure [dbo].[GetProducts]
@Agent nvarchar(max),
@Index nvarchar(max),
@Part int = null,
@Parts int = null
as
begin
set nocount on;
select
[id] = p.ProductID,
[_agent] = @Agent,
[_index] = @Index,
[name] = cast(p.Name as nvarchar(max)),
[price] = p.ListPrice
from Production.Product as p
inner join Production.ProductSubcategory as ps
on ps.ProductSubcategoryID = p.ProductSubcategoryID
inner join Production.ProductCategory as pc
on pc.ProductCategoryID = ps.ProductCategoryID
where FinishedGoodsFlag = 1
and (@Index is null or pc.Name = @Index)
and (@Part is null or @Parts is null or p.ProductID % @Parts = @Part - 1);
end
Эта хранимая процедура будет возвращать идентификатор товара, его название и цену, а также две служебные колонки с названием агента (экземпляра Sphinx) и названием индекса. Служебные колонки не явлются обязательными и будут использоваться нами исключительно в демонстрационных целях.
В качестве параметров хранимая процедура будет получать название экземпляра Sphinx, который индексирует данные, и название индекса для которого запрашиваются данные. Название индекса при этом будет соответствовать названию категории в базе данных, из которой мы хотим получить товары. Помимо этого имеются ещё два необязательных параметра, позволяющих разбить все получаемые товары на несколько частей, используя кратность их идентификаторов, например, в одном запросе можно получить только товары с чётными идентификаторами, а в другом - только с нечётными.
После создания этой процедуры, мы сможем, например, получить товары с чётными идентификаторами из категории «Bikes» для экземпляра «Sphinx01».
exec GetProducts @Agent = 'Sphinx01', @Index = 'Bikes', @Part = 1, @Parts = 2
Распределённый поиск
Теперь, когда мы можем получать товары из базы данных при помощи хранимой процедуры, нам необходимо запустить несколько экземпляров Sphinx и объединить их вместе. Для простоты мы запустим их все на одном компьютере, но ничто не мешает запустить каждый экземпляр на отдельной машине. Различаться при этом будут только ip-адреса в файлах конфигурации.
Нам потребуется три экземпляра Sphinx. Экземпляры, которые мы назовём Sphinx01 и Sphinx02, будут содержать физические индексы, для которых необходимо будет выполнять индексацию. Экземпляр Sphinx01 будет содержать индексы bikes01 и clothing01, а экземпляр Sphinx02 - индексы bikes02 и clothing02. Третий экземпляр, который мы назовём SphinxMain, будет содержать распределённые индексы bikes и clothing, которые будут объединять результаты, полученные от экземпляров Sphinx01 и Sphinx02.
Индекс bikes01 будет содержать в себе только товары с чётными идентификаторами, а индекс bikes02 - с нечётными. При поиске в индексе bikes поиск будет производиться в обоих индексах, bikes01 и bikes02, а затем результаты будут объединяться вместе. Такой подход хорошо использовать для очень больших индексов, поскольку позволяет параллельно выполнять поиск на нескольких серверах.
Индексы clothing01 и clothing02 будут содержать в себе все товары вне зависимости от чётности их идентификаторов. При поиске в индексе clothing поиск будет производиться в одном из индексов, clothing01 или clothing02, и результаты будут возвращаться только из него. Такой подход называется «зеркалированием» и обычно используется для повышения отказоустойчивости кластера, поскольку, если один из серверов выйдет из строя, то поиск может производиться на другом сервере.
Шаг 1: Подготовка файлов конфигурации для экземпляров Sphinx01 и Sphinx02
Создадим в папке c:\sphinx\data\
файл конфинурации config01.txt
,
который будут использоваться экземпляром Sphinx01.
searchd
{
listen = 9301
listen = 9401:mysql41
pid_file = c:/sphinx/data/pid/01.pid
log = c:/sphinx/data/log/01/log.txt
query_log = c:/sphinx/data/log/01/query_log.txt
binlog_path = c:/sphinx/data/binlog/01/
}
source base
{
type = mssql
sql_host = localhost
sql_db = AdventureWorks
sql_user =
sql_pass =
mssql_winauth = 1
mssql_unicode = 1
}
source products_base : base
{
sql_field_string = _agent
sql_field_string = _index
sql_field_string = name
sql_attr_float = price
}
source bikes : products_base
{
sql_query = exec GetProducts @Agent = 'Sphinx01', @Index = 'Bikes', @Part = 1, @Parts = 2
}
source clothing : products_base
{
sql_query = exec GetProducts @Agent = 'Sphinx01', @Index = 'Clothing'
}
index bikes01
{
source = bikes
path = c:/sphinx/data/index/01/bikes
charset_type = utf-8
}
index clothing01
{
source = clothing
path = c:/sphinx/data/index/01/clothing
charset_type = utf-8
}
Обратите внимание, что в блоке search указано две опции listen
. Порт 9301
используется, чтобы экземпляр Sphinx01 был доступен по протоколу SphinxAPI, поскольку именно его
будет использовать экземпляр SphinxMain при обращении к экземплярам Sphinx01 и Sphinx02. Порт
9401 используется, чтобы мы могли подключиться к каждому экземпляру при помощи клиента для базы
данных MySQL.
После этого создадим файл config02.txt
, который будут использоваться экземпляром
Sphinx02. Его содержимое будет практически полностью совпадать с содержимым файла
config01.txt
, но тем не менее будет содержать несколько отличий:
- вместо портов 9301 и 9401 нужно указать 9302 и 9402;
- во всех путях ко всем папкам и файлам вместо «01» нужно использовать «02»;
-
при вызове хранимой процедуры
GetProducts
в блокахsource bikes
иsource clothing
в параметре@Agent
вместо значенияSphinx01
нужно использовать значениеSphinx02
, а в параметре@Part
вместо значения1
- значение2
; -
в блоках
index
названия индексовbikes01
иclothing01
нужно заменить на названияbikes02
,bikes02
.
Шаг 2: Индексация и запуск экземпляров Sphinx01 и Sphinx02
Перед индексацией не забудьте также создать все папки, которые используются в файлах конфигурации (например, c:\sphinx\data\pid\ или c:\sphinx\data\log\01\), иначе Sphinx не запустится.
Выполним индексацию всех индексов в обоих экземплярах Sphinx.
c:\sphinx\bin\indexer --all --rotate --config c:\sphinx\data\config01.txt
c:\sphinx\bin\indexer --all --rotate --config c:\sphinx\data\config02.txt
После того, как индексация успешно завершится, установим и запустим экземпляры Sphinx01 и Sphinx02.
c:\sphinx\bin\searchd --install --servicename Sphinx01 --config c:\sphinx\data\config01.txt
net start Sphinx01
c:\sphinx\bin\searchd --install --servicename Sphinx02 --config c:\sphinx\data\config02.txt
net start Sphinx02
Шаг 3: Проверка экземпляров Sphinx01 и Sphinx02
Теперь, когда у нас есть два запущеных экземпляра Sphinx, мы можем попробовать подключиться к ним, чтобы проверить, что в индексах находятся правильные данные.
Сначала подключимся экземпляру Sphinx01.
c:\mysql\bin\mysql -h 127.0.0.1 -P 9401 --default-character-set=utf8
Попробуем получить все товары из индексов bikes01 и clothing01.
select * from bikes01;
select * from clothing01;
После этого подключимся экземпляру Sphinx02.
c:\mysql\bin\mysql -h 127.0.0.1 -P 9402 --default-character-set=utf8
Попробуем получить все товары из индексов bikes02 и clothing02.
select * from bikes02;
select * from clothing02;
Как мы и хотели, индекс bikes01 содержит только товары с чётными идентификаторами, индекс
bikes02 - с нечётными, а индексы clothing01 и clothing02 содержат все товары и, по сути,
одинаковы за исключением атрибута _agent
. Теперь можно объединить экземпляры
Sphinx01 и Sphinx02 при помощи ещё одного экземпляра SphinxMain.
Шаг 4: Подготовка файла конфигурации для экземпляра SphinxMain
Создадим в папке c:\sphinx\data\
файл конфинурации config.txt
со
следующим содержимым:
searchd
{
listen = 9300
listen = 9400:mysql41
pid_file = c:/sphinx/data/pid/Main.pid
log = c:/sphinx/data/log/Main/log.txt
query_log = c:/sphinx/data/log/Main/query_log.txt
binlog_path = c:/sphinx/data/binlog/Main/
}
index bikes
{
type = distributed
agent = 127.0.0.1:9301:bikes01
agent = 127.0.0.1:9302:bikes02
}
index clothing
{
type = distributed
agent = 127.0.0.1:9301:clothing01|127.0.0.1:9302:clothing02
}
В этом файле конфигурации мы создаём два индекса с типом distributed
.
Индекс bikes
содержит две опции agent
ссылающихся на экземпляры
Sphinx01 и Sphinx02. Такая запись означает, что при поиске в распределённом индексе нужно
получить данные от каждого из агентов, а затем объединить их вместе.
Индекс clothing
содержит одну опцию agent
которая ссылается на
два экземпляра Sphinx01 и Sphinx02. Такая запись означает, что оба агента являются «зеркалами» и
нет разницы, в каком из них выполнять поиск. При этом агент для каждого запроса будет выбираться
случайным образом.
Шаг 5: Запуск экземпляра SphinxMain
Запустим экземпляр SphinxMain, не забыв при этом создать все папки, которые упоминаются в файле конфигурации.
c:\sphinx\bin\searchd --install --servicename SphinxMain --config c:\sphinx\data\config.txt
net start SphinxMain
Шаг 6: Проверка экземпляра SphinxMain
Подключимся к экземпляру SphinxMain и попробуем сделать несколько запросов.
c:\mysql\bin\mysql -h 127.0.0.1 -P 9400 --default-character-set=utf8
Попробуем поискать что-нибудь в индексе bikes
.
select * from bikes
where match('Black') and price < 1000
order by id asc;
Как мы видим, при поиске в индексе bikes
результаты возвращаются от агентов
Sphinx01 и Sphinx02 и объединяются, как мы и хотели.
Теперь попробуем поискать что-нибудь в индексе clothing
.
select * from bikes
where match('Sports')
order by id asc;
Теперь результаты возвращаются только от агента Sphinx01, но если мы попробуем несколько раз повторить запрос, то можем получить результаты и от агента Sphinx02.
Заключение
В этой статье я привёл наиболее простые примеры распределённого поиска: разделение индекса на
части (кстати, его совсем не обязательно было делать, основываясь на чётности или нечётности
идентификаторов) и зеркалирование. В реальных проектах оба подхода могут комбинироваться и
усложняться использованием дельта-индексов. Кроме того, зеркалирование может быть более тонко
настроено при помощи опции
ha_strategy
.
В большинстве случаев, для приложения не будет никакой разницы, обращается оно к локальному
индексу или распределённому, однако пока ещё (по крайней мере в версии 2.1.1-beta) встречаются
исключения (например, функция group_concat
с распределёнными индексами пока не
работает), хотя я уверен, что всё это будет исправлено в следующих версиях Sphinx.
Скачать материалы к данной статье (Хранимая процедура GetProducts, файлы конфигурации, скрипты для Sphinx).
Подскажите, пожалуйста, есть ли какой то прирост производительности у варианта с поднятием нескольких инстансов searchd по сравнению с использованием соответствующего количества dist_threads?
ОтветитьУдалитьПрирост может быть, если их поднимать на нескольких машинах, иначе я никакой разницы не вижу.
ОтветитьУдалитьИ опять же зависит от размера индекса, нагрузки, мощности сервера. Это надо на практике проверять.
Статьи просто супер! Продолжайте в том же духе!!
ОтветитьУдалитьСпасибо!
ОтветитьУдалитьПро Sphinx осталась ещё одна статья. Нельзя же вечно о нём писать. Хочется уже про что-то новое: Erlang, Riak, Neo4j =)
Если на одной машине - нет. Скорее будет небольшое падение (покуда совершенно зря задействуем сетевой стек).
ОтветитьУдалитьНо! Иногда это имеет смысл. Например, если нужно завести гигантский индекс на 32-битной машине с PAE и кучей памяти.
Распределённый индекс не умеет не только group_concat, но и некоторые другие штуки. Некоторые связаны с распределённостью, некоторые нет.
ОтветитьУдалитьНапример, генерация сниппетов - не может быть на чисто распределённом индексе (и это чистая недоделка, покуда технически ничего не мешает).
group_concat они уже сделали (в 2.2.1-dev)
ОтветитьУдалитьhttp://sphinxsearch.com/bugs/view.php?id=1628
Сниппетами не пользовался, но может стоит им про это завести инцидент? Вдруг починят.
Да. Это в очереди - сразу после index type=template. Просто генерация сниппетов - штука довольно атомарная; её сложно распараллелить. Если кидать пачку сниппетов по разным агентам - тут дольше будут пакеты по сети летать. Если источники (тексты) лежат в файлах, а не шлются по сети - да, это вариант, и такой вариант реализован. Причём файлы могут быть разрежены между разными агентами - это уже как раз напоминает обычные запросы к зеркалам. А в обычном случае всё лопатит атомарно локальный индекс. И тут из-за атомарности возникает элементарное неудобство: настроил индекс, отладил приложение - и как только заменил один монолитный индекс в конфиге на дистр main+delta - упс, сниппеты встали!
ОтветитьУдалитьЕсть ещё очевидный случай - count(uniq). Сколько _разных_ элементов в индексе?
ОтветитьУдалитьОчевидно, что в распределённом случае малой кровью такое вычислить невозможно.
(на одном складе картошка и морковка, на другом - тоже, на третьем - свёкла и капуста - но про "обобщённый" склад можно сказать лишь то, что уникальных продуктов там от 2 до 6. И дальше уже уточнить невозможно)
Подскажите, пожалуйста, еще один момент - как работает опция max_matches (допустим = 1000) при распределенном поиске? Из каждого агента приходит по 1000 отсортированных результатов и в основном индексе они объединяются, по новой сортируются и снова берется 1000, либо от каждого агента приходят все найденные результаты (total_found) и основной индекс работает со всеми ими и только он применяет max_matches?
ОтветитьУдалитьНе знаю, но я не думаю, что будут приходить все данные, т.к. это будет плохо для производительности.
ОтветитьУдалить