Codoloper

آموزش برنامه‌نویسی به فارسی

یاد بگیر، پیشرفت کن، بساز.

منبع آموزشی برنامه‌نویسی به زبان فارسی - مستندات، دوره‌ها و مطالب کاربردی برای همه سطوح.

داکیومنت ها:

JavascriptHTMLCSSReact JSبیشتر
1// برنامه‌نویسی به فارسی
2import { learn } from 'codoloper'
3
4const developer = learn({
5"lang": "فارسی"
6"level": "همه سطوح"
7free: true
8})
بلاگ

مطالب جدید

تازه‌ترین مطالب آموزشی و خبرهای مرتبط.

Rate Limiting: چرا هر API باید یک سقف مصرف داشته باشه نوشته شده توسط عرفان دهقانی

Rate Limiting: چرا هر API باید یک سقف مصرف داشته باشه

فرض کن یک endpoint ساده داری که لیست محصولات رو برمیگردونه. یک روز متوجه میشی سرورت زیر فشار عجیبی رفته، درحالی‌که تعداد کاربر واقعی زیاد نشده. وقتی لاگ‌ها رو نگاه میکنی، میبینی یک IP تنها، چند هزار درخواست در ثانیه فرستاده — یا یک اسکریپت خرابه، یا یک نفر داره عمداً داده‌هات رو میخراشه (scrape میکنه)، یا حتی داره تلاش میکنه با حجم درخواست سرویس رو از کار بندازه. rate limiting دقیقاً همون لایه‌ایه که جلوی این سناریو رو میگیره: تعیین میکنه هر کلاینت (بر اساس IP، کاربر، یا API key) توی یک بازه‌ی زمانی مشخص، حداکثر چند درخواست میتونه بفرسته.

چرا واقعاً مهمه

اولین و واضح‌ترین دلیل، محافظت از منابع سرور در برابر سوءاستفاده یا حملات است. حملات denial-of-service (چه عمدی چه ناشی از یک باگ توی کلاینت که وارد یک حلقه‌ی بی‌نهایت از درخواست شده) میتونن یک سرویس سالم رو در عرض چند ثانیه از پا دربیارن. rate limiting یک سقف مشخص میذاره که جلوی این نوع فشار رو میگیره، قبل از این‌که به لایه‌های عمیق‌تر سیستم (مثل دیتابیس) برسه.

دومی، توزیع عادلانه‌ی منابع بین کاربرهاست. اگه یک API عمومی داری که چند هزار کلاینت مختلف ازش استفاده میکنن، بدون rate limiting هیچ تضمینی نیست که یک کلاینت پرمصرف، کل ظرفیت سرور رو برای بقیه اشغال نکنه. با یک سقف مشخص برای هر کلاینت، مطمئن میشی هیچ‌کس نمیتونه سهم بقیه رو بخوره.

سومی، کنترل هزینه است، مخصوصاً وقتی endpoint پشت سرش یک سرویس گرون‌قیمت باشه — مثلاً یک API که هر درخواستش یک فراخوانی به یک مدل هوش مصنوعی یا یک سرویس ثالث پولی میفرسته. بدون سقف، یک باگ ساده توی کد کلاینت (مثلاً یک حلقه‌ی retry بی‌پایان) میتونه صورت‌حساب رو در عرض چند ساعت نجومی کنه.

چهارمی، جلوگیری از brute-force. روی endpoint هایی مثل لاگین، rate limiting جلوی این رو میگیره که یک مهاجم بتونه با امتحان‌کردن هزاران رمز عبور در ثانیه، حساب کاربری رو بشکنه.

الگوریتم‌های رایج rate limiting

چند روش استاندارد برای پیاده‌سازی rate limiting وجود داره، و هرکدوم trade-off متفاوتی دارن.

Fixed Window

ساده‌ترین روش: یک بازه‌ی زمانی ثابت (مثلاً هر دقیقه) در نظر میگیری و میشماری چند درخواست توش اومده. اگه از سقف رد بشه، بقیه رد میشن تا شروع بازه‌ی بعدی.

// pseudo-code ساده برای fixed window
const requests = {}; // { userId: { count, windowStart } }

function isAllowed(userId, limit = 100, windowMs = 60000) {
  const now = Date.now();
  const entry = requests[userId];

  if (!entry || now - entry.windowStart > windowMs) {
    requests[userId] = { count: 1, windowStart: now };
    return true;
  }

  if (entry.count >= limit) return false;
  entry.count++;
  return true;
}

مشکل این روش یک نکته‌ی ظریفه: چون بازه‌ها ثابت و جدا از همن، یک کلاینت میتونه دقیقاً لحظه‌ی آخر یک بازه ۱۰۰ درخواست بفرسته و دقیقاً لحظه‌ی اول بازه‌ی بعدی، ۱۰۰ درخواست دیگه — یعنی توی یک بازه‌ی دو ثانیه‌ای واقعی، ۲۰۰ درخواست رد شده، درحالی‌که سقف روی کاغذ ۱۰۰ در دقیقه بود. به این پدیده burst at boundary میگن.

Sliding Window

این روش با نگه‌داشتن timestamp دقیق هر درخواست (یا یک تقریب وزن‌دار از دو بازه‌ی متوالی)، مشکل burst مرزی رو حل میکنه. دقیق‌تره ولی حافظه و محاسبات بیشتری میخواد، چون باید یا timestamp های تک‌تک درخواست‌ها رو نگه داری، یا یک فرمول ترکیبی حساب کنی.

