MongoDB introduziu recentemente sua nova estrutura de agregação. Essa estrutura fornece uma solução mais simples para calcular valores agregados em vez de depender de estruturas poderosas com um mapa reduzido.
Com apenas alguns primitivos simples, ele permite calcular, agrupar, moldar e projetar documentos contidos em uma coleção específica do MongoDB. O restante deste artigo descreve a refatoração do algoritmo de redução de mapa para uso otimizado da nova plataforma de agregação MongoDB. O código-fonte completo pode ser encontrado no repositório Datablend GitHub disponível publicamente.
1. Estrutura de agregação do MongoDB
A plataforma de agregação MongoDB é baseada no conhecido conceito Linux Pipeline, onde a saída de um comando é transmitida através de um transportador ou redirecionada para ser usada como entrada para o próximo comando . No caso do MongoDB vários operadores são combinados em um único transportador que é responsável pelo processamento do fluxo de documentos.
Alguns operadores como $ match, $ limit e $ ignoram a aceitação do documento como entrada e a saída do mesmo documento se um determinado conjunto de critérios for atendido. Outros operadores, como $ project e $ unwind, aceitam um único documento como dado de entrada e alteram seu formato ou formam vários documentos com base em uma determinada projeção.
O operador $ group finalmente aceita vários documentos como dados de entrada e os agrupa em um documento combinando os valores correspondentes. Expressões podem ser usadas em alguns desses operadores para calcular novos valores ou realizar operações de string.
Vários operadores são combinados em um único pipeline, que se aplica à lista de documentos. O próprio transportador é executado como o comando MongoDB, o que resulta em um único documento MongoDB, que contém uma matriz de todos os documentos que saíram no final do transportador. O próximo parágrafo descreve em detalhes o algoritmo de refatoração de similaridade molecular como transportador de operadores. Certifique-se de (re)ler os dois artigos anteriores para entender completamente a lógica de implementação.
2. Tubulação de similaridade molecular
Ao aplicar um transportador a uma coleção específica, todos os documentos contidos nessa coleção são passados como entrada para o primeiro operador. É recomendável filtrar essa lista o mais rápido possível para limitar o número de documentos transferidos por meio do pipeline. No nosso caso, isso significa filtrar todo o documento que nunca atenderá ao fator Tanimoto de destino.
Portanto, como primeiro passo, comparamos todos os documentos para os quais o número de impressões digitais está dentro de um determinado limite. Se segmentarmos um fator Tanimoto de 0,8 com uma conexão de destino contendo 40 impressões digitais exclusivas, o operador $ match ficará assim:
{"$match" :
{ "fingerprint_count" : {"$gte" : 32, "$lte" : 50}}.
}
Somente conexões com um número de impressões digitais de 32 a 50 serão transferidas para o próximo operador de pipeline. Para realizar essa filtragem, o operador $ match pode usar o índice que definimos para a propriedade fingerprint_count. Para calcular o coeficiente de Tanimoto, precisamos calcular o número de impressões digitais comuns entre uma determinada conexão de entrada e a conexão de destino que estamos direcionando.
Para trabalhar no nível da impressão digital, usamos o operador $ unwind. $ unwind remove os elementos do array um por um, retornando o fluxo de documentos no qual o array especificado é substituído por um de seus elementos. No nosso caso, aplicamos $ unwind às impressões digitais. Consequentemente, cada documento composto resultará em n documentos compostos, onde n é o número de impressões digitais únicas contidas em um documento composto.
{"$unwind" :"$fingerprints"}
Para calcular o número de impressões digitais comuns, começaremos filtrando todos os documentos que não possuem as impressões digitais que estão na lista de impressões digitais da conexão de destino. Para isso, usamos novamente o operador $ match, desta vez filtrando a propriedade fingerprint, onde apenas os documentos que contêm uma impressão digital que está na lista de impressões digitais de destino são suportados.
{"$match" :
{ "fingerprints" :
{"$in" : [ 1960 , 15111 , 5186 , 5371 , 756 , 1015 , 1018 , 338 , 325 , 776 , 3900 , ..., 2473] }
}
}
Como correspondemos apenas às impressões digitais que estão na lista de impressões digitais de destino, a saída pode ser usada para calcular o número total de impressões digitais comuns.
Para fazer isso, aplicamos o operador $ group à conexão composta, embora criemos um novo tipo de documento contendo o número de impressões digitais correspondentes (somando o número de ocorrências), o número total de impressões digitais da conexão de entrada e smileys.
{"$group" :
{ "_id" : "$compound_cid". ,
"fingerprintmatches" : {"$sum" : 1} ,
"totalcount" : { "$first" : "$fingerprint_count"} ,
"smiles" : {"$first" : "$smiles"}
}
}
Agora temos todos os parâmetros para calcular o coeficiente de Tanimoto. Para isso, usaremos o operador $ project que, além de copiar a propriedade composta id e smiles, também adiciona uma propriedade recém-calculada chamada Tanimoto.
{
"$project"
:
{
"_id"
:
1
,
"tanimoto"
:
{
"$divide"
:
[
"$fingerprintmatches."
,
{
"$subtract"
:
[
{
"$add"
:
[
40
,
"$totalcount"
]
}
,
"$fingerprintmatches."
]
}
]
}
,
"smiles"
:
1
}
}
Como estamos interessados apenas em conexões que tenham um coeficiente de destino Tanimoto de 0,8, usamos o operador opcional $ match para filtrar todas aquelas que não atingem esse coeficiente.
{"$match" :
{ "tanimoto" : { "$gte" : 0.8}
}
O comando do pipeline completo pode ser encontrado abaixo.
{"aggregate" : "compounds"} ,
"pipeline" : [
{"$match" :
{ "fingerprint_count" : {"$gte" : 32, "$lte" : 50} }
},
{"$unwind" : "$fingerprints"},
{"$match" :
{ "fingerprints" :
{"$in" : [ 1960 , 15111 , 5186 , 5371 , 756 , 1015 , 1018 , 338 , 325 , 776 , 3900,... , 2473] }
}
},
{"$group" :
{ "_id" : "$compound_cid" ,
"fingerprintmatches" : {"$sum" : 1} ,
"totalcount" : { "$first" : "$fingerprint_count"} ,
"smiles" : {"$first" : "$smiles"}
}
},
{"$project" :
{ "_id" : 1 ,
"tanimoto" : {"$divide" : ["$fingerprintmatches"] , { "$subtract" : [ { "$add" : [ 89 , "$totalcount"]} , "$fingerprintmatches"] }. ] } ,
"smiles" : 1
}
},
{"$match" :
{"tanimoto" : {"$gte" : 0.05} }
} ]
}
A saída desse pipeline contém uma lista de conexões que possuem Tanimoto 0.8 ou superior em relação a uma determinada conexão de destino. Uma representação visual deste transportador pode ser encontrada abaixo:
3. Conclusão
A nova estrutura de agregação do MongoDB fornece um conjunto de operadores fáceis de usar que permitem aos usuários expressar algoritmos do tipo de redução de cartão de forma mais breve. O conceito de um transportador abaixo oferece uma maneira intuitiva de processar dados.
Não surpreendentemente, esse paradigma de pipeline é adotado por várias abordagens NoSQL, incluindo Gremlin Framework Tinkerpop na implementação e Cypher Neo4j na implementação.
Em termos de desempenho, a solução de tubulação é uma melhoria significativa na implementação dos mapas de redução.
Os operadores são inicialmente suportados pela plataforma MongoDB, o que leva a melhorias significativas de desempenho em relação ao Javascript interpretado. Como o Aggregation Framework também pode operar em um ambiente isolado, ele excede facilmente o desempenho da minha implementação original, especialmente quando o número de conexões de entrada é alto e o destino Tanimoto é baixo. Excelente desempenho do comando MongoDB!