1. The Bottleneck: Why Embedding Was the Chokepoint
In the original monolithic OpenCrawling runtime (oc-runtime), the entire pipeline — crawling, text extraction, chunking, embedding, and vector writing — ran inside a single Spring Boot application. While this simplified deployment and local development, it introduced a critical scalability constraint: embedding generation.
Generating high-dimensional embedding vectors (e.g., 1024-dim with mxbai-embed-large or 768-dim with nomic-embed-text) is computationally expensive. Each document chunk requires a forward pass through a deep learning model. In a monolithic deployment, a single slow embedding step blocks the entire Kafka consumer thread, causing backpressure that saturates the chunk topic partition lag and degrades end-to-end throughput.
Key Insight: In a well-architected RAG ingestion system, the embedding step is almost always the primary bottleneck. All other pipeline stages (crawling, Tika extraction, token splitting) are I/O-bound. Embedding is the only CPU/GPU-bound step. It must be isolated and scaled independently.
2. The Solution: oc-embedding-service
The new oc-embedding-service module is a standalone Spring Boot application that contains only the embedding logic, decoupled from the ingestion runtime. It participates in the pipeline purely through Kafka topics, following the Claim Check Pattern:
oc-runtime → opencrawling-chunks
Kafka Topic → EmbeddingConsumer
oc-embedding-service ×N → opencrawling-embedded
Kafka Topic → VectorStoreWriterConsumer
oc-runtime
The EmbeddingConsumer listens to the opencrawling-chunks topic, reads each DocumentChunkMessage, delegates to the EmbeddingModelFactory to route the text to the appropriate model engine (Ollama, OpenAI, or HuggingFace), and publishes the resulting DocumentEmbeddedMessage containing the pre-computed float vector to the opencrawling-embedded topic.
3. Inside the Code: EmbeddingModelFactory
The heart of the new service is the EmbeddingModelFactory. Instead of hardcoding a single embedding model, this factory reads the Transformation Connector engine configuration from the message metadata and dynamically selects the appropriate embedding provider:
@Component
public class EmbeddingModelFactory {
private final OllamaEmbeddingModel ollamaModel;
private final OpenAiEmbeddingModel openAiModel;
public EmbeddingModel resolve(String engineType) {
return switch (engineType.toLowerCase()) {
case "openai" -> openAiModel;
case "ollama" -> ollamaModel;
default -> ollamaModel; // safe fallback
};
}
}
4. Horizontal Scaling with Docker Compose
Because oc-embedding-service is fully stateless, it can be scaled out to any number of replicas using a single Docker Compose command. Kafka automatically rebalances partitions across the consumer group:
# Scale the embedding service to 3 replicas
docker compose -f docker-compose-apps.yml scale oc-embedding-service=3
# Verify the consumer group rebalance
docker exec -it opencrawling-kafka kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--describe --group opencrawling-embedding-group
Performance Tip: For maximum GPU utilization, configure each oc-embedding-service replica to point to a different Ollama instance via the spring.ai.ollama.base-url property. Use an NGINX upstream block to load-balance across multiple GPU-enabled Ollama nodes.
5. Running Locally in Development Mode
For local development, run the embedding service in a separate terminal alongside the main runtime:
# Terminal 1: Start the main ingestion runtime
mvn spring-boot:run -pl oc-runtime -Dspring-boot.run.profiles=dev
# Terminal 2: Start the embedding microservice
mvn spring-boot:run -pl oc-embedding-service
# Terminal 3: Start the Admin React UI
cd oc-admin-ui && npm run dev
6. Fully Decoupled Docker Deployment
For production-grade deployments, the docker-compose-decoupled.yml file spins up five separate containerized services, each communicating exclusively through Kafka topics:
- oc-crawler — Repository Connector & Claim Check Publisher
- oc-ingestion-consumer — Apache Tika Extractor & Token Chunker
- oc-embedding-service — Embedding Generator (horizontally scalable)
- oc-writer-consumer — Vector Store Writer (pgvector/Elasticsearch/Qdrant)
- oc-mcp-server — Secure MCP Server for AI Agents
# Build all decoupled service images
docker compose -f docker-compose-decoupled.yml build
# Start the entire pipeline
docker compose -f docker-compose-decoupled.yml up -d
# Run the automated end-to-end integration test
./scripts/test-docker-decoupled.sh
Shared Storage Requirement: When running multiple oc-embedding-service or oc-ingestion-consumer replicas in a distributed setup, ensure all nodes have access to the same shared filesystem (NFS, S3/MinIO) used by the crawler. The Claim Check pattern references the file by URI.
7. Kafka Topic Partition Configuration
Configure your Kafka topics with a partition count matching your maximum desired replica count. To support up to 8 embedding replicas:
# opencrawling-chunks and opencrawling-embedded topics
partitions: 8
replication-factor: 1
Kafka guarantees that each partition is consumed by at most one consumer within the same consumer group (opencrawling-embedding-group). With 8 partitions and 3 replicas, Kafka will assign approximately 2–3 partitions per replica automatically.
8. Ollama Multi-Model Support
The release adds automatic configuration of multiple Ollama embedding models routed to separate pgvector tables by dimension:
- mxbai-embed-large (1024-dim) →
vector_store_1024 - nomic-embed-text (768-dim) →
vector_store_768 - all-minilm (384-dim) →
vector_store_384
docker exec -it ollama ollama pull mxbai-embed-large
docker exec -it ollama ollama pull nomic-embed-text
docker exec -it ollama ollama pull all-minilm
9. What’s Next
- HuggingFace Inference Endpoint Support — Route chunks to cloud-hosted HuggingFace models without local GPU infrastructure.
- Adaptive Batching — Group multiple chunks into embedding batch requests, reducing API call overhead.
- Prometheus Metrics & Kubernetes HPA — Expose consumer lag metrics enabling Kubernetes to auto-scale embedding replicas based on Kafka partition lag.
- Dynamic Model Routing — Per-connector model configuration, allowing different document types to be vectorized with different models simultaneously.
🚀 Ready to Scale Your Embeddings?
Clone the repository, pull an Ollama model, and run docker compose scale oc-embedding-service=3 to experience instant horizontal throughput.