Token Bucket

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

class TokenBucket {
  constructor(capacity, refillRatePerSec) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillRatePerSec;
    this.lastRefill = Date.now();
  }

  tryConsume() {
    this._refill();
    if (this.tokens < 1) return false;
    this.tokens -= 1;
    return true;
  }

  _refill() {
    const now = Date.now();
    const elapsedSec = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillRate);
    this.lastRefill = now;
  }
}

Leaky Bucket

مشابه token bucket ولی از زاویه‌ی دیگه: درخواست‌ها توی یک صف (سطل) قرار میگیرن و با نرخ ثابتی از صف خارج و پردازش میشن، مثل یک سطل که از یک سوراخ ثابت آب نشت میکنه. اگه صف پر بشه، درخواست‌های جدید رد میشن. این روش خروجی رو کاملاً یکنواخت نگه میداره (بدون burst)، برخلاف token bucket که burst محدود رو مجاز میدونه — انتخاب بین این دو به این بستگی داره که آیا میخوای نرخ خروجی کاملاً ثابت باشه، یا کمی انعطاف برای burst داشته باشه.

کجا پیاده‌سازیش کنیم

rate limiting رو میشه در چند لایه پیاده کرد. توی سطح Nginx یا یک reverse proxy، با ماژول limit_req میشه بدون تغییر کد اپلیکیشن، محدودیت گذاشت:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://backend;
    }
}

توی سطح اپلیکیشن، معمولاً از Redis برای نگه‌داشتن شمارنده‌ها استفاده میشه، چون Redis سریع و atomic عمل میکنه و اگه چند instance از سرور اپلیکیشن داشته باشی، همه بتونن یک شمارنده‌ی مشترک رو ببینن (که با یک شمارنده‌ی in-memory جدا روی هر instance، امکانش نیست).

توی سطح API Gateway (مثل Kong، AWS API Gateway، یا Cloudflare)، rate limiting معمولاً به‌صورت آماده و قابل‌تنظیم از طریق پنل مدیریت وجود داره، بدون نیاز به نوشتن کد.

پاسخ درست به کلاینت رد شده

وقتی یک درخواست به‌خاطر rate limit رد میشه، باید کد وضعیت HTTP استاندارد 429 Too Many Requests رو برگردونی، و بهتره هدر Retry-After رو هم اضافه کنی تا کلاینت بدونه دقیقاً چند ثانیه دیگه باید صبر کنه قبل از تلاش مجدد. این جزئیات کوچیک، تفاوت بین یک API قابل‌اعتماد و یکی که کلاینت‌هاش رو گیج میکنه رو میسازه.

 

چطوری بهتره استفاده بشه ؟

Rate limiting را می‌توان در چند لایه مختلف پیاده‌سازی کرد؛ از لایه زیرساخت مثل Nginx و API Gateway گرفته تا داخل خود اپلیکیشن. انتخاب محل مناسب بستگی دارد به اینکه هدف اصلی، محافظت از منابع سیستم است یا کنترل رفتار کاربران و قوانین کسب‌وکار.

Rate Limiting در Nginx / Reverse Proxy

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

این روش برای جلوگیری از فشار زیاد، حملات حجمی، crawlerها و درخواست‌های غیرعادی بسیار مناسب است.

مزایا:

  • عملکرد بسیار بالا

  • کاهش فشار روی اپلیکیشن

  • عدم نیاز به تغییر کد برنامه

محدودیت اصلی آن این است که معمولاً فقط به اطلاعاتی مثل IP و Header دسترسی دارد و نمی‌تواند تفاوت بین کاربران، پلن‌ها یا سطح دسترسی آن‌ها را تشخیص دهد.

مثلاً نمی‌تواند به‌تنهایی تصمیم بگیرد که یک کاربر رایگان فقط ۱۰۰ درخواست در دقیقه داشته باشد و یک کاربر حرفه‌ای ۱۰۰۰۰ درخواست.

Rate Limiting در Middleware اپلیکیشن

در این روش محدودیت داخل خود برنامه اعمال می‌شود. چون اپلیکیشن اطلاعات بیشتری درباره کاربر دارد، می‌تواند قوانین دقیق‌تری بر اساس User ID، API Key، نقش کاربر یا نوع اشتراک اعمال کند.

برای مثال، یک سرویس هوش مصنوعی می‌تواند برای کاربران رایگان محدودیت ۱۰ درخواست در روز و برای کاربران پولی محدودیت ۱۰۰۰ درخواست در روز قرار دهد.

مزایا:

  • دسترسی به اطلاعات کامل کاربر

  • مناسب برای قوانین کسب‌وکار

  • انعطاف‌پذیری بالا

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

برای مثال اگر یک کاربر ۱۰۰ درخواست ارسال کند و Load Balancer آن را بین دو سرور تقسیم کند، هر سرور ممکن است فقط ۵۰ درخواست ببیند و محدودیت به‌درستی اعمال نشود.

به همین دلیل معمولاً از Redis یا یک ذخیره‌ساز مشترک برای نگهداری وضعیت rate limit استفاده می‌شود.

Rate Limiting در API Gateway

API Gateway یک لایه مدیریتی بین کلاینت و سرویس‌های مختلف قرار می‌گیرد و معمولاً برای معماری‌های بزرگ و چندسرویسی استفاده می‌شود.

