Советы о том, как улучшить текущую реализацию нечеткого поиска - PullRequest
9 голосов
/ 21 октября 2010

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

Во-первых, терминология - это всего лишь несколько имен (или терминов). Для каждого слова мы разделяем его на токены по пробелам, а затем перебираем каждый символ, чтобы добавить его в три. На терминальном узле (например, когда достигается символ y в клубнике) мы сохраняем в списке индекс для основного списка терминов. Таким образом, конечный узел может иметь несколько индексов (поскольку конечный узел для клубники будет соответствовать «клубника» и «аллергия на клубнику»).

Что касается фактического поиска, то поисковый запрос также разбивается на токены по пробелам. Алгоритм поиска запускается для каждого токена. Первым символом поискового токена должно быть совпадение (поэтому траул никогда не совпадет с клубникой). После этого мы проходим дочерние элементы каждого последующего узла. Если есть дочерний элемент с совпадающим символом, мы продолжаем поиск со следующего символа токена поиска. Если дочерний элемент не соответствует данному символу, мы смотрим на дочерних элементов, используя текущий символ маркера поиска (поэтому не продвигаем его). Это часть нечеткости, поэтому «stwb» будет соответствовать «клубничке».

Когда мы дойдем до конца поискового токена, мы проведем поиск по оставшейся части структуры trie в этом узле, чтобы получить все потенциальные совпадения (поскольку индексы для основного списка терминов находятся только на терминальных узлах). Мы называем это сверткой. Мы храним индексы, устанавливая их значение в BitSet. Затем мы просто и BitSets из результатов каждого результата поиска токена. Затем мы берем, скажем, первые 1000 или 5000 индексов из анодированных битовых наборов и находим фактические термины, которым они соответствуют. Мы используем Левенштейна для оценки каждого термина, а затем сортируем по баллам, чтобы получить окончательные результаты.

Это работает довольно хорошо и довольно быстро. В дереве более 390 тыс. Узлов и более 1,1 млн. Имен реальных терминов. Тем не менее, есть проблемы с этим в его нынешнем виде.

Например, поиск 'car cat' вернет катетеризацию, когда мы этого не хотим (поскольку поисковый запрос состоит из двух слов, результат должен быть не менее двух). Это было бы достаточно легко проверить, но это не относится к ситуации, подобной процедуре катетеризации, поскольку это два слова. В идеале, мы бы хотели, чтобы это соответствовало чему-то вроде сердечной катетеризации.

Исходя из необходимости исправить это, мы предложили некоторые изменения. С одной стороны, мы проходим через три в смешанном поиске глубины / ширины. По сути, мы углубляемся до тех пор, пока персонаж совпадает. Те дочерние узлы, которые не совпадают, добавляются в очередь с приоритетами. Очередь приоритетов упорядочена по расстоянию редактирования, которое можно рассчитать при поиске дерева (поскольку при совпадении символов расстояние остается неизменным, а если нет, оно увеличивается на 1). Делая это, мы получаем расстояние редактирования для каждого слова. Мы больше не используем BitSet. Вместо этого это карта индекса для объекта Terminfo. Этот объект хранит индекс фразы запроса, термин фразы и оценку. Таким образом, если поиск - «автомобильная кошка», а соответствующий термин - «процедура катетеризации», индексы фраз будут равны 1, как и индексы фраз запроса. Для «сердечной катетеризации» термины индексы фраз будут равны 1,2, как и индексы фраз запроса. Как вы можете видеть, впоследствии очень просто взглянуть на количество индексов терминов и индексов фраз запроса, и если они по крайней мере не равны количеству поисковых слов, их можно отбросить.

После этого мы складываем расстояния редактирования слов, удаляем слова из термина, которые соответствуют индексу фразы термина, и подсчитываем оставшиеся буквы, чтобы получить истинное расстояние редактирования. Например, если вы соответствовали термину «аллергия на клубнику», а ваш поисковый запрос был «солома», у вас было бы 7 баллов по клубнике, то вы бы использовали термин «индекс фразы», ​​чтобы отбросить клубнику из этого термина, и просто сосчитать «аллергия на» (минус пробелы), чтобы получить оценку 16.

