Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Загадки «Сфинкса»

Сейчас много текста будет, напрягитесь.

«Сфинкс» — это довольно известный поисковый движок, разработанный Андреем Аксёновым. Время от времени мне приходилось использовать его в своих проектах и каждый раз качеством получившегося решения я был доволен, тем более, что за проходившее время он обрастал новыми интересными возможностями.

В первый раз сталкиваюсь с проектом, где использование «Сфинкса» возможно только с некоторым количеством костылей, которыми решение обрастает и меня всё меньше устраивает получившееся. Хочу описать проблемы, с которыми я столкнулся, возможно кому-то в момент выбора поисковой технологии это будет небезынтересно.

«Сфинкс» хорош поиском по одной сущности, это, пожалуй всё. Богатство, описанное документации может создать обманчивое ощущение, что он может больше, но это не так. Моей ошибкой было решение использовать его в поиске по связанным сущностям. Это то, что «Сфинкс» умеет плохо. И хотя то, что получилось работает по скорости выше предыдущего решение, прокручивая в голове исходный код, я пла́чу мысленными кровавами слезами.

Так получилось, что искать мне нужно по наборам сущностей, связанных между собой. Получилось аж пять индексов. Объём данных не даёт надежды упихать всё в плоскую структуру — избыточность ужасает. Например, есть документы (Д) и резолюции к ним (Р), отношение один ко многим. Если попробовать положить эту структуру в одну таблицу, избыточность будет на уровне Д×Рсрср — резолюций на документ в среднем). Причём «Д» на уровне миллионов. И это только две сущности. Немаловажно, что «Д» и «Р» — сущности с целой кучей полей, в том числе полнотекстовых.

Выход есть. В поиске мы смотрим по каким сущностям надо искать, ищем по первой, берём оттуда найденные идентификаторы, подставляем во второй поисковый запрос вместе с критериями и так далее. Это костыль номер раз. Форма поиска устроена таким образом, что использование любого фильтра, как правило, ограничивает количество данных в выборке очень сильно, и всё равно это тысячи айдишников.

Второй костыль нужен, чтобы обойти принципиальное ограничение «Сфинкса» — у него есть параметр (указывается в конфиге), ограничивающий максимальное количество возвращаемых данных. Причём в миллионы (наш объём) его поставить нельзя — максимум в десятки тысяч, не рассчитан «Сфинкс» на большее, да и памяти не напасёшься. Всё бы ничего, но когда мы ищем пересечения сущностей (Д×Р), то, что нам вернул «Сфинкс» со своим лимитом, может и не иметь пересечений — они могут быть за горизонтом выдаваемых данных.

Костыль номер два я походя описывал — если мы обнаруживаем, что айдишники вернулись не все, то делаются ещё запросы, которые я назвал «докачивающими», делается тот же самый поисковый запрос, за исключением айдишников, которые уже попали. Работает, естественно, только с данными отсортироваными по ID.

Как только появились «докачивающие» запросы, то минус один выходной ушёл у меня на костыль номер три — я сделал сбор статистики запросов и оптимизацию порядка выполнения запросов. Вот как всё работает.

У нас, как я уже сказал, может быть от одного до пяти задействованных индексов, в зависимости от того что пользователь в форму введёт. В запрос к каждому следующему передаются идентификаторы из предыдущего. Как только один из запросов вернул пустоту, дальше можно не продолжать — пересечение с пустотой даёт пустоту.

Теперь следите за мыслью. Не знаю очевидно или нет, но логически всё равно в каком порядке будут опрашиваться индексы — результат будет тот же. Например, Р ∩ Д = Д ∩ Р. Зато если впереди поставить запросы, которые вернут меньше айдишников, следующим будет полегче. Скажем, на запрос пользователя у нас вернулось 80 тысяч документов и пять тысяч резолюций. Расточительнее сделать запрос к документам первым и подставить потом 80 тысяч айдишников во второй запрос, чем выбрать сначала пять тысяч резолюций, а потом передать их в запрос к документам.

Но как узнать какой запрос надо выполнить первым? Поможет сбор статистики.

Я рассуждал так. Поскольку у нас в поисковой форме все атрибуты запрашиваются с критерием «И» (например: год=2011 И организация=5), то чем больше разнообразных критериев запрошено, тем меньше объём выборки, значит надо как минимум запомнить какие атрибуты интересовали пользователя, тем более, что они по-разному влияют на объём получающейся выборки. Значения у атрибута ищутся с критерием «ИЛИ» (например: год=2011 ИЛИ 2012), чем больше указано значений атрибута, тем больше результатов вернётся, значит имеет смысл запоминать сколько значений атрибута указано.

В итоге при каждом поиске пользователя я кладу его запрос в специальный стек, плюс указываю какие атрибуты в каком количестве он запрашивал. Эта информация может выглядеть как-то так: год=3, организация=1, авторы=4. Значит, что использовалось три атрибута для поиска, причём пользователь в графе «год» указал три каких-то года, выбрал одну какую-то организацию и четыре каких-то автора. Айдишники из предыдущего запроса не учитываются, они мне не нужны.

В офлайне (ночью, например) срабатывает скрипт, который запускает полученный запрос и смотрит сколько данных получается, если этот запрос выполнится первым (без переданных айдишников). Из информации об атрибутах делается хеш и к нему приписывается полученный результат, учитываются предыдущие результаты по тому же хешу и в другую таблицу записывается пара хеш и средняя оценка значений возвращаемых запросом такого рода.

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

Массовых испытаний ещё не было, но в тех частных случаях, которые я рассмотрел, ускорение бывает до двух раз (чаще всего, конечно, прирост не столь значителен или его нет). Больше всего выигрыш получается, если удаётся избавиться от «докачивающих» запросов.

