سفر تکامل نقشه‌ی دیوار داستان مهندسی پشت نمایش هوشمند میلیون‌ها آگهی

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

وقتی به دنبال خانه یا ملک می‌گردید، «کجا بودن» آن شاید اولین و مهم‌ترین سؤالی باشد که به ذهنتان می‌رسد. در پلتفرمی مثل دیوار که روزانه هزاران آگهی املاک در آن ثبت می‌شود، نمایش این حجم از اطلاعات مکانی به شکلی کارآمد و قابل فهم، یک چالش بزرگ است. ما در دیوار مسیری پر فراز و نشیب را برای بهبود نمایش آگهی‌ها روی نقشه طی کرده‌ایم؛ از نمایش ساده‌ی نقطه‌ای تا سیستم‌های هوشمند کلاستربندی داینامیک. این مقاله داستان این تکامل فنی و تصمیم‌هایی است که در این راه گرفته‌ایم. هدف ما نه تنها ارائه‌ی یک تجربه‌ی کاربری روان، بلکه ساخت زیرساختی پایدار و مقیاس‌پذیر برای آینده‌ی نقشه‌ی دیوار بوده است.

rdltf4j5karb

چرا نقشه؟ نیاز به درک بصری موقعیت

در ابتدا، آگهی‌های املاک در دیوار، مانند سایر دسته‌بندی‌ها، به صورت لیستی نمایش داده می‌شدند. اما ماهیت جستجوی ملک با جستجوی کالاهای دیگر متفاوت است. کاربران نیاز دارند به سرعت موقعیت چندین آگهی را نسبت به هم و نسبت به نقاط مهم شهری بسنجند. نمایش لیستی، هرچقدر هم که با فیلترهای دقیق همراه باشد، نمی‌تواند این درک بصری و مقایسه‌ای را به خوبی یک نقشه ارائه دهد. همین نیاز بود که ما را به سمت استفاده از نقشه برای نمایش آگهی‌های املاک سوق داد. می‌خواستیم به کاربران کمک کنیم تا با یک نگاه، پراکندگی آگهی‌ها را در مناطق مختلف ببینند، گزینه‌های نزدیک به محل کار یا زندگی خود را پیدا کنند و در نهایت، تصمیم آگاهانه‌تری بگیرند.

skliglf3hcib

اولین قدم برای ساخت نقشه!

برای پیاده‌سازی نقشه، ما از کتابخانه‌ی Mapbox استفاده کردیم. Mapbox یک کتابخانه‌ی قدرتمند برای ساخت و نمایش نقشه‌های تعاملی است که ابزارهای متنوعی را در اختیار توسعه‌دهندگان قرار می‌دهد. در ابتدا، مهندسین بلد که تازگی به دیوار اضافه شده بودند، در یک محصول کاملاً جداگانه و خارج از لیست اصلی دیوار، نمونه‌ی اولیه‌ای از نقشه را با استفاده از Mapbox پیاده‌سازی کرد.

یکی از اولین چالش‌های ما، نحوه‌ی نمایش و ایندکس کردن آگهی‌ها بود. تا آن زمان، بیشتر سیستم‌های ما بر اساس «زمان» آگهی‌ها کار می‌کردند؛ یعنی آگهی‌های جدیدتر در اولویت نمایش قرار می‌گرفتند. اما برای نقشه، «مکان» اهمیت اصلی را دارد، و بعد از آن زمان انتشار آگهی. بنابراین، باید به سمت «ایندکس جغرافیایی» (Geographical Indexing) حرکت می‌کردیم. خوشبختانه، داده‌های مکانی آگهی‌ها (طول و عرض جغرافیایی) در Elasticsearch ذخیره می‌شدند و Elasticsearch ابزارهای بسیار خوبی برای جستجو و فیلتر کردن داده‌های مکانی ارائه می‌دهد. با تعریف یک فیلتر ساده، توانستیم آگهی‌های موجود در یک محدوده‌ی جغرافیایی مشخص را استخراج کنیم.

از «تایل»های استاتیک تا «ویوپورت» داینامیک

در نسخه‌های اولیه‌ی نقشه، ما از مفهومی به نام «تایل» (Tile) در Mapbox استفاده می‌کردیم. تایل‌ها در واقع قطعات مربعی کوچکی از نقشه هستند. Mapbox به این صورت کار می‌کند که شما یک URL پایه به آن می‌دهید و Mapbox با توجه به موقعیت فعلی نقشه و سطح بزرگنمایی (Zoom Level)، مقادیر x، y (مختصات تایل) و z (سطح زوم) را در آن URL قرار می‌دهد و درخواست را به سرور ارسال می‌کند. سرور باید آگهی‌های مربوط به آن تایل خاص را برگرداند و Mapbox آن‌ها را روی نقشه نمایش می‌دهد. Mapbox همچنین این تایل‌ها را در سمت کلاینت نگه می‌دارد تا در اگر کاربر مجدد دوربین را روی ان تایل‌ها برد، مقدار قدیمی را نشان دهد و هم‌زمان درخواست برای دریافت مقدار جدید بزند.

