در این قسمت می خواهیم در رابطه با نکته ای باقی مانده و سپس stage بعدی در pipeline صحبت کنیم. اگر یادتان باشد در چند جلسه قبل یک کوئری پیچیده به شکل زیر را نوشته بودیم:
db.persons.aggregate([ { $project: { _id: 0, name: 1, email: 1, birthdate: { $toDate: '$dob.date' }, age: "$dob.age", location: { type: 'Point', coordinates: [ { $convert: { input: '$location.coordinates.longitude', to: 'double', onError: 0.0, onNull: 0.0 } }, { $convert: { input: '$location.coordinates.latitude', to: 'double', onError: 0.0, onNull: 0.0 } } ] } } }, { $project: { gender: 1, email: 1, location: 1, birthdate: 1, age: 1, fullName: { $concat: [ { $toUpper: { $substrCP: ['$name.first', 0, 1] } }, { $substrCP: [ '$name.first', 1, { $subtract: [{ $strLenCP: '$name.first' }, 1] } ] }, ' ', { $toUpper: { $substrCP: ['$name.last', 0, 1] } }, { $substrCP: [ '$name.last', 1, { $subtract: [{ $strLenCP: '$name.last' }, 1] } ] } ] } } }]).pretty();
این کوئری علاوه بر دریافت طول و عرض جغرافیایی و تبدیل کردن تاریخ به نوع داده ISODate، نام افراد را گرفته و حرف اول آن را با حروف بزرگ انگلیسی بازنویسی می کرد. ما یک جلسه کامل را به این کوئری اختصاص دادیم و در مورد آن صحبت کردیم بنابراین مطالب جلسات قبل را تکرار نمی کنم. هدف من از ذکر این کوئری این است که در این جلسه می خواهیم با داده های جغرافیایی کار کنیم ولی مشکلی وجود دارد. داده های اصلی خودمان قابل استفاده نیستند چرا که طول و عرض جغرافیایی در آن ها به صورت رشته ای ذخیره شده اند نه به صورت عددی! از طرفی نمی توانیم اپراتور این stage را به کوئری بالا اضافه کنیم. چرا؟ اگر یادتان باشد در فصل کار با داده های جغرافیایی توضیح دادم که اپراتور هایی مثل geoWithing و غیره حتما باید دارای ایندکس باشند. از طرف دیگر برای استفاده از این ایندکس حتما باید اپراتور خود را در ابتدای pipeline (به عنوان stage اول) بیاوریم اما در stage اول داده های جغرافیایی ما هنوز به شکل رشته ای هستند. به نظر شما راه حل چیست؟
ساده ترین راه حلی که به ذهن همه می رسد این است که داده های تغییر کرده در انتهای pipeline را در یک محل ذخیره کنیم و سپس دوباره آن داده ها را در یک pipeline دیگر به اپراتور های جغرافیایی بدهیم. برای این کار می توانیم از یک اپراتور دیگر به نام out$ استفاده کرده تا داده های خود را درون یک کالکشن جدید ذخیره کنیم:
db.persons.aggregate([ { $project: { _id: 0, name: 1, email: 1, birthdate: { $toDate: '$dob.date' }, age: "$dob.age", location: { type: 'Point', coordinates: [ { $convert: { input: '$location.coordinates.longitude', to: 'double', onError: 0.0, onNull: 0.0 } }, { $convert: { input: '$location.coordinates.latitude', to: 'double', onError: 0.0, onNull: 0.0 } } ] } } }, { $project: { gender: 1, email: 1, location: 1, birthdate: 1, age: 1, fullName: { $concat: [ { $toUpper: { $substrCP: ['$name.first', 0, 1] } }, { $substrCP: [ '$name.first', 1, { $subtract: [{ $strLenCP: '$name.first' }, 1] } ] }, ' ', { $toUpper: { $substrCP: ['$name.last', 0, 1] } }, { $substrCP: [ '$name.last', 1, { $subtract: [{ $strLenCP: '$name.last' }, 1] } ] } ] } } }, { $out: "transformedPersons" } ]).pretty();
من در انتهای این کوئری یک stage دیگر به نام out$ را تعریف کرده ام و سپس یک نام دلخواه (به عنوان نام کالکشن جدید) را به آن می دهم. با اجرای کوئری بالا کالکشن جدیدی به نام transformedPersons ایجاد خواهد شد. برای تست این موضوع می توانیم پس از اجرای کوئری بالا کد زیر را نیز اجرا کنیم:
db.transformedPersons.findOne()
با اجرای کوئری بالا نتیجه زیر را می گیریم:
"_id" : ObjectId("5ec75393317c454295862cbd"), "location" : { "type" : "Point", "coordinates" : [ -70.2264, 76.4507 ] }, "email" : "zachary.lo@example.com", "birthdate" : ISODate("1988-10-17T03:45:04Z"), "age" : 29, "fullName" : "Zachary Lo"
بنابراین داده های ما با موفقیت تغییر کرده و در کالکشن جدیدی به نام transformedPersons ذخیره شده است. حالا که داده های خود را با موفیت ذخیره کرده ایم می توانیم یک geo index را برایش تعریف کنیم تا از stage بعدی (geoNear$) استفاده کنیم. برای این کار می گوییم:
db.transformedPersons.createIndex({location: "2dsphere"})
با این کار index ما ساخته می شود و می توانیم وارد Stage بعدی شویم:
db.transformedPersons.aggregate([ { $geoNear: { near: { type: "Point", coordinates: [-18.4, -42.8] }, maxDistance: 100000, num: 10, query: { gender: "female" }, distanceField: "distance" } } ]).pretty()
یادتان باشد که geoNear$ همیشه باید اولین stage هر pipeline باشد به همین دلیل است که داده هایمان را در کالکشنی جدید ذخیره کردیم. این اپراتور یک شیء می گیرد که خودش چندین آرگومان مختلف دارد. در ابتدا باید near را تعیین کنیم. near (به معنی «نزدیک») همان نقطه مورد نظر ما است که می خواهیم با بقیه نقطه ها مقایسه شود. در واقع می توانید موضوع را بدین شکل در نظر بگیرید که near مختصات خود ما است و حالا می خواهیم ببینیم به چه نقاط دیگری در پایگاه داده نزدیک هستیم. همانطور که می بینید باید یک داده geoJSON را به آن پاس بدهید که قبلا در موردش صحبت کرده ایم. مختصاتی که من به این شیء پاس داده ام، مختصات یکی از کاربران کالکشن است که کمی تغییر داده ام (تا مطمئن شویم حداقل نزدیک یک نفر از کاربران هستیم و حداقل یک نتیجه برایمان برگردانده می شود.
در قسمت بعدی باید maxDistance را تعیین کنیم که حداکثر فاصله دو نقطه (near و دیگر نقاط پایگاه داده) در واحد متر است. من مقدار آن را روی 100 کیلومتر گذاشته ام (فقط برای تست)؛ یعنی تا 100 کیلومتر را نزدیک حساب می کنیم و آن نقطه را برمی گردانیم. سپس num را داریم که مشخص می کند چه تعدادی از نتایج برگردانده شود. تفاوت num و limit$ در این است که در limit داده ها از پایگاه داده دریافت می شدند و سپس از بین آن ها تعداد خاصی را جدا می کردیم اما در num هنوز داده ها از پایگاه داده دریافت نشده اند و مستقیما فقط همان تعداد ذکر شده را دریافت می کنیم بنابراین بسیار سریع تر است.
در مرحله بعد query را داریم که می توانیم هر کوئری دلخواهی را به آن بدهیم. یادتان هست که گفتم geoNear$ باید همیشه اولین Stage باشد؟ به همین دلیل به شما یک قسمت query داده می شود تا بتوانید درون آن کوئری مورد نظرتان را بنویسید. مثلا من در کوئری بالا گفته ام که از بین نقاط نزدیک، فقط خانم ها را برگردان. فیلد آخر نیز distanceField است که باید یک نام دلخواه به آن بدهیم. این نام، نام فیلد جدیدی خواهد شد که فاصله ما با هر نقطه را مشخص می کند.
البته با اجرای کوئری بالا هیچ نتیجه ای برایتان برگردانده نمی شود. چرا؟ به دلیل اینکه در کالکشن transformedPersons هیچ فیلدی به نام gender نداریم و من فقط جهت توضیح قسمت query آن را گذاشته ام. فیلدی که باید اجرا کنیم بدین شکل است:
db.transformedPersons.aggregate([ { $geoNear: { near: { type: "Point", coordinates: [-18.4, -42.8] }, maxDistance: 1000000, num: 10, query: { age: { $gt: 30 } }, distanceField: "distance" } } ]).pretty()
یعنی افراد بزرگ تر از 30 سال.
نکته: ممکن است در نسخه های جدید MongoDB به خطا برخورد کنید که num دیگر پشتیبانی نمی شود. در این صورت آن را حذف کرده و از limit$ استفاده کنید.
با اجرای دستور بالا یک نفر را پیدا می کنیم:
"_id" : ObjectId("5ec75393317c454295862cc5"), "location" : { "type" : "Point", "coordinates" : [ -18.5996, -42.6128 ] }, "email" : "elijah.lewis@example.com", "birthdate" : ISODate("1986-03-29T06:40:18Z"), "age" : 32, "fullName" : "Elijah Lewis", "distance" : 26473.52536319881
همانطور که می بینید distance به این فرد اضافه شده است و می بینیم که 26 کیلومتر با ما فاصله دارد. در ضمن می توانید دیگر Stage های موجود را نیز به انتهای کوئری بالا وصل کنید و از آن ها استفاده کنید. مهم این است که geoNear حتما در ابتدا باشد.
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.