[Из песочницы] Нахождение похожих имен средствами MySQL+PHP

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

Не так давно возникла следующая задача:


  • Есть в составе информационной системы (ИС) база данных (БД) актеров, музыкантов, DJ-ев, а так же иногда политиков, ученых и других публичных лиц.
  • ИС заполнялась любителями и дилетантами, многие из которых затруднялись правильно перевести на русский язык ту или иную фамилию. А иногда случалось и такое, что имя звучало по-русски совсем не так, как писалось на иностранном языке.
  • В БД имена заносились в достаточно свободной форме. Порядок слов в имени часто менялся (особенно для азиатских имен), дополнительно могли указываться (или не указываться) прозвища и псевдонимы.
  • Бывали и просто описки — замены, пропуски или добавления букв. Иногда часть букв по какой-то причине писалась латиницей.
  • Акцидентных символов, к счастью, не было.
  • Имена хранились в MyISAM таблицах MySQL в кодировке UTF-8.
  • Общее число имен в БД составляло примерно 138,5 тысяч.

Это то, что «дано». А «требовалось» следующее:


Автоматическим просмотром БД выявить и сформировать пары похожих по какому-то критерию имен, которые передать для дальнейшего ручного анализа на предмет того, принадлежат ли они одной персоне или разным. Причем, поиск должен был обладать некой параноидностью. Т.е. в отличие от »Возможно, Вы имели в виду …» должен был выдавать несколько больше имен по принципу »Лучше перебдеть, чем недобдеть». А, значит, и анализировать на предмет схожести должен был несколько большее число пар. Причем, делать это не в реальном времени, но за реально конечное время. Т.е., не за сутки и недели, а, в худшем случае, за несколько часов.

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

7d35b05efd3e4272a09897e517834b89.jpg

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

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

  • Фонетическое кодирование (в частности, «Русский Метафон» Петра Каньковского и его модификации[1]).
  • Мера схожести, основанная на дистанциях редактирования Левенштейна[2].
  • Выделение общих форм[3].
  • Алгоритм Джаро-Винклера[4].
  • N-граммный анализ.[5].

По здравому размышлению принято было следующее:


Фонетическое кодирование не подходит в силу специфики наполнения БД. Фонетический поиск способен найти имена, записанные с ошибками на слух, но не переведенные дилетантами-любителями. Поясню на примере:

Жаннетта «Ангел» Деверю

Вот приходит в наш родной паспортный стол капитан третьего ранга Jeannette Devereaux[6] и представляется по уставу четко и громко. И паспортистка может на слух записать ее фамилию и «Диверю» и «Девирю» и даже «Дивиру». Да еще удвоенные согласные в имени пропустить. Все эти «слуховые галлюцинации» прекрасно отслеживаются алгоритмами нечеткого поиска с фонетическим кодированием.

Но вот горе-«переводчик», не знающий специфики французского языка (но знакомый с английским языком), переписывая данные из военного билета, запросто может написать «Жэннетт Деверокс». А особо одаренные могут указать позывной или записать фамилию впереди имени.

Или даже многие англоязычные «переводчики» не знают, что имя «Ralph Fiennes» следует читать не «Ральф», а «Рэйф Файнс».

Так что, фонетическое кодирование здесь не подходит, потому что в данном случае ошибочные имена пишутся не «так, как слышаЦЦа», а «так, как видRTCR».

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

Так, может быть, использовать расстояние редактирования?


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

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

Другие методы


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

Опять таки, при перемене словами мест, релевантность резко уменьшается.

Вычисление дистанции Джаро-Винклера работает существенно быстрее. Но, к сожалению, так же не является устойчивым к перемене слов местами.

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

Этап первый. Лобовой удар.
Итак, для начала, чтобы составить представление о временной сложности триграммного сравнения, я написал самый простой вариант скрипта, попарно сравнивающего имена по всей БД.

Написание и тестирование скрипта проводилось в домашних условиях на компьютере восьмилетней давности еще с одноядерным Intel Pentium D 925 на материнской плате ASUS P5B-E и с обычными заезженными хардами Seagate примерно того же возраста.