Задача была интересной, а вот костыли не радуют. Либо надо научиться с ними жить, либо думать о переходе на какой-то другой поисковый движок.

56 комментариев
Павел Павлович Форкерт (blog.fxposter.org) 2013

Можно выловить Аксёнова на  https://closedcircles.com/ и задать ему там вопросы относительно своего решения.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для blog.fxposter.org:

Какой вопрос-то задать? :) Что делать? Я и сам знаю, что ничего не поделаешь, «Сфинкс» на это не рассчитан.

Danil 2013

А если делать плоский индекс с большой избыточностью? Т. е. делать Д*Р записей в основном индексе. Ну и выборки с группировкой по Д.id. Мне в свое время такое помогло. Большой индекс для сфинкса не проблема.

Danil 2013

Кстати лимит выбираемых значений очень и очень и очень на производительность самого поиска влияет. Ставить больше 5-10К на более-менее солидном индексе — можно убить на 50% производительность.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Danil:

Т. е. делать Д*Р записей в основном индексе. Ну и выборки с группировкой по Д.id. Мне в свое время такое помогло. Большой индекс для сфинкса не проблема.

Избыточных данных будет, во-первых, ДxР×И₁×И₂×И₃, во-вторых, у Д порядок несколько миллионов, как я уже написал, а связь с «Р» — один ко многим, это значит, что «Р» в несколько раз больше, в худшем случае — на порядки. Даже индекс Д×Р будет не просто солидным, а огромным.

Евгений Степанищев (bolknote.ru) 2013

Думаю присмотреться к Apache Solr.

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Прислали ссылку, примерно на задорной шутке про «может только десятки тысяч документов» я сломался и не смог не откомментировать!!!

«Сфинкс» хорош поиском по одной сущности, это, пожалуй всё. Богатство, описанное документации может создать обманчивое ощущение, что он может больше, но это не так.

:) Ну мы типа поисковый движок.
Умеем поэтому именно поиск и еще всякое связанное с поиском.

Моей ошибкой было решение использовать его в поиске по связанным сущностям. Это то, что «Сфинкс» умеет плохо.

Плохо?
Плохо???
Он этого вообще не умеет!
Ну те. нету в самом движке JOIN как концепции.
Вроде (вроде) и нигде более в поисковых движках нет, но может уже ошибаюсь.

Немаловажно, что «Д» и «Р» — сущности с целой кучей полей, в том числе полнотекстовых.

Мне кажется, ошибка проектирования сразу как раз вот тут.
В куче полнотекстовых полей по всем сущностям.
По моему опыту, пользователям они типично таки НЕ нужны.
Даже если пользователи врут иначе.

Что мгновенно открывает ряд интересных возможностей.

В поиске мы смотрим по каким сущностям надо искать, ищем по первой, берём оттуда найденные идентификаторы, подставляем во второй поисковый запрос

Ну те. действительно нужны запросы вида «совпало слово ДУДОЧКА в документе И совпало слово КУВШИНЧИК в любой из резолюций к документу», так?

Второй костыль нужен, чтобы обойти принципиальное ограничение «Сфинкса» — у него есть параметр (указывается в конфиге), ограничивающий максимальное количество возвращаемых данных.

Оно не «принципиальное».
Оно штоп пользователи случайными запросами на 100M матчей себе ногу не отстреливали.

Причём в миллионы (наш объём) его поставить нельзя — максимум в десятки тысяч, не рассчитан «Сфинкс» на большее, да и памяти не напасёшься.

Извините, тупо неправда — причем на 3 (три) порядка.

Поставить — можно.
Движок сам по себе — «рассчитан» вполне.

10M возвращаемых матчей — я тестировал, работает.
Вроде 32 байта на матч в худшем случае — или даже меньше, те. до 320 MB на запрос.
Там есть некий глупый слив производительности в этом случае, не очень большой — но работает.

Ну те. реально желающим потратить до 320 MB памяти и некоторое количество CPU на запрос, возвращающий 10 миллионов объектов в приложение, Сфинкс такую возможность — вполне предоставляет.

А тех, кто не хочет случайно просрать в никуда 30x320 = 10 GB памяти сервера, которого кто-то спросил про [the] — защищает от такого настройками по умолчанию.

Эта информация может выглядеть как-то так: год=3, организация=1, авторы=4.

...

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

Тут у меня возрвался мозг :(
Кусок запроса приведен про атрибуты, причем тут вообще полнотекстовый поиск...

В общем и целом, описание задачи крайне неполное.
Но из обрывков мне кажется (кажется), что тупо неправильно спроектированы индексы и запросы.

Навскидку, JOIN результатов поисков в разных индексах тут вряд ли нужен.
Нужна ловкая денормализация, возможно, частичная (ходовых атрибутов, например).
Нужны фокусы про оптимизацию смешанных атрибутивных и полнотекстовых запросов.

Поиски с ограничением по результату другого поиска, в принципе, уже можно сделать и в движке.
Но случай дико нишевой, плюс типично обходится легким перепроектированием.

Ну и эта, общая фрустрация от изобретенных костылей понятна, конечно.
Непонятно только, зачем при этом ругаться и, хм, писать неправду про моменты, которые как раз давно и отлично работают.

У нас натурально не хватает ряда нужных и полезных штук. Не сделаны, пока еще.
Но якобы невозможность вынуть 100K матчей из движка это, хм, мягко говоря, ни разу не одна из них :-)

Андрей Аксенов 2013

Комментарий для Danil:

Кстати лимит выбираемых значений очень и очень и очень на производительность самого поиска влияет. Ставить больше 5-10К на более-менее солидном индексе — можно убить на 50% производительность.

