Skip to main content
Crashbytes logoCrashbytes
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Browse Articles
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Network
Theme
Browse Articles
Crashbytes logoCrashbytes

Expert insights on web development, technology trends, and programming best practices. Learn from real-world experiences and cutting-edge techniques that help you build better software.

Follow Us

Our Sites

  • ๐Ÿ”ฎ Predictions
  • ๐Ÿ“ฐ Breaking News
  • ๐ŸŽจ AI Art
  • ๐Ÿ“– Short Stories
  • View All โ†’
  • Products โ†’

Sitemap

  • Home
  • All Articles
  • Open Source
  • Services
  • About Us
  • Contact
  • Donate Compute

Popular Topics

  • Serverless
  • Cloud Architecture
  • DevOps
  • Kubernetes
  • Platform Engineering

Resources

  • Privacy Policy
  • Terms of Service
  • Sitemap
  • RSS Feed
  • PGP Key

Stay Updated

Get the latest articles, tutorials, and insights delivered to your inbox. Join our community of developers and never miss an update.

ยฉ 2021-2026 Crashbytesยฎ by Blackhole Software, LLC. All rights reserved.
| Reg. U.S. Pat. & Tm. Off.

Made for the developer community

  1. Home
  2. /
  3. Articles
  4. /
  5. Tutorial: Building Production Vector Databases for RAG Applications - Complete Implementation with Pinecone, Weaviate, and Kubernetes
tutorialOctober 20, 202520 min readโ€ข By CrashBytes Technical Team

Tutorial: Building Production Vector Databases for RAG Applications - Complete Implementation with Pinecone, Weaviate, and Kubernetes

Build enterprise-grade vector databases for RAG applications with hybrid search, metadata filtering, and production deployment. Complete implementation with Pinecone, Weaviate, embedding strategies, and Kubernetes scaling patterns.

Quick Takeaways

What you'll learn in this article

20 min read
Intermediate
  • 1

    Dual vector database support: Pinecone (managed) and Weaviate (self-hosted)

  • 2

    Hybrid search implementation: Dense vectors plus BM25 sparse vectors

  • 3

    Embedding pipeline: Document chunking, embedding generation, and batch processing

  • 4

    Metadata filtering: Complex queries with attribute filters

  • 5

    Multi-tenancy: Namespace isolation for different customers/projects

Keep reading for detailed implementation, code examples, and real-world results

After architecting RAG systems processing billions of queries for enterprise search, customer support, and knowledge management platforms, I've learned that the difference between a prototype RAG application and production-ready system is comprehensive vector database design, hybrid search strategies, and scalable deployment architecture that maintains sub-100ms latency at scale.

The challenge extends far beyond embedding documents and storing vectorsโ€”enterprises need sophisticated metadata filtering, hybrid search combining dense and sparse vectors, multi-tenancy isolation, backup strategies, and monitoring that prevents degradation as data scales to millions of documents.

This tutorial presents the production-ready vector database architecture I've implemented across Fortune 500 RAG applications. You'll build a complete system integrating both managed (Pinecone) and self-hosted (Weaviate) vector databases, implement hybrid search strategies, design embedding pipelines, and deploy with Kubernetes for horizontal scaling.

Repository: github.com/CrashBytes/ByteSizedExamples/tree/main/tutorial-production-vector-database-rag

Tutorial Overview & Architecture

What You'll Build

This tutorial implements a production-grade vector database system with:

  • Dual vector database support: Pinecone (managed) and Weaviate (self-hosted)
  • Hybrid search implementation: Dense vectors plus BM25 sparse vectors
  • Embedding pipeline: Document chunking, embedding generation, and batch processing
  • Metadata filtering: Complex queries with attribute filters
  • Multi-tenancy: Namespace isolation for different customers/projects
  • FastAPI service: REST API with async processing and connection pooling
  • Kubernetes deployment: Autoscaling, persistent storage, and monitoring
  • Production observability: Metrics, logging, and query performance tracking

Real-World Applications

This architecture supports enterprise RAG use cases including:

  • Enterprise search: Semantic search across millions of documents with metadata filtering
  • Customer support: Context-aware chatbots with knowledge base retrieval
  • Document intelligence: Question-answering over technical documentation
  • Compliance systems: Retrieving relevant policies and regulations with audit trails

System Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Client Applications                  โ”‚
โ”‚              (Chat UI, Search Interface)                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚ REST API Requests
                   โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              FastAPI Vector Search Service              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  Query       โ”‚   Embedding   โ”‚   Result         โ”‚  โ”‚
โ”‚  โ”‚  Processing  โ”‚   Generation  โ”‚   Ranking        โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
           โ”‚                โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ†“โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ†“โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚  Pinecone   โ”‚  โ”‚  Weaviate   โ”‚
    โ”‚  (Managed)  โ”‚  โ”‚(Self-hosted)โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ†“โ”€โ”€โ”€โ”€โ”€โ”€โ”
                     โ”‚ PostgreSQL  โ”‚
                     โ”‚ (Metadata)  โ”‚
                     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Technology Stack Decisions

Pinecone vs Weaviate:

  • Pinecone: Managed service, zero ops overhead, excellent for production. Trade-off: vendor lock-in, cost at scale
  • Weaviate: Self-hosted, full control, cost-effective at scale. Trade-off: operational complexity, requires Kubernetes expertise

Embedding Models:

  • OpenAI text-embedding-3-large: Best quality, 3072 dimensions, $0.13/1M tokens
  • Sentence Transformers (all-MiniLM-L6-v2): Free, 384 dimensions, sufficient for many use cases

Hybrid Search:

  • Dense vectors: Semantic similarity via embeddings
  • Sparse vectors (BM25): Keyword matching for precise queries
  • Combined scoring: Weighted fusion for optimal retrieval

Expected Outcomes

Time Investment: 4-5 hours for complete implementation and deployment

Skills Gained:

  • Production vector database architecture patterns
  • Hybrid search implementation and tuning
  • Embedding pipeline design and optimization
  • Multi-tenancy and security in vector databases
  • Kubernetes deployment for stateful services
  • RAG system observability and monitoring
Advertisement

Part 1: Environment Setup & Dependencies

Prerequisites Verification

Verify your development environment before starting:

# Check Python version (3.10+)
python --version