Это дает нам точные результаты, которые мы ожидаем. Тем не менее, это слишком медленно. Если раньше мы могли получить 25-40 мс при поиске по одному слову, то теперь это могло быть целых полсекунды. Это в значительной степени связано с такими вещами, как создание экземпляров объектов TermInfo, использование операций .add (), операций .put () и тот факт, что мы должны возвращать большое количество совпадений. Мы могли бы ограничить каждый поиск только 1000 совпадений, но нет гарантии, что первые 1000 результатов для "автомобиля" будут соответствовать любому из первых 1000 совпадений для "кошки" (помните, что существует более 1,1 миллиона терминов).

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

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

Ответы [ 2 ]

3 голосов
/ 31 октября 2010

Ух ты ... крутой.

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

Однако я хочу поделиться некоторыми мыслями ...

Fuziness - это не что-то вроде соломы *, это скорее неправильное печатание некоторых слов. И каждый пропущенный / неправильный символ добавляет 1 к расстоянию.

Обычно очень и очень трудно иметь частичное совпадение (символы подстановки) и нечеткость одновременно!

Токенизация, как правило, хорошая идея.

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

Я видел несколько довольно хороших реализаций с использованием многомерных деревьев диапазонов.

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

Вы можете, например, использовать семантическую базу данных, такую ​​как сезам, и при импорте документов импортировать каждый токен и документ как узел. Затем в зависимости от позиции в документе и т. Д. Вы можете добавить взвешенное отношение.

Тогда вам понадобятся токены в некоторой структуре, где вы можете делать эффективные нечеткие совпадения, такие как bk-деревья. Я думаю, что вы могли бы индексировать токены в базе данных mysql и выполнять побитовые функции сравнения, чтобы получить различия. Есть функция, которая возвращает все совпадающие биты, если вы транслируете свои строки в ascii и группируете биты, вы можете достичь чего-то довольно быстрого.

Однако, если вы сопоставили токены со строкой, вы можете построить гипотетическое антитело с идеальным соответствием и запросить в вашей семантической базе данных ближайших соседей.

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

Однако вы также можете выполнять подстановочные совпадения (префикс, суффикс или оба), но тогда нет никакой нечеткости.

Вы также можете индексировать целое слово или различные объединения токенов.

Однако могут быть специальные реализации bk-tree, которые поддерживают это, но я никогда не видел ни одного.

2 голосов
/ 21 октября 2010

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

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

Я удивлен размером вашего дерева. Основной словарь приемлемых слов может быть несколько тысяч. Тогда есть общие префиксы и суффиксы. Так как структура - это древовидная структура, вы можете соединить вместе подпрограммы и сэкономить много места. Как три базовых префикса могут подключаться к основному словарю, а затем терминальные узлы основного словаря могут подключаться к общему суффиксу (которые могут фактически содержать циклы). Другими словами, дерево может быть обобщено в конечный автомат. Это дает вам большую гибкость.

Независимо от всего этого, у вас есть проблемы с производительностью. Хорошая вещь о проблемах производительности - чем они хуже, тем легче их найти. Я был настоящим вредителем в StackOverflow, указывая на это. Эта ссылка объясняет, как это сделать, ссылается на подробный пример и пытается развеять некоторые популярные мифы. Короче говоря, чем больше времени вы тратите на выполнение чего-то, что вы могли бы оптимизировать, тем больше вероятность, что вы поймаете это на этом, если просто остановитесь и посмотрите. Я подозреваю, что много времени уходит на операции над раздутой структурой данных, а не просто на получение ответа. Это обычная ситуация, но ничего не исправляйте, пока образцы не укажут вам прямо на проблему.

...