Эээ. Там есть где-то 3 тонкости, но все они именно с размером индекса как раз не связаны. Да и вот именно поиск (матчинг, ранжирование, итп) всегда работает одинаково, это вчистую сортировка найденного отъедает (или не отъедает) лишнее время.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Ну, Андрей, я Сфинкс и не ругаю, вполне себе осознаю, что выбрал для решения задачи не тот инструмент, который не умеет то, что мне нужно.

И, да, увы, специфика документооборота (а я именно им занимаюсь), что поиск может быть и «ДУДОЧКА в документе И совпало слово КУВШИНЧИК в любой из резолюций к документу», просто потому что документ надо найти, а пользователи плохо ориентируются в этим миллионах — в мозгу столько не поместить. Я знаю случай (не единичный), когда весь отдел просматривал 15 тысяч документов, чтобы найти нужный. Могут быть любые поисковые запросы, да.

Про ограничение я, вероятно, не совсем верно выразился. Движок рассчитан, но с оговоркой — он всё делает в памяти и не умеет на диске. А на миллионы, умноженные на миллионы памяти не хватит.

Ну те. нету в самом движке JOIN как концепции..
Вроде (вроде) и нигде более в поисковых движках нет, но может уже ошибаюсь.

В Apache Solr, судя по описаю есть.

Андрей, не стоит в бутылку лезть, у вас хороший продукт.

Андрей Аксенов 2013

А подписаться на комментарии по email тут, кстати, нельзя, да?
Только постоянно рукой заходить на страницу и проверять новые?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Ещё пара ответов на некоторые вопросы.

Ну те. реально желающим потратить до 320 MB памяти и некоторое количество CPU на запрос, возвращающий 10 миллионов объектов в приложение, Сфинкс такую возможность — вполне предоставляет.

В приложение нам столько не нужно, но 100—200 тысяч — бывает необходимо, чтобы подставить в следующий запрос к следующему индексу.

В общем и целом, описание задачи крайне неполное.

Если я напишу обстоятельно из чего состоят индексы, у меня тут будет простыня на несколько экранов :)

Нужна ловкая денормализация, возможно, частичная (ходовых атрибутов, например).
Нужны фокусы про оптимизацию смешанных атрибутивных и полнотекстовых запросов.

Увы, всевозможные фокусы мы уже выполнили.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

А подписаться на комментарии по email тут, кстати, нельзя, да?
Только постоянно рукой заходить на страницу и проверять новые?

По e-mail нет, но если залогиниться через OpenID, можно получить персональный RSS-поток с комментариями (это у меня такая принципиальная позиция была пара лет назад, но видимо надо переделать на простую подписку по e-mail’у).

Андрей Аксенов 2013

выбрал для решения задачи не тот инструмент, который не умеет то, что мне нужно.

Ххха, инструмент на предметку влобно (влобно) не ложится однозначно, кто б спорил-то?

Но по существу — мне кажется (кажется), что даже этот инструмент таки можно приспособить.
Приделать костыли поудобнее. Или даже взять и перепроектироваться без костылей.
Может, ошибочно кажется, но по имеющейся скудной информации вот так.

А по форме — тональность ряда утверждений на фоне фактических ошибок в других вызывают...
Легкое удивление.

специфика документооборота (а я именно им занимаюсь), что поиск может быть и «ДУДОЧКА в документе И совпало слово КУВШИНЧИК в любой из резолюций к документу», просто потому что документ надо найти,

Понятно.
Порядки размеров какие: документы большие, резолюции мелкие?

В любом случае, для случая ДУДОЧКА И КУВШИНЧИК денормализация таки работает.
Нехитрая, все резолюции свалить в 1 текстовое поле документа.

Про ограничение я, вероятно, не совсем верно выразился. Движок рассчитан, но с оговоркой — он всё делает в памяти и не умеет на диске. А на миллионы, умноженные на миллионы памяти не хватит.

OMFG.
Я совершенно не понимаю ни откуда взялось «все в памяти», ни про «миллионы на миллионы», ну ладно.

Вроде (вроде) и нигде более в поисковых движках нет, но может уже ошибаюсь.

В Apache Solr, судя по описаю есть.

Угу, нашел уже тоже.
Прогресс не стоит на месте, действительно, добавили около года назад.

Ну, прикольно!
Когда почитаем тут, как работает?! :-)

не стоит в бутылку лезть

?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

А по форме — тональность ряда утверждений на фоне фактических ошибок в других вызывают…
Легкое удивление.

Легко объяснимо, я не ставил целью рассказать задачу от и до, это всё-таки персональный блог, а не книга, тут большей частью обрывки мыслей.

Порядки размеров какие: документы большие, резолюции мелкие?

Если имеется ввиду текст, то да, дело так и обстоит. Но атрибутов помимо текста хватает. Кроме того, у документа несколько текстовых полей разной природы.

В любом случае, для случая ДУДОЧКА И КУВШИНЧИК денормализация таки работает.
Нехитрая, все резолюции свалить в 1 текстовое поле документа.

Невозможно. У резолюций свои атрибуты есть. Задача сложнее чем кажется.

Я сейчас открою поисковую форму… секунду… и придумаю какой-нибудь комплексный запрос.

«Документ, созданный в 2011 или 2012 году, зарегистрированный под номером, в котором встречается „Д08“ в период между 15.09.2011 и 15.03.2012 в организации №5, в тексте которого есть слово „Бугульма“, а в кратком описании — „роддом“, к которому есть сопроводительное письмо, зарегистрированное пользователем №133231, без куратора, к которому есть утверждённая резолюция за авторством пользователя №13213, подписанная ЭЦП».

Ну, например, так.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

OMFG.
Я совершенно не понимаю ни откуда взялось «все в памяти», ни про «миллионы на миллионы», ну ладно.