این لایه می‌تواند محدودیت‌های مشترکی برای تمام APIها اعمال کند، API Key مدیریت کند، quota تعریف کند و سیاست‌های امنیتی را به‌صورت متمرکز اجرا کند.

مزایا:

  • مناسب برای Microserviceها

  • مدیریت متمرکز APIها

  • پشتیبانی از API Key و quota

در مقابل، پیچیدگی و هزینه بیشتری نسبت به Nginx یا middleware ساده دارد و برای پروژه‌های کوچک ممکن است بیش از نیاز باشد.

بهترین رویکرد: ترکیبی از چند لایه

در سیستم‌های واقعی معمولاً از یک روش واحد استفاده نمی‌شود، بلکه چند لایه با هم کار می‌کنند.

یک معماری رایج:

Client

   |
   v

Cloudflare / WAF

   |
   v

Nginx
(IP Rate Limit)

   |
   v

Application
(User/API Key Limit)

   |
   v

Redis
(Shared Counter)

   |
   v

Database

در این معماری:

  • Nginx جلوی فشار غیرعادی روی زیرساخت را می‌گیرد.

  • Middleware اپلیکیشن محدودیت‌های مربوط به کاربران و منطق کسب‌وکار را اجرا می‌کند.

  • Redis وضعیت rate limit را بین چند سرور هماهنگ نگه می‌دارد.

  • API Gateway در سیستم‌های بزرگ مدیریت متمرکز APIها را انجام می‌دهد.

مثال:

برای یک وب‌سایت بلاگی متوسط، یک نقطه شروع منطقی می‌تواند این باشد:

  • محدودیت عمومی در Nginx:
    حدود 50 تا 100 درخواست در ثانیه برای هر IP
  • محدودیت در سطح کاربر:
    حدود 1000 تا 3000 درخواست در ساعت که مثلا از ۱۰۰۰ تا برای مهمان و ۳۰۰۰ برای بالاترین سطح دسترسی قبل از ادمین
  • endpointهای حساس:
    بین 3 تا 10 درخواست در دقیقه

اما این اعداد قطعی نیستند. یک وب‌سایت خبری با میلیون‌ها بازدید روزانه، یک وبلاگ شخصی و یک SaaS با API عمومی، نیازهای کاملاً متفاوتی دارند.

عدد صحیح زمانی مشخص می‌شود که رفتار واقعی کاربران، میانگین درخواست‌ها، زمان پاسخ API، ظرفیت سرور و مصرف منابع بررسی شود. Rate limiting باید بر اساس ظرفیت سیستم تنظیم شود، نه صرفاً بر اساس یک عدد از پیش تعیین‌شده.

جمع‌بندی

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

cuid و cuid2: چرا نسخه‌ی دوم عملاً از صفر بازنویسی شد نوشته شده توسط عرفان دهقانی

cuid و cuid2: چرا نسخه‌ی دوم عملاً از صفر بازنویسی شد

وقتی میخوای برای رکوردهای دیتابیست یک شناسه‌ی یکتا بسازی، احتمالاً اول به auto increment فکر میکنی، بعد به UUID، و اگه دنبال چیزی سبک‌تر و url-friendly تر باشی، به cuid میرسی. ولی نکته‌ای که خیلی وقت‌ها گم میشه اینه که cuid و cuid2 دو چیز کاملاً متفاوتن — نه یک آپدیت جزئی، بلکه یک بازطراحی کامل با فرض‌های امنیتی متفاوت.

cuid اصلی چی بود

cuid (نسخه‌ی اول) حدود سال ۲۰۱۲ ساخته شد تا یک جایگزین برای auto-increment ID و UUID باشه که هم یکتا باشه، هم قابل مرتب‌سازی بر اساس زمان تولید (chronologically sortable)، و هم برای استفاده توی URL راحت‌تر از UUID باشه (چون کوتاه‌تره و کاراکترهای عجیب مثل خط‌تیره‌ی اجباری UUID رو نداره).

ساختار یک cuid از چند بخش تشکیل شده: یک پیشوند ثابت (c)، timestamp، یک شمارنده (counter)، اطلاعاتی از fingerprint ماشین و پردازش (که شامل چیزهایی مثل هاست‌نیم و شناسه‌ی پردازش میشد)، و یک بخش تصادفی. مثال یک cuid واقعی این شکلیه:

cljk3f2j40000qzrmn831i7rn

چرا cuid کنار گذاشته شد

مشکل اصلی cuid وقتی خودش رو نشون داد که مسائل امنیتی جدی‌تری مطرح شد. چون بخشی از cuid از fingerprint ماشین (host/process info) و timestamp قابل پیش‌بینی ساخته میشه، این یعنی تا حدی میشه از روی خود ID، اطلاعاتی درباره‌ی زیرساخت سرور یا ترتیب دقیق تولید رکوردها استنباط کرد. علاوه بر این، چون بخش زمانی cuid قابل پیش‌بینیه، در برخی سناریوها میشه با حدس زدن IDها، رکوردهای مجاور رو enumerate کرد — دقیقاً همون چیزی که یک شناسه‌ی یکتا نباید اجازه بده.

مشکل دیگه، performance بود: تولید fingerprint ماشین و بعضی محاسبات cuid اصلی روی برخی محیط‌ها (مخصوصاً serverless که هر بار یک instance جدید بالا میاد) کند و ناسازگار عمل میکرد.

