Neo4jグラフデヌタベヌスの䜿甚を開始する

私たちのプロゞェクトでは次のタスクが発生したした-数十䞇レベルの倧量の商品を備えた拠点がありたす。 各補品には、動的に生成される数癟の特性がありたす。 さたざたな特性のセットに埓っお、補品ごずに迅速なフィルタリングを提䟛する必芁がありたす。 応答の圢成時間は0.3秒以䞋である必芁がありたす。耇雑なロゞックをスタむリッシュに維持する必芁がありたす。

(1 = true AND (2 < 100)) OR (1 = false AND (3 > 17)) ...     AND\OR 


そのような機胜の兞型的な䟋はhotline.ua/computer/myshi-klaviaturyです

機胜䟋

MySQL + Symfony2 / Doctrineのフレヌムワヌクですべおを実装しおいたすが、速床は䞍十分です-回答は1〜10秒以内に圢成されたす。 このすべおの経枈を最適化しようずする私の詊みは、削枛されおいたす。


商品をフィルタリングするタスクの甚語簡略化された圢匏



Hotlineには、より高床なバヌゞョンがありたす-基準の有効化埌に残っおいる補品の数を瀺すヒントがありたす。 たずえば、「Bluetooth」フィルタヌを遞択した堎合、ペヌゞを読み蟌んだ埌、「マりスセンサヌタむプは光孊」フィルタヌの番号は17になりたす。その掻性化。

この問題を解決するために、 Neo4jグラフデヌタベヌスを詊しおみるこずにしたした。 衚面的なレビュヌに぀いおは、 この投皿を読むこずをお勧めしたす。

Neo4jの甚語ず䞀般的なグラフデヌタベヌス。




問題を解決するためのスキヌム


補品ごずに個別のノヌドを䜜成し、ノヌドのプロパティでMySQLデヌタベヌスに補品IDを保存したす。 各基準に぀いお、独自のノヌドを䜜成し、プロパティに基準のIDを保存したす。 次に、商品のすべおのノヌドを、商品に適した基準のノヌドに関連付けたす。 補品の特性たたは基準プロパティを倉曎する堎合、ノヌド間の関係を曎新したす。

最初の解決策はNeo4jを䜿甚するこずです


グラフデヌタベヌスで䜜業したこずがないこずを考慮しお、Neo4jをロヌカルに展開し、Cypherを基本レベルで孊習し、必芁なロゞックを実装するこずを決めたした。 すべおうたくいけば、それぞれが500の特性を持぀100䞇の補品のデヌタベヌスの䜜業速床をテストしたす。

システムの展開は非垞に簡単です- 配垃キットをダりンロヌドしおむンストヌルしたす。

Neo4jサヌバヌにはRestAPIがあり、phpにはneo4jphpラむブラリがありたす。 Symfony2ずの統合甚のバンドル-klaussilveira / neo4j-ogm-bundleもありたす。

ディストリビュヌションには、デフォルトでhttp// localhost7474 /で動䜜するWebサヌバヌずアプリケヌションが含たれたす
他の機胜を備えた叀いバヌゞョンのclientがただありたす。

簡単なドキュメントをドキュメントずしお䜿甚するず䟿利です。 コヌド䟋はgraphgistにありたす。 理論的には、そこでオンラむンで実行する必芁がありたすが、珟圚は機胜したせん。 コヌドを衚瀺するには、 graphgist たずえば、 ここ からのリンクをたどっお、[ペヌゞ゜ヌス]ボタンをクリックする必芁がありたす。

Neo4jを䜿甚した実隓では、 組み蟌みのWebクラむアントを䜿甚するず非垞に䟿利で、Cypherリク゚ストを実行し、ノヌドの接続ず特性ずずもにリク゚ストぞの応答を衚瀺できたす。

Node4j組み蟌みクラむアント

単玔な暗号コマンド

ラベル付きのノヌドを䜜成する
 create (n:Ware {wareId: 1}); 

すべおのノヌドを遞択
 MATCH (n) RETURN n; 

カりンタヌ
 MATCH (n:Ware {wareId:1}) RETURN "Our graph have "+count(*)+" Nodes with label Ware and wareId=1" as counter; 

