Files
mobiniqow a464e550c9 feat: LinkedIn profile search engine (Django + React + Elasticsearch + ELK)
Full-stack implementation of the Cyberyan full-stack trial task.

Backend (Django 5 / DRF)
- Normalised PostgreSQL model (Profile, Company, School, Skill, Experience,
  Education) as the source of truth, with GIN + trigram indexes.
- Self-healing CSV reader: the supplied export carries an injected watermark
  column, missing columns, rows split by broken quoting and a duplicated
  header. A signature-scored Needleman-Wunsch alignment reconstructs 284 clean
  records out of 336 physical rows; only 10 rows parse correctly without it.
- Pluggable search layer (Strategy): Elasticsearch by default, PostgreSQL
  full-text search as a fallback selected by a single environment variable.
- Keyword search with per-field boosts, fuzziness and phrase boosting, six
  faceted filters (OR within a filter, AND across filters), an experience
  range, four sort orders, highlighting and facet aggregations.
- JWT auth with rotation + blacklist, three roles, PII masking, scoped rate
  limits, a single error envelope, OpenAPI schema and health probes.

Frontend (React 18 / TypeScript / Vite)
- Debounced search with type-ahead, faceted filter panel, result cards,
  profile drawer, URL-synced search state, automatic token refresh.
- Highlight snippets rendered without dangerouslySetInnerHTML.

Infrastructure
- Hardened multi-stage images (non-root, read-only rootfs, healthchecks).
- docker-compose for the full stack plus dev and production overlays.
- ELK: structured JSON logs shipped asynchronously to Logstash, routed per
  event type, with a composable index template and a Kibana setup script.

Quality
- 61 backend tests (85% coverage), ruff/black/bandit/mypy, ESLint + tsc,
  GitHub Actions running lint, tests and both image builds.
- Persian documentation: README plus architecture, search, API, data,
  security and operations guides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnFo6fNaHCTAkJbfcdkqtG
2026-09-03 08:13:50 +00:00

13 KiB
Raw Permalink Blame History

عملیات: اجرا، مانیتورینگ و عیب‌یابی


۱. اجرا

۱.۱ حالت کامل (پیش‌فرض)

cp .env.example .env
docker compose up --build          # یا: make up

ترتیب بالا آمدن سرویس‌ها با depends_on: condition: service_healthy کنترل می‌شود:

postgres ──┐
redis ─────┼──▶ backend ──▶ frontend
elasticsearch ─┴──▶ logstash
               └──▶ kibana

اولین اجرا ۳ تا ۵ دقیقه طول می‌کشد. entrypoint.sh در کانتینر بک‌اند:

منتظر postgres (حداکثر ۱۲۰ ثانیه)
منتظر elasticsearch (حداکثر ۱۸۰ ثانیه)
python manage.py migrate --noinput
python manage.py seed_users                 # اگر SEED_USERS=true
python manage.py import_profiles            # اگر SEED_DATASET=true و دیتابیس خالی باشد
python manage.py search_index --rebuild
exec gunicorn ...

ایمپورت idempotent است و فقط وقتی اجرا می‌شود که جدول پروفایل خالی باشد؛ ری‌استارت کانتینر داده را دوباره وارد نمی‌کند.

۱.۲ حالت توسعه (hot reload)

make dev
# SPA (Vite)  → http://localhost:5173
# API         → http://localhost:8000/api/docs/

سورس‌ها bind-mount می‌شوند: تغییر در پایتون باعث ری‌استارت runserver و تغییر در TSX باعث HMR می‌شود.

۱.۳ حالت پروداکشن

make prod
# = docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

تفاوت‌ها: احراز هویت Elasticsearch روشن، Kibana منتشر نمی‌شود، seed خاموش، محدودیت CPU/RAM، دو نمونه بک‌اند.

۱.۴ اجرا بدون Elasticsearch

echo "SEARCH_BACKEND=postgres" >> .env
docker compose up -d postgres redis backend frontend

کل API و فرانت‌اند بدون تغییر کار می‌کنند؛ فیلد backend در پاسخ سرچ "postgres" می‌شود.


۲. لاگ ساخت‌یافته

۲.۱ شکل رکوردها

{"@timestamp":"2026-09-03T04:00:08.574Z","level":"INFO","logger":"audit.search",
 "message":"search_executed","service":"linkedin-search-api","environment":"production",
 "request_id":"bb3c21c555a54ae0a6dafcd12ea1a98c","event":"search_executed",
 "user_id":2,"backend":"elasticsearch","result_count":80,"took_ms":31,
 "query":"leadership","filters":{"skills":["management"],"countries":["united states"],
 "roles":[],"seniority":[],"industries":[],"companies":[],
 "min_experience":null,"max_experience":null},
 "sort":"relevance","page":1,"page_size":3}
