Получить объединение и пересечение массива jsonb в Postgresql - PullRequest
1 голос
/ 21 июня 2020

У меня есть БД людей со столбцом jsonb interests. В моем приложении пользователь может искать людей, указывая их хобби, которые задаются некоторыми предопределенными значениями. Я хочу предложить ему наилучшее совпадение, и для этого я бы хотел засчитать совпадение как пересечение / союз интересов. Таким образом, лучшие результаты не будут у людей, у которых много хобби в моей БД. Пример:

Записи БД:

name    interests::jsonb
Mary    ["swimming","reading","jogging"]
John    ["climbing","reading"]
Ann     ["swimming","watching TV","programming"]
Carl    ["knitting"]

пользовательский ввод в приложении:

["reading", "swimming", "knitting", "cars"]

мой скрипт должен вывести это:

Mary    0.4
John    0.2
Ann     0.16667
Carl    0.25

Теперь Я использую

SELECT name 
  FROM people 
 WHERE interests @> 
   ANY (ARRAY ['"reading"', '"swimming"', '"knitting"', '"cars"']::jsonb[])

, но это дает мне даже записи с множеством интересов, и нет возможности их заказать. Есть ли способ добиться этого в разумные сроки - скажем, до 5 секунд в БД с примерно 400 КБ записей?

РЕДАКТИРОВАТЬ : Я добавил еще один пример, чтобы прояснить свои расчеты. Мой расчет должен отфильтровать людей с большим количеством увлечений. Поэтому совпадение должно быть рассчитано как Intersection (input, db_record) / Union (input, db_record).

Пример: input = ["reading"]

Записи DB:

name    interests::jsonb
Mary    ["swimming","reading","jogging"]
John    ["climbing","reading"]
Ann     ["swimming","watching TV","programming"]
Carl    ["reading"]

Соответствие Мэри будет рассчитано как (LENGTH(["reading"]))/(LENGTH(["swimming","reading","jogging"])), что составляет 0,3333, а для Карла это будет (LENGTH(["reading"]))/LENGTH([("reading")]), что равно 1

ОБНОВЛЕНИЕ : Мне удалось сделать это с помощью

SELECT result.id, result.name, result.overlap_count/(jsonb_array_length(persons.interests) + 4 - result.overlap_count)::decimal as score 
FROM (SELECT t1.name as name, t1.id, COUNT(t1.name) as overlap_count
      FROM (SELECT name, id, jsonb_array_elements(interests)
            FROM persons) as t1
      JOIN (SELECT unnest(ARRAY ['"reading"', '"swimming"', '"knitting"', '"cars"'])::jsonb as elements) as t2 ON t1.jsonb_array_elements = t2.elements 
      GROUP BY t1.name, t1.id) as result 
JOIN persons ON result.id = persons.id ORDER BY score desc

Вот моя скрипка https://dbfiddle.uk/?rdbms=postgres_12&fiddle=b4b1760854b2d77a1c7e6011d074a1a3

Однако она недостаточно быстра, и я был бы признателен за любые улучшения.

Ответы [ 2 ]

0 голосов
/ 21 июня 2020

Один из вариантов - использовать jsonb_array_elements() для отмены вложенности столбца jsonb:

SELECT name, count / SUM(count) over () AS ratio
  FROM(
       SELECT name, COUNT(name) AS count
         FROM people 
         JOIN jsonb_array_elements(interests) AS j(elm) ON TRUE
        WHERE interests @> 
          ANY (ARRAY ['"reading"', '"swimming"', '"knitting"', '"cars"']::jsonb[])
        GROUP BY name ) q

Демо

0 голосов
/ 21 июня 2020

Один из вариантов - отключить параметр и использовать оператор ? для проверки каждого элемента массива jsonb:

select
    t.name,
    x.match_ratio
from mytable t
cross join lateral (
    select avg( (t.interests ? a.val)::int ) match_ratio
    from unnest(array['reading', 'swimming', 'knitting', 'cars']) a(val)
) x

Не очень ясно, каковы правила, лежащие в основе результата что вы показываете. Это дает вам соотношение, которое представляет процент значений в массиве параметров, которые могут быть найдены в interests каждого человека (таким образом, Мэри получает 0,5, поскольку у нее есть два общих интереса с параметром поиска, а все другие имена получают 0,25 ).

Демонстрация на DB Fiddle

...