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
This commit is contained in:
2026-09-03 08:13:10 +00:00
commit a464e550c9
133 changed files with 17019 additions and 0 deletions
+420
View File
@@ -0,0 +1,420 @@
# عملیات: اجرا، مانیتورینگ و عیب‌یابی
---
## ۱. اجرا
### ۱.۱ حالت کامل (پیش‌فرض)
```bash
cp .env.example .env
docker compose up --build # یا: make up
```
<div dir="rtl">
ترتیب بالا آمدن سرویس‌ها با `depends_on: condition: service_healthy` کنترل می‌شود:
</div>
```
postgres ──┐
redis ─────┼──▶ backend ──▶ frontend
elasticsearch ─┴──▶ logstash
└──▶ kibana
```
<div dir="rtl">
اولین اجرا ۳ تا ۵ دقیقه طول می‌کشد. `entrypoint.sh` در کانتینر بک‌اند:
</div>
```
منتظر 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 ...
```
<div dir="rtl">
ایمپورت **idempotent** است و فقط وقتی اجرا می‌شود که جدول پروفایل خالی باشد؛ ری‌استارت کانتینر داده را دوباره وارد نمی‌کند.
### ۱.۲ حالت توسعه (hot reload)
</div>
```bash
make dev
# SPA (Vite) → http://localhost:5173
# API → http://localhost:8000/api/docs/
```
<div dir="rtl">
سورس‌ها bind-mount می‌شوند: تغییر در پایتون باعث ری‌استارت `runserver` و تغییر در TSX باعث HMR می‌شود.
### ۱.۳ حالت پروداکشن
</div>
```bash
make prod
# = docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
<div dir="rtl">
تفاوت‌ها: احراز هویت Elasticsearch روشن، Kibana منتشر نمی‌شود، seed خاموش، محدودیت CPU/RAM، دو نمونه بک‌اند.
### ۱.۴ اجرا بدون Elasticsearch
</div>
```bash
echo "SEARCH_BACKEND=postgres" >> .env
docker compose up -d postgres redis backend frontend
```
<div dir="rtl">
کل API و فرانت‌اند بدون تغییر کار می‌کنند؛ فیلد `backend` در پاسخ سرچ `"postgres"` می‌شود.
</div>
---
## ۲. لاگ ساخت‌یافته
### ۲.۱ شکل رکوردها
```json
{"@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}
```
<div dir="rtl">
| لاگر | رویدادها |
|---|---|
| `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) |
### ۲.۲ مسیر انتقال
</div>
```
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
```
<div dir="rtl">
**چرا async؟** اگر Logstash از دسترس خارج شود، یک هندلر همگام ترد درخواست را بلاک می‌کند و کل API کند می‌شود. با `python-logstash-async` رکوردها در حافظه صف می‌شوند و یک ترد جدا آن‌ها را می‌فرستد. این رفتار عملاً آزموده شده: با Logstash خاموش، تنها اثرش یک warning در stdout است.
### ۲.۳ مسیردهی به ایندکس‌ها
</div>
| ایندکس | محتوا |
|---|---|
| `app-logs-search-*` | رویدادهای `search_executed` |
| `app-logs-audit-*` | ورود، خروج، ثبت‌نام |
| `app-logs-error-*` | `ERROR` و `CRITICAL` |
| `app-logs-app-*` | بقیه |
<div dir="rtl">
یک ایندکس در روز برای هر جریان. نتیجه: «همه لاگ‌های قدیمی‌تر از ۳۰ روز را پاک کن» یک حذف ایندکس است، نه یک `delete_by_query` سنگین.
**برچسب‌های خودکار:** `slow_search` (بیش از ۵۰۰ms) و `zero_results`.
</div>
---
## ۳. Kibana
```bash
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/"
→ درخواست‌های ناموفق سرچ
```
<div dir="rtl">
**چطور `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 |
</div>
---
## ۴. عملیات روزمره
```bash
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
```
### بررسی سلامت
```bash
curl -s localhost:8080/api/v1/health/live/ | jq
curl -s localhost:8080/api/v1/health/ready/ | jq
```
---
## ۵. عیب‌یابی
<div dir="rtl">
### Elasticsearch بالا نمی‌آید
**علامت:** کانتینر `elasticsearch` مدام ری‌استارت می‌شود؛ در لاگ: `max virtual memory areas vm.max_map_count [65530] is too low`.
**علت:** Elasticsearch به `vm.max_map_count ≥ 262144` نیاز دارد.
</div>
```bash
sudo sysctl -w vm.max_map_count=262144 # موقت
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf # دائمی
```
<div dir="rtl">
روی Docker Desktop (مک/ویندوز) معمولاً از قبل تنظیم است.
### Elasticsearch به‌خاطر حافظه کشته می‌شود (exit 137)
`ES_JAVA_OPTS` را در `.env` کم کنید:
</div>
```env
ES_JAVA_OPTS=-Xms256m -Xmx256m
LS_JAVA_OPTS=-Xms192m -Xmx192m
```
<div dir="rtl">
یا اگر رم کم است، Kibana و Logstash را بالا نیاورید:
</div>
```bash
docker compose up -d postgres redis elasticsearch backend frontend
```
<div dir="rtl">
### بک‌اند با «DJANGO_SECRET_KEY is required» بالا نمی‌آید
`.env` ساخته نشده یا کلید خالی است:
</div>
```bash
cp .env.example .env
python -c "import secrets; print(secrets.token_urlsafe(64))" # مقدار را در .env بگذارید
```
<div dir="rtl">
### سرچ ۵۰۳ می‌دهد
</div>
```bash
curl -s localhost:8080/api/v1/health/ready/ | jq # کدام وابستگی خراب است؟
docker compose logs elasticsearch | tail -50
make index-status # آیا ایندکس اصلاً وجود دارد؟
make reindex # بازسازی
```
<div dir="rtl">
راه‌حل موقت: `SEARCH_BACKEND=postgres` در `.env` و `docker compose restart backend`.
### سرچ نتیجه نمی‌دهد ولی داده هست
</div>
```bash
make index-status
# {"alias": "linkedin-profiles", "exists": true, "documents": 284}
```
<div dir="rtl">
اگر `documents: 0` بود، ایمپورت انجام شده ولی ایندکس ساخته نشده: `make reindex`.
### لاگ‌ها در Kibana دیده نمی‌شوند
</div>
```bash
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 ساخته شده؟
```
<div dir="rtl">
Logstash حدود ۶۰ ثانیه طول می‌کشد تا آماده شود؛ لاگ‌های قبل از آن در بافر می‌مانند و بعد ارسال می‌شوند.
### فرانت‌اند «Network Error» می‌دهد
</div>
```bash
docker compose ps backend # سالم است؟
docker compose exec frontend wget -qO- http://backend:8000/api/v1/health/live/
```
<div dir="rtl">
اگر با `npm run dev` بیرون از داکر کار می‌کنید، مطمئن شوید مبدأ `http://localhost:5173` در `CORS_ALLOWED_ORIGINS` هست.
### مایگریشن روی `pg_trgm` شکست می‌خورد
مایگریشن `0002_postgres_extensions` نیاز به دسترسی ساخت extension دارد. در ایمیج رسمی Postgres کاربر پیش‌فرض superuser است؛ در دیتابیس مدیریت‌شده باید ادمین یک بار اجرا کند:
</div>
```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS unaccent;
```
---
## ۶. پشتیبان‌گیری و بازیابی
```bash
# پشتیبان
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 بازسازی می‌شود
```
<div dir="rtl">
از Elasticsearch **پشتیبان نمی‌گیریم**. ایندکس یک projection دورریختنی است و همیشه از Postgres قابل بازسازی است — این دقیقاً دلیل انتخاب آن معماری بود.
</div>
---
## ۷. سنجه‌های عملکرد (دیتاست ۲۸۴ رکوردی)
<div dir="rtl">
| عملیات | زمان |
|---|---|
| ایمپورت کامل (خواندن + ترمیم + نوشتن) | ‎~۱۵ ثانیه |
| بازسازی ایندکس | ‎~۲ ثانیه |
| سرچ با Elasticsearch | ‎~۵–۱۵ms |
| سرچ با PostgreSQL | ‎~۱۵–۳۵ms |
| جزئیات پروفایل | ‎~۲۰ms |
| بیلد ایمیج بک‌اند (بدون کش) | ‎~۹۰ ثانیه |
| بیلد ایمیج فرانت‌اند (بدون کش) | ‎~۶۰ ثانیه |
اعداد سرچ روی همین دیتاست و ماشین توسعه اندازه‌گیری شده‌اند؛ فیلد `took_ms` در هر پاسخ همین را گزارش می‌کند و در Kibana قابل نمودار کردن است.
</div>
---
## ۸. چک‌لیست پیش از استقرار
<div dir="rtl">
- [ ] `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` بدون خطا
</div>