Анализ списка терминов и их ближайших соседей в Postgres 11,5 - PullRequest
0 голосов
/ 01 марта 2020

У меня есть база данных, в которой мы регулярно должны выполнять нечеткое / дистанционное сопоставление строк. В этом примере целевое поле 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.

1 Ответ

0 голосов
/ 02 марта 2020

LATERAL join кажется именно тем, что вы хотите, но ваша реализация его странная:

analytic_scan.inv_name <-> frequency_table.inv_name AS distance
...
where frequency_table.inv_name = analytic_scan.inv_name

Конечно, не имеет смысла использовать все подобные элементы. , затем удалите все, что не идентично.

   frequency_table.frequency_percentile = 1

Это также не имеет никакого смысла. Если вы действительно хотите отфильтровать это (а действия, кажется, противоречат вашему прозе), зачем вам это здесь? Этот пункт, похоже, не соответствует ни одному из моих описаний того, что вы пытаетесь сделать. Вы не заполняете столбцы ни для чего, кроме первой строки Frequency_table, потому что вы старались изо всех сил удалить эти результаты.

У вашего "knn" бокового запроса, похоже, есть предложение WHERE, которое ему не нужно, и упустить ORDER BY, который ему действительно необходим.

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

...