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
13 KiB
عملیات: اجرا، مانیتورینگ و عیبیابی
۱. اجرا
۱.۱ حالت کامل (پیشفرض)
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.prodDJANGO_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بدون خطا