cuid2 چطور این مشکلات رو حل کرد

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

اول، امنیت رمزنگاری. cuid2 به‌جای تکیه به fingerprint قابل‌پیش‌بینی ماشین، از یک تابع هش امن (SHA3) روی ترکیبی از entropy تصادفی، timestamp، counter، و یک مقدار fingerprint ضعیف‌تر و کم‌اهمیت‌تر استفاده میکنه. نتیجه‌ی نهایی یک رشته‌ست که از نظر آماری غیرقابل‌پیش‌بینیه — یعنی حتی اگه بدونی الگوریتم چطور کار میکنه، نمیتونی ID بعدی رو حدس بزنی یا از روی یک ID موجود، ID های مجاورش رو enumerate کنی.

دوم، طول متغیر. cuid اصلی طول ثابتی داشت، ولی cuid2 به‌صورت پیش‌فرض طول قابل تنظیم داره (پیش‌فرض ۲۴ کاراکتر) و میتونی بین ۴ تا ۳۲ کاراکتر تنظیمش کنی، بسته به این‌که چقدر فضای نام یا خوانایی برات مهمه.

سوم، شروع با حرف. هر cuid2 با یک حرف (نه عدد) شروع میشه، که این یک نکته‌ی فنی مهمه: بعضی سیستم‌ها و زبان‌های برنامه‌نویسی وقتی یک شناسه با عدد شروع بشه، ممکنه اون رو به‌اشتباه به‌عنوان عدد تفسیر کنن (مثلاً توی نام متغیر جاوااسکریپت یا بعضی زبان‌های کوئری). با اجباری‌کردن شروع با حرف، cuid2 این کلاس از باگ‌ها رو کامل حذف میکنه.

چهارم، همچنان قابل مرتب‌سازی تقریبی بر اساس زمانه (نه دقیق مثل ULID، ولی به‌اندازه‌ی کافی برای اکثر کاربردهای عملی)، بدون این‌که این ترتیب، اطلاعات حساس رو لو بده.

مقایسه‌ی مستقیم

ویژگیcuidcuid2
الگوریتمfingerprint + timestamp + counterSHA3 هش روی entropy تصادفی
قابل پیش‌بینی بودننسبتاً بله (ریسک امنیتی)خیر
طولثابتمتغیر (پیش‌فرض ۲۴، قابل تنظیم ۴ تا ۳۲)
شروع رشتهحرف c ثابتهمیشه یک حرف تصادفی
عملکرد در serverlessضعیف‌تربهینه‌شده
نگهداری پروژهمتوقف شدهفعال

مثال کد

استفاده از cuid2 توی جاوااسکریپت خیلی ساده‌ست:

import { createId } from '@paralleldrive/cuid2';

const id = createId();
// مثال خروجی: tz4a98xxat96iws9zmbrgj3a

// اگه بخوای طول متفاوتی داشته باشی
import { init } from '@paralleldrive/cuid2';
const createShortId = init({ length: 10 });
const shortId = createShortId();

توی پروژه‌های Prisma هم، تعریف یک فیلد با cuid2 به این شکله:

model User {
  id String @id @default(cuid2())
}

کِی از هرکدوم استفاده کنیم

اگه پروژه‌ی جدیدی شروع میکنی، عملاً هیچ دلیلی برای انتخاب cuid اصلی وجود نداره — نگهداریش متوقف شده و مشکلات امنیتیش شناخته‌شده‌ست. سراغ cuid2 برو، مگر این‌که نیازهای خاص‌تری داشته باشی: اگه به ترتیب دقیق زمانی (نه فقط تقریبی) نیاز داری، ULID انتخاب بهتریه؛ اگه سازگاری با استانداردهای جهانی و اکوسیستم‌های قدیمی‌تر مهمه، UUID (به‌خصوص نسخه‌ی ۷ که خودش هم قابل مرتب‌سازی زمانیه) گزینه‌ی امن‌تریه. ولی برای اکثر پروژه‌های وب مدرن که شناسه‌ی کوتاه، url-friendly، و امن میخوان، cuid2 دقیقاً همون‌جاییه که باید بری.

OpenAI قیمت GPT-5.6 رو شکست؛ دوران بی‌حساب‌وکتاب مصرف توکن داره تموم میشه نوشته شده توسط عرفان دهقانی

OpenAI قیمت GPT-5.6 رو شکست؛ دوران بی‌حساب‌وکتاب مصرف توکن داره تموم میشه

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

اعداد دقیق

خانواده‌ی GPT-5.6 از سه مدل تشکیل شده: Sol (پرچمدار)، Terra (رده‌ی میانی)، و Luna (سبک‌ترین و ارزون‌ترین نسخه). قیمت Luna، که پایین‌ترین رده‌ست، ۸۰ درصد کاهش پیدا کرده: از $1 به $0.20 روی هر میلیون توکن ورودی، و از $6 به $1.20 روی خروجی. Terra هم ۲۰ درصد ارزون‌تر شده: ورودی از $2.50 به $2 روی هر میلیون توکن، و خروجی از $15 به $12. قیمت Sol، مدل اصلی، بدون تغییر مونده.

این کاهش قیمت هم روی API اعمال شده و هم روی حساب‌های سازمانی مثل ChatGPT Work و Codex، یعنی استفاده از Luna و Terra حالا سهمیه‌ی کمتری مصرف میکنه، بدون این‌که قیمت اشتراک ماهانه تغییر کنه.