لاگر رویدادها
apps.access http_request — متد، مسیر، وضعیت، duration_ms، IP، user id
audit.search search_executed — کوئری، فیلترها، تعداد نتیجه، زمان، موتور
audit.auth login_succeeded، logout، user_registered
apps.errors unhandled_exception با stack trace
gunicorn.access خط دسترسی وب‌سرور (JSON)

۲.۲ مسیر انتقال

Django logger
   │  JSONFormatter  →  یک شیء JSON در هر رکورد
   │  RequestIDFilter →  افزودن request_id از ContextVar
   ├──▶ stdout                (همیشه؛ قابل دیدن با docker compose logs)
   └──▶ AsynchronousLogstashHandler
             │ بافر درون‌حافظه‌ای + ترد جدا
             ▼
        Logstash :5959 (codec: json_lines)
             │ غنی‌سازی، برچسب‌گذاری، حذف فیلدهای حساس
             ▼
        Elasticsearch  app-logs-{stream}-{YYYY.MM.dd}
             ▼
           Kibana

چرا async؟ اگر Logstash از دسترس خارج شود، یک هندلر همگام ترد درخواست را بلاک می‌کند و کل API کند می‌شود. با python-logstash-async رکوردها در حافظه صف می‌شوند و یک ترد جدا آن‌ها را می‌فرستد. این رفتار عملاً آزموده شده: با Logstash خاموش، تنها اثرش یک warning در stdout است.

۲.۳ مسیردهی به ایندکس‌ها

ایندکس محتوا
app-logs-search-* رویدادهای search_executed
app-logs-audit-* ورود، خروج، ثبت‌نام
app-logs-error-* ERROR و CRITICAL
app-logs-app-* بقیه

یک ایندکس در روز برای هر جریان. نتیجه: «همه لاگ‌های قدیمی‌تر از ۳۰ روز را پاک کن» یک حذف ایندکس است، نه یک delete_by_query سنگین.

برچسب‌های خودکار: slow_search (بیش از ۵۰۰ms) و zero_results.


۳. Kibana

make kibana-setup          # ساخت data view «Application logs»
open http://localhost:5601

کوئری‌های مفید در Discover

event: "search_executed" and result_count: 0
      → کاربران چه چیزی می‌خواستند که پیدا نکردند؟ (بهترین سیگنال برای بهبود سرچ)

event: "search_executed" and took_ms > 200
      → کدام ترکیب فیلترها کند است؟

event: "search_executed" and filters.skills: *
      → کدام مهارت‌ها بیشترین جستجو را دارند؟

level: "ERROR"
      → خطاها با stack trace کامل

request_id: "bb3c21c555a54ae0a6dafcd12ea1a98c"
      → کل ردپای یک درخواست خاص (شامل خط دسترسی و رویداد ممیزی)

http_status >= 400 and http_path: "/api/v1/search/"
      → درخواست‌های ناموفق سرچ

چطور request_id را پیدا کنم؟ هدر X-Request-ID در تب Network مرورگر، یا فیلد request_id داخل بدنه هر پاسخ خطا.

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

ویجت کوئری
نرخ سرچ event: "search_executed" — Date histogram
صدک ۹۵ زمان پاسخ took_ms — Percentiles
نرخ نتیجه‌ی صفر result_count: 0 تقسیم بر کل
پرجستجوترین مهارت‌ها filters.skills — Terms
نرخ خطا http_status >= 500
کاربران فعال user_id — Unique count

۴. عملیات روزمره

make ps                 # وضعیت کانتینرها + healthcheck
make logs               # لاگ زنده همه سرویس‌ها
docker compose logs -f backend        # فقط API

make seed               # ایمپورت مجدد کامل + بازسازی ایندکس
make reindex            # فقط بازسازی ایندکس از روی Postgres
make index-status       # وضعیت alias و تعداد داکیومنت
make users              # ساخت/به‌روزرسانی کاربران نمونه
make superuser          # ساخت superuser تعاملی
make shell              # شل جنگو

make test               # تست‌ها داخل کانتینر
make lint               # lint + type-check
make audit              # اسکن امنیتی وابستگی‌ها
make schema             # نوشتن docs/openapi.yaml

بررسی سلامت

curl -s localhost:8080/api/v1/health/live/  | jq
curl -s localhost:8080/api/v1/health/ready/ | jq

۵. عیب‌یابی

Elasticsearch بالا نمی‌آید

علامت: کانتینر elasticsearch مدام ری‌استارت می‌شود؛ در لاگ: max virtual memory areas vm.max_map_count [65530] is too low.

علت: Elasticsearch به vm.max_map_count ≥ 262144 نیاز دارد.

sudo sysctl -w vm.max_map_count=262144                      # موقت
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf   # دائمی

روی Docker Desktop (مک/ویندوز) معمولاً از قبل تنظیم است.

Elasticsearch به‌خاطر حافظه کشته می‌شود (exit 137)

ES_JAVA_OPTS را در .env کم کنید:

ES_JAVA_OPTS=-Xms256m -Xmx256m
LS_JAVA_OPTS=-Xms192m -Xmx192m

یا اگر رم کم است، Kibana و Logstash را بالا نیاورید:

docker compose up -d postgres redis elasticsearch backend frontend

بک‌اند با «DJANGO_SECRET_KEY is required» بالا نمی‌آید

.env ساخته نشده یا کلید خالی است:

cp .env.example .env
python -c "import secrets; print(secrets.token_urlsafe(64))"   # مقدار را در .env بگذارید

سرچ ۵۰۳ می‌دهد

curl -s localhost:8080/api/v1/health/ready/ | jq       # کدام وابستگی خراب است؟
docker compose logs elasticsearch | tail -50
make index-status                                       # آیا ایندکس اصلاً وجود دارد؟
make reindex                                            # بازسازی

راه‌حل موقت: SEARCH_BACKEND=postgres در .env و docker compose restart backend.

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

make index-status
# {"alias": "linkedin-profiles", "exists": true, "documents": 284}

اگر documents: 0 بود، ایمپورت انجام شده ولی ایندکس ساخته نشده: make reindex.

لاگ‌ها در Kibana دیده نمی‌شوند

docker compose logs logstash | tail -30            # آیا pipeline بالا آمده؟
docker compose exec backend env | grep LOGSTASH    # LOGSTASH_ENABLED=true ؟
curl -s localhost:5601/api/status | jq .status.overall
./scripts/kibana_setup.sh                          # آیا data view ساخته شده؟

Logstash حدود ۶۰ ثانیه طول می‌کشد تا آماده شود؛ لاگ‌های قبل از آن در بافر می‌مانند و بعد ارسال می‌شوند.

فرانت‌اند «Network Error» می‌دهد

docker compose ps backend                          # سالم است؟
docker compose exec frontend wget -qO- http://backend:8000/api/v1/health/live/

اگر با npm run dev بیرون از داکر کار می‌کنید، مطمئن شوید مبدأ http://localhost:5173 در CORS_ALLOWED_ORIGINS هست.

مایگریشن روی pg_trgm شکست می‌خورد

مایگریشن 0002_postgres_extensions نیاز به دسترسی ساخت extension دارد. در ایمیج رسمی Postgres کاربر پیش‌فرض superuser است؛ در دیتابیس مدیریت‌شده باید ادمین یک بار اجرا کند:

CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;

۶. پشتیبان‌گیری و بازیابی

# پشتیبان
docker compose exec -T postgres pg_dump -U linkedin linkedin | gzip > backup-$(date +%F).sql.gz

# بازیابی
gunzip -c backup-2026-09-03.sql.gz | docker compose exec -T postgres psql -U linkedin linkedin
make reindex          # ایندکس از Postgres بازسازی می‌شود

از Elasticsearch پشتیبان نمی‌گیریم. ایندکس یک projection دورریختنی است و همیشه از Postgres قابل بازسازی است — این دقیقاً دلیل انتخاب آن معماری بود.


۷. سنجه‌های عملکرد (دیتاست ۲۸۴ رکوردی)

عملیات زمان
ایمپورت کامل (خواندن + ترمیم + نوشتن) ‎~۱۵ ثانیه
بازسازی ایندکس ‎~۲ ثانیه
سرچ با Elasticsearch ‎~۵–۱۵ms
سرچ با PostgreSQL ‎~۱۵–۳۵ms
جزئیات پروفایل ‎~۲۰ms
بیلد ایمیج بک‌اند (بدون کش) ‎~۹۰ ثانیه
بیلد ایمیج فرانت‌اند (بدون کش) ‎~۶۰ ثانیه

اعداد سرچ روی همین دیتاست و ماشین توسعه اندازه‌گیری شده‌اند؛ فیلد took_ms در هر پاسخ همین را گزارش می‌کند و در Kibana قابل نمودار کردن است.


۸. چک‌لیست پیش از استقرار

  • DJANGO_SECRET_KEY تصادفی و ≥۵۰ کاراکتر
  • POSTGRES_PASSWORD و ELASTIC_PASSWORD عوض شده
  • رمزهای کاربران نمونه عوض یا SEED_USERS=false
  • DJANGO_DEBUG=false و DJANGO_SETTINGS_MODULE=config.settings.prod
  • DJANGO_ALLOWED_HOSTS روی دامنه واقعی
  • DJANGO_ADMIN_URL روی یک مسیر غیرقابل‌حدس
  • HTTPS در لبه + SECURE_SSL_REDIRECT=true
  • SEED_DATASET=false
  • overlay پروداکشن فعال (احراز هویت ES)
  • پشتیبان‌گیری خودکار Postgres
  • سیاست نگهداری لاگ (ILM) روی app-logs-*
  • docker compose exec backend python manage.py check --deploy بدون خطا