Как создать простой нечеткий поиск только с PostgreSQL?

У меня небольшая проблема с функцией поиска на моем сайте на основе RoR. У меня есть много продуктов с некоторыми кодами. Этот код может быть любой строкой, такой как AB-123-lHdfj. Теперь я использую оператор ILIKE для поиска продуктов:

Product.where("code ILIKE ?", "%" + params[:search] + "%")

Он работает нормально, но не может найти продукты с такими кодами, как AB123-lHdfj или AB123lHdfj.

Что я должен сделать для этого? Может быть, в Postgres есть какая-то функция нормализации строк или какие-то другие методы, которые мне помогут?


person Alve    schedule 11.10.2011    source источник


Ответы (2)


Postgres предоставляет модуль с несколькими функциями сравнения строк, такими как soundex и metaphone. Но вы захотите использовать функцию редактирования расстояния левенштейна.

Example:

test=# SELECT levenshtein('GUMBO', 'GAMBOL');
 levenshtein
-------------
           2
(1 row)

2 — это расстояние редактирования между двумя словами. Когда вы примените это к ряду слов и отсортируете по результату расстояния редактирования, вы получите тип нечетких совпадений, который вы ищете.

Попробуйте этот пример запроса: (конечно, с вашими именами объектов и данными)

SELECT * 
FROM some_table
WHERE levenshtein(code, 'AB123-lHdfj') <= 3
ORDER BY levenshtein(code, 'AB123-lHdfj')
LIMIT 10

Этот запрос говорит:

Дайте мне первые 10 результатов всех данных из some_table, где расстояние редактирования между значением кода и входным значением «AB123-lHdfj» меньше 3. Вы получите все строки, где значение кода находится в пределах 3 символов разницы с « AB123-lHdfj'...

Примечание: если вы получаете сообщение об ошибке, например:

function levenshtein(character varying, unknown) does not exist

Установите расширение fuzzystrmatch, используя:

test=# CREATE EXTENSION fuzzystrmatch;
person Paul Sasik    schedule 11.10.2011
comment
Не могли бы вы показать мне простой пример использования этой функции? Я понятия не имею, как это может мне помочь :) - person Alve; 12.10.2011
comment
Я добавил простой, жестко закодированный пример запроса, которого должно быть достаточно для начала работы. Не забудьте убедиться, что вам доступен модуль fuzzystrmatch: postgresonline.com/ журнал/категории/57-fuzzystrmatch - person Paul Sasik; 12.10.2011
comment
Левенштейн — дорогая функция, она запускает Левенштейн дважды за вызов? - person triunenature; 22.11.2015
comment
@triunenature Сколько раз он будет вызываться, на самом деле будет зависеть от оптимизации. Если оптимизатор достаточно умен, он вызовет один раз и кэширует результаты. Я также должен отметить, что даже если он не оптимизирован, сложность вызова является линейной, а не экспоненциальной, поэтому влияние на производительность не так сильно. - person Paul Sasik; 22.11.2015
comment
Спасибо за подсказку о необходимости установки fuzzystrmatch для использования levenshtein в postgresql. Это помогло мне избавиться от HINT: No function matches the given name and argument types. You might need to add explicit type casts - person andilabs; 09.02.2017
comment
@erwin-brandstetter упоминает, что levenshtein довольно медленный и что лучше использовать pg_trgm. Будет ли это по-прежнему верно при использовании postgresql.org/docs/9.0/static /indexes-expressional.html для Левенштейна? - person velop; 26.03.2017
comment
ПРИМЕЧАНИЕ! также есть levenshtein_less_equal()! Это ускоренная версия функции Левенштейна для использования, когда интерес представляют только небольшие расстояния. Если фактическое расстояние меньше или равно max_d, то levenshtein_less_equal возвращает правильное расстояние; в противном случае он возвращает некоторое значение больше, чем max_d. Если max_d отрицательное, то поведение такое же, как у Левенштейна. test=# SELECT levenshtein_less_equal('extensive', 'exhaustive',2); - person simUser; 21.11.2019

Пол рассказал вам о levenshtein(). Это очень полезный инструмент, но он также очень медленный с большими таблицами. Он должен вычислять расстояние Левенштейна от условия поиска для каждой отдельной строки. Это дорого и не может использовать индекс. Ускоренный вариант levenshtein_less_equal() работает быстрее для длинных строк, но все еще медленный без поддержки индекса.

Если ваши требования настолько просты, как показано в примере, вы все равно можете использовать LIKE. Просто замените любое - в поисковом запросе на % в предложении WHERE. Итак, вместо:

WHERE code ILIKE '%AB-123-lHdfj%'

Использовать:

WHERE code ILIKE '%AB%123%lHdfj%'

Или динамически:

WHERE code ILIKE '%' || replace('AB-123-lHdfj', '-', '%') || '%'

% в шаблонах LIKE означает 0-n символов. Или используйте _ ровно для одного символа. Или используйте регулярные выражения для более точного соответствия:

WHERE code ~* 'AB.?123.?lHdfj'

.? ... 0 или 1 символ

Or:

WHERE code ~* 'AB\-?123\-?lHdfj'

\-? ... 0 или 1 тире

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


Если ваша реальная проблема более сложная и вам нужно что-то более быстрое, есть несколько вариантов, в зависимости от ваших требований:

  • Конечно, есть полнотекстовый поиск. Но это может быть излишеством в вашем случае.

  • Более вероятным кандидатом является сопоставление триграмм с дополнительным модулем pg_trgm. Видеть:

    Его можно комбинировать с LIKE, ILIKE, ~ или ~*, начиная с PostgreSQL 9.1.
    Также интересно в этом контексте: функция similarity() или оператор % этого модуля.

  • И последнее, но не менее важное: вы можете реализовать самодельное решение с функцией нормализовать искомые строки. Например, вы можете преобразовать AB1-23-lHdfj --> ab123lhdfj, сохранить его в дополнительном столбце и искать с терминами, преобразованными таким же образом.

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

Обзор методов сопоставления с образцом:

person Erwin Brandstetter    schedule 12.10.2011