mobiniqow 212074c5f5
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled
start project
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30
2026-09-04 12:52:21 +03:30

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

یک وب‌اپلیکیشن کامل برای جستجو و فیلتر روی دیتاست پروفایل‌های لینکدین؛ بک‌اند Django/DRF، فرانت‌اند React/TypeScript، موتور جستجو Elasticsearch (با fallback روی PostgreSQL Full-Text Search) و مانیتورینگ با استک ELK — همه به‌صورت داکرایز‌شده و قابل اجرا با یک دستور.

این پروژه پاسخ تسک آزمایشی «متخصص فول‌استک» مؤسسه Cyberyan است.


فهرست


اجرای سریع

پیش‌نیاز: فقط Docker و Docker Compose (نسخه ۲ به بالا). هیچ چیز دیگری لازم نیست.

git clone <repo> && cd cyberyan-linkedin-search

cp .env.example .env          # مقادیر پیش‌فرض برای اجرای محلی آماده است
docker compose up --build     # یا: make up

اولین اجرا حدود ۳ تا ۵ دقیقه طول می‌کشد (بیلد ایمیج‌ها + بالا آمدن Elasticsearch). کانتینر بک‌اند به‌صورت خودکار:

  1. منتظر آماده شدن Postgres و Elasticsearch می‌ماند،
  2. مایگریشن‌ها را اجرا می‌کند،
  3. دیتاست CSV را ایمپورت می‌کند (۲۸۴ پروفایل یکتا)،
  4. ایندکس Elasticsearch را می‌سازد،
  5. سه کاربر نمونه می‌سازد.

سپس:

سرویس آدرس
وب‌اپلیکیشن (SPA) http://localhost:8080
مستندات تعاملی API (Swagger) http://localhost:8080/api/docs/
مستندات ReDoc http://localhost:8080/api/redoc/
پنل ادمین جنگو http://localhost:8080/admin/
Kibana http://localhost:5601

کاربران نمونه (از .env خوانده می‌شوند):

ایمیل رمز نقش دسترسی
admin@example.com Admin-Passw0rd! admin همه‌چیز + بازسازی ایندکس + دیدن اطلاعات تماس
analyst@example.com Analyst-Passw0rd! analyst سرچ + دیدن اطلاعات تماس
viewer@example.com Viewer-Passw0rd! viewer سرچ، اطلاعات تماس ماسک‌شده

در محیط واقعی حتماً DJANGO_SECRET_KEY، POSTGRES_PASSWORD و رمزهای نمونه را عوض کنید. فایل .env.example دستور تولید کلید امن را هم دارد.

دستورهای پرکاربرد

make help           # فهرست همه دستورها
make up             # بالا آوردن کل استک
make dev            # اجرا با hot-reload (Vite روی :5173، runserver روی :8000)
make down           # خاموش کردن
make clean          # خاموش کردن + پاک کردن volumeها
make logs           # دیدن لاگ همه سرویس‌ها
make seed           # ایمپورت مجدد دیتاست + بازسازی ایندکس
make reindex        # فقط بازسازی ایندکس Elasticsearch
make test           # اجرای تست‌های بک‌اند
make lint           # lint + type-check هر دو بخش
make kibana-setup   # ساخت data view لاگ‌ها در Kibana

اجرا بدون Elasticsearch

اگر می‌خواهید سبک‌تر اجرا کنید (مثلاً برای مرور سریع کد)، کافی است SEARCH_BACKEND=postgres را در .env بگذارید. در این حالت سرچ روی Full-Text Search خود PostgreSQL اجرا می‌شود و کل API و فرانت‌اند بدون هیچ تغییری کار می‌کنند. دلیل این طراحی در بخش سرچ توضیح داده شده است.


