Como implementar sistema de tag

90

Eu queria saber qual é a melhor maneira de implementar um sistema de tags, como o usado no SO. Eu estava pensando nisso, mas não consigo encontrar uma boa solução escalonável.

Eu estava pensando em ter uma solução básica de 3 mesas: ter uma tagsmesa, uma articlesmesa e uma tag_to_articlesmesa.

Esta é a melhor solução para este problema ou existem alternativas? Usando esse método, a tabela ficaria extremamente grande com o tempo, e para pesquisar isso não é muito eficiente, eu suponho. Por outro lado, não é tão importante que a consulta seja executada rapidamente.

Saif Bechan
fonte

Respostas:

119

Acredito que você achará interessante esta postagem do blog: Tags: Esquemas de banco de dados

O problema: você deseja ter um esquema de banco de dados em que possa marcar um favorito (ou uma postagem de blog ou qualquer outra coisa) com quantas marcas desejar. Posteriormente, você deseja executar consultas para restringir os marcadores a uma união ou interseção de tags. Você também deseja excluir (digamos: menos) algumas tags do resultado da pesquisa.

Solução “MySQLicious”

Nesta solução, o esquema possui apenas uma tabela, está desnormalizado. Este tipo é chamado de “solução MySQLicious” porque MySQLicious importa dados del.icio.us em uma tabela com esta estrutura.

insira a descrição da imagem aquiinsira a descrição da imagem aqui

Consulta de interseção (AND) para “search + webservice + semweb”:

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

Consulta de união (OR) para “search | webservice | semweb”:

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

Menos consulta para “search + webservice-semweb”

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

Solução “Scuttle”

Scuttle organiza seus dados em duas tabelas. Essa tabela “scCategories” é a tabela “tag” e tem uma chave estrangeira para a tabela “bookmark”.

insira a descrição da imagem aqui

Consulta de interseção (AND) para “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

Primeiro, todas as combinações de bookmark-tag são pesquisadas, onde a tag é “bookmark”, “webservice” ou “semweb” (c.category IN ('bookmark', 'webservice', 'semweb')), então apenas os favoritos que todas as três marcas pesquisadas são levadas em consideração (HAVING COUNT (b.bId) = 3).

Consulta de união (OR) para “bookmark | webservice | semweb”: Basta omitir a cláusula HAVING e você terá união:

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

Menos (Exclusão) Consulta para “bookmark + webservice-semweb”, ou seja: bookmark AND webservice AND NOT 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

Deixar de fora HAVING COUNT leva à consulta de “bookmark | webservice-semweb”.


Solução “Toxi”

A Toxi criou uma estrutura de três mesas. Por meio da tabela “mapa de tags”, os favoritos e as tags são relacionados de n para m. Cada tag pode ser usada junto com diferentes marcadores e vice-versa. Este esquema de banco de dados também é usado pelo wordpress. As consultas são praticamente as mesmas da solução “scuttle”.

insira a descrição da imagem aqui

Consulta de interseção (AND) para “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

Consulta de União (OR) para “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

Menos (Exclusão) Consulta para “bookmark + webservice-semweb”, ou seja: bookmark AND webservice AND NOT 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

Deixar de fora HAVING COUNT leva à consulta de “bookmark | webservice-semweb”.

Nick Dandoulakis
fonte
3
autor dessa postagem do blog aqui. O blog não é mais bloqueado pelo Chrome (vulnerabilidades idiotas do wordpress, movidas para o tumblr agora). Parabéns por transformá-lo em redução de
preço
oi @Philipp. OK, editei minha resposta. BTW, obrigado pela excelente postagem sobre sistemas de tag de banco de dados.
Nick Dandoulakis
1
Apenas como uma observação: se você quiser que a solução Intersection Query for the Toxi também mostre o favorito, se você pesquisou por 'favorito' E 'serviço da web', você precisará alterar "HAVING COUNT (b.id) = 3" de 3 para "sizeof (array ('bookmark', 'webservice'))". Apenas um pequeno detalhe se você planeja usar isso como uma função de consulta de tag dinâmica.
toxicate20 de
3
algum link para comparação de desempenho para diferentes soluções mencionadas no post?
kampta de
@kampta, não, não tenho nenhum link.
Nick Dandoulakis,
8

Nada de errado com sua solução de três mesas.

Outra opção é limitar o número de tags que podem ser aplicadas a um artigo (como 5 no SO) e adicioná-las diretamente à tabela do artigo.

A normalização do banco de dados tem suas vantagens e desvantagens, assim como conectar as coisas em uma tabela tem vantagens e desvantagens.

Nada diz que você não pode fazer as duas coisas. Repetir informações vai contra os paradigmas relacionais do banco de dados, mas se o objetivo for o desempenho, talvez seja necessário quebrar os paradigmas.

John
fonte
Sim, colocar as tags diretamente na tabela de artigos com certeza seria uma opção, embora haja algumas desvantagens nesse método. Se você armazenar as 5 tags em um campo separado por vírgulas como (tag1,2,3,4), este seria um método fácil. A questão é se a busca será mais rápida. Por exemplo, alguém quer ver tudo com tag1, você tem que percorrer toda a tabela do artigo. Isso seria menos do que passar pela tabela tag_to_article. Mas, novamente, a tabela tags_to_article é mais estreita. Outra coisa é que tem que explodir toda vez em php, não sei se leva tempo.
Saif Bechan
Se você fizer ambos (tags com o artigo e em uma tabela separada), isso oferece desempenho para pesquisas pós-centradas e para pesquisas centradas em tags. A compensação é o fardo de manter as informações repetidas. Além disso, ao limitar o número de tags, você pode colocar cada uma em sua própria coluna. Basta selecionar * dos artigos Onde XXXXX e pronto; não é necessário explodir.
João
6