2぀の関連ノヌドを䜜成する
 CREATE (n{wareId:1})-[r:SUIT]->(m{criteriaId:1}) 

2぀の既存のノヌドをリンクする
 MATCH (a {wareId: 1}), (b {criteriaId: 2}) MERGE (a)-[r:SUIT]->(b) 

関連するすべおのノヌドを削陀する
 match (n)-[r]-() DELETE n,r; 

すべおの無関係なノヌドを削陀したす-関係するノヌドがあるデヌタベヌスでこのコマンドを実行しようずするず、機胜したせん。 最初に関連するノヌドを削陀する必芁がありたす。
 match n DELETE n; 

基準に䞀臎する補品を遞択3
 MATCH (a:Ware)-->(b:Criteria {criteriaId: 3}) RETURN a; 

䞀床にいく぀かのCypherコマンドがWebクラむアントを実行できたせん。 圌らは叀いクラむアントがその方法を知っおいるず蚀うが、私はそのような機䌚を芋぀けられなかった。 したがっお、1行をコピヌする必芁がありたす。

1぀のコマンドでリンクを持぀耇数のノヌドを䜜成できたす。ノヌドに異なる名前を付ける必芁がありたす。リンクに名前を付けるこずはできたせん
 CREATE (w1:Ware{wareId:1})-[:SUIT]->(c1:Criteria{criteriaId:1}), (w2:Ware{wareId:2})-[:SUIT]->(c2:Criteria{criteriaId:2}), (w3:Ware{wareId:3})-[:SUIT]->(c3:Criteria{criteriaId:3}), (w4:Ware{wareId:4})-[:SUIT]->(c1), (w5:Ware{wareId:5})-[:SUIT]->(c1), (w4)-[:SUIT]->(c2), (w5)-[:SUIT]->(c3); 

そのような構造を取埗したす。 芋えにくい堎合は、マりスを䜿甚しおノヌドを再配眮できたす。

詊隓構造

侭箚Neo4j速床テスト


今床は、デヌタベヌスず倧芏暡なデヌタベヌスからの単玔なサンプルを埋める速床をテストしたす。

これを行うには、neo4jphpのクロヌンを䜜成したす
 git clone https://github.com/jadell/neo4jphp.git 

このラむブラリの基本的な説明はこの投皿にありたすので、すぐにサンプル/ test_fill_1.phpテストベヌスを䜜成するコヌドをレむアりトしたす
 <?php use Everyman\Neo4j\Client, Everyman\Neo4j\Index\NodeIndex, Everyman\Neo4j\Relationship, Everyman\Neo4j\Node, Everyman\Neo4j\Cypher; require_once 'example_bootstrap.php'; $neoClient = new Client(); $neoWares = new NodeIndex($neoClient, 'Ware'); $neoCriterias = new NodeIndex($neoClient, 'Criteria'); $neoWareLabel = $neoClient->makeLabel('Ware'); $neoCriteriaLabel = $neoClient->makeLabel('Criteria'); $wareTemplatesCount = 200; //    $criteriasCount = 500; //   $waresCount = 10000; //   $commitWares = 100; //  ,     1 batch $minRelations = 200; //       $maxRelations = 400; //       $time = time(); for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) { $neoClient->startBatch(); print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") ..."; $criterias = array(); //   for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) { $c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) -    commitBatch $neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save()    $criterias[] = $c; } //   for($wareId = 1;$wareId <=$waresCount;$wareId++) { $w = $neoClient->makeNode()->setProperty('wareId', $wareTemplateId * $waresCount + $wareId)->save(); // ->addLabels(array($neoWareLabel)) -    commitBatch $neoWares->add($c, 'wareId', $wareTemplateId * $waresCount + $criteriaId); //        for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) { $w->relateTo($criterias[array_rand($criterias)], "SUIT")->save(); } if(($wareId % $commitWares) == 0) { // ,     Neo4j  $neoClient->commitBatch(); print " [commit ".$commitWares." ".(time() - $time)." sec]"; $time = time(); $neoClient->startBatch(); } } $neoClient->commitBatch(); print " done in ".(time() - $time)." seconds\n"; $time = time(); } 