معماری کلی

                    ┌──────────────────────────────────────────────┐
   مرورگر  ────────▶│  nginx (frontend)   :8080                    │
                    │  • سرو کردن باندل React                       │
                    │  • پراکسی /api → backend  (بدون CORS)         │
                    └───────────────┬──────────────────────────────┘
                                    │
                    ┌───────────────▼──────────────────────────────┐
                    │  Django + DRF + gunicorn   :8000             │
                    │  ┌────────────────────────────────────────┐  │
                    │  │ apps.accounts  JWT / نقش‌ها             │  │
                    │  │ apps.profiles  مدل دامنه + ایمپورتر     │  │
                    │  │ apps.search    لایه سرچ (Strategy)      │  │
                    │  │ apps.core      لاگ، خطا، سلامت، throttle│  │
                    │  └────────────────────────────────────────┘  │
                    └───┬───────────┬──────────────┬───────────────┘
                        │           │              │
              ┌─────────▼──┐  ┌─────▼──────┐  ┌────▼──────────┐
              │ PostgreSQL │  │   Redis    │  │ Elasticsearch │
              │ منبع حقیقت │  │ cache +    │  │  ایندکس سرچ   │
              │ + FTS      │  │ throttle   │  └────┬──────────┘
              └────────────┘  └────────────┘       │
                        │                          │
                        │  لاگ JSON (async TCP)    │
                        └────────▶ Logstash ───────┘
                                       │
                                    Kibana :5601

اصول طراحی:

  • PostgreSQL منبع حقیقت است، Elasticsearch یک projection دورریختنی. ایندکس همیشه از دیتابیس قابل بازسازی است (make reindex)، پس از دست رفتنش فاجعه نیست.
  • لایه سرچ پشت یک interface است (SearchBackend). دو پیاده‌سازی دارد: Elasticsearch و PostgreSQL. ویو، سریالایزر و فرانت‌اند اصلاً نمی‌دانند کدام موتور جواب داده است.
  • ویوها نازک‌اند. اعتبارسنجی در سریالایزر، منطق رتبه‌بندی در بک‌اند سرچ، ماسک‌کردن داده حساس در لایه سریالایز.
  • هیچ اطلاعات تماسی وارد Elasticsearch نمی‌شود. ایمیل و شماره تلفن فقط در Postgres می‌مانند.

جزئیات کامل: docs/ARCHITECTURE.md


منطق سرچ و فیلترها

یک درخواست، سه بخش

هر درخواست سرچ از سه چیز تشکیل شده است:

بخش نقش تأثیر روی امتیاز
q (کلیدواژه) چه کسی «مرتبط» است ✅ امتیاز می‌دهد
فیلترها چه کسی اصلاً «واجد شرایط» است ❌ فقط حذف می‌کند
sort / صفحه‌بندی ترتیب و برش نتایج —

این تفکیک عمدی است: در Elasticsearch فیلترها در filter context قرار می‌گیرند، پس کش می‌شوند و روی scoring اثر نمی‌گذارند.

کلیدواژه چطور امتیاز می‌گیرد

{
  "bool": {
    "must": [{                          // باید جایی مچ شود
      "multi_match": {
        "query": "product manager",
        "type": "best_fields",
        "fields": ["full_name^6", "job_title^4", "skills^3.5", "company^2.5",
                   "headline^2", "past_companies^1.5", "industry^1.5",
                   "majors^1.5", "education", "location_name", "summary", "interests"],
        "minimum_should_match": "2<70%", // یک‌کلمه‌ای: اجباری. چندکلمه‌ای: ۷۰٪ کافی است
        "fuzziness": "AUTO"              // «pyhton» هم «python» را پیدا می‌کند
      }
    }],
    "should": [{                        // فقط امتیاز اضافه می‌کند
      "multi_match": { "type": "phrase", "fields": ["full_name^8", "job_title^6",
                       "skills^5", "company^4", "headline^3"], "boost": 4 }
    }],
    "filter": [ /* فیلترهای ساختاریافته - بدون امتیاز */ ]
  }
}

سه ایده پشت این کوئری:

  1. بوست هر فیلد یک تصمیم محصولی است. وقتی کاربر «سارا» را جستجو می‌کند، منظورش یک آدم است نه کسی که در خلاصه‌اش کلمه «سارا» آمده — پس full_name^6 در برابر summary بدون بوست.
  2. fuzziness: AUTO غلط تایپی را می‌بخشد، ولی prefix_length: 1 جلوی انفجار عبارت‌های بی‌ربط را می‌گیرد.
  3. بوست عبارت دقیق (phrase) باعث می‌شود «product manager» به‌عنوان یک عبارت، بالاتر از پروفایلی بیاید که این دو کلمه را جدا و بی‌ربط دارد.

فیلترها