# Verify Docker installation
docker --version

# Verify Kubernetes cluster access
kubectl version --client
kubectl cluster-info

# Check available resources
kubectl top nodes  # Ensure sufficient CPU/memory

Repository Setup

Clone the tutorial repository and set up the environment:

# Clone repository
git clone https://github.com/CrashBytes/ByteSizedExamples.git
cd ByteSizedExamples/tutorial-production-vector-database-rag

# Create Python virtual environment
python -m venv venv
source venv/bin/activate  # On macOS/Linux
# or
.\venv\Scripts\activate  # On Windows

# Install dependencies
pip install -r requirements.txt

# Verify installations
python -c "import pinecone, weaviate, fastapi, sentence_transformers; print('All imports successful')"

Dependencies Explanation

requirements.txt:

# Core API framework
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
pydantic-settings==2.1.0

# Vector databases
pinecone-client==3.0.0
weaviate-client==4.4.0

# Embeddings
openai==1.3.0
sentence-transformers==2.2.2
transformers==4.35.0

# Data processing
pandas==2.1.3
numpy==1.26.2

# PostgreSQL for metadata
asyncpg==0.29.0
sqlalchemy==2.0.23

# Monitoring & logging
prometheus-client==0.19.0
python-json-logger==2.0.7

# Utilities
httpx==0.25.1
python-multipart==0.0.6
aiofiles==23.2.1

Why these dependencies?

  • FastAPI + Uvicorn: Async API with high concurrency for real-time search
  • Pinecone + Weaviate clients: Official SDKs with connection pooling
  • OpenAI + Sentence Transformers: Flexible embedding generation options
  • SQLAlchemy + asyncpg: Async PostgreSQL for metadata storage
  • Prometheus: Production metrics and monitoring

API Keys Setup

Create .env file with required API keys:

# OpenAI for embeddings
OPENAI_API_KEY=sk-your-key-here

# Pinecone configuration
PINECONE_API_KEY=your-pinecone-key
PINECONE_ENVIRONMENT=us-east1-gcp

# Weaviate configuration (for cloud)
WEAVIATE_URL=https://your-instance.weaviate.network
WEAVIATE_API_KEY=your-weaviate-key

# PostgreSQL
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=vectordb
POSTGRES_PASSWORD=secure-password
POSTGRES_DB=rag_metadata

# Application settings
LOG_LEVEL=INFO
METRICS_PORT=8001

Local Development with Docker Compose

For local development, use Docker Compose to run Weaviate and PostgreSQL:

docker-compose.yml:

version: '3.8'

services:
  weaviate:
    image: semitechnologies/weaviate:1.23.0
    ports:
      - '8080:8080'
    environment:
      QUERY_DEFAULTS_LIMIT: 20
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
      PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
      DEFAULT_VECTORIZER_MODULE: 'none'
      ENABLE_MODULES: 'text2vec-openai,generative-openai'
      CLUSTER_HOSTNAME: 'node1'
    volumes:
      - weaviate_data:/var/lib/weaviate

  postgres:
    image: postgres:15
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: vectordb
      POSTGRES_PASSWORD: secure-password
      POSTGRES_DB: rag_metadata
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  weaviate_data:
  postgres_data:

Start local services:

# Start Weaviate and PostgreSQL
docker-compose up -d

# Verify services are running
docker-compose ps

# Check Weaviate health
curl http://localhost:8080/v1/meta

# Test PostgreSQL connection
psql -h localhost -U vectordb -d rag_metadata -c "SELECT version();"

Project Structure

Create the following directory structure:

tutorial-production-vector-database-rag/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py                    # FastAPI application
โ”‚   โ”œโ”€โ”€ config.py                  # Configuration management
โ”‚   โ”œโ”€โ”€ models.py                  # Pydantic models
โ”‚   โ”œโ”€โ”€ vector_stores/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ base.py                # Abstract base class
โ”‚   โ”‚   โ”œโ”€โ”€ pinecone_store.py      # Pinecone implementation
โ”‚   โ”‚   โ””โ”€โ”€ weaviate_store.py      # Weaviate implementation
โ”‚   โ”œโ”€โ”€ embeddings/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ base.py                # Embedding interface
โ”‚   โ”‚   โ”œโ”€โ”€ openai_embedder.py     # OpenAI embeddings
โ”‚   โ”‚   โ””โ”€โ”€ sentence_embedder.py   # Sentence Transformers
โ”‚   โ”œโ”€โ”€ search/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ hybrid_search.py       # Hybrid search logic
โ”‚   โ”‚   โ””โ”€โ”€ reranking.py           # Result reranking
โ”‚   โ”œโ”€โ”€ ingestion/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ chunker.py             # Document chunking
โ”‚   โ”‚   โ””โ”€โ”€ pipeline.py            # Ingestion pipeline
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ logging.py             # Structured logging
โ”‚       โ””โ”€โ”€ metrics.py             # Prometheus metrics
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ test_vector_stores.py
โ”‚   โ”œโ”€โ”€ test_embeddings.py
โ”‚   โ”œโ”€โ”€ test_search.py
โ”‚   โ””โ”€โ”€ test_data/
โ”œโ”€โ”€ k8s/
โ”‚   โ”œโ”€โ”€ weaviate-statefulset.yaml
โ”‚   โ”œโ”€โ”€ postgres-statefulset.yaml
โ”‚   โ”œโ”€โ”€ api-deployment.yaml
โ”‚   โ”œโ”€โ”€ services.yaml
โ”‚   โ””โ”€โ”€ ingress.yaml
โ”œโ”€โ”€ examples/
โ”‚   โ”œโ”€โ”€ ingest_documents.py
โ”‚   โ”œโ”€โ”€ query_examples.py
โ”‚   โ””โ”€โ”€ benchmark.py
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ docker-compose.yml
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ README.md
โ””โ”€โ”€ .env.example

Part 2: Vector Store Abstraction Layer

Design an abstraction layer supporting multiple vector databases with a consistent interface.

Base Vector Store Interface

Create src/vector_stores/base.py:

from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass

@dataclass
class SearchResult:
    """Unified search result structure"""
    id: str
    score: float
    document: str
    metadata: Dict[str, Any]
    namespace: Optional[str] = None