Скрипт этот работал, что называется, в лоб. Считывал БД, каждое имя, предварительно нормализованное, разбивал на уникальные триграммы и подсчитывал число их вхождений в каждое из предыдущих имен (т.е., находящихся в БД ранее испытуемого). Если результат деления удвоенного числа совпадений на сумму уникальных триграмм в каждом из этих имен превышал некоторый порог (например, 0,75 или 0,8), то это фиксировалось в файле.

Нормализация имени заключалась в следующем:

  • Английские буквы и некоторые цифры заменялись сходными по написанию русскими буквами.
  • Все строчные буквы заменялись прописными.
  • Буква «Ё» заменялась на «Е», а «Ъ» на «Ь».
  • Все прочие буквы, знаки, непечатные символы и их последовательности заменялись двумя пробелами.
  • Имя обрамлялось двойными пробелами.

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

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

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

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

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

С другой стороны, во-первых, потребность в различении подобных имен будет возникать не часто. А, во-вторых, подсчет релевантности по всем триграммам, а не только по уникальным, значительно усложнит эту процедуру. Потому что вместо удвоенного числа общих триграмм придется считать сумму вхождений триграмм из первого имени во второе и триграмм из второго имени в первое. Т.е., грубо это увеличит временные расходы на вычисление релевантности примерно в 2 раза. Ну и, в-третьих, это не позволит применить к процедуре вычисления релевантности некоторые оптимизации, о которых пойдет речь далее.

Итак, лобовой алгоритм был написан и опробован на первых нескольких тысячах имен. Результат был удручающим. Как и следовало ожидать, время поиска совпадений по всему массиву имен имело ярко выраженную квадратичную зависимость от его размера. Прогноз тренда на обработку всего массива из 130–140 тысяч имен составил около двух лет. Многовато будет!

Зато появилась начальная оценка производительности алгоритма.

Этап второй. Поиск способов оптимизации.
Прежде всего, пришлось оценить, какие элементы алгоритма нуждаются в оптимизации, а какой является пресловутым «бутылочным горлышком», тормозящим всю систему. Областей оптимизации было выявлено несколько:
  1. Начальная выборка из БД должна выдавать не весь массив имен, а только те, которые уже хоть чуть сходны с тестируемым, для уменьшения числа прочих операций. Анализ времени выполнения различных участков кода показал, что запросы к БД и являются «бутылочным горлышком». Необходимо было уменьшать, как время выполнения каждого запроса, так и число выдаваемых имен-претендентов в каждой выборке.
  2. Операции сравнения и копирования подстроки, проводимые со строками в мультибайтной кодировке UTF-8, по производительности в несколько раз уступают тем же операциям со строками в однобайтной кодировке.
  3. Не обязательно производить точное вычисление коэффициента релевантности, если доля общих триграмм мала. В этом случае появляется возможность прервать этот процесс, как только станет ясно. что даже если все остальные триграммы окажутся общими, это не приведет к превышению требуемого граничного значения.
  4. Прочее по мелочам.

Этап третий. Реализация.
Прежде всего, как самое простое, была выполнена попытка перехода от кодировки нормализованных имен UTF-8 к кодировке Windows-1251. Однако, оказалось, что MySQL весьма ревностно относится к хранению в БД строк в разной кодировке даже в разных таблицах. Что приводит к возникновению нелокализуемых ошибок при операциях с БД. Поэтому нормализованное имя хранится в дополнительной присоединенной таблице все равно в формате UTF-8, а при необходимости перекодируется в Windows-1251.

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

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

Ну и, самое главное, обработанные имена индексировались триграммным индексом. Т.е. в дополнительной таблице перечислялись все уникальные триграммы, присутствующие в нормализованном имени. При этом, сами триграммы, чтобы не хранить их в «медленной» кодировке UTF-8, хранились в виде целочисленного хеша — трем младшим байтам числа соответствовали числовые значения соответствующих символов в кодировке Windows-1251.

Но при этом был допущен совершенно неэффективный, как было выяснено в дальнейшем, подход — для каждой триграммы из проверяемого имени делалась своя собственная выборка имен-претендентов, которые потом объединялись в единый массив. При этом каждое имя в этом массиве сопровождалось счетчиком — в скольких выборках оно встретилось. Предполагалось, что это даст некоторый выигрыш в вычислении релевантности за счет того, что не надо будет затем раскладывать эти имена на триграммы — достаточно было использовать этот счетчик, чтобы сразу вычислить релевантность.

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

