У меня есть база данных, в которой мы регулярно должны выполнять нечеткое / дистанционное сопоставление строк. В этом примере целевое поле citext
называется analytic_scan.inv_name
. Но такой же код может быть полезен для любого количества других полей text
и citext
. Остальная структура таблицы не вступает в игру для этого запроса.
Начиная с подсказки о текстовом поиске по K-NN от Тома Лейна, я получил триграмма индекс GIST, который реализует оператор расстояния <->
. Используя подсказку, запрос для 10 ближайших соседей к строке выглядит следующим образом:
select distinct on (inv_name <-> 'Pack CT - 002') inv_name,
inv_name <-> 'Pack CT - 002' AS distance
from analytic_scan
order by 2 -- order by the distance column...using the column position saves retyping the formula here.
limit 10;
Это работает нормально, хотя с 7M + строками это занимает некоторое время. Мои цели состоят в том, чтобы вычислить и сохранить набор значений для быстрого поиска, например:
Найти отдельные термины в целевом поле, analytic_scan.inv_name
.
Для каждого термина вычислите частоту термина и процентиль частоты.
Для каждого термина найдите 10 (или 100, et c.) ближайшие соседи и их расстояние.
Оттуда я хочу добавить distance_min
, distance_max
и distance_width
для каждого термина, который я вычисляю, что Я могу сделать с правым окном функцию волхвов c. (Я не пробую эту часть здесь.)
Поиск K-NN выше, поиск по частоте довольно прост:
select distinct inv_name,
count(*) as frequency,
ntile(100) OVER(ORDER BY count(*)) as frequency_percentile
from analytic_scan
group by inv_name
order by 1,2;
Объединение этих двух запросов - вот что у меня есть озадачен. Это похоже на LATERAL JOIN
, но я вполне могу ошибаться. Я экспериментировал с некоторыми, но это не дает мне никаких значений в столбцах из подзапроса KNN, они все NULL
. Кроме того, я получаю по одной строке за семестр, а не 10. Итак, ясно, что я иду неправильно.
Чтобы было ясно, я делаю получаю ожидаемые столбцы:
inv_name
frequency
frequency_percentile
neighbor_name
distance
... но поля на основе KNN не заполнены, и я только получить одну строку вывода вместо 10, установленного в предложении LIMIT 10
поиска KNN. Я знаю, что мне нужно применить код «найди 10 соседей» к каждому элементу в моем образце, и я не знаю, как это сделать правильно. Я набираю LATERAL
, но если есть лучший путь, я за него.
-- Final results I'm after, with one row per *neighbor*.
-- So, 10x the distinct terms, in this case.
select frequency_table.inv_name,
frequency_table.frequency,
frequency_table.frequency_percentile,
knn.neighbor_name,
knn.distance
-- Calculate the distinct terms and their frequencies. There are 6,958 distinct terms in my sample table.
from (
select inv_name,
count(*) as frequency,
ntile(100) OVER(ORDER BY count(*)) as frequency_percentile
from analytic_scan
group by inv_name
) frequency_table
-- I'm wanting to "multiply" the terms above with the 10 neighbors below. LEFT JOIN is obviously wrong.
left join lateral -- CROSS JOIN LATERAL gives me 70 rows on 6,958 distinct terms. ¯\_(ツ)_/¯
-- Find the 10 nearest neighbors.
(select distinct on (analytic_scan.inv_name <-> frequency_table.inv_name) analytic_scan.inv_name AS neighbor_name,
analytic_scan.inv_name <-> frequency_table.inv_name AS distance
from analytic_scan
where frequency_table.inv_name = analytic_scan.inv_name and
frequency_table.frequency_percentile = 1
limit 10
) knn ON TRUE
order by frequency_table.inv_name,
knn.distance
Если кто-то может указать мне правильное направление, это было бы здорово. Я явно нахожусь на лыжах.
Примечание. Скорее всего, я закончу тем, что буду хранить по одной строке за термин с массивом или jsonb
с соседними данными. На данный момент данные будут использоваться клиентским приложением, и им просто нужен массив JSON. Обычно у меня аллергия c на упакованные поля, но в этом случае это имеет смысл. Я не пытаюсь выполнить консолидацию здесь, так как считаю, что имеет смысл правильно выполнить запрос basi c. Но если у кого-то есть решение, которое в итоге создает агрегацию JSON вместо моего «один ряд на соседа», это тоже хорошо. Вот таблица, которую я себе представляю:
CREATE TABLE IF NOT EXISTS analytics.inv_name_frequency (
id uuid NOT NULL DEFAULT extensions.gen_random_uuid(), -- What the boss likes.
inv_name citext NOT NULL DEFAULT 0,
frequency integer NOT NULL DEFAULT 0,
frequency_range int4range NOT NULL DEFAULT '(0,0)'::int4range -- For min, max distances.
frequency_width integer NOT NULL DEFAULT 0, -- Stores min-max value, can use a calculated column in PG12.
neighbors jsonb NOT NULL DEFAULT '{}'::jsonb) -- JSON array with {"term","foo","distance":0.3} for each neighbor.
Дополнительная информация
jjanes, чьи комментарии и код, которые я много использовал из архивов SO, заняли время ответить. Мой ответ не помещается в комментарии, поэтому я добавляю его сюда. Это может помочь уточнить, что да , данные, на которые я смотрю, являются грязными. Первое, что мы делаем, это помогаем людям перейти от нестандартных и противоречивых имен к набору строго стандартизированных имен. Это требует кучу программного обеспечения и столько же человеческих навыков и усилий. Честно говоря, мы должны нанять антропологов, потому что основная часть работы заключается в извлечении местных знаний. Отправной точкой для процесса стандартизации являются годы накопления реальных данных со всеми видами несоответствий. Это данные, которые я смотрю здесь.
Мы атаковали автоматическое сопоставление множеством нечетких сравнений строк, и мне очень нравится реализация Postgres триграмм. Когда я прочитал подсказку по поиску «K-NN», это выглядело как довольно интересный способ найти шаблоны в «суповых» таблицах исторических данных. Это достаточно быстро, для того, что он делает .... даже когда я написал код. С помощью нескольких близких терминов, которые быстро и / или сохраняются для поиска, у вас есть действительно хорошая отправная точка для более дорогой оценки сходства с Левенштейном и др. c.
Итак, в качестве эксперимента я Хотелось бы составить таблицу терминов и соседей по историческим данным. Я могу сделать это легко на языке клиента, даже PL / Pg SQL
select distinct
с Postgres - Перебирать каждый результат, получать соседей, сохранять результаты.
Но это похоже на то, что должно быть возможно на прямой SQL на Postgres, и я хотел бы выяснить, как это сделать. Есть много раз, когда я хотел бы сделать расширенный частотный анализ текста. Как показывают комментарии к моему коду, довольно ясно, что моя ментальная карта того, что происходит во время запросов, ... довольно пуста. Я хотел бы лучше понять, как выполнять lateral join
или подзапросы и т. Д. c. чтобы решить эту проблему с SQL.