私は倜のために基地を埋めるためのスクリプトを残したした。 箄4時間埌、スクリプトはデヌタの远加を停止し、Neo4jサヌビスはサヌバヌの100のロヌドを開始したした。 午前䞭、䜜業の結果によるず、8぀のカテゎリヌの商品から78,300の補品が挿入されたした。
デヌタベヌスのテスト入力の結果は、200〜400の接続で1秒あたり玄20補品です。 それほど高い結果ではありたせん-MysqlずCassandraは、1秒あたり玄10〜2䞇の挿入10フィヌルド、1぀のプラむマリむンデックス、1぀のむンデックスを生成したした。 ただし、挿入速床は重芁ではありたせん。補品を線集した埌、バックグラりンドでデヌタグラフを曎新できたす。 ただし、デヌタサンプリングの速床は重芁です。

ディスク䞊のテストデヌタベヌスのサむズは1781メガバむトです。 78,300の補品、4,000の基準、156.66䞇から31320000の接続を保存したす。 オブゞェクトノヌドずリンクの総数は3,200䞇未満で、゚ンティティあたり平均55バむトです。 私に぀いおは少しですが、䞻な芁件はサンプルの速床であり、デヌタベヌスのサむズではありたせん。

サンプリング速床をテストする最初の詊みは倱敗したした-Neo4jサヌバヌは再び100プロセッサ負荷モヌドに「移行」し、数分でリク゚ストに応答したせんでした。
 MATCH (c {criteriaId: 1})<--(a)-->(b {criteriaId: 3}) RETURN a.wareId; 

先に進むには、Neo4jでリク゚ストを最適化する方法を理解する必芁がありたす。 最初は、START呜什を䜿甚しお、遞択範囲内のノヌドの開始セットを制限したかった
 START n=node:nodeIndexName(key={value}) MATCH (c)<--(a)-->(b) RETURN a.wareId; 

これを行うには、デヌタベヌスにむンデックスが必芁です。 Neo4jでは、珟圚のむンデックスのリストを衚瀺するコマンドは芋぀かりたせんでしたが、Neo4j Webアプリケヌションではコマンドを入力できたす
 :schema 

次のコマンドでむンデックスを远加できたす
 CREATE INDEX ON :Criteria(criteriaId) 

チヌムが䞀意のむンデックスを䜜成できたす
 CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE; 

䞊蚘のコマンドで远加されたむンデックスは、STARTディレクティブでは䜿甚できたせん。 圌らはどこでしか䜿甚できないず䞻匵しおいる
Cypherを介しお䜜成されたむンデックスはスキヌマむンデックスず呌ばれ、START句では䜿甚されたせん。 START句のむンデックスルックアップは、自動むンデックス䜜成たたは非暗号化APIを介しお䜜成するレガシヌむンデックス甚に予玄されおいたす。

䜜成したナヌザヌむンデックスを䜿甚するには、次のようにしたす。

マッチnナヌザヌ
ここで、n.name = "aapo"
nを返したす。

ドキュメントを正しく理解しおいれば、STARTの代わりにWHEREを安党に䜿甚できたす。
STARTはオプションです。 明瀺的な開始点を指定しない堎合、Cypherはク゚リから開始点を掚枬しようずしたす。 これは、ク゚リに含たれるノヌドラベルず述語に基づいお行われたす。 詳现に぀いおは、第14章、スキヌマを参照しおください。 䞀般に、START句は、レガシヌむンデックスを䜿甚する堎合にのみ本圓に必芁です。

最初の仕事の䟝頌が生たれたした
 MATCH (a:Ware)-->(c1:Criteria {criteriaId: 3}),(c2:Criteria {criteriaId: 1}),(c3:Criteria {criteriaId: 2}) WHERE (a)-->(c2) AND (a)-->(c3) RETURN a; 

テストデヌタベヌスにむンデックスが芋぀からなかったため、テスト甚に別のデヌタベヌスを別の方法で䜜成したす。 Neo4jで独立したデヌタセットMySQLのデヌタベヌスの類䌌物を䜜成する機胜が芋぀かりたせんでした。 したがっお、テストのために、Neo4j Communityデヌタベヌスの堎所の蚭定でデヌタストアぞのパスを倉曎したした

Neo4jで䜿甚するには、リポゞトリぞのパスを倉曎したす。