Ну, я не отрицаю, что могу не понимать откуда взялось ограничение на результат, но как я понял по доке и сегодняшней переписке, это чтобы оно всю память не съело. Если съест, то что дальше будет? База (тот же Оракл) некоторые операции проводит на диске, Сфинкс, насколько я понимаю — в памяти, пользователей у меня не один, а десятки тысяч, что будет если каждому понадобится запрос, где выберется сотня-другая тысяч документов? Вот это место я не понимаю.

Ну, прикольно!
Когда почитаем тут, как работает?! :-)

Не знаю пока. :)

Про бутылку — я же помню, что ты очень экспрессивный товарищ :) По выступлениям помню. А уж когда автору обидно за свой продукт, любой полезет в бутылку, что ж я не понимаю что ли?

P.S. Ничего, что на «ты»? Когда мы переписывались во времена моей работы в «Яндексе», были на «ты».
P.P.S. Кстати, твоя фамилия Аксенов или Аксёнов всё-таки, а то я уже сомневаться начал, правильно ли написал.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Порядки такие.

У документа порядка 50 атрибутов, плюс 4 текстовых поля (одно большое, три мелкие), у сопроводительного письма — 7, плюс одно текстовое, у резолюции — 26 атрибутов, плюс одно текстовое поле, у отметки об исполнении — 9 полей, плюс одно текстовое поле.

Андрей Аксенов 2013

В приложение нам столько не нужно, но 100—200 тысяч — бывает необходимо,

Да вообще без проблем обязано работать.

Увы, всевозможные фокусы мы уже выполнили.

Увы, не верю, много всякого в посте не стыкуется.

Денормализация (хоть частичная) явно возможна и должна работать для немалой части запросов. «Докачивающие» запросы из коробки на объеме 100к просто не нужны, sanity check в конфиге на значение max_matches сейчас 10M и это именно sanity check. Индексировать значения фильтров, похоже, никто не пробовал. «Статистика» работать вообще не должна, тк. от перемены мест индексов ценник на поиск ключевиков и фильтрацию на стороне движка не меняется никак, тормозить может только приложение (либо API, если скриптовый язык).

Навскидку как-то так, ну и это я молчу про нехитрые варианты доработки собственно движка.

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

FUD везде FUD и ошибки везде ошибки, в блоге их написать, в книге или камнем на стене выцарапать.

Задача наверняка сложнее, а в аболютную невозможность денормализации все равно не верю.

«Сложный» пример запроса транслируется в 1-2 поиска. Базовый поиск всегда по документам, находим всякое по [роддом бугульма] и фильтрам уровня документам, причем находим, скорее всего, совсем чуть. Далее дожимаем вторым поиском по тем резолюциям, либо сразу кладем атрибуты резолюций в сам документ как JSON колонку и в единственном же запросе дожимаем еще фильтром. NB: в обоих вариантах реализации все равно будут свои костыли и бег по глупым ограничениям, но совсем не такие, на которые жалобы в посте.

Дефолт max_matches=1K и ограничение 10M сверху для того, чтобы не давать просто так брать и отжирать 10x10Mx32 = 3.2 GB памяти впорожняк. Еще раз: чтобы при 10 параллельных запросах помешать просрать 3.2 GB памяти персонажам, которые, например, путают max_matches (ограничение размер result set) с максимальным размером индекса, который можно обыскать (никак не ограничен) и пытаются зачем-то max_matches задрать там, где надо top-20 результатов отдать и все.

10K rps на поиск есть у Гугла, Твиттера и еще горстки компаний, у вас нету и не будет никогда. Запрос на 100K документов без осложнений отожрет 3.2 MB на клиента, по 32 байта на матч (найденный документ), те. ~100 MB при 30 workers. Каждый раз, как ты путаешь общее число аккаунтов в системе и rps, господь убивает котенка.

Шоу на выступлениях, комментарии тут, «обидно за продукт» и прыжки в бутылку это 5 (именно 5) разных, никак не пересекающихся явлений, причем идиоматической бутылки почитай не бывает. Легкое удивление (см. «откуда они это берут?!»), легкое удивление, русским по-белому ведь пишу. Впрочем, «автору обидно» понятная бирка. Абсолютно в жопу неверная, конечно, и если вдуматься и разобраться, слегка оскорбительная, но не вызывает даже легчайшего удивления :-) Тыкать ок, конечно, в интернетах в массе своей выкают только на Хабре и долбоснобы.

Аксёнов.

hshhhhh (hshhhhh.name) 2013

Комментарий для Евгения Степанищева:

Я правильно понял принцип что, до тех пор пока не наберётся критическое значение статистики, то поиск пользователя по запросу которого нет в статистике будет долгим. Сколько бы раз сегодня он не делал запрос, а вот уже завтра -​-​ ему будет искаться быстрее?

смотрит сколько данных получается, если этот запрос выполнится первым (без переданных айдишников).

Совсем не понимаю как выглядит такой запрос.

Томин Алексей (alxt.moikrug.ru) 2013

Комментарий для Евгения Степанищева:

Я что-то туплю...
А зачем вообще поисковый движок?
Почему нельзя искать напрямую в БД? Скорость проседает? Какая БД?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Денормализация (хоть частичная) явно возможна и должна работать для немалой части запросов.

А, вот где камень преткновения. Я не думаю о денормализации только для части запросов, у меня задача — ускорить поиск вообще. Но даже если рассмотреть верхнюю часть запросов, то на первом месте стоит поиск по номеру (уже денормализовано всё, хранится он сильно сложнее), а потом идёт очень разномастная масса запросов, у которых второе место выделить тяжело. Думаю, можно попытаться оценку по времени построить и оптимизировать просто то, что дольше (но для этого надо запускать новый поиск в продакшн и мониторить, статистика для старого поиска тут не поможет, он иначе сделан внутри).