پارامتر مثال معنی
skills skills=python,django مهارت‌ها
roles roles=engineering خانواده شغلی
seniority seniority=senior,manager سطح ارشدیت
countries countries=united states کشور
industries industries=banking صنعت کارفرمای فعلی
companies companies=garver نام کارفرمای فعلی
min_experience / max_experience min_experience=5 بازه سال‌های تجربه

قاعده ترکیب: مقادیر داخل یک فیلتر با OR و فیلترهای مختلف با AND ترکیب می‌شوند.

skills=python,django & countries=germany
  ⇒ (python OR django) AND (germany)

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

Facetها (شمارنده کنار هر فیلتر)

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

fallback روی PostgreSQL

بک‌اند دوم از tsvector وزن‌دار استفاده می‌کند که ایمپورتر آن را می‌سازد:

وزن فیلدها
A نام کامل
B عنوان شغلی، مهارت‌ها، شرکت‌ها
C صنعت، تحصیلات، موقعیت مکانی
D خلاصه

به‌علاوه یک pass با pg_trgm روی نام، تا غلط تایپی (jhon → john) هم کار کند.

چرا اصلاً این بک‌اند دوم وجود دارد؟ چون یک API سرچ که بدون Elasticsearch اصلاً بالا نمی‌آید شکننده است: تست‌ها به کلاستر نیاز پیدا می‌کنند، مرورکننده کد برای اجرای پروژه به کلاستر نیاز دارد، و یک قطعی ES کل محصول را می‌خواباند. با یک متغیر محیطی، سیستم روی حالت کم‌توان‌تر ولی سالم می‌رود.

توضیح کامل‌تر همراه با مثال کوئری‌ها: docs/SEARCH.md


ساختار ذخیره‌سازی داده

دیتاست خام، هر آدم را در یک ردیف بسیار پهن CSV نگه می‌دارد که چند ستونش خودشان لیست دیکشنری سریالایزشده‌اند. ذخیره کردن همان بلوب، فیلتر و aggregation را غیرممکن می‌کند؛ پس داده به پنج جدول نرمال شده است:

Company ◀──── Experience ────▶ Profile ◀──── Education ────▶ School
                                  │
                                  └──── ProfileSkill ────▶ Skill
جدول نکته طراحی
Profile ایندکس GIN روی search_vector و seniority_levels؛ ایندکس ترکیبی روی (کشور، خانواده شغلی)
Skill کلید یکتا slug است؛ «Team Building» و «team building» یک رکورد می‌شوند. profile_count denormalize شده تا مرتب‌سازی فیلترها ارزان باشد
Company / School یکتا بر اساس نام، با کش درون‌حافظه‌ای در ایمپورتر (بدون N+1)
ProfileSkill جدول واسط با position تا ترتیب اصلی مهارت‌ها حفظ شود

سه ستون متنی denormalize‌شده (skills_text، companies_text، education_text) روی Profile نگهداری می‌شوند، چون SearchVector جنگو نمی‌تواند از رابطه many-to-many عبور کند. نتیجه: به‌روزرسانی کل tsvector با یک کوئری UPDATE انجام می‌شود.

ایمپورتر idempotent است: کلید طبیعی، شناسه لینکدین (و در نبودش URL پروفایل) است، پس اجرای دوباره‌اش به‌جای تکرار داده، آن را به‌روز می‌کند.


کیفیت دیتاست ورودی

فایل CSV داده‌شده یک export واقعی و خراب است، نه یک CSV تمیز. اگر مستقیم به csv.DictReader بدهید، بی‌سروصدا داده‌ی غلط تولید می‌کند. چهار نقص مستقل در فایل هست:

نقص نشانه
واترمارک تزریق‌شده خیلی از ردیف‌ها با یک ستون اضافه شروع می‌شوند: H:\New folder\leak\...@BreachedData1 — همه‌چیز یک خانه به راست می‌لغزد
ستون‌های غایب اکثر رکوردها به‌جای مقدار خالی، فیلد اختیاری (مثل job_title_role) را اصلاً ننوشته‌اند — بقیه مقادیر به چپ می‌لغزند
کوتیشن شکسته چند مقدار summary کوتیشن escape‌نشده دارند و یک رکورد را بین چند خط فیزیکی می‌شکنند
دو export به هم چسبیده خط هدر یک بار دیگر وسط فایل تکرار شده و حدود ۴۰ نفر تکراری‌اند