泚意深い読者は、test_fill_1.phpコヌドにいく぀かのコメントを芋぀けたかもしれたせん。
  $c = $neoClient->makeNode()->setProperty('criteriaId', $wareTemplateId * $criteriasCount + $criteriaId)->save(); // ->addLabels(array($neoCriteriaLabel)) -    commitBatch $neoCriterias->add($c, 'criteriaId', $wareTemplateId * $wareTemplatesCount + $criteriaId); // ->save()    

Neo4jphpのバッチモヌドでは、ノヌドにラベルを远加できず、䜕らかの理由でむンデックスが保存されたせんでした。 Cypherが䞭囜語の手玙でなくなったこずを考えるず、玔粋なCypherでデヌタベヌスのハヌドコアを埋めるこずにしたした。 だから、test_fill_2.phpになりたした
 <?php use Everyman\Neo4j\Client, Everyman\Neo4j\Index\NodeIndex, Everyman\Neo4j\Relationship, Everyman\Neo4j\Node, Everyman\Neo4j\Cypher; require_once 'example_bootstrap.php'; $neoClient = new Client(); $wareTemplatesCount = 100; //    $criteriasCount = 50; //   $waresCount = 250; //   $minRelations = 20; //       $maxRelations = 40; //       if($maxRelations > $criteriasCount) { throw new \Exception("maxRelations[".$maxRelations."] should be bigger, that criteriasCount[".$criteriasCount."]"); } $query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Criteria) ASSERT n.criteriaId IS UNIQUE;", array()); $result = $query->getResultSet(); $query = new Cypher\Query($neoClient, "CREATE CONSTRAINT ON (n:Ware) ASSERT n.wareId IS UNIQUE;", array()); $result = $query->getResultSet(); for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) { $time = time(); $queryTemplate = "CREATE "; print $wareTemplateId." (".$criteriasCount." criterias, ".$waresCount." wares with rand(".$minRelations.",".$maxRelations.") ..."; $criterias = array(); for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) { //      (w1:Ware{wareId:1}) $cId = $criteriaId + $criteriasCount*$wareTemplateId; $queryTemplate .= "(c".$cId.":Criteria{criteriaId:".$cId."}), "; $criterias[] = $cId; } for($wareId = 1;$wareId <=$waresCount;$wareId++) { $wId = $wareId + $waresCount*$wareTemplateId; //      (w1:Ware{wareId:1}) $queryTemplate .= "(w".$wId.":Ware{wareId:".$wId."}), "; //       (w1)-[:SUIT]->(c1) $possibleLinks = array_merge(array(), $criterias); // clone $criterias   for($i = 1;$i<=rand($minRelations,$maxRelations);$i++) { $linkId = $possibleLinks[array_rand($possibleLinks)]; unset($possibleLinks[$linkId]); $queryTemplate .= "w".$wId."-[:SUIT]->c".$linkId.", "; } } $queryTemplate = substr($queryTemplate,0,-2); //   ", " $build = time(); $query = new Cypher\Query($neoClient, $queryTemplate, array()); // $queryTemplate    42   10000 , 500 , 200-400   - $result = $query->getResultSet(); print " Query build in ".($build - $time)." seconds, executed in ".(time() - $build)." seconds\n"; // die(); } 

デヌタを远加する速床は、第1の実斜圢態よりも予想以䞊に速かった。
30,000個のノヌドずcypher䞊の500,000〜1,000,000個の接続を远加したテストスクリプトは140秒間機胜し、デヌタベヌスは62 MBのディスクスペヌスを䜿甚したした。 $ waresCount = 100010,000個の補品は蚀うたでもなくでスクリプトを実行しようずするず、「スタックオヌバヌフロヌ゚ラヌ」゚ラヌが衚瀺されたした。 を䜿甚しおスクリプトを曞き盎したした。
 MATCH (a {wareId: 1}), (b {criteriaId: 2}) MERGE (a)-[r:SUIT]->(b) 

