Come implementare il sistema di tag


90

Mi chiedevo quale sia il modo migliore per implementare un sistema di tag, come quello utilizzato su SO. Ci stavo pensando ma non riesco a trovare una buona soluzione scalabile.

Stavo pensando di avere una soluzione base a 3 tavoli: avere un tagstavolo, un articlestavolo e un tag_to_articlestavolo.

È questa la migliore soluzione a questo problema o ci sono alternative? Usando questo metodo la tabella diventerebbe estremamente grande nel tempo e presumo che per la ricerca non sia troppo efficiente. D'altra parte non è così importante che la query venga eseguita velocemente.


Risposte:


119

Credo che troverai interessante questo post del blog: Tag: Schemi di database

Il problema: vuoi avere uno schema di database in cui puoi taggare un segnalibro (o un post di blog o qualsiasi altra cosa) con tutti i tag che desideri. Successivamente, si desidera eseguire query per vincolare i segnalibri a un'unione o intersezione di tag. Vuoi anche escludere (ad esempio: meno) alcuni tag dal risultato della ricerca.

Soluzione "MySQLicious"

In questa soluzione, lo schema ha una sola tabella, viene denormalizzato. Questo tipo è chiamato "soluzione MySQLicious" perché MySQLicious importa i dati del.icio.us in una tabella con questa struttura.

inserisci qui la descrizione dell'immagineinserisci qui la descrizione dell'immagine

Intersezione (AND) Query per "ricerca + servizio web + semweb":

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

Query Union (OR) per "ricerca | webservice | semweb":

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

Query meno per "ricerca + webservice-semweb"

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

Soluzione "Scuttle"

Scuttle organizza i suoi dati in due tabelle. La tabella "scCategories" è la tabella "tag" e ha una chiave esterna per la tabella "segnalibro".

inserisci qui la descrizione dell'immagine

Intersezione (AND) Query per "bookmark + webservice + 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

Innanzitutto, vengono cercate tutte le combinazioni di tag segnalibro, dove il tag è "segnalibro", "webservice" o "semweb" (c.category IN ('bookmark', 'webservice', 'semweb')), quindi solo i segnalibri che sono stati presi in considerazione tutti e tre i tag cercati (HAVING COUNT (b.bId) = 3).

Union (OR) Query per "bookmark | webservice | semweb": basta lasciare fuori la clausola HAVING e hai l'unione:

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

Meno (Esclusione) Query per "bookmark + webservice-semweb", ovvero: bookmark AND webservice E NON 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

Tralasciando HAVING COUNT porta alla Query per "bookmark | webservice-semweb".


Soluzione "Toxi"

Toxi ha inventato una struttura a tre tavoli. Tramite la tabella "tagmap" i segnalibri e i tag sono correlati da n a m. Ogni tag può essere utilizzato insieme a diversi segnalibri e viceversa. Questo schema DB viene utilizzato anche da wordpress. Le query sono quasi le stesse della soluzione "scuttle".

inserisci qui la descrizione dell'immagine

Intersezione (AND) Query per "bookmark + webservice + 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

Query dell'Unione (OR) per "bookmark | webservice | 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

Meno (Esclusione) Query per "bookmark + webservice-semweb", ovvero: bookmark AND webservice E NON 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

Tralasciando HAVING COUNT porta alla Query per "bookmark | webservice-semweb".


3
autore di quel post sul blog qui. Il blog non è più bloccato da Chrome (stupide vulnerabilità di wordpress, ora spostate su tumblr). Complimenti per averlo trasformato in
markdown

ciao @Philipp. OK, ho modificato la mia risposta. BTW, grazie per l'ottimo post sui sistemi di tag del database.
Nick Dandoulakis

1
Solo come nota: se desideri che la query di intersezione per la soluzione Toxi mostri anche il segnalibro se hai cercato "segnalibro" E "servizio web", dovrai modificare "HAVING COUNT (b.id) = 3" da 3 a "sizeof (array ('bookmark', 'webservice'))". Solo un piccolo dettaglio se prevedi di utilizzarlo come funzione di query sui tag dinamici.
toxicate20

3
eventuali link per il confronto delle prestazioni per le diverse soluzioni menzionate nel post?
kampta

@kampta, no, non ho collegamenti.
Nick Dandoulakis

8

Niente di sbagliato nella tua soluzione a tre tavoli.

Un'altra opzione è limitare il numero di tag che possono essere applicati a un articolo (come 5 in SO) e aggiungerli direttamente alla tabella degli articoli.

La normalizzazione del DB ha i suoi vantaggi e svantaggi, proprio come le cose cablate in una tabella ha vantaggi e svantaggi.

Niente dice che non puoi fare entrambe le cose. La ripetizione delle informazioni va contro i paradigmi dei DB relazionali, ma se l'obiettivo è la prestazione potrebbe essere necessario rompere i paradigmi.


Sì, inserire i tag direttamente nella tabella degli articoli sarebbe sicuramente un'opzione, sebbene ci siano alcuni inconvenienti in questo metodo. Se memorizzi i 5 tag in un campo separato da virgole come (tag1,2,3,4), questo sarebbe un metodo semplice. La domanda è se la ricerca andrà più veloce. Ad esempio, qualcuno vuole vedere tutto con tag1, devi andare attraverso l'intera tabella degli articoli. Questo sarebbe meno tho quindi andare attraverso la tabella tag_to_article. Ma poi di nuovo, la tabella tags_to_article è più sottile. Un'altra cosa è che devi esplodere ogni volta in php, non so se questo richiede tempo.
Saif Bechan,

