Есть ли способ, как «расширить» 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