راهکار فنی مقیاسپذیر برای پشتیبانی از پویایی گستردهی سیستم کمیسیون باسلام: Percolate در Elasticsearch

در بسیاری از پلتفرم های فروش آنلاین، محاسبه کمیسیون فروش بر اساس درصد ثابتی از مبلغ سفارش انجام می شود. این روش در نگاه اول ساده و کارآمد به نظر می رسد، اما در عمل و با گذر زمان، همراه با رشد پلتفرم و پیچیده تر شدن نیازهای تجاری، محدودیت های قابل توجهی را نمایان می سازد. باسلام نیز با چنین چالشی مواجه شد: نیاز به اعمال قوانین متنوع و پویای کمیسیون گیری، بدون نیاز به تغییر در کد و بدون وابستگی کامل به تیم فنی.
ماجرا از جایی آغاز شد که تیم های مختلف باسلام—به ویژه تیم های مارکتینگ، بازارسازی و پشتیبانی غرفه ها—درخواست هایی برای اجرای سیاست های خاص کمیسیون داشتند: مثلاً کاهش کمیسیون برای غرفه هایی که در یک کمپین خاص شرکت میکنند، یا اعمال تخفیف موقت برای محصولات تازه وارد یا ارگانیک. این درخواست ها اغلب با اهدافی همچون افزایش جذب غرفه دار، ایجاد انگیزه فروش یا پوشش رویدادهای مناسبتی مطرح می شدند؛ اما اجرای آنها با روش های سنتی مستلزم تغییر کد، تست مجدد و دیپلوی دوباره ی سیستم بود—فرآیندی زمانبر، پرهزینه و مستعد خطا.
در این مقاله، تجربه ی باسلام در مواجهه با این چالش و راه حلی که برای آن انتخاب شد را شرح می دهیم: استفاده از قابلیت Percolator Query در Elasticsearch برای پیاده سازی یک Rule Engine ساده، سریع و قابل گسترش. در ادامه، علاوه بر توضیح فنی این راهکار، مسیرهای جایگزینی که بررسی و رد شدند نیز تحلیل شده اند تا تصویری روشن تر از فرآیند تصمیمگیری ارائه شود.
سناریوهای واقعی که نیاز به انعطاف پذیری داشتند:
- تعریف کمیسیون متفاوت برای یک غرفه خاص در بازه ی زمانی مشخص
- تغییر کمیسیون برای غرفه هایی که در یک کمپین خاص ثبت نام کرده اند
- تغییر کمیسیون سفارش های مرتبط با یک جشنواره خاص
- اعمال کمیسیون بر اساس دسته بندی محصول و حاشیه سود آن
- تعریف سیاست های خاص مانند:
«اگر هزینه ارسال برای مشتری رایگان باشد، کمیسیون نصف شود.»
«برای محصولات جدید ایجادشده در چند دسته بندی خاص، تا یک ماه کمیسیون نصف شود.»
«برای محصولات غذایی ارگانیک، درصد کمتری اعمال شود.»
پیاده سازی چنین تنوعی در یک ساختار سنتی و اصطلاحاً هاردکد (Hard Code)، بسیار پرهزینه، پرریسک و ناکارآمد بود.
راهحل ما: استفاده از Percolate در Elasticsearch
برای حل این مشکل، از قابلیت Percolator Query در Elasticsearch استفاده کردیم.
تعریف Percolate
در حالت معمول، در Elasticsearch شما یک Query (کوئری) می نویسید و آن را روی Documents (اسناد) اجرا میکنید. اما در Percolate این منطق برعکس می شود: شما ابتدا کوئری ها را ذخیره میکنید (درواقع، همان قوانین)، و سپس یک سند جدید (مثلاً اطلاعات یک سفارش) را ارسال می کنید. Elasticsearch به صورت خودکار بررسی میکند که این سند با کدام کوئری ها تطابق دارد و در نهایت، فهرستی از کوئری های منطبق را بازمی گرداند.
برخلاف حالت مرسوم که در آن کوئری اجرا می شود تا سندی پیدا شود، اینجا سند را می فرستید تا مشخص شود با کدام کوئری ها تطبیق داده می شود. به عبارت دیگر، Percolate یعنی: «سند جدید بده، تا بگم کدام کوئری هایی که قبلاً ذخیره کردی، بهش میخورن.»
در اینجا Elasticsearch با استفاده از یک ساختار بهینه (که در ادامه مقاله آن را شرح خواهم داد)، کوئری ها را ذخیره می کند تا بتواند به سرعت تشخیص دهد که یک سند با کدام کوئری ها تطبیق دارد.
نمونه قانون:
#rule
{
”query”: {
”bool”: {
”must”: [
{
”range”: {
”order_time”: {
”gte”: ”2025-03-01T00:00:00”,
”lte”: ”2025-03-30T23:59:59”
}
}
},
{
”term”: {
”product.category”: 222
}
}
]
}
}
}
چرا از روش های دیگر استفاده نکردیم؟
در ادامه، روش هایی که بررسی کردیم و دلایل عدم انتخاب آنها را همراه با جزئیات و مثال توضیح می دهم:
1. تعریف قوانین به صورت شرطی در کد (if/else)
ساده ترین و سنتی ترین روش این است که تمام منطق در کد نوشته شود. مثلاً در بخشی که خرید پردازش می شود، به صورت زیر عمل می کنیم:
if datetime(2025, 3, 1) <= order_time <= datetime(2025, 3, 30) and order[”product”][”category”] == 222:
commission = 0.02
مشکلات این شیوه:
- نیاز به deploy جدید پس از افزودن هر قانون
- افزایش پیچیدگی کد و بالا رفتن احتمال خطا
- وابستگی کامل تیم بیزینس به تیم فنی برای اعمال تغییرات
برای رهایی از استفاده ی بیش از حد از دستورات if/else، راه حلهایی وجود دارد. با بازنگری در ساختار کد و بهره گیری از الگوهای طراحی مناسب مانند Strategy Pattern یا حتی استفاده از rule engineهای آماده (مانند durable_rules در Python یا NRules در C#)، می توان تا حدی پیچیدگی را کنترل کرد.
با این حال، برخی از مشکلات همچنان باقی میماند. در حالی که rule engineهای پیشرفته امکانات متنوعی ارائه میدهند، برای نیاز ما—که بیشتر مبتنی بر simple rule matching با عملکرد بالا بود—استفاده از این ابزارها بیش از حد نیاز (overkill) محسوب می شد.
2. ذخیره سازی قوانین به صورت STRING و پردازش آنها در کد
قوانین به صورت STRING (مثلاً در MySQL یا Redis) ذخیره می شوند. سپس در زمان پردازش خرید، همه قوانین بارگذاری شده و بررسی می شود که آیا خرید با آنها match می کند یا نه.
rules = [
{”id”: 1,
”condition”: ”datetime(2025, 3, 1) <= datetime.fromisoformat(order['order_time']) <= datetime(2025, 3, 30) and order['product']['category'] == 222”,
”commission_rate”: 0.02
},
...
]
در این روش، باید منطق matching در کد نوشته شود:
matched_rules = []
for rule in rules:
if eval(rule[”condition”]):
matched_rules.append(rule)
مشکلات این شیوه:
- در صورت زیاد بودن تعداد ruleها، نیاز است همه روی هر خرید بررسی شوند که منجر به کاهش کارآیی (Performance) می شود.
- امنیت پایین و امکان تزریق کد مخرب
می توان تا حدی مسئله امنیت پایین را بهبود داد. مثلاً در سیستم های .NET، میتوان قوانین را به صورت رشته ای (مثلاً ”OrderTime <= DateTime(2025,3,30) && Product.Category == 22”) در دیتابیس ذخیره کرد، سپس در زمان اجرا با استفاده از کتابخانه هایی مانند System.Linq.Dynamic.Core آنها را به Expression تبدیل و اجرا کرد.
این روش نسبت به استفاده از eval در Python از امنیت بیشتری برخوردار است چون نهایتاً در سطح کوئری اجرا می شود؛ اما همچنان اجرای Expressionها در زمان اجرا هزینه بر است.
3. قوانین را بهصورت کوئری ذخیره کن، سیستم خودش بقیه را انجام می دهد
دلیل انتخاب ما: قوانین را بهصورت کوئری در Elasticsearch ذخیره میکنیم. برای هر خرید، تنها یک سند به سیستم ارسال میشود و Elasticsearch فرآیند تطبیق (Matching) را به صورت خودکار، بهینه و سریع انجام میدهد و نیازی به پیاده سازی منطق Matching یا Engine داخلی نیست.
تعریف Query به عنوان یک قانون:
PUT /rules/_doc/1
{
”query”: {
”bool”: {
”must”: [
{
”range”: {
”order_time”: {
”gte”: ”2025-03-01T00:00:00”,
”lte”: ”2025-03-30T23:59:59”
}
}
},
{
”term”: {
”product.category”: 222
}
}
]
}
}
}
مثال واقعی خرید:
POST /rules/_search
{
”query”: {
”percolate”: {
”field”: ”query”,
”document”: {
”order_time”: ”2025-03-20T18:31:00”,
”product”: {
”category”: 222
}
}
}
}
}
این سند به Percolator ارسال میشود و فهرستی از کوئری هایی که با آن منطبق می شوند، بازگردانده می شود.
چرا Percolate در Elasticsearch را انتخاب کردیم؟
- قوانین به راحتی توسط اپراتورها قابل تعریف هستند.
- امکان اولویتبندی، نسخه بندی و غیرفعال سازی برای ruleها وجود دارد.
- در آینده می توان مدل های Machine Learning را با استفاده از داده های rule match شده آموزش داد (مثلاً برای پیشبینی تأثیر یک rule جدید).
- به دلیل ساختار indexed، عملکرد Elasticsearch بسیار بالا است.
- سطح امنیتی مناسبی را فراهم می کند.
مقایسه با اجرای همین منطق در PostgreSQL
برای درک بهتر، این فرآیند را در PostgreSQL شبیه سازی کردم. روال کلی به این صورت است:
- ابتدا، کوئری ها در دیتابیس ذخیره می شوند.
- سپس، اطلاعات سند جدید (مثلاً یک سفارش) به سیستم ارسال می شود.
- این اطلاعات در یک temporary table (جدول موقت) ذخیره می شوند.
- در مرحله بعد، کوئری های ذخیره شده روی جداول موقت اجرا می شوند.
- اگر یک کوئری آن رکورد را بازگرداند، یعنی با آن منطبق شده است.
کوئریهای PostgreSQL:
--des: جدول برای ذخیره کوئریها
CREATE TABLE rules (
id SERIAL PRIMARY KEY,
query text
);
--des: درج یک کوئری(قانون) نمونه در جدول
INSERT INTO rules (query)
VALUES
('
SELECT *
FROM temp_data t
WHERE t.order_time >= ''2025-03-01T00:00:00''::timestamp
AND t.order_time <= ''2025-03-30T23:59:59''::timestamp
AND t.product_category_id = 222
');
--des: درج یک کوئری(قانون) دیگر که با هر اطلاعاتی مچ میشود
INSERT INTO rules (query) VALUES ('
SELECT * FROM temp_data
');
--des: ایجاد جدول موقت
CREATE TEMPORARY TABLE temp_data (
order_time timestamp,
product_category_id int4
);
--des: درج اطلاعات سفارش در جدول موقت
INSERT INTO temp_data (order_time, product_category_id)
VALUES ('2025-03-22T18:31:00', 222);
--des: پیدا کردن کوئریهایی(قوانینی) که با اطلاعات ورودی مچ شدند
DO $$
DECLARE
rule record;
result record;
BEGIN
FOR rule IN
SELECT id, query FROM rules
LOOP
FOR result IN EXECUTE rule.query
LOOP
RAISE NOTICE 'Match found for Rule ID: %', rule.id;
END LOOP;
END LOOP;
END $$;
در مثال بالا، برای هر سند جدید (یعنی رکورد اطلاعات سفارش ثبت شده در جدول temp_data
) مجبوریم تمام stored queryها را اجرا کنیم. اگر تعداد کوئری ها زیاد باشد، پردازش آنها و یافتن موارد match شده می تواند زمانبر و پرهزینه باشد.
چرا Elasticsearch از پس این کار بهتر برمیآید؟
همانطور که قبلاً اشاره کردم، Elasticsearch، کوئری های ذخیره شده را ایندکس می کند. این دقیقاً همان قابلیتیست که PostgreSQL به صورت native ندارد—یعنی راهی ذاتی برای تبدیل یک Stored Query به ساختاری قابل ایندکس وجود ندارد.
در Elasticsearch، کوئری هایی که در فیلدی با نوع percolator ذخیره می شوند، ابتدا آنالیز شده و به ساختار داخلی خودش تبدیل می شوند (نوعی AST یا Query tree). این ساختارها در یک ایندکس خاص ذخیره می شوند—مشابه با ذخیره ی اسناد معمولی ولی با mapping (نگاشت) خاص.
هنگام انجام عملیات Percolate، سند جدید به صورت موقت ایندکس می شود (نه به شکل دائمی). این فرآیند ایندکس سازی موقتی، از ساختارهایی مانند inverted index، BKD tree و سایر تکنیکهای خاص بسته به نوع فیلد استفاده می کند. سپس، Elasticsearch بررسی می کند که کدام یک از کوئریهای ذخیره شده با این سند منطبق می شوند.
اما نکته کلیدی این است: Elasticsearch همه ی کوئریها را بررسی نمی کند! چون خود کوئری ها هم ایندکس شده اند و با کمک از ساختار ایندکس سازی می تواند به صورت هوشمند و سریع فقط کوئری های منطبق را بیابد.
خلاصه: هم کوئری ها و هم سند جدید، به شکل قابل ایندکس شدن درمی آیند و بعد انطباق صورت می گیرد. این فرآیند سریع و مقیاس پذیر است.
چه کاربردهای دیگری میتوان برای Percolate تصور کرد؟
- هشدار آنی (Real-time alerting)
مثلاً اگر یک لاگ جدید دریافت شود که با شرایط خاصی منطبق شود، بلافاصله هشدار ایجاد شود (برای مانیتورینگ سیستم، امنیت، یا تشخیص رخدادهای بحرانی). - برچسبگذاری پویا (Dynamic tagging)
هر زمان یک سند جدید ثبت شود که به یک موضوع خاص مربوط است، سیستم به صورت خودکار یک یا چند تگ مرتبط به آن اضافه کند. این کاربرد در سیستم های مدیریت محتوا یا طبقه بندی خودکار داده ها بسیار مفید است. - مطابقت محتوا با علایق کاربران (Matching content to user interests)
کاربران علاقهمندی های خود را به صورت کوئری ثبت میکنند. هر زمان محتوای جدیدی ثبت شود که با علاقه مندیهای آنها منطبق شد، نمایش داده می شود یا نوتیفیکیشن ارسال میشود.
مثلاً کاربری در یک فروشگاه لپتاپ فیلترهایی برای جستجو اعمال میکند که فعلاً هیچ محصولی برای آن وجود ندارد. وقتی محصول جدیدی با آن مشخصات ثبت شد، به کاربر اطلاع داده میشود (مثلاً از طریق پیامک یا ایمیل: «موجود شد، به من اطلاع بده»). - محتوای شخصیسازی شده وبسایت (Personalized website content)
نمایش محتوای شخصی سازی شده بر اساس ویژگی های کاربران. برای مثال:
- نمایش محصولات خاص برای کاربران یک شهر یا استان خاص
- ارائه کد تخفیف مخصوص کاربرانی که بیش از ۵ خرید موفق داشته اند
- شخصی سازی ویترین محصولات یا محتوا بر اساس رفتار گذشته کاربر
جمع بندی
پس از بررسی راهکارهای مختلف، Percolate به عنوان راه حلی سریع، مقیاس پذیر و قابل نگهداری انتخاب شد. امروزه، سیستم کمیسیون باسلام به جای استفاده از ساختارهای پراکنده و غیرقابل توسعه ی if/else، بر پایه ی مجموعه ای منظم از Percolator ruleها اجرا می شود—قوانینی که به سادگی قابل تست، تغییر و مدیریت هستند.
تجربه شما چیست؟
امیدواریم شرح این تجربه برای شما مفید بوده باشد و اگر شما هم تجربه ای مشابه داشتید، مشتاقیم تا در بخش نظرات، چالش ها و راهکارهایی که با آنها مواجه شدید را با ما به اشتراک بگذارید.
مطلبی دیگر از این انتشارات
تجربهی بازسازی باسلام پس از تعدیل
مطلبی دیگر از این انتشارات
از خاکستر بحران تا نقشهی آینده: نقشه مناطق بمباران شده ایران
مطلبی دیگر از این انتشارات
پیادهسازی جستوجوی تصویری در باسلام: یک تجربه مقیاسپذیر