← مجموعه مستندات پروژه
NLP · BATCH / CLI JOB · REVIEWS.SENTIMENT

زیرسیستم تحلیل احساس نظرات (Sentiment)

Job دسته‌ای که متن نقدها را با مدل Cardiff XLM-R طبقه‌بندی می‌کند و ستون reviews.sentiment را می‌نویسد — بدون دست زدن Storage Consumer به این ستون در upsert.

~92%دقت روی ≈۹۰۰ رکورد ارزیابی
XLM-Rیک مدل، بدون routing زبانی
BatchJob تمام‌شونده، نه Daemon
3 labelsPOSITIVE / NEUTRAL / NEGATIVE

این مستند معماری، انتخاب مدل، فلوی اجرا، قرارداد مالکیت داده با Storage Consumer، نحوهٔ اجرا در venv و Docker، و استراتژی تست زیرسیستم Sentiment را شرح می‌دهد. این زیرسیستم یک job دسته‌ای / CLI است: هر بار اجرا می‌شود، تمام نقدهای دارای محتوا را (بر اساس id) پیمایش می‌کند، برچسب احساس را با مدل دوباره محاسبه و overwrite می‌کند، سپس خارج می‌شود. Daemon دائمی نیست و با docker compose up معمولی بالا نمی‌آید — پشت پروفایل Compose به نام tools است.

۱ جایگاه در معماری کلی پروژه

مسیر دادهٔ نقدها این‌گونه است: Crawler نقد را از Play Store می‌گیرد و به Kafka می‌فرستد → Storage Consumer آن را در جدول reviews می‌نشاند (بدون نوشتن sentiment) → این Job بعداً ستون sentiment را پر یا به‌روز می‌کند. بنابراین Sentiment یک لایهٔ تحلیل پس از ماندگارسازی است، نه بخشی از مسیر ingest زنده.

