Как реализовать систему тегов - PullRequest
82 голосов
/ 27 ноября 2009

Мне было интересно, как лучше всего внедрить систему тегов, подобную той, что используется в SO. Я думал об этом, но не могу найти хорошее масштабируемое решение.

Я думал о том, чтобы иметь базовое решение для трех таблиц: иметь таблицу tags, таблицы articles и таблицу tag_to_articles.

Это лучшее решение этой проблемы или есть альтернативы? Используя этот метод, таблица будет очень большой по времени, и для поиска это не слишком эффективно, я полагаю. С другой стороны, не так важно, чтобы запрос выполнялся быстро.

Ответы [ 7 ]

110 голосов
/ 27 ноября 2009

Полагаю, вы найдете этот пост интересным: Теги: Схемы базы данных

Проблема: вы хотите иметь схему базы данных, где вы можете пометить Закладка (или сообщение в блоге или что-то еще) с таким количеством тегов, как вы хотите. Позже вы захотите выполнить запросы, чтобы ограничить закладки объединение или пересечение тегов. Вы также хотите исключить (скажем, минус) некоторые теги из результатов поиска.

«MySQLicious» решение

В этом решении схема имеет только одну таблицу, она денормализована. Этот тип называется «MySQLicious solution», потому что MySQLicious импортирует данные del.icio.us в таблицу с такой структурой.

enter image description hereenter image description here

Пересечение (И) Запрос «search + webservice + semweb»:

SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags LIKE "%semweb%"

Союз (ИЛИ) Запрос «search | webservice | semweb»:

SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
OR tags LIKE "%webservice%"
OR tags LIKE "%semweb%"

Минус Запрос «search + webservice-semweb»

SELECT *
FROM `delicious`
WHERE tags LIKE "%search%"
AND tags LIKE "%webservice%"
AND tags NOT LIKE "%semweb%"

Решение "Scuttle"

Scuttle организует свои данные в две таблицы. Эта таблица «scCategories» является таблицей «tag» и имеет внешний ключ для таблицы «bookmark».

enter image description here

Пересечение (И) Запрос на «закладку + веб-сервис + веб-сеть»:

SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId
HAVING COUNT( b.bId )=3

Сначала выполняется поиск всех комбинаций закладка-тег, где тегом является «закладка», «веб-сервис» или «веб-сеть» (c.category IN («закладка», «веб-сервис», «веб-служба»)), затем просто учитываются закладки, в которых были найдены все три тега (HAVING COUNT (b.bId) = 3).

Союз (ИЛИ) Запрос на «закладку | веб-сервис | semweb»: Просто пропустите предложение HAVING, и вы получите union:

SELECT b.*
FROM scBookmarks b, scCategories c
WHERE c.bId = b.bId
AND (c.category IN ('bookmark', 'webservice', 'semweb'))
GROUP BY b.bId

Минус (исключение) Запрос «bookmark + webservice-semweb», то есть: закладка И веб-сервис, а НЕ semweb.

SELECT b. *
FROM scBookmarks b, scCategories c
WHERE b.bId = c.bId
AND (c.category IN ('bookmark', 'webservice'))
AND b.bId NOT
IN (SELECT b.bId FROM scBookmarks b, scCategories c WHERE b.bId = c.bId AND c.category = 'semweb')
GROUP BY b.bId
HAVING COUNT( b.bId ) =2

Отсутствие HAVING COUNT приводит к запросу «bookmark | webservice-semweb».


«Toxi» решение

Toxi придумали структуру из трех столов. Посредством таблицы «tagmap» закладки и теги связаны с n-to-m. Каждый тег можно использовать вместе с разными закладками и наоборот. Эта DB-схема также используется WordPress. Запросы такие же, как и в решении «scuttle».

enter image description here

Пересечение (И) Запрос на «закладку + веб-сервис + веб-сеть»

SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id
HAVING COUNT( b.id )=3

Союз (ИЛИ) Запрос на «закладку | веб-сервис | semweb»

SELECT b.*
FROM tagmap bt, bookmark b, tag t
WHERE bt.tag_id = t.tag_id
AND (t.name IN ('bookmark', 'webservice', 'semweb'))
AND b.id = bt.bookmark_id
GROUP BY b.id

Минус (исключение) Запрос «bookmark + webservice-semweb», то есть: закладка И веб-сервис, а НЕ semweb.

SELECT b. *
FROM bookmark b, tagmap bt, tag t
WHERE b.id = bt.bookmark_id
AND bt.tag_id = t.tag_id
AND (t.name IN ('Programming', 'Algorithms'))
AND b.id NOT IN (SELECT b.id FROM bookmark b, tagmap bt, tag t WHERE b.id = bt.bookmark_id AND bt.tag_id = t.tag_id AND t.name = 'Python')
GROUP BY b.id
HAVING COUNT( b.id ) =2

Отсутствие HAVING COUNT приводит к запросу «bookmark | webservice-semweb».

