Hadi Varposhti
Hadi Varposhti
خواندن ۱۲ دقیقه·۲ سال پیش

شبیه سازی Join کوئری در الستیک سرچ

با تمام مزایایی که دیتابیس های nosql دارن در مواردی پیش میاد که شما نیاز پیدا میکنین مفهوم دیگه ای رو که در دیتابیس RDBMS به راحتی انجام میشه در دیتابیس nosql خودتون شبیه سازی کنین.یکی از همین موارد Join کوئری بین دوتا از table هاست.

به خودی خود Join کوئری بین دو یا چندتا از table ها هزینه بردار هست فرقی هم نمیکنه که شما در چه مدل دیتابیسی دارین اینکار رو میکنین اما چون نیاز وجود داره باید دید که در صورتی که این هزینه می صرفه اینکارو انجام داد در غیر اینصورت باید دنبال راه حل های دیگه بود.

قبل از توضیح بیشتر اول به این مثال دقت کنین: تصور کنین

در یک فروشگاه اینترنتی ۶ table داریم که بصورت بالا باهم ارتباط دارن و درصورتی که لازم بشه شما باید بین حداقل شش table کوئری Join بزنین که باعث میشه "زمان زیادی" برای محاسبه مصرف بشه. این مورد مزیت رقابتی کسب و کار شما رو محیط اینترنت با مشکل مواجه میکنه.

اما در هر حال همیشه شرایطی وجود داره که نمیشه نادیده گرفت و مجبورین این هزینه رو قبول کنین. الستیک سرچ بصورت پیش فرض یک پایگاه داده nosql به حساب میاد و در این شرایط به شما دو راه برای زدن کوئری Join به شما ارائه میده استفاده از تکنیک "parent-child" و یا "nested query".

قبل از اینکه ادامه بدم باید بگم که اصلی ترین مزیت الستیک نسبت به بقیه پایگاه های داده nosql مزیت سرعت که Join کوئری به روش سنتی باعث کندی شدید در الستیک سرچ میشه برای اینکار باید حتما دو یا چند table که میخواین بین اونها Join کوئری بزنین حتما در یک Shard باشن.

در این مطلب سعی میکنم تا Join کوئری بصورت مختصر و بهمراه مثال توضیح بدم، پس در ادامه با من همراه باشین.

تکنیک Parent - Child در الستیک سرچ:

با یک مثال شروع میکنم، تصویر زیر یک درخت خانوادگی رو از سریال گات داریم که شامل سه خانواده ، سه پدر و مادر و نه فرزند و هر عضو دارای دوتا فیلد "gender" و "isAlive "هستش.

درخت خانواده با ارتباط فرزند و والد
درخت خانواده با ارتباط فرزند و والد

با استفاده از این مثال میخوایم که سه تا سناریو رو دنبال کنیم ؛

  • ارتباط والد ، فرزند
  • ارتباط والد و چند فرزندی
  • ارتباط والد و فرزندی چند لایه

ساخت ایندکس "Family-Tree"

در قدم اول با استفاده از دستور زیر mapping لازم رو برای ایجاد ایندکس ایجاد میکنم، با این کار دو table مدنظر parent و child در زمان ذخیره در الستیک سرچ بصورت وابسته با هم در یک node و یک shard ذخیره میشند.

curl -X PUT 'http://localhost:9200/family_tree' -H 'content-type: application/json' \ -d '{ &quotsettings&quot: { &quotindex&quot: { &quotnumber_of_shards&quot: 2, &quotnumber_of_replicas&quot: 2 } }, &quotmappings&quot: { &quotproperties&quot: { &quotfirstName&quot: { &quottype&quot: &quottext&quot }, &quotlastName&quot: { &quottype&quot: &quottext&quot }, &quotgender&quot: { &quottype&quot: &quottext&quot }, &quotisAlive&quot: { &quottype&quot: &quotboolean&quot }, &quotrelation_type&quot: { &quottype&quot: &quotjoin&quot, &quoteager_global_ordinals&quot: true, &quotrelations&quot: { &quotparent&quot: &quotchild&quot } } } } }'
  • فیلد relation-type اسم نوع کوئری ماست که اون رو برای مشخص شدن توسط الستیک اضافه میکنیم.
  • عبارت " type: join " یک عبارت کلیدی برای مشخص کردن نوع کوئری توسط الستیک سرچ.
  • عبارت " parent: child " یک عبارت کلیدی برای انجین الستیک به حساب میاد که متوجه بشه بین این دو داکیومنت سرعت ذخیره سازی مهمتر از سرعت در جستجو.
  • فیلد relation ارتباط بین داکیومنت هاست، هر داکیومنت باید دارای اسامی parent و یا child باشه تا از این mapping پیروی بکنه.