flowchart LR Crawler["Crawler"] -->|reviews topic| Kafka Kafka --> SC["Storage Consumer"] SC --> DB[("PostgreSQL
reviews")] Sent["Sentiment Job
(batch/CLI)"] -->|UPDATE sentiment| DB style Sent fill:#16a34a,color:#fff style SC fill:#2563eb,color:#fff style DB fill:#7c3aed,color:#fff
چرا Job و نه سرویس همیشه روشن؟ تحلیل احساس روی کل جدول (یا دوره‌ای روی همهٔ ردیف‌ها با overwrite) یک کار سنگین CPU/مدل است. جدا کردنش از مسیر Kafka باعث می‌شود ingest نقدها به لود مدل وابسته نباشد، و اپراتور بتواند Job را آگاهانه (مثلاً شبانه یا بعد از یک موج crawl) اجرا کند.

۲ انتخاب مدل و ارزیابی کیفیت

مدل واحد پروژه:

cardiffnlp/twitter-xlm-roberta-base-sentiment

هیچ routing زبانی (مدل جدا برای فارسی/انگلیسی) وجود ندارد؛ همهٔ متن‌ها از همین pipeline عبور می‌کنند.

موضوعنتیجه / تصمیم
ارزیابی داخلی روی حدود ۹۰۰ رکورد برچسب‌خورده، دقت حدود ۹۲٪ به‌دست آمد. در مقایسه با اکثر مدل‌های هم‌وزن (هم‌رده از نظر اندازه/هزینه)، این مدل قوی‌تر ظاهر شد و برای حجم و زبان‌های پروژه انتخاب شد.
چرا نه LLM؟ استفاده از LLMهای API-محور بررسی شد، اما لیمیت نرخ (rate limit) سنگین داشتند و برای حجم نقدهای پروژه مناسب نبودند: Job باید بتواند هزاران/ده‌ها هزار ردیف را بدون صف‌شدن پشت سهمیهٔ ابری و بدون هزینهٔ پیش‌بینی‌ناپذیر per-token جلو ببرد. مدل محلی Cardiff این محدودیت را ندارد.
محدودیت شناخته‌شدهٔ فارسی این مدل رسماً روی فارسی fine-tune نشده؛ دقت فارسی عمدتاً از انتقال بین‌زبانی پایهٔ XLM-RoBERTa می‌آید. آزمایش routing به مدل تخصصی فارسی بهبود معناداری نشان نداد؛ بنابراین معماری تک‌مدلی برای سادگی حفظ شد.
خروجی مجاز فقط POSITIVE / NEUTRAL / NEGATIVE — هم‌تراز با CHECK constraint ستون reviews.sentiment.

۳ معماری لایه‌ای و Composition Root

مثل Crawler و Network Analyzer، این زیرسیستم از کلاس‌های پایتون خالص با تزریق وابستگی ساخته شده است. main.py تنها Composition Root است: Settings را لود می‌کند، اشیاء واقعی را می‌سازد، حلقهٔ batch را می‌چرخاند و در finally اتصال DB را می‌بندد.

flowchart TD Settings["Settings
(frozen, validated)"] --> Main["main.py
Composition Root"] Main --> CL["SentimentClassifier
(transformers pipeline)"] Main --> DB["Database
(psycopg2, یک اتصال)"] Main --> Repo["ReviewRepository"] Main --> Svc["SentimentService"] CL --> Svc DB --> Svc Repo --> Svc Svc --> Reviews[("reviews table")] style Settings fill:#7c3aed,color:#fff style Main fill:#2563eb,color:#fff style Svc fill:#16a34a,color:#fff style CL fill:#f59e0b,color:#fff
CORE

classifier.py

Wrapper همگام روی Hugging Face pipeline. بدون وابستگی به psycopg2. مدل یک‌بار در سازنده لود می‌شود؛ هر batch همان pipeline را reuse می‌کند. متن خالی به مدل نمی‌رسد.

PERSISTENCE

database + repository

الگوی مشابه Storage Consumer: یک اتصال lazy، تراکنش صریح، timeoutهای session. Repository فقط fetch keyset و UPDATE دسته‌ای با execute_values.

RUNTIME

sentiment_service.py

یک چرخه: خواندن صفحه → classify → (مگر dry-run) نوشتن. cursor داخلی after_id صفحه را جلو می‌برد تا Job تمام شود.

۴ فلوی یک Job کامل

هر اجرا از id = 0 شروع می‌کند و تا وقتی run_batch_cycle() مقدار True برگرداند (یعنی صفحه‌ای کار داشته) حلقه ادامه دارد. برگشت False یعنی دیگر ردیفی بعد از cursor نیست — موفقیت با «کاری نمانده»، نه شکست.

sequenceDiagram participant Main as main.py participant Svc as SentimentService participant Repo as ReviewRepository participant CL as SentimentClassifier participant PG as PostgreSQL Main->>Svc: while run_batch_cycle() Svc->>Repo: fetch_batch(after_id, LIMIT batch_size) Repo->>PG: SELECT id, content WHERE content != '' AND id > after_id PG-->>Repo: rows alt هیچ ردیفی نیست Svc-->>Main: False → پایان Job else صفحه غیرخالی Svc->>CL: classify_batch(texts) CL-->>Svc: labels (یا None برای خالی) Note over Svc: None → NEUTRAL
after_id = آخرین id صفحه alt dry-run Svc-->>Main: True (فقط لاگ شمارش، بدون WRITE) else نوشتن واقعی Svc->>Repo: update_sentiments(id, label) Repo->>PG: UPDATE ... FROM (VALUES ...) Svc-->>Main: True end end
جزئیاترفتار
PaginationKeyset روی id (id > after_id) — مستقل از مقدار فعلی sentiment.
Overwriteهر اجرای Job برچسب‌های قبلی را دوباره می‌نویسد؛ برای اعمال مدل جدید یا اصلاح اشتباه مفید است.
Batch پیش‌فرضSENTIMENT_BATCH_SIZE=100 (قابل override با --batch-size).
Truncation مدلpipeline با truncation=True و max_length=512 صدا زده می‌شود تا نقدهای بلند خطا ندهند.
Exit codes0 موفقیت؛ 1 خطای config/classification/عمومی؛ 2 خطای argparse.

۵ قرارداد مالکیت ستون sentiment

ستون در migration مربوط به Storage Consumer ساخته شده (با CHECK)، اما مالک نوشتن فقط این زیرسیستم است:

نتیجه عملی: می‌توانید Job را هر چند وقت یک‌بار اجرا کنید؛ موج‌های بعدی crawl/upsert نقد، برچسب‌های قبلی را خراب نمی‌کنند مگر خود Job دوباره overwrite کند.

۶ متن بدون سیگنال و برچسب پیش‌فرض

Classifier برای متن None / خالی / فقط فاصله، مدل را صدا نمی‌زند و None برمی‌گرداند. سرویس آن را به NEUTRAL نگاشت می‌کند و می‌نویسد.

۷ نحوهٔ اجرا: venv، Docker، فلگ‌ها و env

۷.۱ پیش‌نیاز داده

Postgres باید بالا باشد و جدول reviews (از طریق Storage Consumer / migrations) وجود داشته باشد. معمولاً اول استک را با bring_up یا حداقل postgres + مسیر ingest نقدها بالا می‌آورید، بعد Job را می‌زنید.

۷.۲ اجرا روی Host / venv

حتماً از ریشهٔ مخزن (PlayPulse/) اجرا کنید — نه از داخل پوشهٔ sentiment/ — تا importهای from sentiment... resolve شوند.

cd ~/PlayPulse python3 -m venv sentiment/.venv source sentiment/.venv/bin/activate pip install torch --index-url https://download.pytorch.org/whl/cpu pip install -r sentiment/requirements.txt cp sentiment/.env.example sentiment/.env # POSTGRES_* را برای host پر کنید (معمولاً 127.0.0.1) python -m sentiment.main run python -m sentiment.main run --batch-size 50 --dry-run python -m sentiment.main -v run

۷.۳ سوئیچ‌ها و آرگومان‌های CLI

آرگومانمعنا
run (subcommand الزامی)شروع Job: پیمایش کل نقدهای واجد شرایط و overwrite برچسب‌ها.
--batch-size Nجایگزین موقت SENTIMENT_BATCH_SIZE فقط برای همین اجرا (N > 0).
--dry-runطبقه‌بندی و لاگ شمارش POSITIVE/NEUTRAL/NEGATIVE؛ هیچ UPDATEای نوشته نمی‌شود. Cursor همچنان جلو می‌رود تا Job تمام شود.
-v / --verboseلاگ سطح DEBUG.

۷.۴ متغیرهای محیطی (sentiment/.env)

فایل sentiment/.env فقط برای اجرای host/venv است. در Docker، آدرس‌ها از docker-compose.yml تزریق می‌شوند و load_dotenv متغیرهای از قبل set‌شده را override نمی‌کند.

متغیرالزامی؟نقش
POSTGRES_DB / USER / PASSWORD / HOSTبلهاتصال به همان دیتابیسی که Storage Consumer می‌نویسد.
POSTGRES_PORTخیر (۵۴۳۲)پورت Postgres.
SENTIMENT_MODEL_NAMEخیرپیش‌فرض Cardiff XLM-R بالا.
SENTIMENT_BATCH_SIZEخیر (۱۰۰)اندازهٔ صفحهٔ fetch/classify.
SENTIMENT_DB_*_TIMEOUT_*خیرtimeout اتصال و statement / idle-in-transaction.

۷.۵ ساخت و اجرا در Docker

سرویس Compose: sentiment، پروفایل tools، image tag مشترک playpulse-sentiment:latest. Dockerfile مدل را داخل image bake می‌کند تا اولین run منتظر دانلود چندصدمگابایتی نماند. Torch به‌صورت CPU-only نصب می‌شود.

# ساخت image (چند دقیقه؛ سنگین به‌خاطر torch + مدل) docker compose --profile tools build sentiment # اجرای Job روی شبکهٔ Compose (POSTGRES_HOST=postgres) docker compose run --rm sentiment run docker compose run --rm sentiment run --batch-size 50 --dry-run docker compose run --rm sentiment -v run
نکته: docker compose up معمولی sentiment را بالا نمی‌آورد (پروفایل tools). bring_up.sh هم image سنتیم‌نت را cold-build نمی‌کند؛ فقط اگر playpulse-sentiment:latest از قبل موجود باشد ممکن است کانتینر را با --no-build بسازد — در غیر این صورت skip می‌کند. برای Job واقعی معمولاً باید صریحاً با compose run (و پروفایل tools) صدا زده شود. ENTRYPOINT برابر python -m sentiment.main است؛ آرگومان‌های بعد از نام سرویس مستقیماً به CLI می‌رسند.

۸ استراتژی تست و نحوهٔ اجرا در هر محیط

flowchart TD U["Unit
Mock pipeline / Fake cursor
سریع، بدون وزن مدل"] --> I["Integration
Postgres واقعی + StubClassifier"] I --> M["Manual smoke
مدل واقعی، بدون DB"] M --> D["Docker test profile
sentiment-tests / -integration"] style U fill:#16a34a,color:#fff style I fill:#2563eb,color:#fff style M fill:#f59e0b,color:#fff style D fill:#7c3aed,color:#fff

۸.۱ Unit — چه چیزی را می‌پوشاند؟

فایلچیزی که ثابت می‌کند
tests/unit/test_classifier.py نگاشت برچسب‌های Cardiff (lowercase / Title / LABEL_N) به POSITIVE/NEUTRAL/NEGATIVE؛ متن خالی/None بدون صدا زدن مدل → None؛ حفظ ترتیب در batch ترکیبی؛ خطای ClassificationError برای برچسب ناشناخته؛ لود یک‌باره از طریق pipeline_factory تزریقی (بدون دانلود وزن).
tests/unit/test_review_repository.py شکل SQL: فیلتر محتوا، keyset id > %s، عدم فیلتر روی ستون sentiment؛ پاس‌دادن after_id؛ UPDATE با execute_values؛ skip وقتی لیست نتایج خالی است.

۸.۲ Integration — چه چیزی را می‌پوشاند؟

tests/integration/test_sentiment_e2e.py (مارکر integration): مسیر واقعی DB را تست می‌کند — ثبت اپ تستی، درج نقدها (بعضی با برچسب اشتباه از قبل)، اجرای یک run_batch_cycle، و اثبات overwrite. Classifier یک Stub مبتنی بر keyword است تا وزن مدل دانلود نشود؛ تمرکز تست روی fetch → write → re-read است. اگر Postgres یا env نباشد، تمیز skip می‌شود.

۸.۳ اجرای تست روی Host / venv

cd ~/PlayPulse source sentiment/.venv/bin/activate # Unit — سریع، بدون نیاز به Postgres pytest sentiment/tests/unit -v # Integration — نیاز به Postgres در دسترس + sentiment/.env (یا storage_consumer/.env) docker compose up -d postgres # اگر هنوز بالا نیست pytest sentiment/tests/integration -m integration -v

۸.۴ اجرای تست در Docker (پروفایل test)

دو سرویس جدا، همان image سنگین playpulse-sentiment:latest:

سرویس Composeفرمان داخل کانتینرنیاز
sentiment-tests pytest sentiment/tests/unit -v Postgres healthy (depends_on)؛ تست‌ها از host به‌صورت volume mount می‌شوند چون Dockerfile لایهٔ tests/ را حذف می‌کند.
sentiment-tests-integration pytest sentiment/tests/integration -v -m integration همین‌طور؛ env داخل شبکه Compose با POSTGRES_HOST=postgres.
# اول image را بسازید (اگر هنوز نیست) — cold build طولانی است docker compose --profile tools build sentiment # Unit در شبکهٔ Compose docker compose --profile test run --rm sentiment-tests # Integration docker compose --profile test run --rm sentiment-tests-integration

۸.۵ ارکستراتور run_tests_in_docker.sh

اسکریپت ./scripts/run_tests_in_docker.sh هر دو سرویس sentiment-tests و sentiment-tests-integration را در صف اجرا دارد، اما عمداً sentiment را cold-build نمی‌کند. اگر image playpulse-sentiment:latest روی ماشین نباشد، آن ردیف‌ها SKIPPED می‌شوند (تا یک build چنددقیقه‌ای کل suite را بلوکه نکند). پس برای سبز شدن آن‌ها در این اسکریپت، یک‌بار از قبل:

docker compose --profile tools build sentiment ./scripts/run_tests_in_docker.sh

۸.۶ Smoke دستی با مدل واقعی (بدون DB)

برای اطمینان از اینکه وزن مدل روی ماشین لود می‌شود و خروجی برچسب معقول است — جدا از pytest:

source sentiment/.venv/bin/activate python scripts/manual_sentiment_check.py

مستند اختصاصی این اسکریپت: manual_sentiment_check.py.

۹ اسکریپت‌ها و نقاط اتصال مرتبط

مسیرارتباط با Sentiment
scripts/manual_sentiment_check.py Smoke مدل واقعی Cardiff روی نمونه‌های en/fa بدون دیتابیس.
scripts/run_tests_in_docker.sh اجرای کانتینرهای تست sentiment در صف کلی؛ skip اگر image نباشد.
scripts/bring_up.sh استک داده (Postgres و …) را بالا می‌آورد؛ خودِ Job sentiment را به‌صورت سرویس دائمی استارت نمی‌کند — بعداً باید compose run sentiment بزنید.
Compose sentiment / sentiment-tests* تعریف build، env شبکه، و entrypointهای اجرا/تست در docker-compose.yml.

۱۰ تصمیمات کلیدی

مدل محلی Cardiff به‌جای LLM ابری

دقت ≈۹۲٪ روی ≈۹۰۰ نمونه و برتری نسبی نسبت به مدل‌های هم‌وزن، همراه با نبود rate limit ابری، برای حجم دادهٔ پروژه عملی‌تر از LLMهای محدود به سهمیه بود.

یک مدل برای همهٔ زبان‌ها

Routing به مدل فارسی تخصصی بهبود معنادار نداد؛ پیچیدگی عملیاتی دو مدل (دو cache، دو مسیر خطا) پذیرفته نشد.

Job دسته‌ای با overwrite، نه «فقط NULLها»

هر اجرا کل ردیف‌های واجد شرایط را دوباره برچسب می‌زند تا تعویض مدل یا اصلاح باگ بدون migration دستی ممکن باشد. Pagination روی id جلوی حلقهٔ بی‌نهایت را می‌گیرد.

مالکیت انحصاری ستون sentiment

Storage Consumer عمداً این ستون را در upsert دست نمی‌زند تا مسیر crawl بتواند بدون پاک کردن تحلیل، نقدها را تازه کند.

پروفایل Compose tools + bake مدل در image

استک روزمره با سرویس چندگیگابایتی torch شلوغ نمی‌شود؛ وقتی Job لازم است، image از قبل مدل را دارد و اولین run سریع‌تر است.

جداسازی Unit (Mock) از Integration (Stub+DB) از Smoke (مدل واقعی)

دانلود وزن نباید پیش‌شرط هر pytest سریع باشد؛ e2e روی قرارداد DB تمرکز دارد؛ smoke دستی فقط وقتی کیفیت مدل مهم است.

.env هاست جدا از env داکر

مثل بقیهٔ زیرسیستم‌ها: روی host معمولاً 127.0.0.1؛ داخل Compose postgres — تا فایل توسعه شبکهٔ داخلی را خراب نکند.

۱۱ ابزارها و تکنولوژی‌های استفاده‌شده

ابزارنقش
transformers (Hugging Face)Pipeline تحلیل احساس و لود مدل Cardiff XLM-R
torch (CPU wheel)موتور اجرای مدل؛ نصب جداگانه برای جلوگیری از wheel غول‌پیکر CUDA
sentencepieceوابستگی tokenizer مدل‌های XLM-R
psycopg2اتصال و تراکنش PostgreSQL؛ execute_values برای UPDATE دسته‌ای
python-dotenvبارگذاری sentiment/.env در اجرای host
pytestUnit و Integration با مارکر integration
Docker Compose profiles tools / testاجرای Job و کانتینرهای تست بدون قاطی شدن با استک پیش‌فرض