این روش مزایایی مانند سادگی پیاده‌سازی و سرعت اولیه دارد. اما یک مشکل اساسی هم ایجاد می‌کرد: کنترل ما بر روی نمایش آگهی‌ها محدود بود. وقتی کاربر کمی نقشه را جابجا می‌کرد یا زوم می‌کرد، اگر محدوده‌ی جدید همچنان در تایل‌های کش‌شده قرار داشت، تایل‌های جدیدی از سرور درخواست نمی‌شد. یعنی در صورت تغییر خیلی کم در مکان دوربین یا تغییر کم زوم دوربین، با اینکه تعداد آگهی‌های نمایشی در گوشی کاربر عوض می‌شد، اما ریکوئست جدیدی به بک‌اند ارسال نمی‌شد و این باعث می‌شد تعداد آگهی‌هایی که ما به عنوان شمارش کلی در یک منطقه نشان می‌دادیم با تعداد پین‌های واقعی روی نقشه همخوانی نداشته باشد. چون تعدادی از پین‌ها خارج از دید نقشه بودند. به عبارت دیگر، داده‌ها «استاتیک» (Static) بودند و به صورت لحظه‌ای با تغییرات کوچک کاربر آپدیت نمی‌شدند.

zph8tnerr7q1

برای حل این مشکل و داشتن کنترل کامل بر نمایش داده‌ها، به سراغ پیاده‌سازی «ویوپورت» (Viewport) رفتیم. بسیاری از پلتفرم‌های مشابه نیز از این روش استفاده می‌کنند. در روش ویوپورت، به جای تکیه بر تایل‌های مجزا، ما کل محدوده‌ی قابل مشاهده‌ی نقشه در نمایشگر کاربر را در نظر می‌گیریم. هر بار که کاربر نقشه را جابجا می‌کند یا سطح زوم را تغییر می‌دهد، یک درخواست جدید به سرور ارسال می‌شود که حاوی مختصات دقیق ویوپورت فعلی است. سرور نیز دقیقاً آگهی‌هایی را که در آن محدوده قرار دارند، برمی‌گرداند. با این روش، داده‌ها همیشه «داینامیک» (Dynamic) و دقیق هستند و ما کنترل کاملی بر نحوه‌ی نمایش و تعداد آگهی‌ها داریم.

چالش حجم زیاد آگهی‌ها: راه حل «کلاسترینگ»

پس از پیاده‌سازی ویوپورت، با چالش جدیدی روبرو شدیم: حجم زیاد آگهی‌ها. تصور کنید کاربر در حال مشاهده‌ی نقشه‌ی شهری بزرگ مانند تهران از فاصله‌ی دور است. در این حالت، هزاران آگهی در ویوپورت کاربر قرار می‌گیرند. اگر بخواهیم همه‌ی این آگهی‌ها را به صورت پین‌های مجزا نمایش دهیم، نقشه به سرعت با انبوهی از نقاط قرمز پوشیده می‌شود و عملاً غیرقابل استفاده خواهد بود. از طرفی، اگر تعداد پین‌ها را به یک عدد ثابت (مثلاً ۱۰۰ یا ۲۰۰) محدود کنیم، بسیاری از آگهی‌ها نمایش داده نمی‌شوند و کاربر دید درستی از تراکم آگهی‌ها در مناطق مختلف پیدا نمی‌کند.

راه حل این مشکل، استفاده از تکنیکی به نام «کلاسترینگ» (Clustering) یا خوشه‌بندی بود. ایده‌ی اصلی کلاسترینگ این است که به جای نمایش تعداد زیادی پین نزدیک به هم، آن‌ها را در یک «کلاستر» یا خوشه گروه‌بندی کنیم و به جای چندین پین، یک نشانگر واحد (معمولاً همراه با تعداد آگهی‌های آن خوشه) نمایش دهیم. وقتی کاربر روی یک کلاستر زوم می‌کند، آن کلاستر باز شده و پین‌های داخل آن یا کلاسترهای کوچکتر نمایان می‌شوند.

آشنایی با دنیای کلاسترینگ: مفاهیم و روش‌ها

