با تمام مزایایی که دیتابیس های nosql دارن در مواردی پیش میاد که شما نیاز پیدا میکنین مفهوم دیگه ای رو که در دیتابیس RDBMS به راحتی انجام میشه در دیتابیس nosql خودتون شبیه سازی کنین.یکی از همین موارد Join کوئری بین دوتا از table هاست.
به خودی خود Join کوئری بین دو یا چندتا از table ها هزینه بردار هست فرقی هم نمیکنه که شما در چه مدل دیتابیسی دارین اینکار رو میکنین اما چون نیاز وجود داره باید دید که در صورتی که این هزینه می صرفه اینکارو انجام داد در غیر اینصورت باید دنبال راه حل های دیگه بود.
قبل از توضیح بیشتر اول به این مثال دقت کنین: تصور کنین
در یک فروشگاه اینترنتی ۶ table داریم که بصورت بالا باهم ارتباط دارن و درصورتی که لازم بشه شما باید بین حداقل شش table کوئری Join بزنین که باعث میشه "زمان زیادی" برای محاسبه مصرف بشه. این مورد مزیت رقابتی کسب و کار شما رو محیط اینترنت با مشکل مواجه میکنه.
اما در هر حال همیشه شرایطی وجود داره که نمیشه نادیده گرفت و مجبورین این هزینه رو قبول کنین. الستیک سرچ بصورت پیش فرض یک پایگاه داده nosql به حساب میاد و در این شرایط به شما دو راه برای زدن کوئری Join به شما ارائه میده استفاده از تکنیک "parent-child" و یا "nested query".
قبل از اینکه ادامه بدم باید بگم که اصلی ترین مزیت الستیک نسبت به بقیه پایگاه های داده nosql مزیت سرعت که Join کوئری به روش سنتی باعث کندی شدید در الستیک سرچ میشه برای اینکار باید حتما دو یا چند table که میخواین بین اونها Join کوئری بزنین حتما در یک Shard باشن.
در این مطلب سعی میکنم تا Join کوئری بصورت مختصر و بهمراه مثال توضیح بدم، پس در ادامه با من همراه باشین.
با یک مثال شروع میکنم، تصویر زیر یک درخت خانوادگی رو از سریال گات داریم که شامل سه خانواده ، سه پدر و مادر و نه فرزند و هر عضو دارای دوتا فیلد "gender" و "isAlive "هستش.
با استفاده از این مثال میخوایم که سه تا سناریو رو دنبال کنیم ؛
در قدم اول با استفاده از دستور زیر mapping لازم رو برای ایجاد ایندکس ایجاد میکنم، با این کار دو table مدنظر parent و child در زمان ذخیره در الستیک سرچ بصورت وابسته با هم در یک node و یک shard ذخیره میشند.
curl -X PUT 'http://localhost:9200/family_tree' -H 'content-type: application/json' \ -d '{ "settings": { "index": { "number_of_shards": 2, "number_of_replicas": 2 } }, "mappings": { "properties": { "firstName": { "type": "text" }, "lastName": { "type": "text" }, "gender": { "type": "text" }, "isAlive": { "type": "boolean" }, "relation_type": { "type": "join", "eager_global_ordinals": true, "relations": { "parent": "child" } } } } }'
curl -X PUT 'http://localhost:9200/family_tree/_doc/1?routing=Darren' \ -H 'content-type: application/json' \ -d '{ "firstName":"Darren", "lastName":"Ford", "gender":"Male", "isAlive":false, "relation_type":{ "name":"parent" } }'
دستور بالا داکیومنتی در الستیک سرچ میسازه که اصطلاحا parent به حساب میاد. پارامتر مهمی که باید اون رو در نظر داشته باشین پارامتر routing . هر والد اسم خودش رو به این پارامتر اختصاص میده. اینکار باعث میشه تا ما بتونیم کنترل کنیم که داکیومنت مد نظرمون توی کدوم ایندکس قرار ذخیره بشه .
curl -X PUT 'http://localhost:9200/family_tree/_doc/5?routing=Darren' \ -H 'content-type: application/json' \ -d '{ "firstName":"Pearl", "lastName":"Ford", "gender":"Female", "isAlive":true, "relation_type":{ "name":"child", "parent":"1" } }'
همونطور که میبینین دستور بالا برای ایجاد داکیومنت child هستش اما نکته ای که مهمه پارامتر routing. اگر این تکه کد رو با تکه کد قبلی مقایسه کنین متوجه میشین که مقدار پارامتر routing در هر دو داکیومنت یکسان این به این معنی که (همونطور که بالاتر هم اشاره کردم) هردو داکیومنت parent و child باید در یک shard قرار بگیرین، که به این طریق اونهارو در کنار هم قرار میدیم.
کوئری Join بین این داکیومنت و parent داکیومنت از طریق فیلد relation-type و فیلد parent: 1 که داخلش هست اتفاق می افته.
حالا قسمت جذاب قضیه شروع میشه، از حالا به بعد میتونین روی ایندکسی که ساختین کوئری دلخواهتون رو بزنین.
بزارین با یه مثال شروع کنیم؛ در درخت خانواده بالا sienna evans رو میخوایم که همه فرزندانش رو بدست بیاریم، برای اینکار از طریق دستور زیر
curl -X GET 'http://localhost:9200/family_tree/_search?pretty=true' \ -H 'content-type: application/json' \ -d '{ "query":{ "parent_id":{ "type":"child", "id":"2" } } }'
اقدام میکنیم، که نتیجه زیر رو برای ما مشخص میکنه.
{ "took" : 2, ... "hits" : [ { "_index" : "family_tree", "_type" : "_doc", "_id" : "9", "_routing" : "Sienna", "_source" : { "name" : "Ralph", "house" : "Evans", "gender" : "Male", "isAlive" : true, "relation_type" : { "name" : "child", "parent" : "2" } } } ] ... }
توی این مدل در ابتدا به Darren Ford میخوایم Melissa Ford رو به عنوان همسر اضافه میکنیم که درخت خانواده ما به این شکل تغییر میکنه.
که اینکار باعث میشه تا ایندکس ما به اینصورت تغییر بکنه.
curl -X PUT 'http://localhost:9200/family_tree/_mapping' -H 'content-type: application/json' \ -d '{ "properties":{ "relation_type":{ "type":"join", "eager_global_ordinals":true, "relations":{ "parent":[ "child", "wife" ] } } } }'
در این مرحله اضافه شدن همسر شرایط مثل زمانی که میخواستم تیبل child رو اضافه کنم، به همون صورت و به همون شکل و فقط از کلمه wife به عنوان relation-type استفاده میکنم.
در این حالت اگر بخوام والدی رو پیدا کنم که همسر داره کوئری با استفاده از عبارت has_child و استفاده از type: wife زده میشه.
curl -X GET 'http://localhost:9200/family_tree/_search?pretty=true' \ -H 'content-type: application/json' \ -d '{ "query":{ "has_child":{ "type":"wife", "query":{ "match_all": {} } } } }'
با اجرای کوئری بالا مقدار Darren Ford بر میگرده.
توی این مرحله میخوام که درخت خانواده رو به شکل زیر گسترش بدم.
در حالت نیاز دارم که ایندکس رو دوباره از اول بسازم، این به دلیل پیش میاد که ما برای هم فرزند قبلا یک والد داشته باشم و از اونجایی که در زمان ساخت ایندکس داکیومنت فرزند خودش والد نبوده پس مشکل از دست دادن داده دارم، برای همین باید ایندکس قبلی رو حذف کنم و ایندکس جدید رو با ساختاری متفاوت بصورت زیر بسازم.
curl -X PUT 'http://localhost:9200/family_tree' \ -H 'content-type: application/json' \ -d '{ "settings":{ ... }, "mappings":{ "properties":{ ... "relation_type":{ "type":"join", "eager_global_ordinals":true, "relations":{ "parent":[ "child", "wife" ], "child":"grandchild" } } } } }'
عبارت child در این ایندکس بعنوان parent استفاده میشه و در نتیجه صفات parent رو هم داراست، البته از نوع grandchild . این به ما کمک میکنه تا توالی ارتباط
parent > child > grandchild
رو حفظ کنم .
مثل قبل شرایط اضافه کردن فیلد grandchild مثل اضافه کردن child میمونه باهمون شکل و فرمت.
curl -X PUT 'http://localhost:9200/family_tree/_doc/14?routing=Darren' \ -H 'content-type: application/json' \ -d '{ "firstName":"Douglas", "lastName":"Ford", "gender":"Male", "isAlive":true, "relation_type":{ "name":"grandchild", "parent":"5" } }'
در مثال بالا Douglas Ford بعنوان فرزند Pearl Ford و نوه Darren Ford، توجه داشته باشین که از همون پارامتر روتینگ Darren استفاده میکنم که برای ساختن داکیومنت والد استفاده کردیم.اینکار باعث میشه تا مطمئن بشم که ارتباط بین والد، فرزند و نوه همیشه برقرار هستش.
اگه بخوام لیست همه والد هایی رو داشته باشم که نوه دختری دارن با استفاده از دستور زیر
curl -X POST 'http://localhost:9200/family_tree/_search' \ -H 'content-type: application/json' \ -d '{ "query":{ "has_child":{ "type":"child", "query":{ "has_child":{ "type":"grandchild", "query":{ "match":{ "gender":"Female" } } } } } } }'
بعد از اجرای کوئری بالا Ryan Turner به عنوان تنها والدی که نوه دختری داره جواب ما میشه.
لازم میدونم که باز تاکید کنم اینکه الستیک سرچ اصلا پایگاه داده منسابی برای Join کوئری نیست و درصورتی که هزینه های این کوئری براتون قابل پذیرش باشه این کار رو انجام بدین.
کوئری Join و تکنیک Parent-Child برای مدیریت ایندکس ها زمانی که پرفورمنس از زمان جستجو مهمتر باشه استفاده میشه، اما هزینه های خودش رو هم داره. برای مثال باید آگاه باشین که این مدل ذخیره سازی محدودیت های فیزیکی و پیچیدگی های خاص خودش رو داره. مورد دیگه اینکه کوئری های چند لایه این پیچیدگی ها رو بیشتر هم میکنه.
درنهایت اینکه باید قبل از هرکاری نیازسنجی دقیقی داشته باشین نسبت نیازتون و سعی کنین تا طوری سیستم رو طراحی کنین تا کمترین نیاز رو برای رفتن به این سمت داشته باشین.
پی نوشت: مثل همیشه خوشحال میشم که نوشته من رو بخونین و در صورتی که نقص ها، کمبودها و درصورتی که خطایی رو میبینن برام بنویسین تا اصلاحشون کنم.