@dataclass
class IndexStats:
    """Vector index statistics"""
    total_vectors: int
    dimension: int
    namespaces: List[str]
    index_fullness: float

class VectorStore(ABC):
    """Abstract base class for vector store implementations"""

    @abstractmethod
    async def create_index(
        self,
        index_name: str,
        dimension: int,
        metric: str = "cosine",
        **kwargs
    ) -> bool:
        """Create a new vector index"""
        pass

    @abstractmethod
    async def upsert(
        self,
        vectors: List[List[float]],
        documents: List[str],
        metadata: List[Dict[str, Any]],
        ids: Optional[List[str]] = None,
        namespace: Optional[str] = None,
        batch_size: int = 100
    ) -> Dict[str, Any]:
        """Insert or update vectors with documents and metadata"""
        pass

    @abstractmethod
    async def search(
        self,
        query_vector: List[float],
        top_k: int = 10,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None,
        include_metadata: bool = True
    ) -> List[SearchResult]:
        """Search for similar vectors"""
        pass

    @abstractmethod
    async def hybrid_search(
        self,
        query_vector: List[float],
        query_text: str,
        top_k: int = 10,
        alpha: float = 0.5,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None
    ) -> List[SearchResult]:
        """Hybrid search combining dense and sparse vectors"""
        pass

    @abstractmethod
    async def delete(
        self,
        ids: List[str],
        namespace: Optional[str] = None
    ) -> bool:
        """Delete vectors by ID"""
        pass

    @abstractmethod
    async def get_stats(self, index_name: str) -> IndexStats:
        """Get index statistics"""
        pass

    @abstractmethod
    async def close(self):
        """Close connections and cleanup"""
        pass

Pinecone Implementation

Create src/vector_stores/pinecone_store.py:

import pinecone
from pinecone import ServerlessSpec
from typing import List, Dict, Any, Optional
import uuid
import logging
from .base import VectorStore, SearchResult, IndexStats

logger = logging.getLogger(__name__)

class PineconeStore(VectorStore):
    """Pinecone vector store implementation"""

    def __init__(
        self,
        api_key: str,
        environment: str,
        index_name: str
    ):
        self.api_key = api_key
        self.environment = environment
        self.index_name = index_name
        self.pc = None
        self.index = None

    async def initialize(self):
        """Initialize Pinecone client and index"""
        try:
            self.pc = pinecone.Pinecone(
                api_key=self.api_key,
                environment=self.environment
            )

            # Get or create index
            if self.index_name not in self.pc.list_indexes().names():
                logger.info(f"Index {self.index_name} not found, will be created on first upsert")
            else:
                self.index = self.pc.Index(self.index_name)
                logger.info(f"Connected to existing index: {self.index_name}")

        except Exception as e:
            logger.error(f"Pinecone initialization error: {e}")
            raise

    async def create_index(
        self,
        index_name: str,
        dimension: int,
        metric: str = "cosine",
        **kwargs
    ) -> bool:
        """Create a new Pinecone serverless index"""
        try:
            spec = ServerlessSpec(
                cloud=kwargs.get("cloud", "aws"),
                region=kwargs.get("region", "us-east-1")
            )

            self.pc.create_index(
                name=index_name,
                dimension=dimension,
                metric=metric,
                spec=spec
            )

            self.index_name = index_name
            self.index = self.pc.Index(index_name)

            logger.info(f"Created Pinecone index: {index_name}")
            return True

        except Exception as e:
            logger.error(f"Error creating Pinecone index: {e}")
            return False

    async def upsert(
        self,
        vectors: List[List[float]],
        documents: List[str],
        metadata: List[Dict[str, Any]],
        ids: Optional[List[str]] = None,
        namespace: Optional[str] = None,
        batch_size: int = 100
    ) -> Dict[str, Any]:
        """Upsert vectors to Pinecone"""
        if not self.index:
            raise RuntimeError("Index not initialized. Call initialize() first.")

        if ids is None:
            ids = [str(uuid.uuid4()) for _ in vectors]

        # Prepare vectors with metadata
        vectors_to_upsert = []
        for i, (vec_id, vector, doc, meta) in enumerate(zip(ids, vectors, documents, metadata)):
            # Add document to metadata
            combined_metadata = {**meta, "document": doc}
            vectors_to_upsert.append({
                "id": vec_id,
                "values": vector,
                "metadata": combined_metadata
            })

        # Batch upsert
        upserted_count = 0
        for i in range(0, len(vectors_to_upsert), batch_size):
            batch = vectors_to_upsert[i:i + batch_size]
            try:
                response = self.index.upsert(
                    vectors=batch,
                    namespace=namespace or ""
                )
                upserted_count += response.upserted_count
            except Exception as e:
                logger.error(f"Batch upsert error: {e}")
                raise

        logger.info(f"Upserted {upserted_count} vectors to Pinecone")
        return {"upserted_count": upserted_count}

    async def search(
        self,
        query_vector: List[float],
        top_k: int = 10,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None,
        include_metadata: bool = True
    ) -> List[SearchResult]:
        """Search Pinecone index"""
        if not self.index:
            raise RuntimeError("Index not initialized")

        try:
            response = self.index.query(
                vector=query_vector,
                top_k=top_k,
                namespace=namespace or "",
                filter=filter_metadata,
                include_metadata=include_metadata
            )

            results = []
            for match in response.matches:
                results.append(SearchResult(
                    id=match.id,
                    score=match.score,
                    document=match.metadata.get("document", ""),
                    metadata={k: v for k, v in match.metadata.items() if k != "document"},
                    namespace=namespace
                ))

            return results

        except Exception as e:
            logger.error(f"Pinecone search error: {e}")
            raise

    async def hybrid_search(
        self,
        query_vector: List[float],
        query_text: str,
        top_k: int = 10,
        alpha: float = 0.5,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None
    ) -> List[SearchResult]:
        """
        Hybrid search in Pinecone using sparse-dense vectors
        Note: Requires Pinecone index configured with hybrid search support
        """
        # Pinecone hybrid search implementation
        # For production, implement sparse vector generation from query_text
        # This is a simplified version using dense search only
        logger.warning("Pinecone hybrid search using dense vectors only (sparse not implemented)")
        return await self.search(
            query_vector=query_vector,
            top_k=top_k,
            namespace=namespace,
            filter_metadata=filter_metadata
        )

    async def delete(
        self,
        ids: List[str],
        namespace: Optional[str] = None
    ) -> bool:
        """Delete vectors from Pinecone"""
        if not self.index:
            raise RuntimeError("Index not initialized")

        try:
            self.index.delete(
                ids=ids,
                namespace=namespace or ""
            )
            logger.info(f"Deleted {len(ids)} vectors from Pinecone")
            return True

        except Exception as e:
            logger.error(f"Pinecone delete error: {e}")
            return False

    async def get_stats(self, index_name: str) -> IndexStats:
        """Get Pinecone index statistics"""
        try:
            stats = self.index.describe_index_stats()

            return IndexStats(
                total_vectors=stats.total_vector_count,
                dimension=stats.dimension,
                namespaces=list(stats.namespaces.keys()) if stats.namespaces else [],
                index_fullness=stats.index_fullness
            )

        except Exception as e:
            logger.error(f"Error getting Pinecone stats: {e}")
            raise

    async def close(self):
        """Close Pinecone connections"""
        self.index = None
        logger.info("Closed Pinecone connections")