ذخیره سازی داده های تیبل Parent

curl -X PUT 'http://localhost:9200/family_tree/_doc/1?routing=Darren' \ -H 'content-type: application/json' \ -d '{ &quotfirstName&quot:&quotDarren&quot, &quotlastName&quot:&quotFord&quot, &quotgender&quot:&quotMale&quot, &quotisAlive&quot:false, &quotrelation_type&quot:{ &quotname&quot:&quotparent&quot } }'

دستور بالا داکیومنتی در الستیک سرچ میسازه که اصطلاحا parent به حساب میاد. پارامتر مهمی که باید اون رو در نظر داشته باشین پارامتر routing . هر والد اسم خودش رو به این پارامتر اختصاص میده. اینکار باعث میشه تا ما بتونیم کنترل کنیم که داکیومنت مد نظرمون توی کدوم ایندکس قرار ذخیره بشه .

ذخیره سازی داده های تیبل Child

curl -X PUT 'http://localhost:9200/family_tree/_doc/5?routing=Darren' \ -H 'content-type: application/json' \ -d '{ &quotfirstName&quot:&quotPearl&quot, &quotlastName&quot:&quotFord&quot, &quotgender&quot:&quotFemale&quot, &quotisAlive&quot:true, &quotrelation_type&quot:{ &quotname&quot:&quotchild&quot, &quotparent&quot:&quot1&quot } }'

همونطور که میبینین دستور بالا برای ایجاد داکیومنت 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 '{ &quotquery&quot:{ &quotparent_id&quot:{ &quottype&quot:&quotchild&quot, &quotid&quot:&quot2&quot } } }'

اقدام میکنیم، که نتیجه زیر رو برای ما مشخص میکنه.

{ &quottook&quot : 2, ... &quothits&quot : [ { &quot_index&quot : &quotfamily_tree&quot, &quot_type&quot : &quot_doc&quot, &quot_id&quot : &quot9&quot, &quot_routing&quot : &quotSienna&quot, &quot_source&quot : { &quotname&quot : &quotRalph&quot, &quothouse&quot : &quotEvans&quot, &quotgender&quot : &quotMale&quot, &quotisAlive&quot : true, &quotrelation_type&quot : { &quotname&quot : &quotchild&quot, &quotparent&quot : &quot2&quot } } } ] ... }

ارتباط والد و چند فرزندی

توی این مدل در ابتدا به Darren Ford میخوایم Melissa Ford رو به عنوان همسر اضافه میکنیم که درخت خانواده ما به این شکل تغییر میکنه.

که اینکار باعث میشه تا ایندکس ما به اینصورت تغییر بکنه.

curl -X PUT 'http://localhost:9200/family_tree/_mapping' -H 'content-type: application/json' \ -d '{ &quotproperties&quot:{ &quotrelation_type&quot:{ &quottype&quot:&quotjoin&quot, &quoteager_global_ordinals&quot:true, &quotrelations&quot:{ &quotparent&quot:[ &quotchild&quot, &quotwife&quot ] } } } }'
  • لازمه که بگم الان یک ارایه از والد ها داریم که بین همسر و فرزند مرتبطه.

در این مرحله اضافه شدن همسر شرایط مثل زمانی که میخواستم تیبل child رو اضافه کنم، به همون صورت و به همون شکل و فقط از کلمه wife به عنوان relation-type استفاده میکنم.

کوئری زدن بر روی داده ها از طریق فیلد Wife

در این حالت اگر بخوام والدی رو پیدا کنم که همسر داره کوئری با استفاده از عبارت has_child و استفاده از type: wife زده میشه.

curl -X GET 'http://localhost:9200/family_tree/_search?pretty=true' \ -H 'content-type: application/json' \ -d '{ &quotquery&quot:{ &quothas_child&quot:{ &quottype&quot:&quotwife&quot, &quotquery&quot:{ &quotmatch_all&quot: {} } } } }'

