Одна из задач, где Gearman позволяет получить удобство и скорость работы, — обработка текстовых поисковых запросов. Детально я хочу рассказать об этом на YAPC::Europe, а в этом посте покажу несколько фрагментов кода, который сейчас работает в продакшне.
Полученный поисковый запрос раздается на несколько сканеров, каждый из которых отвечает за свою узкую область знаний, например, за географический поиск. В свою очедерь, каждый сканер может уточнять задачу с помощью одного или нескольких парсеров, которые непосредственно и анализируют текст запроса. С точки зрения job-сервера, все это оформлено в несколько воркеров; всего их около двух десятков:
misc::e — поиск по кодам пищевых добавок,
misc::typo — проверка орфографии;place::airport — поиск аэропортов,
place::country — поиск стран,
place::locality — поиск населенных пунктов;whl::money — конвертер валют;
whl::area, whl::volume, whl::temperature, whl::speed, whl::pressure, whl::power, whl::mass, whl::length, whl::information, whl::force, whl::energy, whl::calendar — конвертеры единиц измерения;
whl::calculator — калькулятор.
Каждый воркер одновременно и разбирает текстовый запрос, и, если разбор оказался успешным, вычисляет результат.
Интересно, что каждая отдельная часть реализации довольно проста и уныла, примерно тот же эффект наблюдается, когда из бесконечных однострочных методов на Java выстраивается нетривиальное приложение, или как в ассемблере, где количество атомарных действий mov в итоге превращается в качество.
Сканеры подгружаются автоматически при старте. О том, как это делается, недавно упоминалось в рассылке Moscow.pm. Расстановка тасков — перебор по списку:
my $tasks = $client->new_task_set;
for(@{$self->{scanners}}) {
$tasks->add_task(
$_ => nfreeze([$query, $self->{locale}]),
{
on_complete => sub {
push @{$self->{results}}, thaw(${$_[0]});
},
},
);
}
$tasks->wait;
Типовой сканер содержит метод, принимающий строку запроса. В частности, сканер единиц измерения и валют раздает задачи своим парсерам:
sub scan {
my $query = shift @args;
for my $parser (@{$self->{parsers}}) {
my $result =
$parser->parse($query, $self->{locale}) // next;
push @{$self->{results}}, $result;
}
}
(Обатите внимание, кстати, на использование оператора defined-or для завершения итерации цикла.)
После того, как сканеры собрали результаты от парсеров, итоговый ответ попадает (в случайном порядке) в общий XML, и всю дальнейшую работу по визуализации данных выполняют с помощью XSLT.
Разумеется, в большинстве случаев ответ находят лишь некоторые из двух десятков воркеров. Более того, часть ответов игнорируется на уровне XSLT в зависимости от того, насколько полными были ответы от тех или иных воркеров.
Вот пример запроса: sin(pi/2) + cos(pi/2). Нефильтрованные данные, полученные от всех сканеров, выглядят следующим образом:

Те же данные с точки зрения XML-наблюдателя:
<pack for="sin(pi/2) + cos(pi/2)" scanner="whl::calculator">
<item>
<from expr="sin(π/2) + cos(π/2)"/>
<to value="1"/>
</item>
</pack>
<pack for="sin" scanner="place::locality">
<item country-code="FR" geonameid="2974494">Sin-le-Noble</item>
<item country-code="AF" geonameid="1124363">Sīn</item>
. . .
</pack>
<pack for="pi" scanner="place::locality">
<item country-code="FR" geonameid="2984891">Py</item>
<item country-code="ES" geonameid="6424319">Pi</item>
<item country-code="IN" geonameid="1259715">Pi</item>
</pack>
<pack for="cos" scanner="place::locality">
<item country-code="FR" geonameid="3023491">Cos</item>
<item country-code="ES" geonameid="3124419">Cos</item>
. . .
</pack>
Парсер калькулятора вернул один результат (собственно, наиболее релевантный), а географический сканнер нашел города с названиями и Синус, и Косинус, и Пи — эти результаты при наличии первого результата, который поглотил всю строку, показывать, разумеется, не обязательно. Большинство возможностей, которые сегодня умеет делать упоминаемый здесь поиск, описаны на соответствующей странице.
Комментировать