Soumik-404 commited on
Commit
3379d24
·
1 Parent(s): 47bd5bb

add embedding service

Browse files
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM python:3.12-slim
2
 
3
  LABEL maintainer="All API Collection"
4
  LABEL description="All API Collection - Document extraction, conversion, and database query API"
@@ -24,6 +24,8 @@ RUN pip install --no-cache-dir "youtube-transcript-api>=1.2.4"
24
 
25
  COPY --chown=appuser:appuser . .
26
 
 
 
27
  RUN mkdir -p /app/logs && \
28
  chown -R appuser:appuser /app/logs
29
 
 
1
+ FROM python:3.11-slim
2
 
3
  LABEL maintainer="All API Collection"
4
  LABEL description="All API Collection - Document extraction, conversion, and database query API"
 
24
 
25
  COPY --chown=appuser:appuser . .
26
 
27
+ RUN mkdir -p /app/models && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='BAAI/bge-small-en-v1.5', local_dir='/app/models/bge-384'); snapshot_download(repo_id='BAAI/bge-base-en-v1.5', local_dir='/app/models/bge-768'); snapshot_download(repo_id='BAAI/bge-large-en-v1.5', local_dir='/app/models/bge-1024')" && chown -R appuser:appuser /app/models
28
+
29
  RUN mkdir -p /app/logs && \
30
  chown -R appuser:appuser /app/logs
31
 
app/api/deps.py CHANGED
@@ -6,6 +6,7 @@ from app.core.security import require_api_key
6
  from app.services.auth_service import AuthService
7
  from app.services.converter_service import ConverterService
8
  from app.services.database_service import DatabaseService
 
9
  from app.services.extraction_service import ExtractionService
10
  from app.services.ocr_service import OCRService
11
 
@@ -30,5 +31,10 @@ def get_database_service() -> DatabaseService:
30
  return DatabaseService()
31
 
32
 
 
 
 
 
 
33
  def require_auth(token: str = Depends(require_api_key)) -> str:
34
  return token
 
6
  from app.services.auth_service import AuthService
7
  from app.services.converter_service import ConverterService
8
  from app.services.database_service import DatabaseService
9
+ from app.services.embeddings_service import EmbeddingService
10
  from app.services.extraction_service import ExtractionService
11
  from app.services.ocr_service import OCRService
12
 
 
31
  return DatabaseService()
32
 
33
 
34
+ def get_embeddings_service() -> EmbeddingService:
35
+ from app.api.server import _embedding_service
36
+ return _embedding_service
37
+
38
+
39
  def require_auth(token: str = Depends(require_api_key)) -> str:
40
  return token
app/api/server.py CHANGED
@@ -10,11 +10,14 @@ from fastapi.middleware.gzip import GZipMiddleware
10
  from app.config import get_settings
11
  from app.core.database import pool_manager
12
  from app.core.logger import get_logger
 
13
  from app.api.v1.router import api_v1_router
14
 
15
  _logger = get_logger(__name__)
16
  _settings = get_settings()
17
 
 
 
18
 
19
  async def _self_ping():
20
  import httpx
@@ -34,6 +37,11 @@ async def _self_ping():
34
 
35
  @asynccontextmanager
36
  async def lifespan(app: FastAPI):
 
 
 
 
 
37
  asyncio.create_task(_self_ping())
38
  yield
39
  _logger.info("Shutting down database connection pools...")
@@ -51,6 +59,7 @@ def create_application() -> FastAPI:
51
  {"name": "Convert", "description": "Single-file and single-URL conversion"},
52
  {"name": "Batch", "description": "Bulk conversion of files and URLs"},
53
  {"name": "System", "description": "Health, info, and supported formats"},
 
54
  {"name": "Verify", "description": "Phone number and identity verification"},
55
  ],
56
  lifespan=lifespan,
 
10
  from app.config import get_settings
11
  from app.core.database import pool_manager
