Как реализовать систему тегов

90

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

Я думал о простом решении из трех таблиц: tagsтаблица, таблица articlesи tag_to_articlesтаблица.

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

Саиф Бечан
источник
stackoverflow.com/questions/50297616/…
Канагавелу Сугумар

Ответы:

119

Думаю, вам будет интересно это сообщение в блоге: Теги: схемы баз данных

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

«MySQLicious» решение

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

введите описание изображения здесьвведите описание изображения здесь

Пересечение (И) Запрос «поиск + веб-сервис + semweb»:

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

Union (OR) Запрос для «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%"

Раствор «Скаттл»

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

введите описание изображения здесь

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

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

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

Union (OR) Запрос для «bookmark | webservice | 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

Минус (исключение) Запрос «закладка + веб-сервис-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 приводит к запросу «закладка | webservice-semweb».


Раствор «Токси»

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

введите описание изображения здесь

Пересечение (И) Запрос «закладка + веб-сервис + 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
HAVING COUNT( b.id )=3

Union (OR) Запрос для «закладка | веб-сервис | 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

Минус (исключение) Запрос «закладка + веб-сервис-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 приводит к запросу «закладка | webservice-semweb».

Ник Дандулакис
источник
3
автор сообщения в блоге здесь. Блог больше не блокируется Chrome (глупые уязвимости wordpress, теперь перенесены в tumblr). Престижность за преобразование в
уценку
привет @Philipp. ОК, отредактировал свой ответ. Кстати, спасибо за отличный пост о системах тегов баз данных.
Ник Дандулакис 04
1
В качестве примечания: если вы хотите, чтобы в запросе на пересечение для решения Toxi также отображалась закладка, если вы искали «закладка» И «веб-сервис», вам нужно будет изменить «HAVING COUNT (b.id) = 3» с 3 на «sizeof (array ('закладка', 'веб-сервис'))». Небольшая деталь, если вы планируете использовать это как функцию запроса динамического тега.
toxicate20
3
есть ссылки для сравнения производительности для различных решений, упомянутых в сообщении?
kampta
@kampta, нет, у меня нет ссылок.
Ник Дандулакис
8

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

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

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

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

Джон
источник
Да, размещение тегов непосредственно в таблице статей было бы вариантом, хотя у этого метода есть несколько недостатков. Если вы сохраните 5 тегов в поле, разделенном запятыми, например (tag1,2,3,4), это будет простой способ. Вопрос в том, пойдет ли поиск быстрее. Например, кто-то хочет видеть все с помощью tag1, вам нужно просмотреть всю таблицу статей. Это будет меньше, чем при просмотре таблицы tag_to_article. Но опять же, таблица tags_to_article более тонкая. Другое дело, что вам нужно каждый раз взрывать php, я не знаю, займет ли это время.
Саиф Бечан,
Если вы делаете и то, и другое (теги со статьей и в отдельной таблице), то это дает вам производительность как для пост-ориентированного поиска, так и для поиска, ориентированного на теги. Компромисс - это бремя поддержания повторяющейся информации. Кроме того, ограничив количество тегов, вы можете поместить каждый в отдельный столбец. Просто выберите * из статей Куда ХХХХХ и вперед; взрыв не требуется.
Джон
6

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

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

Юха Сюрьяля
источник
4
Это кажется супер неэффективным. А как насчет порядка тегов? Или похожие теги? (например, «процесс» похож на «алгоритм» или что-то в этом роде)
Ричард Дуэрр
3

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

Дэвид просит восстановить Монику
источник
Согласен. Эти теги и таблицы TagMap имеют небольшой размер записи и при правильной индексации не должны резко снижать производительность. Ограничение количества тегов od на элемент также может быть хорошей идеей.
PanJanek
2

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

Ник Джонсон
источник
POstgreSQL поддерживает индексы только для целочисленных массивов: postgresql.org/docs/current/static/intarray.html
Майк Чемберлен,
1
Теперь он также поддерживает текст: postgresql.org/docs/9.6/static/arrays.html
luckydonald
2

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

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

Мое решение:

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

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

введите описание изображения здесь

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

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

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

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

Канагавелу Сугумар
источник
Команда, пожалуйста, поделитесь своим мнением о недостатках этого решения в комментариях.
Канагавелу Сугумар
@Nick Dandoulakis Пожалуйста, помогите мне, предоставив свои комментарии к вышеуказанному решению, будет ли работать?
Канагавелу Сугумар,
@Juha Syrjälä Хорошо ли это решение?
Канагавелу Сугумар,
0
CREATE TABLE Tags (
    tag VARHAR(...) NOT NULL,
    bid INT ... NOT NULL,
    PRIMARY KEY(tag, bid),
    INDEX(bid, tag)
)

Примечания:

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

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

Рик Джеймс
источник