Algoritmo para pesquisa rápida de tags

16

O problema é o seguinte.

  • Há um conjunto de entidades simples E, cada uma com um conjunto de tags T anexadas. Cada entidade pode ter um número arbitrário de tags. O número total de entidades é próximo de 100 milhões e o número total de tags é de cerca de 5000.

Portanto, os dados iniciais são mais ou menos assim:

E1 - T1, T2, T3, ... Tn
E2 - T1, T5, T100, ... Tk
..
Ez - T10, T12, ... Tl

Esses dados iniciais raramente são atualizados.

  • De alguma forma, meu aplicativo gera uma expressão lógica em tags como esta:

    T1 e T2 e T3 | (T5 e! T6)

  • O que eu preciso é calcular um número de entidades que correspondam a determinada expressão (observe - não as entidades, mas apenas o número). Este pode não ser totalmente preciso, é claro.

O que eu tenho agora é uma simples pesquisa na tabela na memória, proporcionando um tempo de execução de 5 a 10 segundos em um único thread.

Estou curioso, existe alguma maneira eficiente de lidar com essas coisas? Que abordagem você recomendaria? Existem algoritmos ou estruturas de dados comuns para isso?

Atualizar

Um pouco de esclarecimento, conforme solicitado.

  1. Tobjetos são na verdade cadeias constantes relativamente curtas. Mas isso realmente não importa - sempre podemos atribuir alguns IDs e operar com números inteiros.
  2. Definitivamente, podemos classificá-los.
Andy
fonte
1
é T1o mesmo objecto de referência para E1, E2, etc?
Reactgular
como as tags são comparáveis? as tags podem ser classificadas para que isso T2 < T3seja sempre verdadeiro?
Reactgular
As tags são binárias? T1Ou seja, é um trueou falsepara um dado E, e não variável com base na entrada? (ie Model = "V5") Ou é T1uma expressão variável como Model = <input>?
Bobson

Respostas:

4

eu faria isso no sql com uma tabela de links EntityCategoryentre a eidentidade de cidreferência e a categoria de referência usando auto-junções:

    select count(ec1.eid)
    from EntityCategory ec1 
    left join EntityCategory ec2 on ec1.eid=ec2.eid 
    left join EntityCategory ec3 on ec1.eid=ec3.eid 
    ...
    where 
      ec1.cid={categoryId1} and 
      ec2.cid={categoryId2} and
      ec3.cid={categoryId3} ...
k3b
fonte
1
+1, este é o território clássico do banco de dados. A outra resposta pode ter idéias razoáveis ​​de como codificá-lo manualmente, mas esse deve ser o último recurso.
MSalters
Eu também escolheria o sql como a técnica para resolver isso. A maioria dos bancos de dados são bastante otimizado para estes algoritmos :)
winkbrace
3

Depois de escrever esta resposta, eu provavelmente deveria sinalizar a pergunta como “muito ampla” - podemos conversar por séculos sobre várias estratégias; no final, uma referência terá que ser executada com seus dados.

Cada tag pode ser representada com eficiência por um número inteiro. Cada entidade possui um conjunto de tags. É importante escolher a implementação correta do conjunto - são possíveis árvores B e matrizes ordenadas. Com esse conjunto, faremos apenas testes de associação. Como ambas as estruturas fazem isso em O (log t) (com tags t por entidade), eu preferiria matrizes devido à sua representação mais densa.

Agora podemos filtrar todas as entidades em uma operação O (n · log t · p) , em que p é o comprimento médio do caminho na árvore de decisão predicada. Essa árvore de decisão pode ser ordenada para que uma decisão possa ser alcançada rapidamente. Sem dados estatísticos, só é possível fatorar uma subexpressão comum.

A ordem na qual as entidades são pesquisadas não é realmente importante. Por outro lado, pode ser vantajoso para classificá-lo de tal forma que as entidades a índices 0para itodos têm um certo tag, enquanto o resto não. Isso reduz o n ao procurar por essa tag específica (na árvore de decisão, esse deve ser o primeiro teste). Isso pode ser expandido para vários níveis, mas isso complica e leva O (2 k ) de memória com kníveis. Com vários níveis, as tags com maior ganho devem ser decididas primeiro, onde o ganho é o número de entidades que não precisam ser pesquisadas vezes a probabilidade de descartá-las. O ganho se torna máximo para chances 50:50 ou quando 50% das entidades possuem essa tag específica. Isso permitiria otimizar mesmo que os padrões de acesso não fossem conhecidos.