12
  from app.core.logger import get_logger
13
+ from app.services.embeddings_service import EmbeddingService
14
  from app.api.v1.router import api_v1_router
15
 
16
  _logger = get_logger(__name__)
17
  _settings = get_settings()
18
 
19
+ _embedding_service: EmbeddingService = EmbeddingService()
20
+
21
 
22
  async def _self_ping():
23
  import httpx
 
37
 
38
  @asynccontextmanager
39
  async def lifespan(app: FastAPI):
40
+ _logger.info("Initializing embedding service (loading all models)...")
41
+ loop = asyncio.get_running_loop()
42
+ await loop.run_in_executor(None, _embedding_service.load_all_models)
43
+ _logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
44
+
45
  asyncio.create_task(_self_ping())
46
  yield
47
  _logger.info("Shutting down database connection pools...")
 
59
  {"name": "Convert", "description": "Single-file and single-URL conversion"},
60
  {"name": "Batch", "description": "Bulk conversion of files and URLs"},
61
  {"name": "System", "description": "Health, info, and supported formats"},
62
+ {"name": "Embeddings", "description": "Text embedding generation using transformer models"},
63
  {"name": "Verify", "description": "Phone number and identity verification"},
64
  ],
65
  lifespan=lifespan,
app/api/v1/embeddings.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import concurrent.futures
5
+ import os
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException
8
+
9
+ from app.api.deps import require_auth, get_embeddings_service
10
+ from app.core.logger import get_logger
11
+ from app.models.schemas import EmbeddingRequest, EmbeddingResponse
12
+ from app.services.embeddings_service import EmbeddingService
13
+
14
+ router = APIRouter()
15
+ _logger = get_logger(__name__)
16
+ _MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
17
+ _thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
18
+
19
+
20
+ @router.post(
21
+ "/embeddings",
22
+ response_model=EmbeddingResponse,
23
+ summary="Generate text embeddings",
24
+ )
25
+ async def create_embeddings(
26
+ body: EmbeddingRequest,
27
+ token: str = Depends(require_auth),
28
+ embedding_service: EmbeddingService = Depends(get_embeddings_service),
29
+ ) -> EmbeddingResponse:
30
+ _logger.info("Embedding request: dim=%s, content_len=%s", body.dimension, len(body.content))
31
+
32
+ if not embedding_service.is_loaded(body.dimension):
33
+ _logger.error("Model dim=%s not loaded. Loaded: %s", body.dimension, embedding_service.loaded_dimensions)
34
+ raise HTTPException(
35
+ status_code=503,
36
+ detail={
37
+ "success": False,
38
+ "message": f"Model for dimension {body.dimension} not loaded. Loaded: {embedding_service.loaded_dimensions}",
39
+ },
40
+ )
41
+
42
+ try:
43
+ loop = asyncio.get_running_loop()
44
+ embeddings = await loop.run_in_executor(
45
+ _thread_pool,
46
+ embedding_service.generate_embedding,
47
+ body.content,
48
+ body.dimension,
49
+ )
50
+
51
+ _logger.info("Embedding success: dim=%s, vector_len=%s", body.dimension, len(embeddings))
52
+ return EmbeddingResponse(
53
+ success=True,
54
+ embeddings=embeddings,
55
+ dimension=body.dimension,
56
+ )
57
+ except Exception as exc:
58
+ _logger.error("Embedding error: %s", exc)
59
+ raise HTTPException(
60
+ status_code=500,
61
+ detail={"success": False, "message": str(exc)},
62
+ )
app/api/v1/router.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
 
3
  from fastapi import APIRouter
4
 
5
- from app.api.v1 import batch, convert, database, system
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
@@ -10,4 +10,5 @@ api_v1_router.include_router(convert.router, tags=["Convert"])
10
  api_v1_router.include_router(batch.router, tags=["Batch"])
11
  api_v1_router.include_router(system.router, tags=["System"])
