Як реалізувати систему тегів


90

Мені було цікаво, яким найкращим способом є впровадження системи тегів, на зразок тієї, що використовується на SO. Я думав про це, але не можу придумати гарного масштабованого рішення.

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

Це найкраще рішення цієї проблеми чи є альтернативи? За допомогою цього методу таблиця за часом стала б надзвичайно великою, і для пошуку це не надто ефективно, я вважаю. З іншого боку, не так важливо, щоб запит виконувався швидко.


Відповіді:


119

Я вірю, що ця публікація в блозі вам стане цікавою: Теги: Схеми баз даних

Проблема: Ви хочете мати схему бази даних, де ви можете позначити закладку (або допис у блозі чи що завгодно) такою кількістю тегів, скільки хочете. Пізніше ви захочете запустити запити, щоб обмежити закладки об'єднанням або перетином тегів. Ви також хочете виключити (скажімо: мінус) деякі теги з результату пошуку.

Рішення “MySQLicious”

У цьому рішенні схема має лише одну таблицю, вона денормалізована. Цей тип називається «рішення MySQLicious», оскільки MySQLicious імпортує дані del.icio.us до таблиці з цією структурою.

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

Запит на перетин (І) для “пошук + веб-сервіс + напівавтоматична мережа”:

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

Запит на об'єднання (АБО) "пошук | веб-служба | semweb":

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

Мінус запит для "пошук + веб-сервіс-semweb"

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

Рішення “Scuttle”

Scuttle упорядковує свої дані у дві таблиці. Ця таблиця “scCategories” є таблицею “tag” і має зовнішній ключ до таблиці “bookmark”.

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

Запит на перетин (І) для “закладки + веб-сервіс + напівавтоматична мережа”:

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»)), потім лише закладки, які отримали, що всі три пошукові теги враховані (МАЮЧИ КІЛЬКІ (b.bId) = 3)

Запит на об'єднання (АБО) для "закладки | веб-сервіс | semweb": Просто залиште речення HAVING, і ви отримаєте об'єднання:

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 веде до запиту “bookmark | webservice-semweb”.


Розчин “Toxi”

Toxi придумав структуру з трьох таблиць. За допомогою таблиці “tagmap” закладки та теги пов’язані між собою. Кожен тег можна використовувати разом із різними закладками та навпаки. Цю схему DB також використовує wordpress. Запити цілком такі ж, як і у "розмиті" рішення.

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

Запит на перетин (І) для “закладки + веб-сервіс + напівавтоматична мережа”

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

Мінус (виключення) Запит для “закладки + веб-сервіс-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”.


3
автор цього повідомлення в блозі тут. Блог більше не блокується Chrome (дурні вразливості WordPress, перенесено на tumblr зараз). Похвала за перетворення його на
знижку

привіт @Philipp. Добре, редагував мою відповідь. До речі, дякую за чудовий допис про системи тегів баз даних.
Нік Дандулакіс

1
Просто як примітка: Якщо ви хотіли, щоб запит на перетин для рішення Toxi також відображав закладку, якщо ви шукали "закладку" І "веб-послугу", вам потрібно буде змінити "HAVING COUNT (b.id) = 3" з 3 до "sizeof (array ('bookmark', 'webservice'))". Просто незначна деталь, якщо ви плануєте використовувати це як функцію динамічного запиту тегів.
токсикат 20

3
будь-які посилання для порівняння продуктивності різних рішень, згаданих у дописі?
kampta

@kampta, ні, у мене немає посилань.
Нік Дандулакіс,

8

У вашому рішенні з трьох таблиць немає нічого поганого.

Інший варіант - обмежити кількість тегів, які можна застосувати до статті (наприклад, 5 у SO), і додати їх безпосередньо до таблиці статей.

Нормалізація БД має свої переваги та недоліки, так само, як жорсткі з'єднання речей в одній таблиці мають переваги та недоліки.

Ніщо не говорить, що ви не можете робити і те, і інше. Повторення інформації суперечить реляційним парадигмам БД, але якщо метою є продуктивність, можливо, вам доведеться порушити парадигми.


Так, розміщення тегів безпосередньо в таблиці статей, безсумнівно, є варіантом, хоча у цього методу є кілька недоліків. Якщо ви зберігаєте 5 тегів у полі, розділеному комами, як (tag1,2,3,4), це буде простий спосіб. Питання в тому, чи буде пошук йти швидше. Наприклад, хтось хоче побачити все з тегом1, вам потрібно пройти всю таблицю статей. Це було б менше, ніж проходження через таблицю tag_to_article. Але знову ж таки, таблиця tags_to_article стає тоншою. Інша справа, що вам доведеться вибухати щоразу в php, я не знаю, чи потрібно це часу.
Сайф Бечан,

