یکی از ویژگی های منحصر به فرد MongoDB قابلیت جست و جوی مکان های مختلف است. منظور من واقعا مکان های مختلفی مانند «رستوران ها تا شعاع 500 متر» و غیره است. geospatial از ترکیب دو کلمه geo (پیشوند به معنی «جغرافیا») و spatial (فضایی) تشکیل شده است بنابراین داده های geospatial به این قسمت اشاره دارند و در این فصل می خواهیم به کار با این نوع داده ها بپردازیم. برای شروع این جلسه از یک پایگاه داده جدید به نام awesomeplaces استفاده می کنم:
use awesomeplaces
حالا باید از google maps تعدادی از مکان های مختلف را پیدا کنیم تا با آن ها در پایگاه داده خود کار کنیم. مثلا من با آکادمی علوم کالیفرنیا شروع می کنم:
حتما می دانید که در google maps زمانی که روی یک محل خاص کلیک کنید، مختصات آن در URL نمایش داده می شود. به طور مثال در URL بالا پس از علامت @ دو عدد داریم که با ویرگول از هم جدا شده اند:
@37.7692285,-122.468455,
اولین عدد پس از @ عرض جغرافیایی (latitude) و دومین عدد طول جغرافیایی (longitude) می باشد. بنابراین می توانیم از این مختصات استفاده کرده و محل را ذخیره کنیم.
هشدار: مختصات به دست آمده بر اساس مرکز صفحه نمایش شما است بنابراین حتی اگر محلی را انتخاب کنید اما آن محل در گوشه صفحه شما باشد، باز هم مختصات نقطه ای به شما داده می شود که در وسط صفحه نمایش شما باشد. البته این موضوع برای ما مشکلی ایجاد نمی کند و مهم یادگیری فنون کار است نه به دست آوردن اعداد دقیق!
من یک کالکشن به نام places درون پایگاه داده خودمان ایجاد کرده و یک سند جدید را به آن اضافه می کنم اما سوالی ایجاد می شود: برای ذخیره داده های geospatial باید چه کار کنیم؟ داده های geospatial از فرمت خاصی به نام GeoJSON استفاده می کنند که مختص به MongoDB نیست و یک استاندارد خاص و کلی می باشد. GeoJSON انواع مختلفی دارد و فقط یک نوع نیست اما مهم ترین بخش آن برای ما این است که هنگام اضافه کردن یک مکان به MongoDB باید به شکل زیر عمل کنید:
با این حساب می توان گفت:
db.places.insertOne({name: "California Academy of Sciences", location: {type: "Point", coordinates: [-122.468455, 37.7692285]}})
name کاملا بر اساس سلیقه ما است و می توانید هر نام دیگری برایش انتخاب کنید (location هم همینطور) اما type و coordinates دقیقا باید به همین نام باشند. type یا نوع داده GeoJSON خودم را روی point گذاشته ام که یک نقطه جغرافیایی را مشخص می کند. از آنجایی که ما طول و عرض یک نقطه خاص را داریم بنابراین باید type را روی point بگذاریم که یکی از تایپ های پشتیبانی شده توسط MongoDB است. برای مشاهده انواع دیگر type به صفحه زیر مراجعه کنید:
https://docs.mongodb.com/manual/reference/geojson/
برای coordinates نیز باید یک آرایه بنویسید که عضو اول آن طول جغرافیایی (longitude) و عضو دوم آن عرض جغرافیایی (latitude) می باشد. حالا می توانیم با یک دستور ساده findOne ساختار سند خود را مشاهده کنیم:
"_id" : ObjectId("5ec0bd1b7bc8c6a2b7e4ab85"), "name" : "California Academy of Sciences", "location" : { "type" : "Point", "coordinates" : [ -122.468455, 37.7692285 ] }
همانطور که می بینید این ساختار اصلا پیچیده نیست. حالا که داده GeoJSON خود را تعریف کرده ایم باید بدانیم که چطور به آن کوئری بزنیم. برای این کار ابتدا باید موقعیت خود را مشخص کنیم. در برنامه های واقعی این موقعیت توسط GPS کاربر یا یک فیلد input یا داده های مشابه انجام می شود اما ما در Shell هستیم بنابراین باید یک نقطه فرضی را در نظر بگیریم. من یک نقطه را در نظر می گیرم که نزدیک به آکادمی علوم کالیفرنیا باشد:
longitude: -122.471114 latitude: 37.771104
این مکان را فعلا در ذهن داشته باشید. حالا فرض کنید که می خواهیم این سوال را پاسخ بدهیم: آیا این مکان نزدیک به آکادمی علوم کالیفرنیا می باشد یا خیر؟ برای این کار می توانیم از اپراتور near$ (به معنی «نزدیک») استفاده کنیم:
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.471114, 37.771104]}}}})
پس از استفاده از اپراتور near$ باید از اپراتور geometry$ استفاده کنیم که یک GeoJSON را می گیرد. از آنجایی که داده پاس داده شده به geometry$ از نوع GeoJSON است بنابراین باید دقیقا طبق ساختاری که قبلا توضیح دادم عمل کرده و حتما type و coordinates را مشخص کنید. با اجرای این کوئری یک خطا می گیریم که در قسمتی از آن می گوید:
unable to find index for $geoNear query
به عبارتی به یک geospatial index نیاز داریم که نوع خاصی از ایندکس ها است. از آنجایی که این نوع ایندکس به شدت وابسته به داده های geospatial است، آن را در فصل ایندکس ها نیاوردم. برای ساخت geospatial index باید به شکل زیر عمل کرد:
db.places.createIndex({location: "2dsphere"})
location در کوئری بالا اشاره به فیلد location در کالکشن ما دارد بنابراین اگر نام آن را چیز دیگری گذاشته اید باید در این کوئری نیز همان نام را بگذارید. مقدار 2dsphere نیز یک مقدار ثابت است که برای geospatial index استفاده می شود و انتخابی نیست. حالا دوباره کوئری خود را اجرا می کنیم:
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.471114, 37.771104]}}}}).pretty()
نتیجه:
"_id" : ObjectId("5ec0bd1b7bc8c6a2b7e4ab85"), "name" : "California Academy of Sciences", "location" : { "type" : "Point", "coordinates" : [ -122.468455, 37.7692285 ] }
بنابراین کوئری ما صحیح ارزیابی شده است، یعنی مختصات جغرافیایی که به کوئری پاس داده ایم نزدیک به مختصات جغرافیایی این سند برگردانده شده بوده است. اگر اسناد دیگری داشتیم که موقعیت آن ها نیز به این نقطه نزدیک بود، آن ها نیز برگردانده می شدند اما هنوز ابهام بزرگی وجود دارد: نزدیک بودن چطور تعریف می شود؟ در اصل near$ بدون اعمال محدودیت از سمت ما هیچ معنی خاصی ندارد و باید حتما مقدار حداکثری برای این اپراتور تعریف کنید. برای انجام این کار اپراتور دیگری به نام maxDistance$ (به معنی «حداکثر فاصله») را به geometry$ پاس می دهیم:
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.471114, 37.771104]}, $maxDistance: 30, $minDistance: 10}}}).pretty()
همانطور که می بینید من maxDistance$ (حداکثر فاصله) و minDistance$ (حداقل فاصله) را به geometry$ پاس داده ام که هر دو در واحد متر هستند. با این کار تعریف کرده ایم که نقطه مورد نظر باید 10 متر از هر نقطه ای که در کالکشن داریم دور باشد اما از 30 متر نزدیک تر باشد. با اجرای کوئری بالا هیچ مقداری برایمان برگردانده نمی شود چرا که ما فقط یک نقطه را در کالکشن خود داریم (آکادمی علوم کالیفرنیا) و آن هم بیشتر از 30 متر با نقطه پاس داده شده ما فاصله دارد. ما می توانیم در google maps این فاصله را با ابزار های ارائه شده اندازه بگیریم (در نقطه مورد نظر کلیک راست کرده و measure distance را کلیک کنید). این فاصله حدود 400 متر است (بسته به اینکه کجای صفحه کلیک کنید) بنابراین می توانیم آن را تست کنیم. اگر maxDistance$ را روی 300 متر بگذاریم هنوز هم نباید نتیجه ای بگیریم:
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.471114, 37.771104]}, $maxDistance: 300, $minDistance: 10}}}).pretty()
حرفمان درست است! با اجرای کوئری بالا هیچ مقداری برگردانده نمی شود. حالا آن را روی 500 می گذاریم:
db.places.find({location: {$near: {$geometry: {type: "Point", coordinates: [-122.471114, 37.771104]}, $maxDistance: 500, $minDistance: 10}}}).pretty()
با این کار سند موجود در کالکشن ما برگردانده می شود بنابراین همه چیز به درستی کار می کند. ممکن است این اندازه گیری برای شما متفاوت باشد (مثلا در قسمت متفاوتی از صفحه کلیک کرده باشید) بنابراین نگران 30 متر یا 40 متر تفاوت با نتیجه من نباشید.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.