8 голосов
/ 27 ноября 2009

Ничего плохого в вашем решении с тремя столами.

Другой вариант - ограничить количество тегов, которые можно применить к статье (например, 5 в SO), и добавить их непосредственно в таблицу статей.

Нормализация БД имеет свои преимущества и недостатки, точно так же, как жесткие элементы в одной таблице имеют свои преимущества и недостатки.

Ничто не говорит, что вы не можете сделать оба. Повторение информации противоречит парадигмам реляционных БД, но если целью является производительность, возможно, вам придется нарушить парадигмы.

6 голосов
/ 28 ноября 2009

Предлагаемая реализация трех таблиц будет работать для тегирования.

Переполнение стека использует, однако, другую реализацию. Они хранят теги в столбце varchar в таблице сообщений в виде простого текста и используют полнотекстовое индексирование для извлечения сообщений, соответствующих тегам. Например posts.tags = "algorithm system tagging best-practices". Я уверен, что Джефф упоминал об этом где-то, но я забыл, где.

3 голосов
/ 27 ноября 2009

Предлагаемое решение - лучший, если не единственный, практически осуществимый, способ, которым я могу придумать, для решения отношения «многие ко многим» между тегами и статьями. Так что мой голос за «да, он все еще лучший». Я был бы заинтересован в любых альтернативах, хотя.

2 голосов
/ 21 марта 2018

Я хотел бы предложить оптимизированный MySQLicious для повышения производительности. До этого недостатки решения Toxi (3 таблицы) составляют

Если у вас есть миллионы вопросов и в каждом есть 5 тегов, то в таблице тегов будет 5 миллионов записей. Итак, сначала мы должны отфильтровать 10 тысяч записей в теговой карте на основе поиска по тегам, а затем снова отфильтровать соответствующие вопросы из этих 10 тысяч. Таким образом, при фильтрации, если художественный идентификатор является простым числовым, тогда все в порядке, но если это тип UUID (32 varchar), тогда фильтрация требует большего сравнения, хотя и индексируется.

Мое решение:

Всякий раз, когда создается новый тег, используйте counter ++ (base 10) и конвертируйте этот счетчик в base64. Теперь у каждого имени тега будет идентификатор base64. и передать этот идентификатор в пользовательский интерфейс вместе с именем. Таким образом, у вас будет максимум два идентификатора, пока в нашей системе не будет создано 4095 тегов. Теперь объедините эти несколько тегов в каждый столбец тега таблицы вопросов. Добавьте также разделитель и сделайте его отсортированным.

Так выглядит таблица

enter image description here

При запросе запрашивайте идентификатор вместо реального имени тега. Поскольку условие SORTED , and для тега будет более эффективным (LIKE '%|a|%|c|%|f|%).

Обратите внимание, что одного разделителя пробелов недостаточно, и нам нужен двойной разделитель для разграничения таких тегов, как sql и mysql, потому что LIKE "%sql%" вернет также mysql результаты. Должно быть LIKE "%|sql|%"

Я знаю, что поиск не проиндексирован, но, тем не менее, вы могли проиндексировать другие столбцы, относящиеся к статье, такие как author / dateTime, иначе это приведет к полному сканированию таблицы.

Наконец, с этим решением не требуется никакого внутреннего объединения, когда необходимо сравнить миллион записей с 5 миллионами записей при условии соединения.

2 голосов
/ 28 ноября 2009

Если ваша база данных поддерживает индексируемые массивы (например, PostgreSQL), я бы рекомендовал полностью денормализованное решение - хранить теги в виде массива строк в одной таблице. Если нет, лучшим решением будет сопоставление вторичной таблицы объектов с тегами. Если вам нужно хранить дополнительную информацию относительно тегов, вы можете использовать отдельную таблицу тегов, но нет смысла вводить второе объединение для каждого поиска тегов.

0 голосов
/ 03 октября 2018
CREATE TABLE Tags (
    tag VARHAR(...) NOT NULL,
    bid INT ... NOT NULL,
    PRIMARY KEY(tag, bid),
    INDEX(bid, tag)
)

Примечания:

  • Это лучше, чем TOXI, поскольку он не проходит через таблицу «многие: многие», что затрудняет оптимизацию.
  • Конечно, мой подход может быть немного более громоздким (чем TOXI) из-за избыточных тегов, но это небольшой процент от всей базы , и улучшения производительности могут быть значительными.
  • Это очень масштабируемый.
  • У него нет (потому что оно не нужно) суррогата AUTO_INCREMENT PK. Следовательно, это лучше, чем Скаттл.
  • MySQLicious отстой, потому что он не может использовать индекс (LIKE с лидирующим подстановочным знаком; ложные попадания в подстроки)
  • Для MySQL обязательно используйте ENGINE = InnoDB, чтобы получить эффекты кластеризации.

Связанные обсуждения (для MySQL):
многие: много оптимизация таблицы сопоставления
упорядоченные списки

...