Você também pode criar conjuntos que indexam as entidades por cada tag usada - um conjunto com todas as entidades para T1e o próximo para T2. Uma otimização óbvia (espaço e tempo) é parar quando um conjunto contém mais da metade de todos os elementos e salvar os elementos que não possuem essa tag - dessa forma, a criação de índices para todas as tags ocupará menos ½ · n · tespaço (com t tags no total). Observe que salvar conjuntos complementares pode dificultar outras otimizações. Mais uma vez, eu (ordenava) matrizes para os conjuntos.

Se você também representar suas entidades por meio de um intervalo inteiro, poderá compactar o espaço usado para os conjuntos de índices armazenando apenas o membro inicial e final de um intervalo contínuo. Em termos de implementação, isso provavelmente seria feito com um bit alto para indicar se uma entrada é uma entrada vinculada ao intervalo ou regular.

Se agora tivermos conjuntos de índices (e, portanto, estatísticas sobre as tags), podemos otimizar nossos predicados para que propriedades improváveis ​​sejam testadas primeiro (estratégia à prova de falhas). Isso significa que, se T1é comum e T2raro, o predicado T1 & T2deve ser avaliado através da iteração em todas as T2entradas do conjunto de índices e testando cada elemento T1.

Se usarmos matrizes classificadas para implementar os conjuntos de índices, muitas etapas de avaliação poderão ser implementadas como operações de mesclagem. T1 & T2significa que pegamos as listas T1e T2, alocamos uma matriz de destino do tamanho das entradas maiores e executamos o seguinte algoritmo até que ambas estejam vazias: Se T1[0] < T2[0], então T1++(descarte a cabeça). Se T1[0] > T2[0]então T2++. Se ambas as cabeças são iguais, então copiar esse número até a matriz de destino, e incrementar todos os três ponteiros ( T1, T2, destino). Se o predicado for T1 | T2, nenhum elemento será descartado, mas o menor será copiado. Um predicado do formulário T1 & ¬T2também pode ser implementado usando uma estratégia fusão, mas ¬T1ou T1 | ¬T2não pode.

Isso deve ser considerado ao solicitar a árvore de decisão predicada: Complementos devem ocorrer no RHS de um &, ou no final, quando a contagem final está sendo determinada e os elementos reais não precisam ser examinados.

Sem usar conjuntos de índices, cada encadeamento pode filtrar sua parte das entidades e retornar a contagem de elementos que correspondem ao predicado, que podem ser resumidos. Ao usar conjuntos de índices, cada segmento receberá um nó na árvore de decisão. Ele usa dois fluxos de entrada que correspondem aos conjuntos ordenados e retorna um fluxo mesclado. Observe que cada nó na árvore de decisão tem um conjunto correspondente que representa todas as entidades que cumprem essa subexpressão e que, devido à ordem dos conjuntos, não é necessário conhecer o conjunto inteiro de uma só vez para mesclá-los .

Diferentes estratégias, como mesclar conjuntos indexados ou filtrar uma lista de entidades, podem ser combinadas até um certo grau. A filtragem tem um desempenho muito previsível. Se uma consulta for muito específica, para que o uso de conjuntos de índices reduza drasticamente o espaço de pesquisa, as operações de mesclagem poderão ser melhores. É importante observar que a fusão de muitos conjuntos grandes de entradas pode resultar em desempenho muito pior do que a pesquisa por força bruta. Um algoritmo muito otimizado escolherá uma estratégia adequada com base no tamanho da entrada, na estrutura da consulta e nos indicadores estatísticos.

Além disso, o armazenamento em cache de resultados parciais pode ser benéfico se for esperado que consultas semelhantes sejam executadas no futuro, mesmo que não acelerem a execução inicial.

amon
fonte