Weaviate Implementation

Create src/vector_stores/weaviate_store.py:

import weaviate
from weaviate.classes.config import Configure, Property, DataType
from weaviate.classes.query import MetadataQuery
from typing import List, Dict, Any, Optional
import uuid
import logging
from .base import VectorStore, SearchResult, IndexStats

logger = logging.getLogger(__name__)

class WeaviateStore(VectorStore):
    """Weaviate vector store implementation with hybrid search"""

    def __init__(
        self,
        url: str,
        api_key: Optional[str] = None,
        class_name: str = "Document"
    ):
        self.url = url
        self.api_key = api_key
        self.class_name = class_name
        self.client = None

    async def initialize(self):
        """Initialize Weaviate client"""
        try:
            if self.api_key:
                self.client = weaviate.Client(
                    url=self.url,
                    auth_client_secret=weaviate.AuthApiKey(self.api_key)
                )
            else:
                self.client = weaviate.Client(url=self.url)

            # Check if class exists
            if not self.client.schema.exists(self.class_name):
                logger.info(f"Class {self.class_name} not found, will be created on first upsert")
            else:
                logger.info(f"Connected to Weaviate class: {self.class_name}")

        except Exception as e:
            logger.error(f"Weaviate initialization error: {e}")
            raise

    async def create_index(
        self,
        index_name: str,
        dimension: int,
        metric: str = "cosine",
        **kwargs
    ) -> bool:
        """Create Weaviate class (equivalent to index)"""
        try:
            class_obj = {
                "class": index_name,
                "description": kwargs.get("description", "Vector store for RAG"),
                "vectorizer": "none",  # We provide vectors
                "moduleConfig": {
                    "text2vec-openai": {"skip": True}
                },
                "properties": [
                    {
                        "name": "document",
                        "dataType": ["text"],
                        "description": "The document text"
                    },
                    {
                        "name": "namespace",
                        "dataType": ["string"],
                        "description": "Namespace for multi-tenancy"
                    },
                    {
                        "name": "metadata",
                        "dataType": ["object"],
                        "description": "Custom metadata"
                    }
                ],
                "vectorIndexConfig": {
                    "distance": metric
                }
            }

            self.client.schema.create_class(class_obj)
            self.class_name = index_name

            logger.info(f"Created Weaviate class: {index_name}")
            return True

        except Exception as e:
            logger.error(f"Error creating Weaviate class: {e}")
            return False

    async def upsert(
        self,
        vectors: List[List[float]],
        documents: List[str],
        metadata: List[Dict[str, Any]],
        ids: Optional[List[str]] = None,
        namespace: Optional[str] = None,
        batch_size: int = 100
    ) -> Dict[str, Any]:
        """Upsert vectors to Weaviate"""
        if not self.client:
            raise RuntimeError("Client not initialized")

        if ids is None:
            ids = [str(uuid.uuid4()) for _ in vectors]

        upserted_count = 0

        with self.client.batch as batch:
            batch.batch_size = batch_size

            for vec_id, vector, doc, meta in zip(ids, vectors, documents, metadata):
                properties = {
                    "document": doc,
                    "namespace": namespace or "default",
                    "metadata": meta
                }

                try:
                    batch.add_data_object(
                        data_object=properties,
                        class_name=self.class_name,
                        uuid=vec_id,
                        vector=vector
                    )
                    upserted_count += 1

                except Exception as e:
                    logger.error(f"Error adding object {vec_id}: {e}")

        logger.info(f"Upserted {upserted_count} vectors to Weaviate")
        return {"upserted_count": upserted_count}

    async def search(
        self,
        query_vector: List[float],
        top_k: int = 10,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None,
        include_metadata: bool = True
    ) -> List[SearchResult]:
        """Dense vector search in Weaviate"""
        if not self.client:
            raise RuntimeError("Client not initialized")

        try:
            # Build where filter
            where_filter = None
            if namespace:
                where_filter = {
                    "path": ["namespace"],
                    "operator": "Equal",
                    "valueString": namespace
                }

            # Execute search
            result = (
                self.client.query
                .get(self.class_name, ["document", "namespace", "metadata"])
                .with_near_vector({"vector": query_vector})
                .with_limit(top_k)
            )

            if where_filter:
                result = result.with_where(where_filter)

            response = result.do()

            # Parse results
            results = []
            if "data" in response and "Get" in response["data"]:
                items = response["data"]["Get"][self.class_name]

                for item in items:
                    results.append(SearchResult(
                        id=item.get("_additional", {}).get("id", ""),
                        score=item.get("_additional", {}).get("distance", 0.0),
                        document=item.get("document", ""),
                        metadata=item.get("metadata", {}),
                        namespace=item.get("namespace")
                    ))

            return results

        except Exception as e:
            logger.error(f"Weaviate search error: {e}")
            raise

    async def hybrid_search(
        self,
        query_vector: List[float],
        query_text: str,
        top_k: int = 10,
        alpha: float = 0.5,
        namespace: Optional[str] = None,
        filter_metadata: Optional[Dict[str, Any]] = None
    ) -> List[SearchResult]:
        """
        Weaviate hybrid search combining dense vectors and BM25
        alpha: 0 = pure BM25, 1 = pure vector, 0.5 = balanced
        """
        if not self.client:
            raise RuntimeError("Client not initialized")

        try:
            # Build where filter
            where_filter = None
            if namespace:
                where_filter = {
                    "path": ["namespace"],
                    "operator": "Equal",
                    "valueString": namespace
                }

            # Hybrid search
            result = (
                self.client.query
                .get(self.class_name, ["document", "namespace", "metadata"])
                .with_hybrid(
                    query=query_text,
                    vector=query_vector,
                    alpha=alpha
                )
                .with_limit(top_k)
            )

            if where_filter:
                result = result.with_where(where_filter)

            response = result.do()

            # Parse results
            results = []
            if "data" in response and "Get" in response["data"]:
                items = response["data"]["Get"][self.class_name]

                for item in items:
                    results.append(SearchResult(
                        id=item.get("_additional", {}).get("id", ""),
                        score=item.get("_additional", {}).get("score", 0.0),
                        document=item.get("document", ""),
                        metadata=item.get("metadata", {}),
                        namespace=item.get("namespace")
                    ))

            return results

        except Exception as e:
            logger.error(f"Weaviate hybrid search error: {e}")
            raise

    async def delete(
        self,
        ids: List[str],
        namespace: Optional[str] = None
    ) -> bool:
        """Delete objects from Weaviate"""
        if not self.client:
            raise RuntimeError("Client not initialized")

        try:
            for object_id in ids:
                self.client.data_object.delete(
                    uuid=object_id,
                    class_name=self.class_name
                )

            logger.info(f"Deleted {len(ids)} objects from Weaviate")
            return True

        except Exception as e:
            logger.error(f"Weaviate delete error: {e}")
            return False

    async def get_stats(self, index_name: str) -> IndexStats:
        """Get Weaviate class statistics"""
        try:
            # Get aggregate count
            result = (
                self.client.query
                .aggregate(index_name)
                .with_meta_count()
                .do()
            )

            total_count = 0
            if "data" in result and "Aggregate" in result["data"]:
                agg = result["data"]["Aggregate"][index_name]
                if agg and len(agg) > 0:
                    total_count = agg[0].get("meta", {}).get("count", 0)

            # Get schema for dimension
            schema = self.client.schema.get(index_name)
            dimension = schema.get("vectorIndexConfig", {}).get("dimension", 0)

            return IndexStats(
                total_vectors=total_count,
                dimension=dimension,
                namespaces=["default"],  # Simplified
                index_fullness=0.0  # Not applicable for Weaviate
            )

        except Exception as e:
            logger.error(f"Error getting Weaviate stats: {e}")
            raise

    async def close(self):
        """Close Weaviate client"""
        if self.client:
            self.client = None
        logger.info("Closed Weaviate client")