چرا این اتفاق افتاد

OpenAI این کاهش قیمت رو نتیجه‌ی بهینه‌سازی‌های گسترده در کل زیرساختش معرفی کرده: مسیریابی بهتر روی سخت‌افزار، نرم‌افزار inference بهینه‌تر، و الگوریتم‌های context-caching هوشمندتر که جلوی تکرار محاسبات قبلاً انجام‌شده رو توسط agent ها میگیره. نکته‌ی جالب‌تر اینه که بخشی از این بهینه‌سازی رو خود مدل Sol انجام داده — طبق گزارش OpenAI، این مدل به‌صورت خودکار kernel های تولیدی (production code kernels) رو بازنویسی کرده و همین کار به‌تنهایی حدود ۲۰ درصد از هزینه‌ی سرویس‌دهی رو کم کرده. این یعنی مدل داشته روی بهبود کارایی خودش کار میکرده، نه فقط روی پاسخ‌دادن به کاربر.

زمینه‌ی واقعی: فشار هزینه روی مشتری‌های سازمانی

پشت این تصمیم یک واقعیت بازار جدی‌تر هم هست. طبق گزارش CNBC، شرکت‌ها این روزها نسبت به هزینه‌های AI حساس‌تر از قبل شدن، چون خیلی از سازمان‌ها بدون تصویر روشنی از بازگشت سرمایه (ROI)، تمایلی به استقرار مدل‌های گرون‌قیمت ندارن. دوران اولیه‌ی ChatGPT رو بهش میگفتن دوران «tokenmaxxing» — جایی که کارفرماها کارمندها رو تشویق میکردن هرچقدر میخوان از AI استفاده کنن، بدون نگرانی درباره‌ی هزینه. حالا این دوران داره جای خودش رو به یک رویکرد حساب‌شده‌تر میده، و سم آلتمن، مدیرعامل OpenAI، اخیراً هزینه رو «یک مسئله‌ی بزرگ» توصیف کرده بود.

فشار رقابتی از سمت استارتاپ‌های چینی و رقبای بزرگ‌تر مثل مایکروسافت و گوگل هم نقش داره — همه‌شون دارن مدل‌های ارزون‌تر برای مشتری‌های سازمانی معرفی میکنن.

Fast mode جدید برای Sol

همراه با این کاهش قیمت، OpenAI یک قابلیت جدید هم برای Sol معرفی کرده به اسم Fast mode، که به‌جای Priority Processing قبلی نشسته. این حالت تا ۲.۵ برابر سرعت پردازش استاندارد رو ارائه میده، ولی با دو برابر قیمت معمول. یعنی برای کاربردهایی که تأخیر (latency) بحرانیه — مثل agent هایی که باید بلادرنگ جواب بدن — یک گزینه‌ی صریح برای «سرعت در ازای هزینه‌ی بیشتر» وجود داره، به‌جای این‌که مجبور باشی کل ساختار قیمت‌گذاری رو حدس بزنی.

مقایسه با رقبا

نکته‌ای که گزارش رویترز بهش اشاره کرده جالبه: با قیمت جدید، Terra حالا حتی از Claude Sonnet 4.6 آنتروپیک هم ارزون‌تر تموم میشه — Sonnet 4.6 روی $3 ورودی و $15 خروجی قیمت‌گذاری شده، درحالی‌که Terra بعد از کاهش قیمت روی $2 و $12 قرار گرفته. این یعنی رقابت قیمتی بین لابراتوارهای بزرگ AI داره واقعی‌تر و ملموس‌تر میشه، نه فقط روی کاغذ.

چرا این برای دولوپرها مهمه

اگه داری یک pipeline خودکار میسازی که حجم بالایی از توکن مصرف میکنه — مثل پردازش batch داده، تولید محتوا، یا یک agent که مدام درخواست میفرسته — این کاهش قیمت مستقیماً روی صورت‌حساب ماهانه‌ات اثر میذاره، بدون این‌که مجبور باشی کدی رو تغییر بدی. تنها کاری که لازمه، اطمینان از این‌که مدل درستی (Luna برای کارهای سبک، Terra برای کارهای متوسط) رو برای هر بخش از workflow انتخاب کرده باشی، دقیقاً همون فلسفه‌ای که این چند وقت پشت تنظیمات effort در مدل‌های دیگه هم دیدیم: هزینه رو متناسب با واقعی‌بودن نیاز کار تنظیم کن، نه یک انتخاب یکسان برای همه‌چیز.

جمع‌بندی

این کاهش قیمت فقط یک تخفیف تجاری ساده نیست؛ نشونه‌ایه از این‌که رقابت بین مدل‌های AI داره وارد فاز جدیدی میشه که در اون کارایی و هزینه به‌اندازه‌ی هوش خام مهم شدن. برای کسی که روی این مدل‌ها پروژه میسازه، این خبر خوبیه: همون کیفیت، با هزینه‌ی کمتر. برای کل صنعت، این نشونه‌ی این‌که دوران بی‌حساب‌وکتاب مصرف توکن داره به سمت یک بازار بالغ‌تر و حساس‌تر به هزینه حرکت میکنه.

Elasticsearch: قدرتمند، سنگین، و نه همیشه انتخاب درست نوشته شده توسط عرفان دهقانی

Elasticsearch: قدرتمند، سنگین، و نه همیشه انتخاب درست