Не думаю, что нам стоит разбирать частности, разбор что какие данные хранит и каков их объём займёт не один час, а без этого ты будешь делать ошибки оценки, подобные этой:

«Сложный» пример запроса транслируется в 1-2 поиска. Базовый поиск всегда по документам, находим всякое по [роддом бугульма] и фильтрам уровня документам, причем находим, скорее всего, совсем чуть

Тут найдётся очень много, это слабое ограничение.

Давай я лучше спрошу о вещах, которые меня сильно волнуют.

1) ты посоветовал использовать JSON, тем не менее, он есть только в бете. Ставить бету в продакшн я не буду. В этой связи два вопроса — скоро ли релиз? Сильно ли деградирует по скорости поиск, если использовать JSON (всё-таки фича новая, вряд ли там код вылизан до идеала).

2) из объяснения про память я понял, что если 10 клиентов запустили десять запросов, где выпало 10 миллионов результатов для выдачи в клиент, то это сожрёт 10×10M×32 = 2,98ГиБ. Правильно ли я понимаю, что «Сфинкс» на любой запрос теперь будет выделять под 320 мегабайт, вне зависимости от того, понадобятся они или нет (только если я ему в OPTIONS не уменьшу max_matches, конечно)?

3) и правильно ли я понял, что если памяти хватает (система не ушла в своп), то от размера max_matches производительность «Сфикса» не деградирует на отметке в десяток тысяч, потому что мне показалось, что в доке между строк написано иное: http://sphinxsearch.com/docs/archives/1.10/conf-max-matches.html​. Я имею ввиду фразу

This parameter noticeably affects per-query RAM and CPU usage. Values of 1,000 to 10,000 are generally fine, but higher limits must be used with care

Конечно, там дальше речь идёт исключительно о RAM, но вначале-то ещё и CPU упоминается, про который дальше молчок.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для hshhhhh.name:

Я правильно понял принцип что, до тех пор пока не наберётся критическое значение статистики, то поиск пользователя по запросу которого нет в статистике будет долгим. Сколько бы раз сегодня он не делал запрос, а вот уже завтра — ему будет искаться быстрее?

Если запускать лопатилку только по ночам — да.

«смотрит сколько данных получается, если этот запрос выполнится первым (без переданных айдишников)» Совсем не понимаю как выглядит такой запрос.

Просто же всё.

Условно, простой запрос с айдишниками:
SELECT document_id FROM cards WHERE org_id=5 AND year IN(2010,2012) AND document_id IN (32312,32145,56456,4342…)

Запрос без айдишников:

SELECT document_id FROM cards WHERE org_id=5 AND year IN(2010,2012)

Евгений Степанищев (bolknote.ru) 2013

Комментарий для alxt.moikrug.ru:

А зачем вообще поисковый движок?
Почему нельзя искать напрямую в БД? Скорость проседает? Какая БД?

База Оракл. Причины две:
1) более сложный продукт по определению не может работать быстрее узкоспециализированного и простого, только если автор более простого не дебил (Аксёнов не дебил). Ну да, скорость проседает.
2) у Оракла есть полнотекстовый поиск, но полностью отсутствуют морфология, язык запросов и формулы релевантности.

Томин Алексей (alxt.moikrug.ru) 2013

Комментарий для Евгения Степанищева:

Да, второй пункт решает.
Я бы посмотрел в сторону 25 попарных индексов, ну и апачевского чуда.
Все же делать оболочку над поиском- решение стратегически неверное — нету простоты.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для alxt.moikrug.ru:

Я бы посмотрел в сторону 25 попарных индексов, ну и апачевского чуда

А не 25 попарных индексов и не нужно, проблемы возникают с первой выборкой, как правило. Именно первый запрос даёт много айдишников, остальные редко приходится докачивать.

Вопрос ещё как «Сфинкс» поведёт себя под той же загрузкой, которую держит «Оракл». Нормального нагрузочного тестирования ещё не было, простые тесты показывают, что «Сфинкс» (вроде) лучше держится, но посмотрим.

Томин Алексей (alxt.moikrug.ru) 2013

Ой. 25 это я переборщил. Попарных 10, если в выборке порядок не важен.
Можно и еще 10 тройных попробовать сделать.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для alxt.moikrug.ru:

Так же мы вчера наваяли вариант когда атрибут держатся в денормализованных таблицах «Оракла», а полнотекстовый делается «Сфинксом» (тут нам на max_matches плевать, первых несколько тысяч ответов с сортировкой по релевантности хватит). Сегодня потестируем.

Томин Алексей (alxt.moikrug.ru) 2013

Я про то, что если буду попарные индексы, то:

  1. Должно чаще хватать одного индекса.
  2. Выборка будет много меньше.

Кстати, если помещать индексы на отдельных компах, то можно запускать несколько поисков и отбирать минимальный результат ...

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Андрей, если ты тут ещё появишься, вопрос технического плана:

4) max_matches в том числе потому появился, что «Сфинкс» не умеет кидать в клиент небуферизированный ответ?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для alxt.moikrug.ru:

Я про то, что если буду попарные индексы, то:

  1. Должно чаще хватать одного индекса.
  2. Выборка будет много меньше.

Кажется выигрыш от использовании попарных индексов перекроет вред от потери в скорости переиндексации (сейчас на нашей неполной тестовой базе переиндексация день занимает, в случае, если мы где-то ошиблись, у нас нет возможности остановить продакшн на несколько дней, чтобы поправить ошибку и переиндексировать).

Кстати, если помещать индексы на отдельных компах, то можно запускать несколько поисков и отбирать минимальный результат