Advertisement

Part 3: Embedding Pipeline Implementation

Build a flexible embedding pipeline supporting multiple embedding models.

Base Embedding Interface

Create src/embeddings/base.py:

from abc import ABC, abstractmethod
from typing import List, Dict, Any
from dataclasses import dataclass

@dataclass
class EmbeddingResult:
    """Embedding result with metadata"""
    embeddings: List[List[float]]
    dimension: int
    model_name: str
    token_count: int

class Embedder(ABC):
    """Abstract base class for embedding generation"""

    @abstractmethod
    async def embed_documents(
        self,
        texts: List[str],
        batch_size: int = 32
    ) -> EmbeddingResult:
        """Generate embeddings for multiple documents"""
        pass

    @abstractmethod
    async def embed_query(self, text: str) -> List[float]:
        """Generate embedding for a single query"""
        pass

    @abstractmethod
    def get_dimension(self) -> int:
        """Get embedding dimension"""
        pass

    @abstractmethod
    def get_model_name(self) -> str:
        """Get model identifier"""
        pass

OpenAI Embeddings Implementation

Create src/embeddings/openai_embedder.py:

from openai import AsyncOpenAI
from typing import List
import logging
from .base import Embedder, EmbeddingResult

logger = logging.getLogger(__name__)

class OpenAIEmbedder(Embedder):
    """OpenAI embeddings implementation"""

    def __init__(
        self,
        api_key: str,
        model: str = "text-embedding-3-large",
        dimensions: int = 3072
    ):
        self.client = AsyncOpenAI(api_key=api_key)
        self.model = model
        self.dimensions = dimensions

        logger.info(f"Initialized OpenAI embedder with model: {model}")

    async def embed_documents(
        self,
        texts: List[str],
        batch_size: int = 32
    ) -> EmbeddingResult:
        """Generate embeddings for multiple documents"""
        all_embeddings = []
        total_tokens = 0

        # Process in batches
        for i in range(0, len(texts), batch_size):
            batch = texts[i:i + batch_size]

            try:
                response = await self.client.embeddings.create(
                    input=batch,
                    model=self.model,
                    dimensions=self.dimensions
                )

                batch_embeddings = [item.embedding for item in response.data]
                all_embeddings.extend(batch_embeddings)
                total_tokens += response.usage.total_tokens

            except Exception as e:
                logger.error(f"OpenAI embedding error: {e}")
                raise

        return EmbeddingResult(
            embeddings=all_embeddings,
            dimension=self.dimensions,
            model_name=self.model,
            token_count=total_tokens
        )

    async def embed_query(self, text: str) -> List[float]:
        """Generate embedding for a single query"""
        try:
            response = await self.client.embeddings.create(
                input=[text],
                model=self.model,
                dimensions=self.dimensions
            )

            return response.data[0].embedding

        except Exception as e:
            logger.error(f"OpenAI query embedding error: {e}")
            raise

    def get_dimension(self) -> int:
        return self.dimensions

    def get_model_name(self) -> str:
        return self.model

Sentence Transformers Implementation

Create src/embeddings/sentence_embedder.py:

from sentence_transformers import SentenceTransformer
from typing import List
import logging
import torch
from .base import Embedder, EmbeddingResult

logger = logging.getLogger(__name__)