خیلی وقت‌ها یک پروژه فقط به یک جستجوی ساده نیاز داره — کاربر یک کلمه تایپ میکنه و باید سریع نتیجه ببینه. اولین گزینه‌ای که معمولاً به ذهن میاد Elasticsearch است، چون اسمش همه‌جا هست و مستنداتش زیاده. ولی قبل از این‌که سراغش بری، ارزش داره بدونی این ابزار برای چه مقیاسی ساخته شده، چقدر منابع میخواد، و کِی یک جایگزین سبک‌تر تصمیم بهتریه.

Elasticsearch واقعاً چیه

Elasticsearch یک موتور جستجو و آنالیز توزیع‌شده‌ست که روی Apache Lucene ساخته شده. کارش فقط «پیدا کردن یک رشته توی متن» نیست؛ یک سیستم کامله برای indexing متن با امتیازدهی relevance، فیلتر کردن روی فیلدهای ساختاریافته، اجرای aggregation های پیچیده (مثل «میانگین قیمت به تفکیک دسته‌بندی»)، و مقیاس‌پذیری روی چند نود همزمان. برای همینه که توی سیستم‌های لاگ‌مانیتورینگ (مثل استک ELK)، جستجوی محصولات فروشگاه‌های بزرگ، و تحلیل داده‌ی حجیم این‌قدر رایجه.

چرا سنگین حساب میشه

مشکل اصلی Elasticsearch این نیست که کند باشه؛ مشکل اینه که برای اجرا شدن، منابع قابل توجهی میخواد. چون روی JVM (جاوا) اجرا میشه، حداقل نیاز به چند صد مگابایت تا چند گیگابایت heap memory داره، حتی برای یک نود توسعه‌ای کوچیک. توصیه‌ی رسمی برای یک نود production معمولاً از ۲ تا ۴ گیگابایت رم شروع میشه و برای دیتاست‌های بزرگ‌تر به‌سرعت بالا میره. علاوه بر این، Elasticsearch برای کار درست نیازمند تنظیمات سیستم‌عاملیه (مثل افزایش vm.max_map_count روی لینوکس)، و راه‌اندازی یک کلاستر واقعی (با نودهای master و data جدا) خودش یک پروژه‌ی مجزاست، نه یک نصب پنج‌دقیقه‌ای.

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

Meilisearch و رقبای سبک‌تر

Meilisearch یک موتور جستجوی متن‌باز است که با Rust نوشته شده و از ابتدا برای سادگی و سرعت راه‌اندازی طراحی شده، نه برای مقیاس عظیم. تفاوتش با Elasticsearch از همون لحظه‌ی نصب معلومه: یک باینری تک‌فایلی که بدون JVM، بدون تنظیمات پیچیده، و با مصرف رم بسیار پایین‌تر (معمولاً چند ده مگابایت برای دیتاست‌های متوسط) اجرا میشه. جستجوش هم به‌صورت پیش‌فرض typo-tolerant است — یعنی حتی اگه کاربر کلمه رو غلط تایپ کنه، نتیجه‌ی درست رو پیدا میکنه — بدون این‌که نیاز به تنظیم دستی داشته باشه.

Typesense هم رقیب مشابهیه، با فلسفه‌ی تقریباً یکسان: نصب ساده، پاسخ سریع، API قابل‌فهم. هر دو این ابزارها برای autocomplete و جستجوی محصول توی فروشگاه‌های کوچیک تا متوسط، مستندات، یا هر جایی که کاربر انتظار جواب فوری داره، انتخاب‌های خیلی خوبی هستن.

اما این سادگی رایگان نیست. تریدآف اصلی اینه که Meilisearch و Typesense قابلیت‌های aggregation پیچیده‌ای که Elasticsearch داره رو ندارن — یعنی اگه نیازت فقط جستجوی متنیه، عالی جواب میدن، ولی اگه میخوای همزمان تحلیل آماری روی میلیون‌ها رکورد لاگ انجام بدی یا dashboard های پیچیده‌ی مانیتورینگ بسازی (کاری که Kibana روی Elasticsearch انجام میده)، این ابزارهای سبک‌تر برات کافی نیستن. خلاصه‌اش اینه: اگه مسئله‌ات «جستجو»ست، سبک‌ترها رو انتخاب کن؛ اگه مسئله‌ات «جستجو + آنالیز حجم عظیم داده»ست، سراغ Elasticsearch برو.

نصب Elasticsearch با Docker

راحت‌ترین و تمیزترین راه برای بالا آوردن Elasticsearch، به‌خصوص برای توسعه یا تست، استفاده از Docker است، چون همه‌ی وابستگی‌ها (شامل خود JVM) داخل ایمیج پکیج شدن و مجبور نیستی هیچی رو دستی روی سیستم نصب کنی.

docker run -d --name elasticsearch \
  -p 9200:9200 -p 9300:9300 \
  -e "discovery.type=single-node" \
  -e "xpack.security.enabled=false" \
  -e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
  docker.elastic.co/elasticsearch/elasticsearch:8.15.0

اگه بخوای همراهش Kibana هم بالا بیاری (برای مشاهده‌ی گرافیکی داده‌ها)، بهتره از docker-compose استفاده کنی:

version: "3.8"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.15.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data

  kibana:
    image: docker.elastic.co/kibana/kibana:8.15.0
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch

volumes:
  es_data:

بعد از اجرای docker compose up -d، میتونی با curl localhost:9200 مطمئن بشی سرویس بالا اومده.