12
  api_v1_router.include_router(database.router, tags=["Database"])
 
13
  api_v1_router.include_router(verify_router, prefix="/verify", tags=["Verify"])
 
2
 
3
  from fastapi import APIRouter
4
 
5
+ from app.api.v1 import batch, convert, database, embeddings, system
6
  from app.api.verify import router as verify_router
7
 
8
  api_v1_router = APIRouter()
 
10
  api_v1_router.include_router(batch.router, tags=["Batch"])
11
  api_v1_router.include_router(system.router, tags=["System"])
12
  api_v1_router.include_router(database.router, tags=["Database"])
13
+ api_v1_router.include_router(embeddings.router, tags=["Embeddings"])
14
  api_v1_router.include_router(verify_router, prefix="/verify", tags=["Verify"])
app/config.py CHANGED
@@ -22,7 +22,7 @@ class Settings(BaseSettings):
22
  enable_colors: bool = True
23
 
24
  api_key: str = "changeme"
25
- max_upload_bytes: int = 100 * 1024 * 1024
26
  max_batch_files: int = 10
27
  max_batch_urls: int = 20
28
 
 
22
  enable_colors: bool = True
23
 
24
  api_key: str = "changeme"
25
+ max_upload_bytes: int = 15 * 1024 * 1024
26
  max_batch_files: int = 10
27
  max_batch_urls: int = 20
28
 