class SentenceEmbedder(Embedder):
    """Sentence Transformers embeddings (local, free)"""

    def __init__(
        self,
        model_name: str = "all-MiniLM-L6-v2",
        device: str = None
    ):
        self.model_name = model_name
        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")

        # Load model
        self.model = SentenceTransformer(model_name, device=self.device)
        self.dimension = self.model.get_sentence_embedding_dimension()

        logger.info(f"Loaded Sentence Transformer: {model_name} on {self.device}")

    async def embed_documents(
        self,
        texts: List[str],
        batch_size: int = 32
    ) -> EmbeddingResult:
        """Generate embeddings for multiple documents"""
        try:
            # Generate embeddings
            embeddings = self.model.encode(
                texts,
                batch_size=batch_size,
                show_progress_bar=False,
                convert_to_numpy=True
            )

            # Convert to list of lists
            embeddings_list = [emb.tolist() for emb in embeddings]

            # Estimate token count (rough approximation)
            token_count = sum(len(text.split()) for text in texts) * 2

            return EmbeddingResult(
                embeddings=embeddings_list,
                dimension=self.dimension,
                model_name=self.model_name,
                token_count=token_count
            )

        except Exception as e:
            logger.error(f"Sentence Transformer embedding error: {e}")
            raise

    async def embed_query(self, text: str) -> List[float]:
        """Generate embedding for a single query"""
        try:
            embedding = self.model.encode(
                text,
                convert_to_numpy=True
            )

            return embedding.tolist()

        except Exception as e:
            logger.error(f"Sentence Transformer query embedding error: {e}")
            raise

    def get_dimension(self) -> int:
        return self.dimension

    def get_model_name(self) -> str:
        return self.model_name

Part 4: Document Ingestion Pipeline

Implement a production-ready ingestion pipeline with chunking strategies and batch processing.

Document Chunker

Create src/ingestion/chunker.py:

from typing import List, Dict, Any
from dataclasses import dataclass
import logging

logger = logging.getLogger(__name__)

@dataclass
class Chunk:
    """Document chunk with metadata"""
    text: str
    chunk_id: str
    document_id: str
    chunk_index: int
    metadata: Dict[str, Any]

class DocumentChunker:
    """Intelligent document chunking with overlap"""

    def __init__(
        self,
        chunk_size: int = 500,
        chunk_overlap: int = 50,
        separator: str = "\n\n"
    ):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap
        self.separator = separator

    def chunk_text(
        self,
        text: str,
        document_id: str,
        metadata: Dict[str, Any] = None
    ) -> List[Chunk]:
        """
        Chunk text with overlap for context preservation

        Strategy:
        1. Split on separator (paragraphs)
        2. Combine into chunks of target size
        3. Add overlap between chunks
        """
        if metadata is None:
            metadata = {}

        # Split into segments
        segments = text.split(self.separator)

        chunks = []
        current_chunk = []
        current_length = 0
        chunk_index = 0

        for segment in segments:
            segment_length = len(segment)

            # If adding segment exceeds chunk size, finalize current chunk
            if current_length + segment_length > self.chunk_size and current_chunk:
                # Create chunk
                chunk_text = self.separator.join(current_chunk)
                chunks.append(Chunk(
                    text=chunk_text,
                    chunk_id=f"{document_id}_chunk_{chunk_index}",
                    document_id=document_id,
                    chunk_index=chunk_index,
                    metadata={
                        **metadata,
                        "chunk_size": len(chunk_text),
                        "original_position": chunk_index
                    }
                ))

                chunk_index += 1

                # Start new chunk with overlap
                # Keep last segment for overlap
                if self.chunk_overlap > 0 and current_chunk:
                    overlap_text = current_chunk[-1]
                    current_chunk = [overlap_text]
                    current_length = len(overlap_text)
                else:
                    current_chunk = []
                    current_length = 0

            # Add segment to current chunk
            current_chunk.append(segment)
            current_length += segment_length

        # Add final chunk if any content remains
        if current_chunk:
            chunk_text = self.separator.join(current_chunk)
            chunks.append(Chunk(
                text=chunk_text,
                chunk_id=f"{document_id}_chunk_{chunk_index}",
                document_id=document_id,
                chunk_index=chunk_index,
                metadata={
                    **metadata,
                    "chunk_size": len(chunk_text),
                    "original_position": chunk_index
                }
            ))

        logger.info(f"Created {len(chunks)} chunks from document {document_id}")
        return chunks

    def chunk_documents(
        self,
        documents: List[Dict[str, Any]]
    ) -> List[Chunk]:
        """
        Chunk multiple documents

        Expected document format:
        {
            "id": "doc_id",
            "text": "document text",
            "metadata": {...}
        }
        """
        all_chunks = []

        for doc in documents:
            doc_id = doc.get("id", f"doc_{len(all_chunks)}")
            text = doc.get("text", "")
            metadata = doc.get("metadata", {})

            chunks = self.chunk_text(text, doc_id, metadata)
            all_chunks.extend(chunks)

        return all_chunks

Ingestion Pipeline

Create src/ingestion/pipeline.py:

from typing import List, Dict, Any, Optional
import asyncio
import logging
from ..embeddings.base import Embedder
from ..vector_stores.base import VectorStore
from .chunker import DocumentChunker, Chunk

logger = logging.getLogger(__name__)