Этап четвертый. Дальнейшие улучшения.
В результате описанных оптимизаций удалось достичь существенного сокращения временных затрат:

Применение триграммного индекса для начальной выборки сократило прогноз результирующего времени с двух лет до менее чем трех месяцев. Использование в его качестве числовых значений вместо подстрок в формате UTF-8 позволило дополнительно сократить время общей обработки еще на 10–12%. Но все равно 2,5 месяца непрерывной работы представлялись слишком большим сроком. Тонким местом оставалась начальная выборка похожих имен.

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

Пришлось отказаться от идеи этого счетчика, но получать весь начальный набор имен-претендентов за один запрос при помощи SQL-конструкции WHERE IN. Это позволило сократить прогноз еще в несколько раз до одного месяца.

Далее была изменена процедура вычисления релевантности. На основании нижней границы релевантности и числа уникальных триграмм в именах вычислялось максимально допустимое число «промахов» — число триграмм в имени-претенденте, не входящих в проверяемое имя. Затем все триграммы имени-претендента проверялись на вхождение в проверяемое имя. Как только число «промахов» превышало максимально допустимое, сравнение прекращалось. Это позволило сократить прогноз времени еще на несколько процентов. Но до промышленных значений было все равно очень далеко.

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

Найти подход помог простой просмотр найденных пар сходных имен. Практически во всех таких парах находились общие подпоследовательности длиной 6–7 символов (включая окаймляющие двойные пробелы). Путем нехитрых рассуждений (которые здесь не приводятся ввиду их нестрогости) можно прийти к выводу, что для достижения релевантности более 0,75, нормализованные имена должны иметь общую подстроку из 5 символов или длиннее.

Таким образом, от индексации предварительного поиска триграммами переходим к индексации пентаграммами (5-символьными подстроками). Чтобы уместить хеш пентаграммы в целочисленной переменной, имеющей в 32-разрядной версии PHP размер 32 бита, каждый символ представляем в виде 5-бит, благо символов всего 31 (без «Ё» и «Ъ») и пробел.

Еще одно небольшое дополнение.
Как писал выше, при вычислении релевантности уникальные триграммы одного из имен сравнивались на предмет совпадения с триграммами второго имени. При этом расчет прекращался при превышении числа «промахов» максимально допустимого количества.

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

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

Это несколько спорное усовершенствование. При задании граничного значения релевантности 0,75 оно не приводит к ускорению, а, наоборот, приводит к потере производительности на 3% за счет увеличения сложности выборки. Но если задать границу релевантности 0,8, то получаем уже 3% выигрыша.

Тем не менее, данный прием был оставлен. Потери не такие уж большие, а вначале можно сделать предварительный поиск похожих пар с высокой степенью релевантности. И только после предварительной чистки задать граничное значение равным 0,75.

Итог.
В результате перехода от начальной выборки по общим триграммам к выборке по пентаграммам удалось существенно ускорить работу скрипта. Поиск похожих с релевантностью 0,8 пар среди 138,5 тысячи имен составил около 5 часов вместо одного месяца. Т.е. единичный поиск похожих имен во всей БД для заданного имени составляет (если принять квадратичную зависимость времени обработки) около 0,26 с. Несколько много, но надо учитывать, что это был тестовый прогон не на сервере с мощным процессором и высокопроизводительной дисковой системой, а на полумертвом домашнем компьютере восьмилетней давности.

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

Зато индексация гексаграммами вполне пригодна для подсказок вида «Возможно, Вы имели в виду …», когда излишняя параноидность не требуется, а наоборот, следует находить минимальное количество наиболее подходящих имен.

В принципе, при работе с длинными именами можно использовать индексы семисимвольных подстрок. А чтобы поместить их в 32-битные переменные произвести предварительное фонетическое кодирование с целью сокращения числа символов до 15 и пробела. Но такая задача в тот момент не стояла.

К сожалению, вскоре после написания скрипта, сайт был заблокирован по обвинению в нарушении авторских прав. Хотя вскоре он переехал на другой адрес, из-за навалившихся проблем стало не до поиска дублирующих имен.