Se esegui entrambe le operazioni (tag con l'articolo e in una tabella separata), otterrai prestazioni sia per le ricerche postcentriche che per le ricerche incentrate sui tag. Il compromesso è l'onere di mantenere le informazioni ripetute. Inoltre, limitando il numero di tag, puoi inserire ciascuno nella propria colonna. Seleziona * dagli articoli Dove XXXXX e vai; non è necessario esplodere.
Giovanni

6

L'implementazione di tre tabelle proposta funzionerà per la codifica.

Stack overflow utilizza, tuttavia, un'implementazione diversa. Memorizzano i tag nella colonna varchar nella tabella dei post in testo normale e utilizzano l'indicizzazione del testo completo per recuperare i post che corrispondono ai tag. Ad esempio posts.tags = "algorithm system tagging best-practices". Sono sicuro che Jeff ne abbia parlato da qualche parte, ma non ricordo dove.


4
Questo sembra super inefficiente. E l'ordine dei tag? O tag correlati? (ad esempio "processo" che è simile a "algoritmo" o qualcosa di simile)
Richard Duerr

3

La soluzione proposta è il modo migliore, se non l'unico praticabile, a cui posso pensare per affrontare la relazione molti-a-molti tra tag e articoli. Quindi il mio voto è per "sì, è ancora il migliore". Tuttavia, sarei interessato a qualsiasi alternativa.


Sono d'accordo. Queste tabelle Tag e TagMap hanno dimensioni di record ridotte e, se correttamente indicizzate, non dovrebbero diminuire drasticamente le prestazioni. Anche limitare il numero di tag per articolo potrebbe essere una buona idea.
PanJanek

2

Se il tuo database supporta array indicizzabili (come PostgreSQL, ad esempio), consiglierei una soluzione completamente denormalizzata: memorizza i tag come array di stringhe sulla stessa tabella. In caso contrario, una tabella secondaria che associa gli oggetti ai tag è la soluzione migliore. Se è necessario memorizzare informazioni aggiuntive sui tag, è possibile utilizzare una tabella di tag separata, ma non ha senso introdurre un secondo join per ogni ricerca di tag.


POstgreSQL supporta solo gli indici su array di interi: postgresql.org/docs/current/static/intarray.html
Mike Chamberlain

1
Adesso supporta anche il testo: postgresql.org/docs/9.6/static/arrays.html
luckydonald

2

Vorrei suggerire MySQLicious ottimizzato per prestazioni migliori. Prima di ciò gli svantaggi della soluzione Toxi (3 tabelle) sono

Se hai milioni di domande e ha 5 tag in ciascuna, ci saranno 5 milioni di voci nella tabella tagmap. Quindi prima dobbiamo filtrare 10mila voci di tagmap in base alla ricerca di tag, quindi filtrare nuovamente le domande corrispondenti di quelle 10mila. Quindi, mentre si filtra se l'ID artistico è un numero semplice, va bene, ma se è una specie di UUID (32 varchar), il filtraggio richiede un confronto più ampio sebbene sia indicizzato.

La mia soluzione:

Ogni volta che viene creato un nuovo tag, avere counter ++ (base 10) e convertire quel contatore in base64. Ora ogni nome di tag avrà l'id base64. e passa questo ID all'interfaccia utente insieme al nome. In questo modo avrai un massimo di due char id finché non avremo 4095 tag creati nel nostro sistema. Ora concatena questi più tag in ciascuna colonna di tag della tabella delle domande. Aggiungi anche un delimitatore e rendilo ordinato.

Quindi il tavolo assomiglia a questo

inserisci qui la descrizione dell'immagine

Durante la query, eseguire una query sull'ID anziché sul nome del tag reale. Poiché è SORTED , la andcondizione sul tag sarà più efficiente ( LIKE '%|a|%|c|%|f|%).

Nota che il delimitatore di spazio singolo non è sufficiente e abbiamo bisogno di un delimitatore doppio per differenziare tag come sqle mysqlperché LIKE "%sql%"restituiranno anche mysqlrisultati. Dovrebbe essereLIKE "%|sql|%"

So che la ricerca non è indicizzata ma potresti comunque averla indicizzata su altre colonne relative ad articoli come author / dateTime altrimenti porterai alla scansione completa della tabella.

Infine, con questa soluzione, non è necessario alcun join interno in cui è necessario confrontare milioni di record con 5 milioni di record in condizione di join.


Team, si prega di fornire il proprio contributo sullo svantaggio di questa soluzione nei commenti.
Kanagavelu Sugumar

@ Nick Dandoulakis Per favore aiutami fornendo i tuoi commenti sulla soluzione di cui sopra funzionerà?
Kanagavelu Sugumar

@ Juha Syrjälä La soluzione di cui sopra va bene?
Kanagavelu Sugumar

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

Appunti:

  • Questo è migliore di TOXI in quanto non passa attraverso una tabella molti: molti in più, il che rende difficile l'ottimizzazione.
  • Certo, il mio approccio potrebbe essere leggermente più ingombrante (rispetto a TOXI) a causa dei tag ridondanti, ma questa è una piccola percentuale dell'intero database e i miglioramenti delle prestazioni potrebbero essere significativi.
  • È altamente scalabile.
  • Non ha (perché non necessita) una AUTO_INCREMENTPK surrogata . Quindi, è meglio di Scuttle.
  • MySQLicious fa schifo perché non può utilizzare un indice ( LIKEcon leader wild card; falsi visite per sottostringhe)
  • Per MySQL, assicurati di utilizzare ENGINE = InnoDB per ottenere effetti di "clustering".

Discussioni correlate (per MySQL):
molti: molti elenchi ordinati di ottimizzazione della tabella di mappatura

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.