نصب بدون Docker

اگه بخوای مستقیم روی یک سرور لینوکسی (مثلاً Ubuntu) نصبش کنی، مراحل کمی بیشتره چون باید repository رسمی Elastic رو اضافه کنی:

# اضافه‌کردن کلید و ریپازیتوری رسمی
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list

sudo apt update
sudo apt install elasticsearch

# تنظیم حداقل حافظه‌ی JVM (توصیه: نصف رم سرور، حداکثر ۳۱ گیگ)
sudo nano /etc/elasticsearch/jvm.options.d/heap.options
# داخلش: -Xms2g و -Xmx2g

sudo systemctl enable elasticsearch
sudo systemctl start elasticsearch

همچنین لازمه تنظیم vm.max_map_count رو دستی انجام بدی، چون Elasticsearch برای مدیریت حافظه‌ی داخلیش به مقدار بالاتری از پیش‌فرض لینوکس نیاز داره:

sudo sysctl -w vm.max_map_count=262144

کدوم روش رو انتخاب کنیم

برای توسعه، تست محلی، یا حتی خیلی از سناریوهای production کوچیک تا متوسط، Docker (یا بهتر، docker-compose همراه با volume برای persistence) گزینه‌ی پیشنهادیه. مزیتش اینه که آپدیت نسخه، ری‌استارت، و پاک‌سازی کامل محیط، یک دستور بیشتر نیست. نصب مستقیم روی سیستم‌عامل رو معمولاً وقتی توصیه میکنن که تیم زیرساخت از قبل روی مدیریت سرویس‌های systemd و مانیتورینگ سطح OS تجربه داره، یا محدودیت سازمانی برای استفاده از Docker وجود داره. برای اکثر تیم‌ها، مسیر Docker هم سریع‌تره هم قابل‌تکرارتر (reproducible)، پس معمولاً انتخاب اول همینه.

نوشتن Query در Elasticsearch

Elasticsearch با یک زبان JSON-based به اسم Query DSL کار میکنه. ساده‌ترین حالت، جستجوی متنی روی یک فیلد است:

GET /products/_search
{
  "query": {
    "match": {
      "title": "کفش ورزشی"
    }
  }
}

اگه بخوای همزمان فیلتر دقیق (مثل بازه‌ی قیمت) رو هم اعمال کنی، از bool query استفاده میکنی که چند شرط رو با هم ترکیب میکنه:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "کفش ورزشی" } }
      ],
      "filter": [
        { "range": { "price": { "gte": 500000, "lte": 2000000 } } }
      ]
    }
  }
}

نکته‌ی مهم اینجا فرق must و filter است: بخش must روی امتیاز relevance تأثیر میذاره (یعنی نتایج بر اساس میزان تطابق رتبه‌بندی میشن)، ولی filter فقط true/false هست و روی امتیاز اثر نداره — و چون کش میشه، سریع‌تر هم اجرا میشه. برای شرط‌هایی مثل بازه‌ی قیمت یا وضعیت موجودی که فقط باید فیلتر کنن نه رتبه‌بندی، همیشه از filter استفاده کن.

برای aggregation، یعنی محاسبات آماری روی نتایج، مثلاً میانگین قیمت به تفکیک برند:

GET /products/_search
{
  "size": 0,
  "aggs": {
    "avg_price_by_brand": {
      "terms": { "field": "brand.keyword" },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

این همون قابلیتیه که Elasticsearch رو از یک موتور جستجوی ساده فاصله میده و بهش اجازه میده هم‌زمان نقش یک ابزار آنالیز داده رو هم بازی کنه.

جستجو روی چند فیلد همزمان (multi_match)

وقتی میخوای یک عبارت رو همزمان توی چند فیلد بگردی — مثلاً هم توی عنوان و هم توی توضیحات محصول — به‌جای چند تا match جدا، از multi_match استفاده کن:

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "کفش دویدن",
      "fields": ["title^2", "description"]
    }
  }
}

عدد ^2 کنار title یعنی تطابق توی این فیلد دو برابر وزن بیشتری توی محاسبه‌ی relevance داره — یعنی اگه کلمه توی عنوان پیدا بشه، نسبت به پیدا شدن توی توضیحات، امتیاز بالاتری میگیره.

جستجوی مقاوم به غلط تایپی (fuzzy)

اگه بخوای حتی وقتی کاربر کلمه رو اشتباه تایپ کرده جواب پیدا کنی:

GET /products/_search
{
  "query": {
    "match": {
      "title": {
        "query": "کتونی",
        "fuzziness": "AUTO"
      }
    }
  }
}

fuzziness: AUTO به Elasticsearch اجازه میده بر اساس طول کلمه، تا یک یا دو حرف اختلاف رو نادیده بگیره.

جستجوی wildcard و prefix

برای موقعیت‌هایی مثل autocomplete که فقط بخشی از کلمه رو داری:

GET /products/_search
{
  "query": {
    "prefix": {
      "sku.keyword": "SHOE-2024"
    }
  }
}

wildcard انعطاف بیشتری میده (میتونی از * و ? استفاده کنی) ولی روی دیتاست بزرگ کندتره، پس فقط وقتی لازمه که prefix یا match جواب نمیده:

GET /products/_search
{
  "query": {
    "wildcard": {
      "sku.keyword": "SHOE-*-RED"
    }
  }
}