Фрагменты алгоритма:


Структуры таблиц БД
CREATE TABLE IF NOT EXISTS `prs_persons` (
  `id` int(10) unsigned NOT NULL,
  `name_ru` varchar(255) collate utf8_unicode_ci default NULL, //Переведенное на русский язык имя
//Прочие поля
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `prs_normal` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) collate utf8_unicode_ci default NULL, //Нормализованное имя
  `num` int(2) unsigned NOT NULL, //Число уникальных триграмм в нормализованном имени
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `prs_5gramms` (
  `code` int(10) unsigned NOT NULL, //Числовой код пентаграммы
  `id` int(10) unsigned NOT NULL, //Идентификатор имени, в котором присутствует
  KEY `code` (`code`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;


Константы и вспомогательные переменные, предварительные действия:
	$opt_debug_show_sql = 0;
	$opt_debug_mode = 0;
	$opt_table_prefix = "prs";

	define('MIN_RELEVANT', 0.8);
	define('SITE_PREFIX', "…"); //Здесь храним префикс страницы персоны на сайте.

//Максимально допустимое соотношение количества уникальных триграмм,
//чтобы имена еще могли быть схожими.
	$len_ratio = MIN_RELEVANT / (2 - MIN_RELEVANT);

	$suitablesin = iconv("Windows-1251", "UTF-8",
"АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЬъЫыЪьЭэЮюЯяAaBbCcEegHKkMmnOoPprTuXxYy03468");
	$suitablesout =
"ААББВВГГДДЕЕЕЕЖЖЗЗИИЙЙККЛЛММННООППРРССТТУУФФХХЦЦЧЧШШЩЩЬЬЫЫЬЬЭЭЮЮЯЯААВЬССЕЕДНККМТНООРРГТИХХУУОЗЧБВ";

	$charcode = array( ' ' => 0, 'А' => 1, 'Б' => 2, 'В' => 3, 'Г' => 4, 'Д' => 5, 'Е' => 6, 'Ж' => 7,
		'З' => 8, 'И' => 9, 'Й' => 10, 'К' => 11, 'Л' => 12, 'М' => 13, 'Н' => 14, 'О' => 15,
		'П' => 16, 'Р' => 17, 'С' => 18, 'Т' => 19, 'У' => 20, 'Ф' => 21, 'Х' => 22, 'Ц' => 23,
		'Ч' => 24, 'Ш' => 25, 'Щ' => 26, 'Ь' => 27, 'Ы' => 28, 'Э' => 29, 'Ю' => 30, 'Я' => 31); //Пятибитные коды символов.

	set_time_limit(0); //Время выполнения скрипта неограниченно.

	function message_die($errno, $error, $file, $line)
	{
		if ($errno)
		{
			print "

Error " . $errno . " " . $file . "(" . $line . "): " . $error; die(); } }; $fout = fopen("…", "w"); //Открываем файл для записи похожих пар. //Здесь должны находиться подготовка списка имен к анализу и основной цикл сравнения. fclose($fout);



Тело основного цикла сравнения:
//Для каждого еще непроанализированного имени:

	$id = …; $name_ru = …; //Считываем ID персоны и имя в кодировке UTF-8.

	$rusn = "  "; //Нормализованное имя начинается двумя пробелами.
	$ischar = FALSE;

	for ($j = 0; $j < mb_strlen($name_ru, "UTF-8"); $j++)
	{
		$char = mb_substr($name_ru, $j, 1, "UTF-8");

		if (($pos = mb_strpos($suitablesin, $char, 0, "UTF-8")) === FALSE)
		{
			if ($ischar)
			{
			//Все недопустимые символы заменяем двойными пробелами.
				$rusn .= "  ";
				$ischar = FALSE;
			}
		}
		else
		{
		//Прочие перекодируем в Windows-1251 с учетом возможного транслита.
			$rusn .= $suitablesout{$pos};
			$ischar = TRUE;
		}
	}

	if ($ischar) $rusn .= "  "; //Добавляем два пробела в конце.

	if (strlen($rusn) < 5) continue; // Имена из одних пробелов не рассматриваются.

	$norm = iconv("Windows-1251", "UTF-8", $rusn); //Перекодируем в UTF-8 для записи в БД.

//Заносим в массив уникальные пентаграммы нормализованного имени:

	$subgramms = array();

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

	$code = ($charcode[$rusn{2}] << 5) | $charcode[$rusn{3}];

	for ($j = 4; $j < strlen($rusn); $j++)
	{
		$code = (($code << 5) | $charcode[$rusn{$j}]) & 0x1FFFFFF;

		$subgramms[$code] = $code; //Записываем хеш пентаграммы в массив.
	}

//Составляем список уникальных триграмм в нормализованном имени и подсчитываем их количество.

	$trigramms = array();

	for ($k = 0; $k < strlen($rusn) - 2; $k++)
		$trigramms[$trigramm = substr($rusn, $k, 3)] = $trigramm;

	$n = count($trigramms);

//Вычисляем граничные значения числа уникальных триграмм в сходных именах:

	$nmin = ceil($n * $len_ratio);
	$nmax = floor($n / $len_ratio);

//Считываем из БД список кандидатов на сходные имена.

	$similars = fquery("SELECT n.id AS id, n.name AS name, n.num AS num FROM prs_5gramms AS g, prs_normal AS n WHERE g.code IN (^N) AND n.id = g.id AND 

n.num >= ^N AND n.num <= ^N", $subgramms, $nmin, $nmax);

//Записываем в БД нормализованное имя и количество уникальных триграмм.

	fquery("INSERT INTO prs_normal (id, name, num) VALUES (^N, ^S, ^N)", $id, $norm, $n);

//Записываем в БД список пентаграмм этого нормализованного имени.

	foreach ($subgramms as $key=>$code)
		fquery("INSERT INTO prs_5gramms (code, id) VALUES (^N, ^N)", $code, $id);

	unset($subgramms); //И освобождаем его.

//Для каждого имени-кандидата:

	for ($i = 0; $i < @mysql_num_rows($similars); $i++)
	{
		$similar = @mysql_fetch_assoc($similars) OR
			message_die(@mysql_errno(), @mysql_error(), __FILE__, __LINE__);

		$name = iconv("UTF-8", "Windows-1251", $similar['name']);
		$simid = $similar['id'];
		$m = $similar['num']; //Количество уникальных триграмм.
		$nm = 0; //Число общих триграмм.
		$done = TRUE;
		$miss = floor($m - MIN_RELEVANT * ($n + $m) / 2); //Число допустимых "промахов".

		for ($k = 0; ($k < strlen($name) - 2) & ($miss >= 0); $k++)
		{
			if (strpos($name, $trigramm = substr($name, $k, 3), 0) == $k)
			{
			//Если триграмма встретилась впервые (уникальная):
				if (isset($trigramms[$trigramm])) $nm++; //Увеличиваем счетчик общих.
				else $miss--; //Иначе уменьшаем оставшееся число допустимых промахов.
			}
		}

		if ($miss >= 0)
		//Если число промахов не превышено, выводим в файл информацию о
		//схожей паре имен и величину релевантности.
			fwrite($fout, SITE_PREFIX . $id ."\t" . $rusn ."\t" . SITE_PREFIX . $key . "\t" . $name . "\t" . ($nm + $nm) / ($m + $n) . "\r\n");
	}

	@mysql_free_result($similars) OR message_die(@mysql_errno(), @mysql_error(), __FILE__, __LINE__);

	unset($trigramms); //Освобождаем список триграмм для повторного использования.

	fclose($fout); //Закрываем файл со списком схожих пар.


Функция форматированных запросов к MySQL
[7]
// Syntax: fquery($query_template_text, $argument's_1_value, $argument's_2_value, ...)
// Special characters for a query template:
// ^@TableName - indicates that the combination ^@ is to be replaced with table prefix
// ^N - numeric parameter(s) (is not to be quoted), separated by comma if is array
// ^S - string parameter(s) (is to be quoted), separated by comma if is array
// ^0 - "NULL" or "NOT NULL"

// (c) Grigoryev Andrey aka GrAnd aka Pochemuk
// Thanks to Kamnev Artjom (Kamnium), Mesilov Maxim (Severus) for idea
// http://life.screenshots.ru

// When query successed returns Recordset for SELECT or True for others.
// When error occurs returns False.

function fquery()
{
	global $opt_debug_mode;
	global $opt_debug_show_sql;
	global $opt_table_prefix; // getting prefix from the site's options

	if (is_array(func_get_arg(0))) $args = func_get_arg(0);
	else $args = func_get_args();

	$qtext = $args[0]; // the first argument is always query template text
	if (empty($qtext)) return false; // Hmm, nothing to do!

	$qtext = str_replace("^@", ($opt_table_prefix == "") ? "" : ($opt_table_prefix . '_'), $qtext); // replacing with table prefixes

	$i = 0; $curArg = 1;

	$query = "";

	while ($i < strlen($qtext)) // strlen is always up-to-date, even if special chars are replaced
	{
		if ($qtext{$i} == '^')
		{
			if ($curArg >= count($args)) return false; // too many parameters in the query template!

			$i++;

			switch ($qtext{$i})
			{
				case 'N':
				{
					if (is_null($args[$curArg]))
					{
						$query .= "NULL";
						continue;
					}

					if (is_array($args[$curArg])) $query .= implode(", ", $args[$curArg]);
					else $query .= $args[$curArg];

					break;
				}
				case 'S':
				{
					if (is_null($args[$curArg]))
					{
						$query .= "NULL";
						continue;
					}

					if (is_array($args[$curArg])) $query .= "'" . implode("', '", $args[$curArg]) . "'";
					else $query .= "'" . $args[$curArg] . "'";

					break;
				}
				case '0':
				{
  					if (is_null($args[$curArg])) return false; // incorrect parameter, nulls are not allowed!

					$args[$curArg] = strtoupper($args[$curArg]);

					if (($args[$curArg] != "NULL") && ($args[$curArg] != "NOT NULL")) return false; // incorrect parameter, "NULL" or 

"NOT NULL" only!

					$query .= $args[$curArg];

					break;
				}
				default: $query .= $qtext{$i};
			}

			$i++; $curArg++;
		}
		else $query .= $qtext{$i++};
	}

	if ($opt_debug_show_sql == 1) print('

Query string: "' . $query . '"

' . "\r\n"); $ResultData = mysql_query($query); if (mysql_errno() <> 0) { if ($opt_debug_mode == 1) { print('

MySQL error: #' . mysql_errno() . ': ' . mysql_error() . ' '); print('Query string: ' . $query . '

'); } } return($ResultData); } // End of fquery


Результат тестового прогона на первой тысяче записей:


Результат работы
АДРЕС-1 ИМЯ-1 АДРЕС-2 ИМЯ-2 РЕЛЕВАНТНОСТЬ
/people/784 ПИТЕР ДЖЕЙСОН /people/389 ПИТЕР ДЖЕКСОН 0.8125
/people/1216 ЧАРЛЬЗ ДЭНС /people/664 ЧАРЛЬЗ ДЭННИС 0.8
/people/1662 СТЮАРТ Ф УИЛСОН /people/1251 СТЮАРТ УИЛСОН 0.914285714286
/people/1798 МАЙКЛ МАНН /people/583 МАЙКЛ ЛЕМАНН 0.846153846154
/people/2062 МАЙКЛ БИРН /people/265 МАЙКЛ БИН 0.8
/people/2557 ДЖИНА ДЭВИС /people/963 ДЖИН ДЭВИС 0.8
/people/3093 ДЖ ДЖ ДЖОНСОН /people/911 ДОН ДЖОНСОН 0.818181818182
/people/3262 ТОМ ЭВЕРЕТТ /people/586 ТОМ ЭВЕРЕТТ СКОТТ 0.848484848485
/people/3329 РОБЕРТ РИТТИ /people/3099 РОБЕРТ БИТТИ 0.827586206897
/people/3585 ТРЭЙСИ КЭЙ ВУЛЬФ /people/2810 ТРЭЙСИ ВУЛЬФ 0.857142857143
/people/3598 АЛЕКСАНДР КАЛУГИН /people/2852 АЛЕКСАНДР КАЛЯГИН 0.85
/people/3966 СЕРГЕЙ БОДРОВ /people/2991 СЕРГЕЙ БОДРОВ МЛ 0.888888888889
/people/3994 СЕРГЕЙ БОБРОВ /people/3966 СЕРГЕЙ БОДРОВ 0.8125
/people/4049 РИЧАРД ЛЬЮИС /people/2063 РИЧАРД ДЖ ЛЬЮИС 0.882352941176
/people/4293 ДЖЕРРИ ЛИВЛИ /people/2006 ДЖЕРРИ ЛИ 0.88
/people/4377 ДЖОАН КЬЮСАК /people/3774 ДЖОН КЬЮСАК 0.827586206897
/people/4396 ДИН МАКДЕРМОТТ /people/2614 ДИЛАН МАКДЕРМОТТ 0.833333333333
/people/4608 ШОН ДЖОНСТОН /people/2036 ДЖ ДЖ ДЖОНСТОН 0.8
/people/4981 КРИСТОФЕР МЭЙ /people/3233 КРИСТОФЕР МЮРРЭЙ 0.8
/people/5019 ДЖЕЙН АЛЕКСАНДР /people/381 ДЖЕЙСОН АЛЕКСАНДР 0.842105263158
/people/5551 КАРЛОС АНДРЕС ГОМЕЗ /people/1311 КАРЛОС ГОМЕЗ 0.810810810811
/people/5781 АЛЕКС НОЙБЕРГЕР /people/4288 АЛЕКС НЮБЕРГЕР 0.8
/people/5839 ДЖОИ ТРАВОЛТА /people/935 ДЖОН ТРАВОЛТА 0.8125
/people/5917 ДЖО ДЖОНСТОН /people/2036 ДЖ ДЖ ДЖОНСТОН 0.833333333333
/people/5917 ДЖО ДЖОНСТОН /people/4608 ШОН ДЖОНСТОН 0.8
/people/6112 ТОМАС РАЙАН /people/4869 ТОМАС ДЖЕЙ РАЙАН 0.823529411765
/people/6416 БРАЙАН ДЖОРДЖ /people/3942 ДЖОРДЖ БРАЙАНТ 0.848484848485
/people/6520 ДЖОН КАРНИ /people/5207 ДЖОН КАНИ 0.8
/people/6834 ДЖОН ДЖ АНДЕРСОН /people/5049 ДЖО АНДЕРСОН 0.838709677419
/people/6836 МАЙКЛ ЭСТОН /people/5056 МАЙКЛ УЭСТОН 0.827586206897
/people/6837 ДЭВИД БАРОНЕ /people/5884 ДЭВИД БАРОН 0.827586206897
/people/7261 БИЛЛИ ГРЭЙ /people/1695 БИЛЛИ РЭЙ 0.8
/people/7361 АЛАН ДЭВИД /people/3087 ДЭВИД АЛАН БАШ 0.838709677419
/people/7447 ДЭВИД ЭЙЕР /people/2277 ТЭЙЕР ДЭВИД 0.814814814815
/people/7497 АЛЕКСАНДР КАРАМНОВ /people/3857 АЛЕКСАНДР КАРПОВ 0.8
/people/7499 НИКОЛАС ЛАМЛИ /people/4424 НИКОЛАС ЛИ 0.827586206897
/people/7534 РИЧАРД РИХЛЬ /people/3547 РИЧАРД НИХЛЬ 0.857142857143
/people/7547 СВЕТЛАНА СТАРИКОВА /people/1985 СВЕТЛАНА СВЕТИКОВА 0.8
/people/7677 ДЖЕЙС АЛЕКСАНДР /people/381 ДЖЕЙСОН АЛЕКСАНДР 0.842105263158
/people/7677 ДЖЕЙС АЛЕКСАНДР /people/5019 ДЖЕЙН АЛЕКСАНДР 0.833333333333
/people/8000 ГРЕГОРИ СМИТ /people/7628 ГРЕГОРИ П СМИТ 0.909090909091
/people/8137 КАСПЕР КРИСТЕНСЕН /people/128 ДЖЕСПЕР КРИСТЕНСЕН 0.8
/people/8186 ШЭЙН КОСУГИ /people/6235 КЭЙН КОСУГИ 0.814814814815
/people/8219 БРЭНДОН ДЖЕЙМС ОЛСОН /people/797 ДЖЕЙМС ОЛСОН 0.810810810811
/people/8442 ГУННАР ЙОЛФССОН /people/7033 ГУННАР ЙОНССОН 0.8
/people/8458 ДЖОН АЛЕКСАНДР /people/381 ДЖЕЙСОН АЛЕКСАНДР 0.810810810811
/people/8458 ДЖОН АЛЕКСАНДР /people/5019 ДЖЕЙН АЛЕКСАНДР 0.8
/people/8614 ДЭВИД ХЕРМАН /people/4945 ДЭВИД ХЕЙМАН 0.8
/people/8874 НИКОЛАС РОУГ /people/1667 НИКОЛАС РОУ 0.827586206897
/people/8987 ДЭВИД РОСС /people/4870 ДЭВИД КРОСС 0.814814814815
/people/9132 РОБЕРТ ЛОНГ /people/7683 РОБЕРТ ЛОНГО 0.827586206897
/people/9202 РОБЕРТ МЕНДЕЛ /people/3410 РОБЕРТ МЭНДЕЛ 0.8125
/people/9229 ЭШЛИ ЛОУРЕНС /people/2534 ЛОУРЕНС ЭШЛИ 1
/people/9303 ДЖОН ЭЙЛАРД /people/8703 ДЖОН ЭЙЛУАРД 0.827586206897
/people/9308 ШОН РОБЕРТС /people/6552 ШОН О РОБЕРТС 0.903225806452
/people/9347 СТЕФЕН СЕРЖИК /people/2911 СТЕФЕН СУРЖИК 0.8
/people/9432 ПОЛЛИ ШЕННОН /people/2240 МОЛЛИ ШЕННОН 0.8
/people/9583 ДЖУЛИ ХАРРИС /people/904 ДЖУЛИУС ХАРРИС 0.838709677419
/people/9788 ЭНТОНИ СТАРР /people/8308 ЭНТОНИ СТАРК 0.8
/people/9835 МАЙКЛ У УОТКИНС /people/4727 МАЙКЛ В УОТКИНС 0.864864864865
/people/9893 СТИВ МАРТИНО /people/6457 СТИВ МАРТИН 0.827586206897


Ссылки и примечания:


1. Фонетическое кодирование.
→ Фонетические алгоритмы. Хабрахабр
→ «Как ваша фамилия», или Русский MetaPhone (Russian Metaphone description)

2. Расстояние Левенштейна. Википедия

3. Вычисление схожести слов на основе выделения общих форм реализована в PHP в функции similar_text. В документации PHP указывается, что в основе этой функции лежит алгоритм Оливера [1993]. Однако первоначально данный алгоритм под названием «Ratcliff/Obershelp Pattern Recognition Algorithm» был опубликован John W. Ratcliff на сайте программистов Dr. Dobb’s (Pattern Matching: the Gestalt Approach) в 1988 году. А Ian Oliver всего лишь использовал его в своей книге «Programming Classics: Implementing the World’s Best Algorithms».

4. Исходники алгоритма Джаро-Винклера можно посмотреть, например, здесь: Jaro-Winkler Distance

5. Триграммный индекс или «Поиск с опечатками». Хабрархабр

6. Жаннетта «Ангел» Деверю — персонаж культовой медиафраншизы «Wing Commander», включающей компьютерные космосимуляторы, стратегии, прочие формы игр, литературные произведения и фильм. Здесь приведена исключительно в связи с тем, что я, не разбираясь в звучании французских фамилий, долгое время считал, что она звучит «Деверо». Иллюстрация ошибочного перевода не «на слух», а «на глаз».

7. Основа функции форматированных SQL-запросов честно позаимствована у Камнева Артема и Месилова Максима из статьи «SQL-инъекции: борьба в удовольствие». К сожалению, сайт этих ребят в последнее время не загружается, но копию статьи еще можно найти: Sql-инъекции: борьба в удовольствие (к сожалению, без исходников). Убраны некоторые спорные возможности. Вместо них добавлена возможность передачи в качестве аргумента массива.

Комментарии (0)

© Habrahabr.ru