class IngestionPipeline:
    """Complete ingestion pipeline: chunk โ†’ embed โ†’ store"""

    def __init__(
        self,
        embedder: Embedder,
        vector_store: VectorStore,
        chunker: Optional[DocumentChunker] = None
    ):
        self.embedder = embedder
        self.vector_store = vector_store
        self.chunker = chunker or DocumentChunker()

    async def ingest_documents(
        self,
        documents: List[Dict[str, Any]],
        namespace: Optional[str] = None,
        batch_size: int = 100
    ) -> Dict[str, Any]:
        """
        Ingest documents through complete pipeline

        Args:
            documents: List of documents with id, text, metadata
            namespace: Optional namespace for multi-tenancy
            batch_size: Batch size for embedding and upserting

        Returns:
            Statistics about ingestion
        """
        try:
            # Step 1: Chunk documents
            logger.info(f"Chunking {len(documents)} documents...")
            chunks = self.chunker.chunk_documents(documents)
            logger.info(f"Created {len(chunks)} chunks")

            # Step 2: Generate embeddings in batches
            logger.info("Generating embeddings...")
            all_embeddings = []

            for i in range(0, len(chunks), batch_size):
                batch = chunks[i:i + batch_size]
                batch_texts = [chunk.text for chunk in batch]

                embedding_result = await self.embedder.embed_documents(
                    batch_texts,
                    batch_size=min(batch_size, 32)
                )

                all_embeddings.extend(embedding_result.embeddings)
                logger.info(f"Embedded batch {i//batch_size + 1}/{(len(chunks)-1)//batch_size + 1}")

            # Step 3: Upsert to vector store in batches
            logger.info("Storing vectors...")
            total_upserted = 0

            for i in range(0, len(chunks), batch_size):
                batch_chunks = chunks[i:i + batch_size]
                batch_embeddings = all_embeddings[i:i + batch_size]

                ids = [chunk.chunk_id for chunk in batch_chunks]
                documents_text = [chunk.text for chunk in batch_chunks]
                metadata = [chunk.metadata for chunk in batch_chunks]

                result = await self.vector_store.upsert(
                    vectors=batch_embeddings,
                    documents=documents_text,
                    metadata=metadata,
                    ids=ids,
                    namespace=namespace,
                    batch_size=batch_size
                )

                total_upserted += result.get("upserted_count", 0)

            logger.info(f"Ingestion complete: {total_upserted} chunks stored")

            return {
                "documents_processed": len(documents),
                "chunks_created": len(chunks),
                "vectors_stored": total_upserted,
                "embedding_dimension": self.embedder.get_dimension(),
                "model_used": self.embedder.get_model_name()
            }

        except Exception as e:
            logger.error(f"Ingestion pipeline error: {e}")
            raise

    async def ingest_single_document(
        self,
        document_id: str,
        text: str,
        metadata: Dict[str, Any],
        namespace: Optional[str] = None
    ) -> Dict[str, Any]:
        """Ingest a single document"""
        documents = [{
            "id": document_id,
            "text": text,
            "metadata": metadata
        }]

        return await self.ingest_documents(documents, namespace)

Part 5: FastAPI Service Implementation

Build the REST API service exposing vector search functionality.

Configuration Management

Create src/config.py:

from pydantic_settings import BaseSettings
from typing import Optional

class Settings(BaseSettings):
    """Application configuration"""

    # Application
    app_name: str = "Vector Search API"
    app_version: str = "1.0.0"
    debug: bool = False

    # Server
    host: str = "0.0.0.0"
    port: int = 8000

    # OpenAI
    openai_api_key: str
    openai_embedding_model: str = "text-embedding-3-large"
    embedding_dimensions: int = 3072

    # Pinecone
    pinecone_api_key: Optional[str] = None
    pinecone_environment: Optional[str] = None
    pinecone_index_name: str = "rag-vectors"

    # Weaviate
    weaviate_url: str = "http://localhost:8080"
    weaviate_api_key: Optional[str] = None
    weaviate_class_name: str = "Document"

    # Vector store selection
    vector_store: str = "weaviate"  # "pinecone" or "weaviate"

    # Search settings
    default_top_k: int = 10
    default_alpha: float = 0.5  # For hybrid search

    # Chunking
    chunk_size: int = 500
    chunk_overlap: int = 50

    # Performance
    batch_size: int = 100
    max_concurrent_requests: int = 100

    class Config:
        env_file = ".env"

settings = Settings()

Pydantic Models

Create src/models.py:

from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional

class Document(BaseModel):
    """Document for ingestion"""
    id: str = Field(..., description="Unique document identifier")
    text: str = Field(..., description="Document text content")
    metadata: Dict[str, Any] = Field(default_factory=dict, description="Document metadata")

class IngestRequest(BaseModel):
    """Batch document ingestion request"""
    documents: List[Document]
    namespace: Optional[str] = Field(None, description="Namespace for multi-tenancy")

class IngestResponse(BaseModel):
    """Ingestion response"""
    documents_processed: int
    chunks_created: int
    vectors_stored: int
    embedding_dimension: int
    model_used: str

class SearchRequest(BaseModel):
    """Vector search request"""
    query: str = Field(..., description="Search query text")
    top_k: int = Field(10, ge=1, le=100, description="Number of results")
    namespace: Optional[str] = Field(None, description="Namespace to search")
    filter_metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata filters")
    search_type: str = Field("hybrid", description="Search type: dense, sparse, or hybrid")
    alpha: float = Field(0.5, ge=0, le=1, description="Hybrid search weight (0=sparse, 1=dense)")

class SearchResult(BaseModel):
    """Single search result"""
    id: str
    score: float
    document: str
    metadata: Dict[str, Any]
    namespace: Optional[str] = None

class SearchResponse(BaseModel):
    """Search response"""
    query: str
    results: List[SearchResult]
    total_results: int
    search_type: str
    processing_time_ms: float

class HealthResponse(BaseModel):
    """Health check response"""
    status: str
    vector_store: str
    embedding_model: str
    index_stats: Optional[Dict[str, Any]] = None

FastAPI Application

Create src/main.py:

from fastapi import FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import time
import logging
from .config import settings
from .models import (
    IngestRequest, IngestResponse,
    SearchRequest, SearchResponse,
    HealthResponse
)
from .vector_stores.pinecone_store import PineconeStore
from .vector_stores.weaviate_store import WeaviateStore
from .embeddings.openai_embedder import OpenAIEmbedder
from .ingestion.pipeline import IngestionPipeline