با اجرای کوئری بالا مقدار Darren Ford بر میگرده.

ارتباط والد و فرزندی چند لایه (نوه ها)

توی این مرحله میخوام که درخت خانواده رو به شکل زیر گسترش بدم.

در حالت نیاز دارم که ایندکس رو دوباره از اول بسازم، این به دلیل پیش میاد که ما برای هم فرزند قبلا یک والد داشته باشم و از اونجایی که در زمان ساخت ایندکس داکیومنت فرزند خودش والد نبوده پس مشکل از دست دادن داده دارم، برای همین باید ایندکس قبلی رو حذف کنم و ایندکس جدید رو با ساختاری متفاوت بصورت زیر بسازم.

curl -X PUT 'http://localhost:9200/family_tree' \ -H 'content-type: application/json' \ -d '{ &quotsettings&quot:{ ... }, &quotmappings&quot:{ &quotproperties&quot:{ ... &quotrelation_type&quot:{ &quottype&quot:&quotjoin&quot, &quoteager_global_ordinals&quot:true, &quotrelations&quot:{ &quotparent&quot:[ &quotchild&quot, &quotwife&quot ], &quotchild&quot:&quotgrandchild&quot } } } } }'

عبارت 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 '{ &quotfirstName&quot:&quotDouglas&quot, &quotlastName&quot:&quotFord&quot, &quotgender&quot:&quotMale&quot, &quotisAlive&quot:true, &quotrelation_type&quot:{ &quotname&quot:&quotgrandchild&quot, &quotparent&quot:&quot5&quot } }'

در مثال بالا Douglas Ford بعنوان فرزند Pearl Ford و نوه Darren Ford، توجه داشته باشین که از همون پارامتر روتینگ Darren استفاده میکنم که برای ساختن داکیومنت والد استفاده کردیم.اینکار باعث میشه تا مطمئن بشم که ارتباط بین والد، فرزند و نوه همیشه برقرار هستش.

کوئری زدن بر روی دادهای چند سطحی‌ (نوه)

اگه بخوام لیست همه والد هایی رو داشته باشم که نوه دختری دارن با استفاده از دستور زیر

curl -X POST 'http://localhost:9200/family_tree/_search' \ -H 'content-type: application/json' \ -d '{ &quotquery&quot:{ &quothas_child&quot:{ &quottype&quot:&quotchild&quot, &quotquery&quot:{ &quothas_child&quot:{ &quottype&quot:&quotgrandchild&quot, &quotquery&quot:{ &quotmatch&quot:{ &quotgender&quot:&quotFemale&quot } } } } } } }'

بعد از اجرای کوئری بالا Ryan Turner به عنوان تنها والدی که نوه دختری داره جواب ما میشه.

لازم میدونم که باز تاکید کنم اینکه الستیک سرچ اصلا پایگاه داده منسابی برای Join کوئری نیست و درصورتی که هزینه های این کوئری براتون قابل پذیرش باشه این کار رو انجام بدین.

نتیجه گیری :

کوئری Join و تکنیک Parent-Child برای مدیریت ایندکس ها زمانی که پرفورمنس از زمان جستجو مهمتر باشه استفاده میشه، اما هزینه های خودش رو هم داره. برای مثال باید آگاه باشین که این مدل ذخیره سازی محدودیت های فیزیکی و پیچیدگی های خاص خودش رو داره. مورد دیگه اینکه کوئری های چند لایه این پیچیدگی ها رو بیشتر هم میکنه.

درنهایت اینکه باید قبل از هرکاری نیازسنجی دقیقی داشته باشین نسبت نیازتون و سعی کنین تا طوری سیستم رو طراحی کنین تا کمترین نیاز رو برای رفتن به این سمت داشته باشین.


پی نوشت: مثل همیشه خوشحال میشم که نوشته من رو بخونین و در صورتی که نقص ها، کمبودها و درصورتی که خطایی رو میبینن برام بنویسین تا اصلاحشون کنم.




الستیک سرچjoin کوئریکدنویسیپایگاه دادهnosql
سعی میکنم چیزی رو بنویسم که نیاز آدما باشه
شاید از این پست‌ها خوشتان بیاید