Якщо ви виконуєте обидва дії (теги до статті та в окремій таблиці), це дає вам ефективність як для постцентричного пошуку, так і для тегоцентричного пошуку. Компроміс - це тягар підтримки постійної інформації. Крім того, обмеживши кількість тегів, ви можете помістити кожен у свій стовпець. Просто виберіть * зі статей Куди XXXXX і йдіть; не потрібно вибухати.
Джон

6

Ваша запропонована реалізація трьох таблиць буде працювати для позначення.

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


4
Це здається надзвичайно неефективним. А як щодо замовлення тегів? Або пов’язані теги? (наприклад, "процес" схожий на "алгоритм" або щось подібне)
Річард Дюрр,

3

Запропоноване рішення є найкращим - якщо не єдиним практичним - я можу придумати для вирішення взаємозв'язку "багато-до-багатьох" між тегами та статтями. Тож я голосую за "так, це все-таки найкраще". Хоча мене цікавлять будь-які альтернативи.


Я згоден. Ці таблиці TagMap і TagMap мають невеликий розмір запису і при правильному індексуванні не повинні різко знижувати продуктивність. Обмеження кількості тегів на товар також може бути гарною ідеєю.
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 тегів у кожному, тоді в таблиці таблиць тегів буде 5 мільйонів записів. Отже, спочатку ми повинні відфільтрувати 10 тисяч записів тегів на основі пошуку тегів, а потім знову відфільтрувати відповідні запитання з цих 10 тисяч. Отже, під час фільтрації, якщо художній ідентифікатор є простим числовим, це нормально, але якщо це якийсь тип UUID (32 варчара), тоді фільтрація потребує більшого порівняння, хоча він індексується.

Моє рішення:

Щоразу, коли створюється новий тег, використовуйте лічильник ++ (база 10) і перетворюйте цей лічильник у base64. Тепер кожна назва тегу матиме ідентифікатор base64. і передайте цей ідентифікатор користувацькому інтерфейсу разом з ім'ям. Таким чином, у вас буде максимум два ідентифікатори символів, поки в нашій системі не буде створено 4095 тегів. Тепер об’єднайте ці кілька тегів у кожен стовпець тегів таблиці запитань. Додайте також роздільник та зробіть його сортуванням.

Тож таблиця виглядає так

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

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

Зверніть увагу, що одного пробілу обмежувача недостатньо, і нам потрібен подвійний роздільник, щоб диференціювати теги типу sqlі mysqlтому, що LIKE "%sql%"також поверне mysqlрезультати. Має бутиLIKE "%|sql|%"

Я знаю, що пошук не індексується, але все-таки ви могли б проіндексувати інші стовпці, пов’язані зі статтею, наприклад автор / дата і час, інакше призведе до повного сканування таблиці.

Нарешті, з цим рішенням не потрібне внутрішнє об’єднання, де мільйони записів мають бути порівняні з 5 мільйонами записів за умови об’єднання.


Команда, будь ласка, надайте свої коментарі щодо недоліку цього рішення у коментарях.
Канагавелу Сугумар,

@ Нік Дандулакіс Будь ласка, допоможіть мені, надавши свої коментарі щодо наведеного вище рішення, чи спрацює?
Канагавелу Сугумар

@Juha Syrjälä Чи добре вказане вище рішення?
Канагавелу Сугумар,

0
CREATE TABLE Tags (
    tag VARHAR(...) NOT NULL,
    bid INT ... NOT NULL,
    PRIMARY KEY(tag, bid),
    INDEX(bid, tag)
)

Примітки:

  • Це краще, ніж TOXI, тим, що він не переживає зайвого: багато таблиць, що ускладнює оптимізацію.
  • Звичайно, мій підхід може бути трохи громіздкішим (ніж TOXI) через надлишкові теги, але це невеликий відсоток від усієї бази даних, і покращення продуктивності може бути значним.
  • Це дуже масштабовано.
  • Він не має (бо йому не потрібен) сурогатний AUTO_INCREMENTПК. Отже, це краще, ніж Скаттл.
  • MySQLicious відстій, тому що він не може використовувати індекс ( LIKEіз провідними символами підстановки ; помилкові звернення до підрядків)
  • Для MySQL обов’язково використовуйте ENGINE = InnoDB, щоб отримати ефекти «кластеризації».

Пов'язані обговорення (для MySQL):
багато: багато упорядкованих списків оптимізації
таблиць

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.