Spaces:
Running
Running
| """Unified embedding interface supporting local, OpenAI, and Gemini backends.""" | |
| import os | |
| import time | |
| import numpy as np | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| LOCAL_MODEL_NAME = "all-MiniLM-L6-v2" | |
| OPENAI_MODEL_NAME = "text-embedding-3-small" | |
| AZURE_OPENAI_MODEL_NAME = "text-embedding-3-large" | |
| GEMINI_MODEL_NAME = "gemini-embedding-2" | |
| BATCH_SIZE = 128 | |
| GEMINI_BATCH_SIZE = 100 # stay within Gemini rate limits | |
| def get_provider() -> str: | |
| return os.environ.get("EMBEDDING_PROVIDER", "local").lower() | |
| def embeddings_filename(provider: str | None = None, multi: bool = False) -> str: | |
| p = provider or get_provider() | |
| suffix = ".multi" if multi else "" | |
| return f"embeddings.{p}{suffix}.npy" | |
| def default_min_score(provider: str | None = None) -> float: | |
| p = provider or get_provider() | |
| if p in ("openai", "azure", "gemini"): | |
| return 0.45 | |
| return 0.35 | |
| class Embedder: | |
| def __init__(self): | |
| self.provider = get_provider() | |
| if self.provider == "openai": | |
| from openai import OpenAI | |
| self._client = OpenAI() | |
| self.dim = 1536 | |
| elif self.provider == "azure": | |
| from openai import AzureOpenAI | |
| self._client = AzureOpenAI( | |
| api_key=os.environ["AZURE_OPENAI_API_KEY"], | |
| azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], | |
| api_version=os.environ.get("AZURE_OPENAI_API_VERSION", "2024-09-01-preview"), | |
| ) | |
| self._azure_deployment = os.environ.get("AZURE_OPENAI_DEPLOYMENT", AZURE_OPENAI_MODEL_NAME) | |
| self.dim = 3072 | |
| elif self.provider == "gemini": | |
| from google import genai | |
| self._client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) | |
| self.dim = 1536 | |
| else: | |
| from sentence_transformers import SentenceTransformer | |
| self._model = SentenceTransformer(LOCAL_MODEL_NAME) | |
| self.dim = self._model.get_embedding_dimension() | |
| def embed_batch(self, texts: list[str], show_progress: bool = True) -> np.ndarray: | |
| if self.provider == "openai": | |
| return self._embed_batch_openai(texts) | |
| if self.provider == "azure": | |
| return self._embed_batch_azure(texts) | |
| if self.provider == "gemini": | |
| return self._embed_batch_gemini(texts, show_progress) | |
| return self._embed_batch_local(texts, show_progress) | |
| def embed_query(self, query: str) -> np.ndarray: | |
| if self.provider == "openai": | |
| resp = self._client.embeddings.create( | |
| model=OPENAI_MODEL_NAME, input=[query] | |
| ) | |
| vec = np.array(resp.data[0].embedding, dtype=np.float32) | |
| elif self.provider == "azure": | |
| resp = self._client.embeddings.create( | |
| model=self._azure_deployment, input=[query] | |
| ) | |
| vec = np.array(resp.data[0].embedding, dtype=np.float32) | |
| elif self.provider == "gemini": | |
| resp = self._client.models.embed_content( | |
| model=GEMINI_MODEL_NAME, | |
| contents=query, | |
| config={"output_dimensionality": 1536}, | |
| ) | |
| vec = np.array(resp.embeddings[0].values, dtype=np.float32) | |
| else: | |
| vec = self._model.encode(query, convert_to_numpy=True).astype(np.float32) | |
| return vec / np.linalg.norm(vec) | |
| def _embed_batch_azure(self, texts: list[str]) -> np.ndarray: | |
| all_embeddings: list[list[float]] = [] | |
| total = len(texts) | |
| for i in range(0, total, BATCH_SIZE): | |
| batch = texts[i : i + BATCH_SIZE] | |
| n = i // BATCH_SIZE + 1 | |
| total_batches = (total + BATCH_SIZE - 1) // BATCH_SIZE | |
| print(f" Embedding batch {n}/{total_batches} ({len(batch)} texts) ...") | |
| resp = self._client.embeddings.create( | |
| model=self._azure_deployment, input=batch | |
| ) | |
| all_embeddings.extend([d.embedding for d in resp.data]) | |
| return np.array(all_embeddings, dtype=np.float32) | |
| def _embed_batch_openai(self, texts: list[str]) -> np.ndarray: | |
| all_embeddings: list[list[float]] = [] | |
| total = len(texts) | |
| for i in range(0, total, BATCH_SIZE): | |
| batch = texts[i : i + BATCH_SIZE] | |
| n = i // BATCH_SIZE + 1 | |
| total_batches = (total + BATCH_SIZE - 1) // BATCH_SIZE | |
| print(f" Embedding batch {n}/{total_batches} ({len(batch)} texts) ...") | |
| resp = self._client.embeddings.create( | |
| model=OPENAI_MODEL_NAME, input=batch | |
| ) | |
| all_embeddings.extend([d.embedding for d in resp.data]) | |
| return np.array(all_embeddings, dtype=np.float32) | |
| def _embed_batch_gemini(self, texts: list[str], show_progress: bool) -> np.ndarray: | |
| # The Gemini SDK returns one aggregated embedding for list inputs, | |
| # so we embed one text at a time and rate-limit per GEMINI_BATCH_SIZE. | |
| all_embeddings: list[list[float]] = [] | |
| total = len(texts) | |
| for idx, text in enumerate(texts): | |
| if show_progress and idx % GEMINI_BATCH_SIZE == 0: | |
| batch_n = idx // GEMINI_BATCH_SIZE + 1 | |
| total_batches = (total + GEMINI_BATCH_SIZE - 1) // GEMINI_BATCH_SIZE | |
| print(f" Embedding batch {batch_n}/{total_batches} ({min(GEMINI_BATCH_SIZE, total - idx)} texts) ...") | |
| resp = self._client.models.embed_content( | |
| model=GEMINI_MODEL_NAME, | |
| contents=text, | |
| config={"output_dimensionality": 1536}, | |
| ) | |
| all_embeddings.append(resp.embeddings[0].values) | |
| # Brief pause every GEMINI_BATCH_SIZE texts to respect rate limits | |
| if (idx + 1) % GEMINI_BATCH_SIZE == 0 and (idx + 1) < total: | |
| time.sleep(1) | |
| return np.array(all_embeddings, dtype=np.float32) | |
| def _embed_batch_local(self, texts: list[str], show_progress: bool) -> np.ndarray: | |
| return self._model.encode( | |
| texts, | |
| batch_size=BATCH_SIZE, | |
| show_progress_bar=show_progress, | |
| convert_to_numpy=True, | |
| ).astype(np.float32) | |