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 رو هم امتحان کن — احتمالاً همون کاری که میخوای رو با یکدهم پیچیدگی انجام میدن.