اثر عملی: با خواندن ساده، فقط ۱۰ ردیف از ۳۲۳ کاملاً سالم‌اند.

راهکار: خواندن خودترمیم‌گر

هر ستون یک امضا دارد (linkedin_url باید شامل linkedin.com/in/ باشد، skills باید یک literal لیست پایتون باشد، location_geo باید شبیه lat,lon باشد و…). ماژول apps/profiles/dataset.py هر جفت (ستون، مقدار) را با این امضاها امتیاز می‌دهد و بعد یک الگوریتم هم‌ترازی به‌سبک Needleman–Wunsch اجرا می‌کند که دو عمل ویرایشی مجاز دارد:

  • رد کردن ستون — رکورد اصلاً این فیلد را ندارد؛
  • دور انداختن مقدار — یک تکه سرگردان که به هیچ ستونی تعلق ندارد.

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

$ docker compose exec backend python manage.py import_profiles --truncate
{
  "profiles_created": 284,   "skills": 2376,   "companies": 1371,  "schools": 610,
  "experiences": 2128,       "educations": 870,
  "physical_rows": 336,      "duplicate_headers": 1,   "repaired_rows": 16,
  "orphan_fragments": 12,    "duplicate_records": 39,  "discarded_values": 175,
  "records": 284,            "elapsed_seconds": 15.4
}

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

فیلد فقط ستون سطح‌بالا بعد از غنی‌سازی
job_title_role ۱۰۸ ۱۸۴
کارفرمای فعلی ۲۱۹ ۲۶۵

هفت تست اختصاصی (tests/test_dataset_repair.py) هر کدام یکی از این نقص‌ها را بازتولید می‌کنند تا رگرسیون در منطق ترمیم، بی‌صدا رد نشود.

جزئیات کامل: docs/DATA.md


API

پایه: /api/v1/ — مستندات تعاملی و همیشه به‌روز: http://localhost:8080/api/docs/

متد مسیر توضیح دسترسی
POST /auth/register/ ثبت‌نام (همیشه نقش viewer) عمومی
POST /auth/login/ گرفتن access/refresh token عمومی
POST /auth/refresh/ چرخش توکن عمومی
POST /auth/logout/ بلاک‌لیست کردن refresh token لاگین
GET /auth/me/ هویت و سطح دسترسی فعلی لاگین
GET /search/ سرچ کلیدواژه‌ای + فیلتر + facet لاگین
GET /search/suggest/ پیشنهاد تایپ‌آهسته (autocomplete) لاگین
POST /search/reindex/ بازسازی ایندکس فقط admin
GET /profiles/ لیست ساده پروفایل‌ها لاگین
GET /profiles/{id}/ پروفایل کامل + سوابق لاگین
GET /profiles/skills/ دیکشنری مهارت‌ها برای type-ahead لاگین
GET /health/live/ liveness probe عمومی
GET /health/ready/ readiness (چک DB و موتور سرچ) عمومی

نمونه

# ۱) لاگین
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login/ \
  -H 'Content-Type: application/json' \
  -d '{"email":"analyst@example.com","password":"Analyst-Passw0rd!"}' | jq -r .access)

# ۲) سرچ: کلیدواژه + دو فیلتر + مرتب‌سازی
curl -s -G http://localhost:8080/api/v1/search/ \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'q=leadership' \
  --data-urlencode 'skills=management,training' \
  --data-urlencode 'countries=united states' \
  --data-urlencode 'min_experience=10' \
  --data-urlencode 'sort=-experience' | jq
{
  "count": 41,  "page": 1,  "page_size": 20,  "total_pages": 3,
  "took_ms": 12, "backend": "elasticsearch",
  "results": [{
    "id": 118, "score": 14.82,
    "full_name": "joseph holland",
    "headline": "recruiting manager at garver",
    "job_title_role": "human_resources",
    "seniority_levels": ["manager"],
    "location_country": "united states",
    "years_experience": 12,
    "skills": ["recruiting", "leadership", "human resources"],
    "highlight": { "skills": ["<mark>leadership</mark>"] }
  }],
  "facets": {
    "skills":  [{ "key": "leadership", "count": 41 }, { "key": "management", "count": 33 }],
    "country": [{ "key": "united states", "count": 41 }]
  }
}

قالب یکسان خطاها

