A production-ready, hybrid REST + gRPC NLP microservice that performs language detection, sentiment analysis, toxicity detection, and zero-shot text classification with semantic caching powered by Qdrant.
ββββββββββββββββ ββββββββββββββββ βββββββββββββββββββββββββ
β REST Client ββββββΆβ FastAPI βββββββΆβ β
β (HTTP/JSON) β β main_rest.py β β NLP Engine β
ββββββββββββββββ ββββββββββββββββ β nlp_engine.py β
β β
ββββββββββββββββ ββββββββββββββββ β βββββββββββββββββββ β
β gRPC Client ββββββΆβ gRPC Server βββββββΆβ β Lingua β β
β (Protobuf) β β main_grpc.py β β β NLTK β β
ββββββββββββββββ ββββββββββββββββ β β FastEmbed β β
β β HF Models β β
β β Qdrant Cache β β
β βββββββββββββββββββ β
βββββββββββββ¬ββββββββββββ
β
βββββββββββββΌβββββββββββββ
β OpenTelemetry β
β Jaeger / OTLP Exporter β
ββββββββββββββββββββββββββ
- Language Detection β Lingua detects the dominant language of the full text.
- Sentence Tokenization β NLTK splits the text into sentences.
- Batch Embedding β FastEmbed (
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) encodes all sentences. - Semantic Cache Lookup β Each sentence embedding is searched in Qdrant (in-memory). On a cache hit (cosine similarity β₯ threshold), the cached inference is reused.
- AI Inference β Cache misses go through:
- Sentiment via
nlptown/bert-base-multilingual-uncased-sentiment
- Sentiment via
- Toxicity detection via
unitary/multilingual-toxic-xlm-roberta - Zero-shot classification via
MoritzLaurer/mDeBERTa-v3-base-mnli-xnli
- Cache Store β New inferences are stored in Qdrant for future reuse.
- Aggregation β Sentence-level results are aggregated into a single response.
| Layer | Technology |
|---|---|
| REST API | FastAPI + Uvicorn |
| gRPC API | grpcio + grpcio-tools |
| API Docs | Scalar (replaces Swagger UI) |
| Language Detection | Lingua |
| Sentence Splitting | NLTK |
| Embeddings | FastEmbed (sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2) |
| Vector Cache | Qdrant (in-memory) |
| NLP Inference | Hugging Face Transformers (mDeBERTa XNLI, mBERT sentiment, multilingual toxicity) |
| Observability | OpenTelemetry β Jaeger, structlog (JSON) |
| Linting | Ruff (strict config) |
| CI/CD | GitLab CI |
| Containerization | Docker multi-stage + Docker Compose |
- Python 3.11+
- Docker & Docker Compose (for containerized deployment)
# 1. Clone and enter the project
cd nlp-microservice
# 2. Create a virtual environment
python -m venv .venv && source .venv/bin/activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Generate gRPC stubs
make proto
# 5. Copy environment config
cp .env.example .env
# 6. Start the REST server
make run-rest
# 7. (In another terminal) Start the gRPC server
make run-grpc# Build and start everything (REST, gRPC, Jaeger)
docker compose up --build -d
# View logs
docker compose logs -f nlp-rest
# Access services
# REST API: http://localhost:8000
# API Docs: http://localhost:8000/docs
# Jaeger UI: http://localhost:16686
# gRPC: localhost:50051Request:
{
"text": "Artificial intelligence is making remarkable progress in healthcare and drug discovery.",
"categories": ["technology", "healthcare", "finance", "sports"],
"tonalities": ["formal", "objective", "enthusiastic"]
}Response:
{
"language": "en",
"sentiment": "positive",
"sentiment_score": 0.62,
"is_toxic": false,
"highest_toxicity_score": 0.07,
"matched_categories": [
{"category": "healthcare", "score": 0.78},
{"category": "technology", "score": 0.71},
{"category": "finance", "score": 0.15},
{"category": "sports", "score": 0.04}
],
"processing_time_ms": 1842.37
}Request:
{
"texts": [
"AI systems are helping doctors find diseases faster.",
"You are useless and should disappear."
],
"categories": ["technology", "healthcare"],
"tonalities": ["formal", "aggressive"]
}Response:
{
"results": [
{
"language": "en",
"sentiment": "positive",
"sentiment_score": 0.73,
"matched_categories": [{"category": "healthcare", "score": 0.81}],
"matched_tonalities": [{"tonality": "formal", "score": 0.76}],
"is_toxic": false,
"highest_toxicity_score": 0.06,
"processing_time_ms": 412.15
},
{
"language": "en",
"sentiment": "negative",
"sentiment_score": 0.89,
"matched_categories": [{"category": "technology", "score": 0.39}],
"matched_tonalities": [{"tonality": "aggressive", "score": 0.88}],
"is_toxic": true,
"highest_toxicity_score": 0.93,
"processing_time_ms": 405.72
}
]
}Returns {"status": "ok"}.
Serves the Scalar interactive API reference.
Defined in nlp_service.proto β accepts AnalyzeRequest, returns AnalyzeResponse.
Defined in nlp_service.proto β accepts AnalyzeBatchRequest, returns AnalyzeBatchResponse.
All settings are loaded from .env via pydantic-settings. See .env.example for the full list:
| Variable | Default | Description |
|---|---|---|
REST_HOST |
0.0.0.0 |
REST server bind host |
REST_PORT |
8000 |
REST server port |
GRPC_HOST |
0.0.0.0 |
gRPC server bind host |
GRPC_PORT |
50051 |
gRPC server port |
EMBEDDING_MODEL |
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 |
FastEmbed model name |
CLASSIFIER_MODEL |
MoritzLaurer/mDeBERTa-v3-base-mnli-xnli |
Zero-shot classifier model |
SENTIMENT_MODEL |
nlptown/bert-base-multilingual-uncased-sentiment |
Sentiment analysis model |
TOXICITY_MODEL |
unitary/multilingual-toxic-xlm-roberta |
Toxicity classification model |
TOXICITY_THRESHOLD |
0.8 |
Score threshold for is_toxic |
QDRANT_COLLECTION |
nlp_cache |
Qdrant collection name |
QDRANT_CACHE_THRESHOLD |
0.92 |
Min cosine similarity for cache hit |
LOG_LEVEL |
INFO |
Logging verbosity |
OTEL_SERVICE_NAME |
nlp-microservice |
OpenTelemetry service name |
OTEL_EXPORTER_ENDPOINT |
http://jaeger:4317 |
OTLP collector endpoint |
All logs are JSON-formatted via structlog with meaningful fields:
{"event": "cache_hit", "score": 0.96, "request_id": "abc-123", "timestamp": "2026-03-17T10:00:00Z", "level": "info"}OpenTelemetry instruments both FastAPI and gRPC. Spans include:
nlp_analyze(root span per request)detect_language,sentence_tokenize,embed_textscache_lookup,cache_storeanalyze_sentiment,analyze_toxicity,classify_categories,classify_tonalityrest_analyze,rest_analyze_batch,grpc_analyze,grpc_analyze_batch
View traces in Jaeger at http://localhost:16686.
The .gitlab-ci.yml defines three stages:
| Stage | What it does |
|---|---|
lint |
Runs ruff check and ruff format --check |
test |
Installs deps, generates proto, runs pytest with coverage gate |
build |
Builds and pushes Docker image to registry |
Coverage is enforced with pytest-cov and fails if total coverage drops below 90%.
pytest --cov=. --cov-report=term-missing --cov-fail-under=90Use this optional benchmark to compare ONNX Runtime vs the default Transformers backend:
pip install optimum[onnxruntime]
RUN_ONNX_BENCHMARKS=1 pytest tests/test_benchmark.py -k onnx_vs_pytorch -s
# or
make bench-onnxOptional environment knobs:
BENCHMARK_MODEL_ID(default:distilbert-base-uncased-finetuned-sst-2-english)BENCHMARK_LOOPS(default:30)
βββ .env.example # Environment variable template
βββ .gitlab-ci.yml # GitLab CI/CD pipeline
βββ config.py # Pydantic Settings (loads .env)
βββ docker-compose.yml # Multi-service orchestration
βββ Dockerfile # Multi-stage container build
βββ main_grpc.py # gRPC server entrypoint
βββ main_rest.py # FastAPI REST server entrypoint
βββ Makefile # Developer convenience commands
βββ nlp_engine.py # Core NLP pipeline with semantic cache
βββ nlp_service.proto # gRPC service definition
βββ observability.py # OTel + structlog setup
βββ pyproject.toml # Ruff & pytest configuration
βββ README.md # This file
βββ requirements.txt # Pinned dependencies
βββ tests/
βββ test_api.py # REST integration and schema tests
βββ test_grpc.py # gRPC service mapping tests
βββ test_nlp_engine.py # NLP engine unit tests
Internal use only. All rights reserved.