Полнотекстовый поиск в PostgreSQL с пользовательским рейтингом, основанным на разных оценках для каждого ключевого слова - PullRequest
0 голосов
/ 23 апреля 2020

Есть ли способ, как «расширить» ts_rank функцию в PostgreSQL или создать собственную setweight?

У меня есть 2 таблицы, records и tags, records банка иметь несколько тегов. Существует множество ассоциаций, использующих таблицу records_tags. records_tags имеет столбец score, что означает, что оценка одного и того же тега отличается для каждой записи, и в качестве веса в наборе PostgreSQL доступно больше уровней, чем просто 4.

Пример упрощенных данных:

records таблица

 id |         title          |             description              | privacy
----+------------------------+--------------------------------------+---------
  1 | 'The best record ever' | 'Long and meaningful description...' |       1
  2 |       'Another record' | 'Description of the other record...' |       2

tags таблица

 id |           name
----+---------------------------
  1 | 'artificial intelligence'
  2 |        'machine learning'
  3 |            'life science'

records_tags таблица

 record_id | tag_id | score
-----------+--------+-------
         1 |      1 |    87
         1 |      2 |    23
         2 |      1 |    54
         2 |      2 |    67
         2 |      3 |    90

Данные из этих таблиц объединены в другую таблицу search_documents, которая имеет столбец body типа jsonb и включает агрегированные имена тегов для каждой записи.

search_documents.body выглядит следующим образом:

{
  title: 'The best record ever',
  description: 'Long and meaningful description...',
  tags: ['artificial intelligence', 'machine learning']
}

Прямо сейчас Я реализовал полнотекстовый поиск с использованием tsvector и setweight следующим образом:

setweight(to_tsvector('simple', (body ->> 'tags')), 'A') || ' ' ||
setweight(to_tsvector('english', (body ->> 'title')), 'B') || ' ' ||
setweight(to_tsvector('english', (body ->> 'description')), 'C')

И поисковый запрос выглядит так:

SELECT
  ts_rank(sd.tsv, to_tsquery('english', ''' ' || :query || ' ''' || ':*'), 1) +
  ts_rank(sd.tsv, to_tsquery('simple', ''' ' || :query || ' ''' || ':*'), 1) AS rank
  sd.id AS id
FROM
  search_documents sd
WHERE
  sd.tsv @@ to_tsquery('english', ''' ' || :query || ' ''' || ':*') OR
  sd.tsv @@ to_tsquery('simple', ''' ' || :query || ' ''' || ':*')

Но это не позволяет мне использовать оценку тега вообще.

Моя идея состоит в том, что у меня есть функция нормализации для оценки тега, которая возвращает счет в диапазоне от 0 до 1, и я использую ее для умножения веса А. Это выглядело бы примерно так - (x_i − min(x)) / (max(x) − min(x))

Есть ли способ, как я мог бы использовать тег score в дополнение к текущей реализации при вычислении rank?

EDIT:

search_documents - это таблица, не материализованное представление, которая имеет (или будет иметь, если что-то будет добавлено для этой работы) все данные, когда начнется процесс поиска. Он содержит все элементы, которые я хочу найти, и не только records, но и другие - accounts и speakers. Когда исходные таблицы обновляются, это также search_documents. Существует также столбец конфиденциальности, поскольку у каждого пользователя есть разные права, и я не хочу, чтобы они видели элементы в результатах поиска, когда они не могут получить к ним доступ.

Пример таблицы search_documents:

 tsv | searchable_id | searchable_type | privacy | body
-----+---------------+-----------------+---------+-----------------------------------------------------
 ... |             1 |        'record' |       1 | { title: '...', description: '...', tags: ['...'] }
 ... |             1 |       'account' |       1 | { name: '...', description: '...' }
 ... |             1 |       'speaker' |       1 | { name: '...', description: '...' }

tsv - это ts_vector, созданный с триггером при вставке / обновлении таблицы. Он создается так:

IF NEW.searchable_type = 'record' THEN
  NEW.tsv := (
    setweight(to_tsvector('simple', (NEW.body ->> 'tags')), 'A') || ' ' ||
    setweight(to_tsvector('english', (NEW.body ->> 'title')), 'B') || ' ' ||
    setweight(to_tsvector('english', (NEW.body ->> 'description')), 'C')
  )::tsvector;
ELSE
  NEW.tsv := (
    setweight(to_tsvector('simple', (NEW.body ->> 'name')), 'A') || ' ' ||
    setweight(to_tsvector('english', (NEW.body ->> 'description')), 'C')
  )::tsvector;
END IF;
return NEW;

И вот как данные records создаются в search_documents:

SELECT GREATEST(MAX(r.privacy), MAX(f.privacy), MAX(a.privacy)) AS privacy,
  'record' AS searchable_type,
  r.id AS searchable_id,
  json_build_object(
    'tags', array_remove(array_agg(t.name), NULL),
    'title', r.title,
    'description', r.description
  ) AS body
FROM records r
  LEFT JOIN folders f ON r.folder_id = f.id
  LEFT JOIN accounts a ON r.account_id = a.id
  LEFT JOIN records_tags rt ON r.id = rt.record_id
  LEFT JOIN tags t ON rt.tag_id = t.id
WHERE r.id = :id
GROUP BY searchable_id
ON CONFLICT(searchable_type, searchable_id)
  DO UPDATE
    SET privacy = EXCLUDED.privacy,
        body = EXCLUDED.body

1 Ответ

0 голосов
/ 23 апреля 2020

Вы можете использовать форму с тремя аргументами setweight , чтобы установить веса для заданных c лексем, вместо этого установите все веса одинаково для всего tsvector. Предположительно, вы бы встроили это в процесс создания таблицы «search_documents», но я не могу предложить конкретную реализацию c, так как вы не показывали нам этот процесс создания. Если у вас есть правильно взвешенный tsvector, возможно, имеет смысл сохранить его как отдельный столбец, а не внутри JSONB.

...