Sua proposta de implementação de três tabelas funcionará para marcação.

O estouro de pilha usa, no entanto, implementações diferentes. Eles armazenam tags na coluna varchar na tabela de posts em texto simples e usam indexação de texto completo para buscar posts que correspondem às tags. Por exemplo posts.tags = "algorithm system tagging best-practices". Tenho certeza de que Jeff mencionou isso em algum lugar, mas esqueci onde.

Juha Syrjälä
fonte
4
Isso parece super ineficiente. E a ordem das tags? Ou tags relacionadas? (como "processo" sendo semelhante a "algoritmo" ou algo semelhante)
Richard Duerr
3

A solução proposta é a melhor - se não a única - maneira praticável que posso pensar de abordar a relação muitos-para-muitos entre tags e artigos. Portanto, meu voto é 'sim, ainda é o melhor.' Eu estaria interessado em quaisquer alternativas.

David diz reintegrar Monica
fonte
Concordo. Essas tags e tabelas TagMap têm tamanho de registro pequeno e, quando indexadas corretamente, não devem diminuir o desempenho drasticamente. Limitar o número de tags por item também pode ser uma boa ideia.
PanJanek
2

Se o seu banco de dados suportar matrizes indexáveis ​​(como PostgreSQL, por exemplo), eu recomendaria uma solução totalmente desnormalizada - armazene as tags como uma matriz de strings na mesma tabela. Caso contrário, uma tabela secundária mapeando objetos para tags é a melhor solução. Se você precisar armazenar informações extras em relação às tags, poderá usar uma tabela de tags separada, mas não há motivo para introduzir uma segunda junção para cada pesquisa de tag.

Nick Johnson
fonte
POstgreSQL suporta apenas índices em matrizes de inteiros: postgresql.org/docs/current/static/intarray.html
Mike Chamberlain
1
Agora também suporta texto: postgresql.org/docs/9.6/static/arrays.html
luckydonald
2

Eu gostaria de sugerir MySQLicious otimizado para melhor desempenho. Antes disso, as desvantagens da solução Toxi (tabela 3) são

Se você tiver milhões de perguntas e houver 5 tags em cada uma, haverá 5 milhões de entradas na tabela de mapas de tags. Portanto, primeiro temos que filtrar 10 mil entradas de mapa de tag com base na pesquisa de tag e, novamente, filtrar as perguntas correspondentes dessas 10 mil. Portanto, ao filtrar se o id ártico é numérico simples, está tudo bem, mas se for do tipo UUID (32 varchar), a filtragem precisa de uma comparação maior, embora seja indexada.

Minha solução:

Sempre que uma nova tag for criada, tenha o contador ++ (base 10) e converta esse contador em base64. Agora, cada nome de tag terá um id de base64. e passe esse id para a IU junto com o nome. Desta forma, você terá no máximo dois char id até termos 4095 tags criadas em nosso sistema. Agora concatene essas várias tags em cada coluna de tag da tabela de perguntas. Adicione o delimitador também e torne-o classificado.

Então a mesa é assim

insira a descrição da imagem aqui

Durante a consulta, consulte o id em vez do nome real da tag. Uma vez que é SORTED , a andcondição na tag será mais eficiente (LIKE '%|a|%|c|%|f|% ).

Observe que um único delimitador de espaço não é suficiente e precisamos de um delimitador duplo para diferenciar as tags como sqle mysqlporque LIKE "%sql%"retornarámysql resultados também. Deveria estarLIKE "%|sql|%"

Eu sei que a pesquisa não está indexada, mas você ainda pode ter indexado em outras colunas relacionadas ao artigo, como autor / data / hora, caso contrário, levará a uma verificação completa da tabela.

Finalmente, com essa solução, nenhuma junção interna necessária, onde milhões de registros devem ser comparados com 5 milhões de registros na condição de junção.

Kanagavelu Sugumar
fonte
Equipe, forneça sua opinião sobre a desvantagem desta solução nos comentários.
Kanagavelu Sugumar
@Nick Dandoulakis Por favor, ajude-me fornecendo seus comentários sobre a solução acima.
Kanagavelu Sugumar
@Juha Syrjälä A solução acima é boa?
Kanagavelu Sugumar
0
CREATE TABLE Tags (
    tag VARHAR(...) NOT NULL,
    bid INT ... NOT NULL,
    PRIMARY KEY(tag, bid),
    INDEX(bid, tag)
)

Notas:

  • Isso é melhor do que TOXI porque não passa por uma tabela a mais: muitos, o que torna a otimização difícil.
  • Claro, minha abordagem pode ser um pouco mais volumosa (do que TOXI) devido às tags redundantes, mas essa é uma pequena porcentagem do todo banco de dados e as melhorias de desempenho podem ser significativas.
  • É altamente escalonável.
  • Ele não tem (porque não precisa) de um substituto de AUTO_INCREMENTPK. Portanto, é melhor do que Scuttle.
  • MySQLicious suga porque não pode usar um índice ( LIKEcom líder wild card; falsas visitas em substrings)
  • Para MySQL, certifique-se de usar ENGINE = InnoDB para obter efeitos de 'agrupamento'.

Discussões relacionadas (para MySQL):
muitos: muitos listas ordenadas de otimização de tabelas de mapeamento

Rick James
fonte