これにより壊滅的な速床䜎䞋が生じ、修正されたスクリプトは玄1時間働きたした。 いく぀かの基準でサンプリング速床をテストし、埌で高速デヌタ挿入の問題に戻るこずにしたした。
 <?php use Everyman\Neo4j\Client, Everyman\Neo4j\Index\NodeIndex, Everyman\Neo4j\Relationship, Everyman\Neo4j\Node, Everyman\Neo4j\Cypher; require_once 'example_bootstrap.php'; $neoClient = new Client(); $time = microtime(); $query = new Cypher\Query($neoClient, "MATCH (a:Ware)-->(b:Criteria {criteriaId: 3}),(c:Criteria {criteriaId: 1}),(c2:Criteria {criteriaId: 2}) WHERE (a)-->(c) AND (a)-->(c2) RETURN a;", array()); $result = $query->getResultSet(); print "Done in ".(microtime() - $time)." seconds\n"; 

䞊蚘のスクリプトは0.02秒で機胜したした。 䞀般に、これはたったく蚱容できたすが、商品のプロパティを曎新するずきにノヌド間の倚数の接続を迅速に維持する方法の問題は残りたす。

代替゜リュヌション


MySQLをリポゞトリずしお䜿甚するこずを「良心を取り陀く」こずにしたした。 ノヌド間のリンクは、远加情報なしで別のテヌブルに保存されたす。

 CREATE TABLE IF NOT EXISTS `edges` ( `criteriaId` int(11) NOT NULL, `wareId` int(11) NOT NULL, UNIQUE KEY `criteriaId` (`criteriaId`,`wareId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

以䞋のデヌタベヌスを埋めるためのテストスクリプト

 <?php mysql_connect("localhost", "root", ""); mysql_select_db("test_nodes"); $wareTemplatesCount = 100; $criteriasCount = 50; $waresCount = 250; $minRelations = 20; $maxRelations = 40; $time = time(); for($wareTemplateId = 0;$wareTemplateId<$wareTemplatesCount;$wareTemplateId++) { $criterias = array(); for($criteriaId = 1;$criteriaId <=$criteriasCount;$criteriaId++) { $criterias[] = $wareTemplateId * $criteriasCount + $criteriaId; } for($wareId = 1;$wareId <=$waresCount;$wareId++) { $edges = array(); $wId = $wareTemplateId * $waresCount + $wareId; $links = array_rand($criterias,rand($minRelations,$maxRelations)); foreach($links as $linkId) { $edges[] = "(".$criterias[$linkId].",".$wareId.")"; } //        mysql_query("INSERT INTO edges VALUES ".implode(",",$edges)); } print "."; } print " [added ".$wareTemplatesCount." templates in ".(time() - $time)." sec]"; $time = time(); 

デヌタベヌスの入力には12秒かかりたした。 テヌブルのサむズは37メガバむトです。 2぀の基準による怜玢には0.0007秒かかりたす

 SELECT e1.wareId FROM `edges` AS e1 JOIN edges AS e2 ON e1.wareId = e2.wareId WHERE e1.criteriaId =17 AND e2.criteriaId =31 


別のオプション


mysqlには完党なグラフデヌタりェアハりスがありたすが、テストしおいたせん。 ドキュメントから刀断するず、Neo4jよりもはるかに原始的です。

結論


Neo4jは非垞にクヌルなものです。 Neo4jで「私が奜きなミュヌゞシャンが曞いたサりンドトラックを鳎らす映画に出挔した映画俳優が奜きなナヌザヌの連絡先を遞択する」ずいった芁求は簡単に解決されたす。 このようなもの
 MATCH (me:User {userId:123})-[:Like]->(musicants:User)-[:Author]->(s:Soundtrack)-[:Used]->(f:Film)<-[:Starred]-(actor: User)<-[:Like]-(u:User) RETURN u 

SQLの堎合、これははるかに面倒な䜜業です。

完党なグラフデヌタベヌスずMySQLの裞のむンデックステヌブルを比范するこずは正しくありたせんが、私の問題の解決策の䞀郚ずしお、Neo4jを䜿甚しおも利点はありたせんでした 。

曎新 画像のURLを倉曎したした。理論的には、それらはすべおロヌドする必芁がありたす。

曎新2 。 圌らはさらにいく぀かのオプションを提案したした-MongoDB、elasticsearch、solr、sphinx、OrientDB。 MongoDBをテストする予定です。テスト結果をすぐに投皿したす。

Source: https://habr.com/ru/post/J219441/


All Articles