Files
cyberyan_linkedin_search/docs/OPERATIONS.md
T
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

421 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# عملیات: اجرا، مانیتورینگ و عیب‌یابی
---
## ۱. اجرا
### ۱.۱ حالت کامل (پیش‌فرض)
```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>