Есть такой вариант, но это костыль, да ещё и дорогой костыль.

artemp.pip.verisignlabs.com 2013

Комментарий для Евгения Степанищева:

В Apache Solr, судя по описанию есть.

JOIN в Solr отнюдь не аналог sql JOIN. В терминах sql это скорее подзапрос. То бишь по сути те же самые костыли что сделали вы, но с очень серьезными ограничениями — нельзя вернуть поля из ’from’, нельзя искать по ’to’(разьве что фильтрами) и постоянный score для всех результатов.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для artemp.pip.verisignlabs.com:

Это уже интересно! Подзапрос — это тоже неплохо, по крайней мере оно где-то у себя в кишках это передаёт, мне не придётся туда-сюда гонять данные. Я вот это место не понял:

нельзя вернуть поля из ’from’, нельзя искать по ’to’(разьве что фильтрами) и постоянный score для всех результатов.

artemp.pip.verisignlabs.com 2013

Комментарий для Евгения Степанищева:

from и to в синтаксисе solr это что с чем джойнится.

  1. Поля из сущностей которые во from вы вернуть в результате не можете. Только те сущности которые в to. То бишь если вы делаете join from документы to резолюции то на выходе у вас только резолюции, а не документы с резолюциями вместе.
  1. Поиск только по from. Аналогично примеру выше искать вы можете только в документах, но не в документах и резолюциях одновременно. Но вы можете наложить фильтр на результат (резолюции).
  1. Результаты после join не ранжированы. Вообще. Score для всех результатов равен 1. Сортировка в этом случает будет по дате попадания в индекс, то бишь кто раньше проиндексирован тот и выше.
Евгений Степанищев (bolknote.ru) 2013

Комментарий для artemp.pip.verisignlabs.com:

Поля из сущностей которые во from вы вернуть в результате не можете. Только те сущности которые в to. То бишь если вы делаете join from документы to резолюции то на выходе у вас только резолюции, а не документы с резолюциями вместе.

Мне, по сути, только ID документов нужны, в резолюциях они указаны.

Поиск только по from. Аналогично примеру выше искать вы можете только в документах, но не в документах и резолюциях одновременно. Но вы можете наложить фильтр на результат (резолюции).

Звучит немного туманно. Что такое фильтр на результат, чем он отличается от поиска?

Результаты после join не ранжированы. Вообще. Score для всех результатов равен 1. Сортировка в этом случает будет по дате попадания в индекс, то бишь кто раньше проиндексирован тот и выше.

Т. е. в случае, если мы ищем по тексту, релевантность идёт лесом. Печально.

artemp.pip.verisignlabs.com 2013

Комментарий для Евгения Степанищева:

Мне, по сути, только ID документов нужны, в резолюциях они указаны.

Значит вам повезло.

Что такое фильтр на результат, чем он отличается от поиска?

Технически это такой же поисковый запрос. Но фильтр может лишь ограничить исходную выборку, но не расширить её. То бишь для случая «дудочка в документах И кувшинка в резолюциях» фильтр применим, а для «дудочка в документах ИЛИ кувшинка в резолюциях» очевидно нет. Ну и фильтры не влияют на score, но с учетом следующего пункта в данном случае это не имеет значения.

релевантность идёт лесом. Печально.

Именно.

Bagir 2013

В оракле есть и морфология и релевантность.

Разбей задачу фильтрации и поиска.
Фильтруй в оракле, ищи в сфинксе с уже более уточненными параметрами и упрощенным запросом.

artemp.pip.verisignlabs.com 2013

Комментарий для Евгения Степанищева:

Технически фильтр это такой же поисковый запрос

Кстати благодаря этой же особенности существует хитрость позволяющая в некоторых случаях обойти проблему constant score. Join можно использовать в фильтре вместо запроса.

Вот пример из документации solr:
Find all products matching ipod (sorted by score) and filter that by the set of products produced by joining manufacturers named «Belkin» or «Apple»
q=ipod&fq={!join from=id to=manu_id_s}compName_s:(Belkin Apple)

hshhhhh (hshhhhh.name) 2013

Комментарий для Евгения Степанищева:

Запрос без айдишников:
SELECT document_id FROM cards WHERE org_id=5 AND year IN(2010,2012)

Я подумал что запрос без _всех_ айдишников, а не только тех что в IN () :)

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Дык разные категории запросов и ускорять надо по-разному. Выделить топ самых тормозных (!) запросов, проанализировать их структуру и пооптимизировать под них вполне годный подход. В бой пускать новую версию необязательно, можно лог запросов взять с боя и проиграть в тестовой среде же.

Определения «совсем чуть» vs «очень много» итп у нас наверняка расходятся :-) Но частности обсужать действительно нету смысла, тут в блоге это дико неудобно. Надо как минимум человеческой почтой, а то скайпом. А то и чуток консалтинга вам цинично продать!!!

Теги beta либо release всегда довольно условны, есть желание от них когда-нибудь цинично отказываться. Например, текущий транк от названия 2.1.2-release отделяет документация. Традиционно, его уже вполне себе используют в бою. Как только допишем, так сразу.

Фильтрация по WHERE json.key1=123 очевидно медленнее, чем по WHERE key1=123, но только фильтрация (поиск, ранжирование, сортировка итп не меняются ведь никак). Стало интересно, сделал тестовый запрос, 170K полнотекстовых матчей, крохотные JSON атрибуты вида {«p»:1112547810,«c»:1065929}. Получилось 0.055 sec WHERE channel_id=1065929 с обычными атрибутами и 0.063 sec WHERE j.c=1065929, фильтрация согласно SHOW PROFILE жрет 0.012 sec и 0.021 sec соответственно.

