Как стать автором
Обновить

Делаем релевантный поиск

Хочу поделиться идеей, которую я использовал для реализации релевантного поиска почтовых адресов.

Дано: база адресов Великобритании (это не обязательно, идею можно использовать для чего угодно, но для примера я буду использовать эту базу), примерно 35 000 адресов. Необходимо организовать быстрый, релевантный поиск по этой базе.

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

За основу идеи я взял одну небезызвестную статью написанную однажды двумя парнями — Сергеем и Ларри, вы могли слышать о них.

Итак суть идеи. Каждый адрес разбивается на части — на слова. Каждому слову соответствует ссылка на полный адрес и вес этого слова во всей базе данных.
Несмотря на то что почтовый код представляется одним словом, мы все равно разделим его на части, т.к., например, коды «AB», «AB1», «AB11» указывают на одну и ту же область только каждый следующий более точнее детализирует предыдущий. Поэтому код AB11 мы будет разбивать на «AB11», «AB1», «AB».

Например, исходная база:
ID Postcode City
1 AB53 Cuminestown Industrial Estate
2 BT94 Knockroe Archdall
3 PE36 Holme
4 TR10 Busvannah
... ... ...

После построения индекса данные примут примерно следующий вид.
Word Address ID Rank
Cuminestown 1 1.691000
Industrial 1 1.884000
Estate 1 2.907000
AB 1 2.691093
AB5 1 3.884349
AB53 1 4.608096
Knockroe 2 2.385000
Archdall 2 2.385000
BT 2 2.691093
BT9 2 3.884349
BT94 2 4.608096
Holme 3 3.312000
PE 3 2.691093
PE3 3 3.884349
PE36 3 4.608096
Busvannah 4 2.194000
TR 4 2.691093
TR1 4 3.884349
TR10 4 4.608096
... ... ...

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

Например, пользователь вводит в строку поиска: "BT9, Holme". Из строки удаляем знаки препинания, пробелы, разбиваем на слова: «BT9», «BT» и «Holme».

Запрос к базе выглядит примерно так:

SELECT id, SUM(rank) AS total_rank FROM locations_index WHERE word LIKE 'BT9' OR word LIKE 'BT' OR word LIKE 'Holme%' GROUP BY address_id ORDER BY total_rank DESC;

В результате получаем:
2 6.575442
3 3.312000

Что и требовалось найти. Осталось только заменить результат реальными значениями адреса.

Веса слов


Самое сложное — это найти веса для каждого слова.

Для начала определимся какими условиям должен удовлетворять вес слова.
  1. Вес почтового кода выше веса города.
    Почему так? Сделать ошибку при написании 4-х символов (половина из которых цифры) гораздо сложнее чем при написании 7-10 букв.
  2. Чем меньше количество слов в названии города — тем выше его приоритет.
    По той же причине — чем короче адрес, тем меньше вероятность допустить ошибку при наборе.
  3. Чем короче слова в названии города, тем выше его приоритет.
  4. Чем точнее почтовый код — тем выше его приоритет.
    Почтовый код точнее если указано больше его составляющих. (подробнее о почтовых кодах Великобритании)
    Например код «AB21» — указывающий на город «Kirkhill Industrial Estate, Aberdeen City, Scotland» более точный чем «AB» который указывает на область «Aberdeen City, Scotland»
  5. Чем больше совпадение пользовательского запроса с адресом или посткодом — тем выше его приоритет.

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

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

«TR10» = «TR10» + «TR1» + «TR» = 4.61 + 3.88 + 2.69 = 11.18
«Сuminestown Industrial Estate» = «Сuminestown» + «Industrial» + «Estate» = 1.69 + 1.88 + 2.91 = 6.48

Вес посткода

Предположим что минимальный и максимальный сумарный вес посткода — P_MIN и P_MAX соответственно.
SX, SXX, SXXX, ..., SXN — части посткода.

В соответствии с условием 4, чем больше N тем ближе сумарный вес будет к P_MAX и наоборот — чем N ниже, тем ближе вес будет к P_MIN.
Т.е. посткоды вроде AB, TR,… (N=0) должны иметь минимальный вес — P_MIN.



На графике я показал как меняется сумарный вес посткода в зависимости от N (количество частей на которые можно разбить посткод). Зависимость экспоненциальная.
Здесь хорошо видно что сумма лежит в диапазоне меджду P_MIN и P_MAX.
Мы знаем что:



Возьмем теперь для примера 2 посткода: «AB12» и «AB1».
Они раскладываются соотвественно на P1(AB12 + AB1 + AB) и на P2(AB1 + AB).
Как мы видим у них есть одинаковая часть P1(AB1 + AB) и P2(AB1 + AB), однако если немного подумать то становится понятно что они не должны быть равны и более того, чтобы выполнились условия 4 и 5, которые мы ввели выше должно выполняться следующее условие:

P1(AB1 + AB) < P2(AB1 + AB) < P1(AB12 + AB1 + AB)

Графически это выглядит так:



S + SX + SXX в нашем случае это AB12 + AB1 + AB.

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



где,

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





Вес города

Принцип нахождения веса города похожий. Я не буду расписывать процесс. Скажу только что минимальный и максимальные веса для города были взяты меньшими.
Формула:



Length — длинна всего адреса, Count — количество слов в адресе.

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

При размере базы данных примерно в 35 000 адресов, размер индекса составляет примерно 140 000 записей. Поиск происходит довольно быстро. При желании довольно просто можно разнести данные по разным таблицам или даже серверам.

Есть ещё одна хитрость. Можно сделать поиск по неточному соответствию, для этого нужно индексы в базе привести к какой-то основе. Например в простейшем случае можно использовать metaphone-ключ строки. Искать соответственно тоже нужно по этой основе.

Поиск можно посмотреть в действии.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.