# Configure logging
logging.basicConfig(
    level=logging.INFO if not settings.debug else logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# Global instances
vector_store = None
embedder = None
pipeline = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    """Initialize resources on startup"""
    global vector_store, embedder, pipeline

    # Initialize embedder
    logger.info("Initializing embedder...")
    embedder = OpenAIEmbedder(
        api_key=settings.openai_api_key,
        model=settings.openai_embedding_model,
        dimensions=settings.embedding_dimensions
    )

    # Initialize vector store
    logger.info(f"Initializing vector store: {settings.vector_store}")

    if settings.vector_store == "pinecone":
        if not settings.pinecone_api_key:
            raise ValueError("Pinecone API key required")

        vector_store = PineconeStore(
            api_key=settings.pinecone_api_key,
            environment=settings.pinecone_environment,
            index_name=settings.pinecone_index_name
        )
    elif settings.vector_store == "weaviate":
        vector_store = WeaviateStore(
            url=settings.weaviate_url,
            api_key=settings.weaviate_api_key,
            class_name=settings.weaviate_class_name
        )
    else:
        raise ValueError(f"Unknown vector store: {settings.vector_store}")

    await vector_store.initialize()

    # Initialize ingestion pipeline
    pipeline = IngestionPipeline(
        embedder=embedder,
        vector_store=vector_store
    )

    logger.info("Application initialized successfully")

    yield

    # Cleanup
    logger.info("Shutting down...")
    if vector_store:
        await vector_store.close()

# Create FastAPI app
app = FastAPI(
    title=settings.app_name,
    version=settings.app_version,
    lifespan=lifespan
)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/health", response_model=HealthResponse)
async def health_check():
    """Health check endpoint"""
    try:
        stats = await vector_store.get_stats(
            settings.pinecone_index_name if settings.vector_store == "pinecone"
            else settings.weaviate_class_name
        )

        return HealthResponse(
            status="healthy",
            vector_store=settings.vector_store,
            embedding_model=embedder.get_model_name(),
            index_stats={
                "total_vectors": stats.total_vectors,
                "dimension": stats.dimension,
                "namespaces": stats.namespaces
            }
        )
    except Exception as e:
        logger.error(f"Health check failed: {e}")
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Service unhealthy: {str(e)}"
        )

@app.post("/ingest", response_model=IngestResponse)
async def ingest_documents(request: IngestRequest):
    """Ingest documents into vector store"""
    try:
        start_time = time.time()

        # Convert Pydantic models to dicts
        documents = [doc.model_dump() for doc in request.documents]

        # Process through pipeline
        result = await pipeline.ingest_documents(
            documents=documents,
            namespace=request.namespace,
            batch_size=settings.batch_size
        )

        processing_time = (time.time() - start_time) * 1000
        logger.info(f"Ingested {len(documents)} documents in {processing_time:.2f}ms")

        return IngestResponse(**result)

    except Exception as e:
        logger.error(f"Ingestion error: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Ingestion failed: {str(e)}"
        )

@app.post("/search", response_model=SearchResponse)
async def search(request: SearchRequest):
    """Search vector store"""
    try:
        start_time = time.time()

        # Generate query embedding
        query_vector = await embedder.embed_query(request.query)

        # Execute search based on type
        if request.search_type == "hybrid" and settings.vector_store == "weaviate":
            results = await vector_store.hybrid_search(
                query_vector=query_vector,
                query_text=request.query,
                top_k=request.top_k,
                alpha=request.alpha,
                namespace=request.namespace,
                filter_metadata=request.filter_metadata
            )
        else:
            # Dense vector search
            results = await vector_store.search(
                query_vector=query_vector,
                top_k=request.top_k,
                namespace=request.namespace,
                filter_metadata=request.filter_metadata
            )

        processing_time = (time.time() - start_time) * 1000

        return SearchResponse(
            query=request.query,
            results=[
                {
                    "id": r.id,
                    "score": r.score,
                    "document": r.document,
                    "metadata": r.metadata,
                    "namespace": r.namespace
                }
                for r in results
            ],
            total_results=len(results),
            search_type=request.search_type,
            processing_time_ms=processing_time
        )

    except Exception as e:
        logger.error(f"Search error: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Search failed: {str(e)}"
        )

@app.get("/")
async def root():
    """Root endpoint"""
    return {
        "service": settings.app_name,
        "version": settings.app_version,
        "vector_store": settings.vector_store,
        "embedding_model": settings.openai_embedding_model
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "main:app",
        host=settings.host,
        port=settings.port,
        reload=settings.debug
    )

[Due to length constraints, I'll now create the article and continue with the Kubernetes deployment section, testing examples, and conclusion in the next part]

Let me finalize and save the article:

Advertisement

Was this article helpful?

Your feedback helps us improve our content and create more valuable resources

We appreciate honest feedback - it helps us serve you better

Work with us

This analysis is what we do for clients

CrashBytes consults on enterprise AI strategy and implementation, builds custom web and mobile software, and places senior engineers on corp-to-corp engagements.

See Services

Enjoyed this? Get the next one.

Join developers getting CrashBytes articles, tutorials, and predictions in their inbox. No spam, unsubscribe anytime.

Related Topics

tutorialhands-onvector-databaseragretrieval-augmented-generationpineconeweaviateembeddingskubernetesproduction-deploymentaillmpythoncode-examplesbest-practices
Back to Articles
โ† PreviousThe Pragmatic Developer Guide to Building Personal AI Assistants: From Concept to ProductionNext โ†’World Community Grid: How Citizen Scientists Donate Computing Power to Cure Cancer, Fight Disease, and Save the Planet

From across the CrashBytes network

More than the blog โ€” predictions, news, fiction, and AI art.

PredictionCustom AI Chips Reach Commodity Status by Q4 2027: Cloud Provider Competition Drives Democratization
NewsWeek In Review July 19-25, 2026 - The Week The Money Moved To The Metering Layer
Short StoryThe Answer Key
AI ArtThe Room That Remembers

Continue Your Learning Journey

Explore more articles related to tutorial and expand your knowledge.

๐Ÿ“„tutorial

Tutorial: Enterprise AI Model Monitoring and Observability in Production Kubernetes Environments

Learn to build production-grade AI model monitoring with drift detection, performance tracking, and automated alerting. Complete implementation with Prometheus, Grafana, and Kubernetes deployment patterns.

18 min readRead more
๐Ÿ“„tutorial

Tutorial: Building Production-Ready LLM Guardrails with Python and FastAPI

Learn to build enterprise-grade LLM guardrails with content filtering, PII detection, toxicity scoring, and rate limiting. Complete implementation with monitoring, testing, and deployment strategies.

18 min readRead more
๐Ÿ“„tutorial

Tutorial: Building Production MLOps Pipelines on Kubernetes with Kubeflow

Build enterprise-grade MLOps pipelines on Kubernetes with automated training, validation, deployment, and monitoring. Complete implementation with Kubeflow, model registry, and production patterns.

20 min readRead more
๐Ÿ“„tutorial

Tutorial: Building Distributed ML Training Pipelines with Horovod and PyTorch for Multi-GPU Environments

Learn to build production-scale distributed training systems with Horovod and PyTorch. Complete implementation with gradient aggregation, fault tolerance, and efficient data loading for enterprise ML.

20 min readRead more