هر خطا — اعتبارسنجی، دسترسی، rate limit یا خطای پیش‌بینی‌نشده — دقیقاً یک شکل دارد:

{
  "error": {
    "code": "validation_error",
    "message": "The request payload failed validation.",
    "details": { "min_experience": ["min_experience cannot be greater than max_experience."] },
    "request_id": "9f2c1a7e8b4d4c0e"
  }
}

request_id همان مقداری است که در هدر X-Request-ID پاسخ برمی‌گردد؛ کپی کردنش در Kibana، کل ردپای آن درخواست را نشان می‌دهد.

مرجع کامل: docs/API.md — اسکیمای OpenAPI: docs/openapi.yaml


امنیت

لایه اقدام
احراز هویت JWT با عمر کوتاه (۱۵ دقیقه) + refresh چرخشی + بلاک‌لیست بعد از چرخش
رمز عبور هش Argon2، حداقل ۱۰ کاراکتر، اعتبارسنج‌های استاندارد جنگو
مجوزدهی پیش‌فرض deny؛ سه نقش (admin / analyst / viewer)؛ بازسازی ایندکس فقط admin
حریم خصوصی ایمیل و تلفن برای نقش viewer ماسک می‌شود و اصلاً وارد Elasticsearch نمی‌شود
Rate limiting سقف جدا برای anon / user / search / login (روی IP برای مسیرهای اعتبارسنجی)
ورودی همه پارامترها از سریالایزر رد می‌شوند؛ سقف طول کوئری، تعداد فیلترها و اندازه صفحه
هدرها HSTS، nosniff، X-Frame-Options: DENY، Referrer-Policy، CSP سخت‌گیرانه در nginx
XSS snippetهای highlight بدون dangerouslySetInnerHTML رندر می‌شوند
اسرار همه از متغیر محیطی؛ settings/prod.py اگر کلید ضعیف یا DEBUG=true ببیند بالا نمی‌آید
کانتینر کاربر غیر‌root، no-new-privileges، فایل‌سیستم read-only برای nginx، بیلد چندمرحله‌ای
شبکه Postgres/Redis/Elasticsearch روی شبکه‌ی internal داکر؛ فقط SPA و Kibana منتشر می‌شوند
CI bandit، pip-audit، npm audit و manage.py check --deploy

توضیح تصمیم‌ها و trade-offها (از جمله اینکه چرا احراز هویت Elasticsearch در حالت dev خاموش است): docs/SECURITY.md


مانیتورینگ و لاگ (ELK)

هر لاگ یک شیء JSON است، نه یک خط متن. هر رکورد request_id درخواستی که تولیدش کرده را حمل می‌کند، پس یک کلیک کاربر تا انتها قابل ردیابی است.

{"@timestamp":"2026-09-03T04:00:08Z","level":"INFO","logger":"audit.search",
 "event":"search_executed","request_id":"bb3c21c5","user_id":2,
 "backend":"postgres","result_count":80,"took_ms":31,
 "query":"leadership","filters":{"skills":["management"],"countries":["united states"]},
 "sort":"relevance","page":1,"page_size":3}

مسیر: Django → (TCP غیرمسدودکننده) → Logstash → Elasticsearch → Kibana

  • ارسال با python-logstash-async انجام می‌شود: بافر درون‌حافظه‌ای و ترد جدا، پس قطعی Logstash هیچ‌وقت یک ترد درخواست را بلاک نمی‌کند (این رفتار در همین ریپو تست شده: با خاموش بودن Logstash فقط یک warning می‌آید و API سالم می‌ماند).
  • pipeline لاگ‌ها را بر اساس نوع رویداد به ایندکس‌های جدا مسیردهی می‌کند: app-logs-search-*، app-logs-audit-*، app-logs-error-*، app-logs-app-* — پس «هر چیز قدیمی‌تر از ۳۰ روز را پاک کن» یک دستور حذف ایندکس است.
  • سرچ کندتر از ۵۰۰ms برچسب slow_search و سرچ بدون نتیجه برچسب zero_results می‌گیرد.
  • یک index template قالب‌بندی فیلدها را قفل می‌کند تا Kibana مجبور به حدس زدن نوع داده نشود.

make kibana-setup را اجرا کنید تا data view ساخته شود، بعد در Discover:

event: "search_executed" and result_count: 0     # کوئری‌هایی که چیزی پیدا نکردند
event: "search_executed" and took_ms > 200       # سرچ‌های کند
level: "ERROR"                                   # خطاها با stack trace
request_id: "bb3c21c5..."                        # ردیابی یک درخواست خاص

جزئیات و runbook: docs/OPERATIONS.md


تست و کیفیت کد

make test        # ۶۱ تست بک‌اند
make lint        # ruff + black + mypy + eslint + tsc
make audit       # bandit + pip-audit + npm audit
فایل تست چه چیزی را تضمین می‌کند
test_dataset_repair.py هر چهار نقص واقعی فایل CSV درست ترمیم می‌شوند
test_importer.py نرمال‌سازی، غنی‌سازی از داده تودرتو، idempotent بودن
test_query_builder.py کوئری Elasticsearch (تابع خالص — بدون نیاز به کلاستر)
test_search_api.py قرارداد کامل API: کلیدواژه، OR/AND فیلترها، facet، مرتب‌سازی، صفحه‌بندی، خطاها
test_auth_api.py ثبت‌نام، لاگین، سطح دسترسی، ماسک شدن PII بر اساس نقش
test_indexing.py داکیومنت ایندکس با mapping هم‌خوان است و هرگز PII حمل نمی‌کند
test_privacy.py توابع ماسک‌کردن هیچ‌وقت مقدار اصلی را لو نمی‌دهند

تست‌ها روی بک‌اند PostgreSQL اجرا می‌شوند و به Elasticsearch نیاز ندارند؛ pipeline گیت‌هاب (.github/workflows/ci.yml) همین‌ها را به‌علاوه بیلد هر دو ایمیج داکر اجرا می‌کند.


ساختار پروژه

.
├── backend/
│   ├── config/settings/{base,dev,prod,test}.py   تنظیمات تفکیک‌شده بر اساس محیط
│   ├── apps/
│   │   ├── core/        لاگ ساخت‌یافته، middleware، قالب خطا، throttle، health
│   │   ├── accounts/    کاربر سفارشی، نقش‌ها، JWT
│   │   ├── profiles/    مدل دامنه، خواننده خودترمیم CSV، ایمپورتر، ماسک PII
│   │   └── search/      types، ایندکس ES، دو بک‌اند سرچ، ویو و سریالایزر
│   ├── tests/           ۶۱ تست
│   ├── Dockerfile       بیلد چندمرحله‌ای، کاربر غیر‌root
│   └── entrypoint.sh    انتظار برای وابستگی‌ها، migrate، seed
├── frontend/
│   ├── src/
│   │   ├── api/         کلاینت axios با refresh خودکار توکن
│   │   ├── components/  SearchBar، FilterPanel، FacetGroup، ResultCard، Drawer
│   │   ├── hooks/       debounce، همگام‌سازی وضعیت سرچ با URL
│   │   └── pages/       Login، Search
│   ├── Dockerfile       بیلد Node → سرو با nginx
│   └── nginx.conf       پراکسی API + هدرهای امنیتی + CSP
├── elk/
│   └── logstash/        pipeline، تنظیمات، index template
├── data/                دیتاست خام CSV
├── docs/                معماری، API، سرچ، داده، امنیت، عملیات، openapi.yaml
├── docker-compose.yml           استک کامل
├── docker-compose.dev.yml       overlay توسعه (hot reload)
├── docker-compose.prod.yml      overlay پروداکشن (auth روی ES، محدودیت منابع)
└── Makefile

مستندات تکمیلی

سند محتوا
docs/ARCHITECTURE.md تصمیم‌های معماری، مسیر یک درخواست، الگوی Strategy در لایه سرچ
docs/SEARCH.md منطق سرچ و فیلتر، آنالایزرها، رتبه‌بندی، مقایسه دو بک‌اند
docs/API.md مرجع کامل endpointها با نمونه درخواست/پاسخ
docs/DATA.md تحلیل کیفیت دیتاست و الگوریتم ترمیم
docs/SECURITY.md مدل تهدید، لایه‌های دفاعی، trade-offها
docs/OPERATIONS.md اجرا، ELK، runbook خطاهای رایج
S
Description
No description provided
Readme 1.2 MiB
Languages
Python 67.9%
TypeScript 23.1%
CSS 3.4%
Dockerfile 2.1%
Shell 1.7%
Other 1.8%