ترکیب چند شرط با bool کامل (must, should, must_not, filter)

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

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "کفش" } }
      ],
      "should": [
        { "term": { "brand.keyword": "Nike" } },
        { "term": { "brand.keyword": "Adidas" } }
      ],
      "must_not": [
        { "term": { "material.keyword": "leather" } }
      ],
      "filter": [
        { "term": { "in_stock": true } }
      ]
    }
  }
}

چک کردن وجود یک فیلد (exists)

مفید برای پیدا کردن رکوردهایی که یک فیلد خاص توشون پر شده یا خالیه — مثلاً محصولاتی که تخفیف دارن:

GET /products/_search
{
  "query": {
    "exists": {
      "field": "discount_percent"
    }
  }
}

مرتب‌سازی و صفحه‌بندی (sort و pagination)

GET /products/_search
{
  "query": { "match": { "title": "کفش" } },
  "sort": [
    { "price": "asc" },
    "_score"
  ],
  "from": 20,
  "size": 10
}

اینجا نتایج اول بر اساس قیمت صعودی مرتب میشن و در صورت تساوی قیمت، بر اساس امتیاز relevance. from و size هم صفحه‌ی سوم رو برمیگردونن (فرض بر ۱۰ آیتم در هر صفحه).

هایلایت کردن کلمه‌ی جستجوشده در نتیجه

وقتی میخوای مثل گوگل، بخشی از متن که با جستجو مطابقت داشته رو بولد نشون بدی:

GET /articles/_search
{
  "query": { "match": { "content": "ریپلیکیشن دیتابیس" } },
  "highlight": {
    "fields": {
      "content": {
        "pre_tags": ["<mark>"],
        "post_tags": ["</mark>"]
      }
    }
  }
}

جستجوی جغرافیایی (geo_distance)

پیدا کردن همه‌ی موارد نزدیک به یک نقطه‌ی مشخص، مثلاً رستوران‌های نزدیک یک مختصات توی شعاع ۲ کیلومتر:

GET /restaurants/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_distance": {
          "distance": "2km",
          "location": {
            "lat": 35.6892,
            "lon": 51.3890
          }
        }
      }
    }
  }
}

اگریگیشن بر اساس بازه‌ی زمانی (date_histogram)

مفید برای dashboard هایی که میخوان نشون بدن مثلاً تعداد سفارش‌ها به تفکیک روز چطور تغییر کرده:

GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_per_day": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "day"
      }
    }
  }
}

جستجوی برداری ساده (kNN)

برای سیستم‌هایی که روی embedding کار میکنن (مثلاً جستجوی معنایی به‌جای جستجوی کلمه‌به‌کلمه):

GET /articles/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [0.021, -0.13, 0.045, "..."],
    "k": 5,
    "num_candidates": 50
  }
}

اینجا k تعداد نزدیک‌ترین نتایجی که میخوای برگرده رو مشخص میکنه، و num_candidates تعداد کاندیدهایی که Elasticsearch قبل از انتخاب k تای نهایی بررسی میکنه — عدد بزرگ‌تر یعنی دقت بالاتر ولی سرعت پایین‌تر.

قابلیت‌های اضافه‌ای که کمتر دیده میشن

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

geo queries به تو اجازه میدن جستجوی مکانی انجام بدی — مثلاً «همه‌ی رستوران‌های نزدیک این مختصات جغرافیایی توی شعاع ۲ کیلومتر». این برای اپلیکیشن‌های مبتنی بر موقعیت مکانی حیاتیه و بدون ابزار جانبی مستقیم پشتیبانی میشه.

kNN search یا جستجوی برداری، قابلیتیه که این چند سال با رشد هوش مصنوعی خیلی مهم شده: میتونی به‌جای جستجوی متنی، بردارهای embedding (خروجی مدل‌های زبانی) رو ایندکس کنی و نزدیک‌ترین بردارها به یک query رو پیدا کنی — دقیقاً همون چیزی که پشت سیستم‌های RAG (retrieval-augmented generation) قرار داره.

percolate query برعکس جستجوی معمولیه: به‌جای این‌که یک query رو روی مجموعه‌ای از داده اجرا کنی، یک داده‌ی جدید رو روی مجموعه‌ای از query های از قبل ذخیره‌شده تست میکنی — مفید برای سیستم‌های هشدار و فیلترینگ که باید بفهمن یک رکورد جدید با کدوم شرط‌های ازپیش‌تعریف‌شده مطابقت داره.

و در نهایت، ecosystem کامل ELK/Elastic Stack — یعنی Kibana برای visualization، Logstash و Beats برای جمع‌آوری و انتقال داده — چیزیه که Elasticsearch رو از یک دیتابیس جستجو به یک پلتفرم کامل observability تبدیل میکنه، که این یکی از دلایل اصلیه که با وجود سنگین بودنش، توی محیط‌های enterprise همچنان انتخاب اول میمونه.

جمع‌بندی

Elasticsearch ابزار درستیه وقتی هم جستجوی متنی پیچیده و هم آنالیز حجم بالای داده رو همزمان نیاز داری. ولی اگه فقط دنبال یک جستجوی سریع و typo-tolerant برای سایت یا اپلیکیشنت هستی، قبل از این‌که وقت بذاری برای راه‌اندازی یک کلاستر Elasticsearch، حتماً Meilisearch یا Typesense رو هم امتحان کن — احتمالاً همون کاری که میخوای رو با یک‌دهم پیچیدگی انجام میدن.