برای پیاده‌سازی کلاسترینگ، باید چندین مرحله را طی می‌کردیم و در هر مرحله، تصمیم‌های فنی مهمی می‌گرفتیم. ابتدا برخی مفاهیم کلیدی را مرور می‌کنیم:

  • تایل (Tile): در نقشه‌های دیجیتال، تایل‌ها بخش‌های مربعی کوچکی از نقشه هستند که با کنار هم قرار گرفتن، نمای کلی نقشه را تشکیل می‌دهند. این تقسیم‌بندی به بارگذاری سریع‌تر و کارآمدتر نقشه‌ها کمک می‌کند.
  • پالیگان (Polygon): یک محدوده‌ی جغرافیایی بسته روی نقشه (مانند یک شهر یا محله).
  • اچ۳ (H3): یک سیستم شبکه‌بندی جغرافیایی که سطح زمین را به شش‌ضلعی‌های منظم تقسیم می‌کند. این سیستم توسط اوبر توسعه داده شده و برای تحلیل داده‌های مکانی بسیار مفید است.
  • کلاسترینگ استاتیک (Static Clustering): کلاستر‌ها و مراکز‌ آنها از پیش تعیین شده و ثابت هستند (مثلاً بر اساس محله‌ها).
  • کلاسترینگ داینامیک (Dynamic Clustering): مراکز کلاسترها بر اساس توزیع لحظه‌ای آگهی‌ها در ویوپورت کاربر محاسبه می‌شوند.

مراحل اصلی پیاده‌سازی کلاسترینگ عبارت بودند از:

۱. تهیه‌ی پالیگان‌های مبنا: باید تصمیم می‌گرفتیم که کلاسترها بر اساس چه واحدهای جغرافیایی شکل بگیرند. گزینه‌ها شامل استفاده از سیستم‌های گریدبندی منظم مانند تایل‌های خود نقشه یا سلول‌های H3، یا استفاده از پالیگان‌های نامنظم بر اساس توزیع واقعی آگهی‌ها یا تقسیمات جغرافیایی موجود (مانند شهرها و محله‌ها) بود. هر کدام مزایا و معایب خود را داشتند؛ مثلاً گریدهای منظم پوشش کاملی ارائه می‌دهند اما ممکن است مرکزشان با تراکم واقعی آگهی‌ها همخوانی نداشته باشد.

۲. شمارش آگهی‌ها در هر پالیگان: پس از مشخص شدن پالیگان‌ها، باید تعداد آگهی‌های (فیلتر شده) موجود در هرکدام را از Elasticsearch استخراج می‌کردیم. اینجا هم دو رویکرد اصلی وجود داشت: ۱.مالتی‌سرچ (Multi-Search): برای هر پالیگان یک درخواست شمارش جداگانه به Elasticsearch ارسال کنیم و نتایج را تجمیع کنیم. ۲.اگریگیشن (Aggregation): از قابلیت‌های تجمیع خود Elasticsearch استفاده کنیم تا در یک درخواست، تعداد آگهی‌ها را برای چندین پالیگان به صورت گروه‌بندی شده دریافت کنیم. طبق بررسی‌ها و بنچ‌مارک‌هایی که گرفتیم، استفاده از اگریگیشن‌های مبتنی بر گرید (مانند geotile_grid یا geohash_grid) عملکرد بهتری از نظر زمان پاسخ‌دهی داشت.

۳. تجمیع نتایج و نمایش کلاسترها: پس از دریافت تعداد آگهی‌ها در پالیگان‌های پایه، باید تصمیم می‌گرفتیم که چگونه این نتایج را به کلاسترهای نهایی تبدیل کنیم. ۱.روش استاتیک: نتایج را مستقیماً در مرکز همان پالیگان‌های پایه نمایش دهیم. این روش ساده‌تر بود اما ممکن بود مراکز کلاسترها با تراکم واقعی آگهی‌ها همخوانی نداشته باشد. ما یک «اکسپریمنت کلاسترینگ استاتیک» اولیه را پیاده‌سازی کردیم که در آن تعداد آگهی‌ها برای هر استان، شهر و محله (بر اساس دسته‌بندی‌های املاک) به صورت روزانه محاسبه و در Redis ذخیره می‌شد. این داده‌ها سپس برای نمایش کلاسترها در زوم‌های پایین استفاده می‌شدند. ۲.روش داینامیک: نتایج پالیگان‌های پایه (که معمولاً ریزتر و پرتعدادتر هستند) را با الگوریتم‌های خاصی تجمیع کرده و کلاسترهای جدید و بهینه‌تری ایجاد کنیم که مرکزشان به تراکم واقعی آگهی‌ها نزدیک‌تر باشد. این روش پیچیده‌تر بود اما نتیجه‌ی بهتری ارائه می‌داد.