app/models/__init__.py CHANGED
@@ -7,6 +7,8 @@ from app.models.schemas import (
7
  BatchUrlRequest,
8
  ConversionMetadata,
9
  ConversionResponse,
 
 
10
  HealthResponse,
11
  InfoResponse,
12
  SpacyLabelsResponse,
@@ -19,6 +21,8 @@ __all__ = [
19
  "ConversionResult",
20
  "ConversionMetadata",
21
  "ConversionResponse",
 
 
22
  "UrlRequest",
23
  "BatchUrlRequest",
24
  "BatchFileResult",
 
7
  BatchUrlRequest,
8
  ConversionMetadata,
9
  ConversionResponse,
10
+ EmbeddingRequest,
11
+ EmbeddingResponse,
12
  HealthResponse,
13
  InfoResponse,
14
  SpacyLabelsResponse,
 
21
  "ConversionResult",
22
  "ConversionMetadata",
23
  "ConversionResponse",
24
+ "EmbeddingRequest",
25
+ "EmbeddingResponse",
26
  "UrlRequest",
27
  "BatchUrlRequest",
28
  "BatchFileResult",
app/models/schemas.py CHANGED
@@ -222,3 +222,14 @@ class DatabaseQueryResponse(BaseModel):
222
  execution_time_ms: float
223
  results: Optional[List[StatementResultSchema]] = None
224
  error: Optional[DatabaseQueryError] = None
 
 
 
 
 
 
 
 
 
 
 
 
222
  execution_time_ms: float
223
  results: Optional[List[StatementResultSchema]] = None
224
  error: Optional[DatabaseQueryError] = None
225
+
226
+
227
+ class EmbeddingRequest(BaseModel):
228
+ content: str = Field(..., min_length=1, description="Text to embed")
229
+ dimension: int = Field(default=384, ge=384, le=1024, description="Target embedding dimension (384, 768, or 1024)")
230
+
231
+
232
+ class EmbeddingResponse(BaseModel):
233
+ success: bool
234
+ embeddings: List[float]
235
+ dimension: int
app/services/__init__.py CHANGED
@@ -2,12 +2,14 @@ from __future__ import annotations
2
 
3
  from app.services.auth_service import AuthService
4
  from app.services.converter_service import ConverterService
 
5
  from app.services.extraction_service import ExtractionService
6
  from app.services.ocr_service import OCRService
7
 
8
  __all__ = [
9
  "AuthService",
10
  "ConverterService",
 
11
  "ExtractionService",
12
  "OCRService",
13
  ]
 
2
 
3
  from app.services.auth_service import AuthService
4
  from app.services.converter_service import ConverterService
5
+ from app.services.embeddings_service import EmbeddingService
6
  from app.services.extraction_service import ExtractionService
7
  from app.services.ocr_service import OCRService
8
 
9
  __all__ = [
10
  "AuthService",
11
  "ConverterService",
12
+ "EmbeddingService",
13
  "ExtractionService",
14
  "OCRService",
15
  ]
app/services/embeddings_service.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ from typing import Dict, List, Optional
6
+
7
+ import numpy as np
8
+ from sentence_transformers import SentenceTransformer
9
+
10
+ _logger = logging.getLogger(__name__)
11
+
12
+ _MODEL_MAP: Dict[int, str] = {
13
+ 384: "BAAI/bge-small-en-v1.5",
14
+ 768: "BAAI/bge-base-en-v1.5",
15
+ 1024: "BAAI/bge-large-en-v1.5",
16
+ }
17
+
18
+
19
+ class EmbeddingService:
20
+ def __init__(self, models_dir: Optional[str] = None) -> None:
21
+ self._models: Dict[int, SentenceTransformer] = {}
22
+ self._models_dir = models_dir or os.path.join(os.getcwd(), "models")
23
+ self._device = "cuda"
24
+ try:
25
+ import torch
26
+ if not torch.cuda.is_available():
27
+ self._device = "cpu"
28
+ except ImportError:
29
+ self._device = "cpu"
30
+
31
+ self._loaded_dimensions: List[int] = []
32
+
33
+ def load_model(self, dimension: int) -> None:
34
+ if dimension in self._models:
35
+ return
36
+ if dimension not in _MODEL_MAP:
37
+ raise ValueError(f"Unsupported dimension {dimension}. Supported: {list(_MODEL_MAP.keys())}")
38
+
39
+ model_name = _MODEL_MAP[dimension]
40
+ local_path = os.path.join(self._models_dir, f"bge-{dimension}")
41
+
42
+ _logger.info("Loading embedding model dim=%s from %s", dimension, local_path if os.path.isdir(local_path) else model_name)
43
+ model = SentenceTransformer(
44
+ local_path if os.path.isdir(local_path) else model_name,
45
+ device=self._device,
46
+ )
47
+ model.eval()
48
+ self._models[dimension] = model
49
+ self._loaded_dimensions.append(dimension)
50
+ _logger.info("Loaded embedding model dim=%s (device=%s)", dimension, self._device)
51
+
52
+ def load_all_models(self) -> None:
53
+ for dim in _MODEL_MAP:
54
+ self.load_model(dim)
55
+
56
+ def generate_embedding(self, text: str, dimension: int) -> List[float]:
57
+ if dimension not in self._models:
58
+ raise ValueError(f"Model for dimension {dimension} not loaded")
59
+ model = self._models[dimension]
60
+ result: np.ndarray = model.encode(
61
+ text,
62
+ normalize_embeddings=True,
63
+ convert_to_numpy=True,
64
+ show_progress_bar=False,
65
+ )
66
+ return result.tolist()
67
+
68
+ @property
69
+ def loaded_dimensions(self) -> List[int]:
70
+ return list(self._loaded_dimensions)
71
+
72
+ def is_loaded(self, dimension: int) -> bool:
73
+ return dimension in self._models
requirements.txt CHANGED
@@ -11,6 +11,7 @@ onnxruntime>=1.18.0
11
  pillow>=10.0.0
12
  pypdfium2>=4.30.0
13
  pandas>=2.0.0
 
14
  spacy>=3.7.0
15
  phonenumbers>=8.13.0
16
 
 
11
  pillow>=10.0.0
12
  pypdfium2>=4.30.0
13
  pandas>=2.0.0
14
+ sentence-transformers==3.3.1
15
  spacy>=3.7.0
16
  phonenumbers>=8.13.0
17