MongoDB niedawno wprowadził nową strukturę agregacji. Ta struktura zapewnia prostsze rozwiązanie do obliczania zagregowanych wartości, zamiast polegania na potężnych strukturach ze zredukowaną mapą.
Za pomocą kilku prostych prymitywów umożliwia obliczanie, grupowanie, kształtowanie i projektowanie dokumentów zawartych w określonej kolekcji MongoDB. W dalszej części tego artykułu opisano refaktoryzację algorytmu redukcji mapy w celu optymalnego wykorzystania nowej platformy agregacji MongoDB. Pełny kod źródłowy można znaleźć w publicznie dostępnym repozytorium Datablend GitHub.
1. Struktura agregacji MongoDB
Platforma agregacji MongoDB opiera się na dobrze znanej koncepcji Linux Pipeline, w której dane wyjściowe jednego polecenia są przesyłane przez przenośnik lub przekierowywane w celu użycia jako dane wejściowe dla następnego polecenia . W przypadku MongoDB kilku operatorów jest połączonych w jeden przenośnik, który odpowiada za przetwarzanie przepływu dokumentów.
Niektóre operatory, takie jak $ dopasowanie, $ limit i $ pomijają akceptowanie dokumentu jako danych wejściowych i wyjściowych tego samego dokumentu, jeśli zostanie spełniony określony zestaw kryteriów. Inni operatorzy, tacy jak $ project i $ unwind, akceptują pojedynczy dokument jako dane wejściowe i zmieniają jego format lub tworzą kilka dokumentów na podstawie określonej projekcji.
Operator grupy $ ostatecznie akceptuje kilka dokumentów jako dane wejściowe i grupuje je w jeden dokument, łącząc odpowiednie wartości. Wyrażenia mogą być używane w niektórych z tych operatorów do obliczania nowych wartości lub wykonywania operacji na ciągach.
Kilku operatorów jest połączonych w jeden potok, co dotyczy listy dokumentów. Sam przenośnik jest wykonywany jako polecenie MongoDB, co skutkuje pojedynczym dokumentem MongoDB, który zawiera tablicę wszystkich dokumentów, które pojawiły się na końcu przenośnika. Kolejny akapit opisuje szczegółowo algorytm refaktoryzacji podobieństwa molekularnego jako przenośnika operatorów. Koniecznie przeczytaj (ponownie) poprzednie dwa artykuły, aby w pełni zrozumieć logikę implementacji.
2. Orurowanie podobieństwa molekularnego
Przy stosowaniu przenośnika do określonej kolekcji, wszystkie dokumenty zawarte w tej kolekcji są przekazywane jako dane wejściowe do pierwszego operatora. Zaleca się jak najszybsze przefiltrowanie tej listy, aby ograniczyć liczbę dokumentów przesyłanych za pośrednictwem potoku. W naszym przypadku oznacza to filtrowanie całego dokumentu, który nigdy nie osiągnie docelowego współczynnika Tanimoto.
Dlatego w pierwszym kroku porównujemy wszystkie dokumenty, dla których liczba odcisków palców mieści się w określonym progu. Jeśli celujemy na współczynnik Tanimoto równy 0,8 z połączeniem docelowym zawierającym 40 unikalnych odcisków palców, operator dopasowania $ będzie wyglądał następująco:
{"$match" :
{ "fingerprint_count" : {"$gte" : 32, "$lte" : 50}}.
}
Tylko połączenia z liczbą odcisków palców od 32 do 50 zostaną przeniesione do następnego operatora rurociągu. Aby przeprowadzić to filtrowanie, operator dopasowania $ może użyć indeksu zdefiniowanego dla właściwości fingerprint_count . Aby obliczyć współczynnik Tanimoto, musimy obliczyć liczbę wspólnych odcisków palców między określonym połączeniem wejściowym a docelowym połączeniem.
Do pracy na poziomie odcisku palca używamy operatora $ unwind. $ unwind usuwa elementy tablicy jeden po drugim, zwracając strumień dokumentu, w którym określona tablica jest zastępowana przez jeden z jej elementów. W naszym przypadku na odciski palców nakładamy $ unwind. W konsekwencji każdy dokument złożony da w wyniku n dokumentów złożonych, gdzie n to liczba unikalnych odcisków palców zawartych w dokumencie złożonym.
{"$unwind" :"$fingerprints"}
Aby obliczyć liczbę typowych odcisków palców, zaczniemy od filtrowania wszystkich dokumentów, które nie mają odcisków palców, które znajdują się na liście odcisków palców połączenia docelowego. Aby to zrobić, ponownie używamy operatora dopasowania $, tym razem filtrując właściwość odcisku palca, gdzie obsługiwane są tylko dokumenty zawierające odcisk palca znajdujący się na docelowej liście odcisków palców.
{"$match" :
{ "fingerprints" :
{"$in" : [ 1960 , 15111 , 5186 , 5371 , 756 , 1015 , 1018 , 338 , 325 , 776 , 3900 , ..., 2473] }
}
}
Ponieważ dopasowujemy tylko odciski palców, które znajdują się na liście docelowych odcisków palców, dane wyjściowe można wykorzystać do obliczenia całkowitej liczby powszechnych odcisków palców.
W tym celu stosujemy operator grupy $ do połączenia złożonego, chociaż tworzymy nowy typ dokumentu zawierający liczbę pasujących odcisków palców (poprzez zsumowanie liczby wystąpień), całkowitą liczbę odcisków palców połączenia wejściowego i emotikony.
{"$group" :
{ "_id" : "$compound_cid". ,
"fingerprintmatches" : {"$sum" : 1} ,
"totalcount" : { "$first" : "$fingerprint_count"} ,
"smiles" : {"$first" : "$smiles"}
}
}
Teraz mamy wszystkie parametry do obliczenia współczynnika Tanimoto. W tym celu użyjemy operatora projektu $, który oprócz kopiowania właściwości złożonych id i smiles dodaje również nowo obliczoną właściwość o nazwie Tanimoto.
{
"$project"
:
{
"_id"
:
1
,
"tanimoto"
:
{
"$divide"
:
[
"$fingerprintmatches."
,
{
"$subtract"
:
[
{
"$add"
:
[
40
,
"$totalcount"
]
}
,
"$fingerprintmatches."
]
}
]
}
,
"smiles"
:
1
}
}
Ponieważ interesują nas tylko połączenia, które mają docelowy współczynnik Tanimoto równy 0,8, używamy opcjonalnego operatora dopasowania $, aby odfiltrować wszystkie te, które nie osiągają tego współczynnika.
{"$match" :
{ "tanimoto" : { "$gte" : 0.8}
}
Polecenie całego potoku można znaleźć poniżej.
{"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} }
} ]
}
Dane wyjściowe tego potoku zawierają listę połączeń, które mają wartość Tanimoto 0,8 lub wyższą w odniesieniu do konkretnego połączenia docelowego. Wizualne przedstawienie tego przenośnika można znaleźć poniżej:
3. Wniosek
Nowa struktura agregacji MongoDB zapewnia zestaw łatwych w użyciu operatorów, które umożliwiają użytkownikom krótsze wyrażanie algorytmów typu redukcji kart. Koncepcja przenośnika pod nim oferuje intuicyjny sposób przetwarzania danych.
Nic dziwnego, że ten paradygmat potoku jest przyjmowany przez różne podejścia NoSQL, w tym Gremlin Framework Tinkerpop w implementacji i Cypher Neo4j w implementacji.
Pod względem wydajności rozwiązanie orurowania stanowi znaczne ulepszenie we wdrażaniu map redukcyjnych.
Operatory są początkowo obsługiwane przez platformę MongoDB, co prowadzi do znacznej poprawy wydajności w stosunku do interpretowanego JavaScript. Ponieważ Aggregation Framework może również działać w odizolowanym środowisku, łatwo przewyższa wydajność mojej oryginalnej implementacji, zwłaszcza gdy liczba połączeń wejściowych jest wysoka, a cel Tanimoto niski. Doskonała wydajność polecenia MongoDB!