ترکیب نهایی که ما برای پیاده‌سازی کلاسترینگ داینامیک انتخاب کردیم، استفاده از «گرید سیستم مبتنی بر تایل‌های جغرافیایی (Geotile)»، «اگریگیشن Elasticsearch» برای شمارش آگهی‌ها در این تایل‌ها (با استفاده از داده‌های ایندکس شده برای سرعت بیشتر) و سپس «تجمیع داینامیک» نتایج برای ساخت کلاسترهای نهایی بود.

مدیریت «State» در دنیای «Stateless» دیوار: راه حل «هش»

یکی دیگر از چالش‌های مهم ما، مدیریت State نقشه در زیرساخت عمدتاً Stateless دیوار بود. در سیستم‌های Stateless، هر درخواست به سرور به صورت مستقل و بدون وابستگی به درخواست‌های قبلی پردازش می‌شود. سرور اطلاعاتی از وضعیت قبلی کاربر (مثلاً اینکه قبلاً روی چه چیزی کلیک کرده یا چه فیلترهایی را انتخاب کرده) نگه نمی‌دارد و تمام اطلاعات مورد نیاز با هر درخواست ارسال می‌شود.

اما نقشه ذاتاً یک کامپوننت «با وضعیت» (Stateful) است. وضعیت دوربین نقشه (موقعیت، سطح زوم)، فیلترهای اعمال‌شده، پین‌های انتخاب‌شده و … همگی بخشی از وضعیت فعلی نقشه هستند که با تعاملات کاربر تغییر می‌کنند. برای مثال، وقتی کاربر شهر یا محله‌ی خود را در فیلترها تغییر می‌دهد، انتظار دارد دوربین نقشه به صورت خودکار به آن موقعیت جدید منتقل شود. این یعنی بک‌اند باید از وضعیت قبلی (شهر قبلی) و وضعیت جدید (شهر جدید) مطلع باشد تا بتواند این انتقال دوربین را به درستی مدیریت کند.

در نسخه‌های قدیمی‌تر از روش‌های ساده‌تری مانند ارسال همزمان وضعیت قدیم و جدید به بک‌اند استفاده می‌شد. اما این روش‌ها در زیرساخت اصلی و استاندارد دیوار که برای مقیاس‌پذیری و پایداری بالا طراحی شده، قابل قبول نبودند و باعث پیچیدگی و انتقال داده‌های اضافی می‌شدند.

راه حلی که ما برای این مشکل پیدا کردیم، استفاده از «هش» (Hash) بود. ایده به این صورت است که فیلترها و پارامترهای مهمی که بر وضعیت دوربین و نمایش نقشه تأثیرگذار هستند (مانند شهر، محله، دسته‌بندی و …) در هر درخواست، در بک‌اند، به یک رشته‌ی هش تبدیل می‌شوند. این هش به همراه هر درخواست، توسط کلاینت، به بک‌اند ارسال می‌شود. بک‌اند مقدار دقیق این هش برایش مهم نیست؛ فقط کافی است هش دریافتی از کلاینت را با هشی که خودش بر اساس state فعلی محاسبه می‌کند، مقایسه کند. اگر این دو هش متفاوت بودند، بک‌اند متوجه می‌شود که یکی از پارامترهای کلیدی تغییر کرده و در نتیجه، باید وضعیت دوربین (مثلاً مختصات و سطح زوم) را مجدداً محاسبه و برای کلاینت ارسال کند. در غیر این صورت، دوربین در همان وضعیت قبلی باقی می‌ماند. این رویکرد به ما کمک کرد تا بدون نیاز به ذخیره‌سازی وضعیت کامل کاربر در بک‌اند، تغییرات مهم را تشخیص داده و تجربه‌ی کاربری روانی را فراهم کنیم.

جمع‌بندی: نقشه‌ای برای آینده

تلاش ما همواره بر این بوده که با نگاهی عمیق به مسائل فنی و با استفاده از بهترین راهکارهای موجود، زیرساختی قوی و انعطاف‌پذیر برای نقشه‌ی دیوار بسازیم؛ زیرساختی که نه تنها پاسخگوی نیازهای امروز کاربران باشد، بلکه بتواند در آینده نیز با رشد دیوار و پیچیده‌تر شدن نیازها، به تکامل خود ادامه دهد. مسیر ساخت نقشه‌ی دیوار را هیچ وقت تمام‌شده نمی‌دانیم؛ چون هر بار با چشم کاربران به این نقشه نگاه می‌کنیم، نیازهای تازه و چالش‌های جدیدی جلوی رویمان ظاهر می‌شود. هنوز هم کارهای زیادی پیش رو است، اما مطمئنیم هر قدم رو به جلو، تجربه‌ی کاربران‌مان را معنادارتر می‌کند.

نوشته های مشابه