Буфер размером в max_matches сейчас аллоцируется сразу, поэтому при 10M и при 10 параллельных поисках сразу минус 3.2 GB, даже если там по 1 документу в итоге найдется. Еще этот буфер сразу обнуляется, что при 10K и даже 100K пофиг, а при 10M важно. Это технически возможно изжить и заоптимизировать, сделать буфер сделать динамическим. Мы даже начинали, но почему-то не доделали, не помню, почему. Никаких резких прыжков скорости вниз на отметке ровно 10001 матч, очевидно, нету. Но при больших размерах буфера и запросах, которые находят МНОГО матчей, мы совершенно неизбежно больше CPU тратим на сортировку найденного (там приоритетная очередь внутри), на отметке 10K уже потери около 10% по сравнению с 1K, дальше больше, поэтому и надо аккуратно. На запросах, которые находят 1 документ при буфере 100K, разница неощутимая, порядка 0.001 sec. Про буферизацию отклика вообще не понял вопроса. Без буферизации в той или иной мере никакого ответа еще нет и быть не может, тк. сортировка не выполнена.

Запросы с чистым атрибутивным поиском, которые ты регулярно зачем-то приводишь в пример, типа тех WHERE org_id=5 AND year IN(2010,2012) мы заведомо хреновато обрабатываем. Индексов по атрибутам-то нету! И случается полный перебор. Если селективность высокая, база с такими справляется (много) лучше, тем более, если JOIN. Такое поведение у нас можно проэмулировать виртуальными ключевиками типа _org_id_5. Если низкая, то все равно мы, полный перебор подмножества колонок без оверхедов это таки довольно быстро.

Переиндексация длиной в день это несколько терабайт данных или это dict=crc и включенные подстроки? Во втором случае dict=keywords и переиндексация будет занимать, ориентировочно, 2-3 часа. Плюс появятся wildcards. Минус затормозит поиск по запросам [а] (по одной или двум буквам), который все равно непонятно, зачем.

Если все то, что описано artemp верно, то это у Solr ни разу не полноценный JOIN, а настолько херовенькое пересечение, что реализовать механизм поудобнее (без глупых ограничений п.2 и п.3) мы можем совсем нетяжело. Тупо реализовать SET local_var:=(SELECT id FROM ...), плюс возможность использовать такой local_var в фильтре для последующих запросов в текщуей сессии, и всех дел.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Определения «совсем чуть» vs «очень много» итп у нас наверняка расходятся :-) Но частности обсужать действительно нету смысла, тут в блоге это дико неудобно. Надо как минимум человеческой почтой, а то скайпом. А то и чуток консалтинга вам цинично продать!!!

Будешь ли ты на 404?

Но при больших размерах буфера и запросах, которые находят МНОГО матчей, мы совершенно неизбежно больше CPU тратим на сортировку найденного (там приоритетная очередь внутри), на отметке 10K уже потери около 10% по сравнению с 1K, дальше больше, поэтому и надо аккуратно. На запросах, которые находят 1 документ при буфере 100K, разница неощутимая, порядка 0.001 sec. Про буферизацию отклика вообще не понял вопроса. Без буферизации в той или иной мере никакого ответа еще нет и быть не может, тк. сортировка не выполнена.

Погоди-погоди. Тут я кажется одну вещь не понимаю и это меня тревожит.

Скажи, если я выбираю, условно
SELECT document_id FROM document ORDER BY document_id, то отсортированы будут все данные или только те, которые лезут в max_matches?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Такое поведение у нас можно проэмулировать виртуальными ключевиками типа _org_id_5.

Не подойдёт такое решение — организаций тысячи и это число быстро растёт. Каждый раз для новой организации перестраивать полностью индекс?

Андрей Аксенов 2013

На 404 не буду, вроде бы не звали, да и все равно конфликт дат.

Запрос SELECT document_id FROM document ORDER BY document_id OPTION max_matches=1234 создаст приоритетную очередь размером 1234 элемента по 32 байта на элемент, просканирует все 10050011 строк индекса document (ну те. пробежится по лежащему в оперативке содержимому document.spa), выберет top-1234 элемента по условию document_id ASC.

Индекс перестраивать вообще незачем, если у организаций не меняются постоянно org_id, откуда ты это взял?! Запись id=123, orgid=5, name=«ООО Ромашка», xkeywords=«_orgid5» можно найти перебором 10050011 строк по условию WHERE orgid=5, а можно чтением списка только нужных записей по условию WHERE MATCH(’_orgid5’). В этом месте не удержусь от подколки про «попробовали все фокусы»; ага, угу, конечно, наверняка!!!

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Запрос SELECT document_id FROM document ORDER BY document_id OPTION max_matches=1234 создаст приоритетную очередь размером 1234 элемента по 32 байта на элемент, просканирует все 10050011 строк индекса document (ну те. пробежится по лежащему в оперативке содержимому document.spa), выберет top-1234 элемента по условию document_id ASC.

Ффух, значит работает как и думал. Хорошо.

Индекс перестраивать вообще незачем, если у организаций не меняются постоянно org_id, откуда ты это взял?! Запись id=123, orgid=5, name=«ООО Ромашка», xkeywords=«_orgid5» можно найти перебором 10050011 строк по условию WHERE orgid=5, а можно чтением списка только нужных записей по условию WHERE MATCH(’_orgid5’)

Я просто не сообразил чем «поиск» отличается от «фильтрации».

В этом месте не удержусь от подколки про «попробовали все фокусы»; ага, угу, конечно, наверняка!!!

Э, товарищ, твои костыли детские костылёнки по сравнению с нашими ;) Посмотри как я придумал упаковывать текст для разных организаций:

ORGID5 тут текст ENDID5 ORGID6 тут другой текст ENDID6

а потом поиск: ORGID5 >> текст >> ENDID5

у нас даже такой способ используется.

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Э, ну везде перед этим ты довольно методично и не раз приводишь примеры про WHERE orgid=5, а вовсе не WHERE MATCH(’_orgid5’), плюс странноватые ламентации про max_matches=10K. Это все в целом как бы намекает, поэтому я по-прежнему готов ставить классические 25 центов, что какой-нибудь из фокусов таки не опробован.

Способ с BEFORE (ну то есть a>>b) прикольный. Еще можно [ZONE:orgid5 (текст)], вроде бы в этом случае ограничений на [текст] меньше. Навскидку, можно нарваться на беду со скоростью, если есть частотные словами в части [текст]. Синтаксически для избавления от такой упаковки оптимально уметь, условно говоря, свой набор полей для каждого документа, Solr как раз вроде умеет. Насколько эффективно оно внутри у них устроено и исполняется, впрочем, не знаю. C точки зрения производительности клевее всего сработает фокус с предобработкой индексируемых данных и запросами типа [_orgid5_дудочка _orgid5_кувшинчик]. Тоже все пробовали? ;)

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

C точки зрения производительности клевее всего сработает фокус с предобработкой индексируемых данных и запросами типа [_orgid5_дудочка _orgid5_кувшинчик]. Тоже все пробовали? ;)

Такое не пробовали — слов многовато, страшновато применять. Я всё-таки смотрю в сторону JSON, раз уж ты говоришь, что он стабильный, но вот что смущает.

Если я засуну все резолюции в структуру вида
[ { поля резолюции1 }, [ { поля резолюции2 }, … ] (понятно, что текстовые поля где-то снаружи должны быть, то как мне найти документ, у которого есть резолюция, созданная пользователем №1 в организации №2? Сфинкс не умеет же массивы просматривать?

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

У меня тут слюньки потекли на твою фразу, что можно было бы реализовать SET local_var:=(SELECT id FROM …). Насколько вероятно, что ты или кто-то из твоих ребят это уже программируют? ;)

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Поддержка JSON в транке полная (те. можно сохранить любой объект), довольно прилично оттестирована, более того, даже сколько-то и пооптимизирована уже под частые юзкейсы. У меня про нее осталась одна большая концептуальная головная боль, она тоже упирается в дурацкие родовые ограничения формата (4 GB строк на индекс, аррргх). Увы, это теперь решается только довольно большой внутренней переделкой, за полдня не сделать!!!

Синтаксиса для просмотра массивов пока нету, обсуждали варианты на будущее буквально сегодня. Склоняюсь к питоновскому, те. нечто вроде SELECT * FROM myindex WHERE ANY(x.uid=1 AND x.orgid=2 FOR x IN jsoncol)

Кроме SET local_var нам «самим по себе» еще много чего куда более насущного есть запрограммировать. Однако можем попробовать (попробовать) сделать вот что. Я попробую очень быстро нахакать прототип, отдам снапшот тебе потестировать, и если (если) выйдет приятно по функционалу и производительности, обсудим коммерческую доводку до ума. Если такое интересно, напиши в скайп, чуть обсудим технические детали.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Я попробую очень быстро нахакать прототип, отдам снапшот тебе потестировать, и если (если) выйдет приятно по функционалу и производительности, обсудим коммерческую доводку до ума. Если такое интересно, напиши в скайп, чуть обсудим технические детали.

Я ж не владелец бизнеса, попробую предложить.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Синтаксиса для просмотра массивов пока нету, обсуждали варианты на будущее буквально сегодня. Склоняюсь к питоновскому, те. нечто вроде SELECT * FROM myindex WHERE ANY(x.uid=1 AND x.orgid=2 FOR x IN jsoncol)

Мне кажется логичным рассматривать такой массив как таблицу и делать это синтаксисом подзапросов, но кажется пайтонячий будет просто короче.

Это не Пайтон, кстати,

ANY(x.uid=1 AND x.orgid=2 FOR x IN jsoncol)

Пайтон вот:

ANY(x.uid FOR x IN jsoncol IF x.uid=1 AND x.orgid=2)

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

В скайп-то вылези все равно.

C:\Temp>cat 1.py
a = [1,3,5,7]
print any(x==1 for x in a)
print any(x==10 for x in a)

C:\Temp>python 1.py
True
False

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Сейчас поем, вылезу.

print any(x==1 for x in a)

Да, но они работают по-разному вообще. На выходе будут массивы разной длины. Мой вариант — фильтрация, твой — итерация по всему.

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Так мне и не нужны массивы. Мне нужны проверки!

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Евгения Степанищева:

Погоди, ты кажется не понимаешь в чём выгода.

Такое: [x for x in [1,0,0,0,0,0,0,0,0,10] if x]
Даст два элемента: [1, 10]

А такое: [x != 0 for x in [1,0,0,0,0,0,0,0,0,10]]
Кучу: [True, False, False, False, False, False, False, False, False, True]

Мне кажется, что первое выгоднее, особенно, если массивы большие.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Причём, если с True и False ничего полезного не сделаешь, то со значениями можно:

max(x for x in [1,0,0,0,0,0,0,0,0,10] if x)
Выдаст 10

Андрей Аксенов 2013

Комментарий для Евгения Степанищева:

Причем тут вообще детали реализации any() внутри питоновского рантайма? Я ж не собираюсь питон встраивать, я собираюсь тупо синтаксис позаимствовать. Этот пока выглядит наиболее внятным.

Евгений Степанищев (bolknote.ru) 2013

Комментарий для Андрей Аксенов:

Да, я понимаю :) просто хочется выжать из этого синтаксиса как можно больше :)