From 6fc558ec87ffe49f920205cd3872484490546275 Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Thu, 9 Jul 2026 22:45:26 +0530 Subject: [PATCH 1/6] feat: dockerized e2e ai pipeline, pgvector relationships, and ocr fixes --- app/models/relationship.py | 10 +++---- app/processing/expansion.py | 54 ++++++++++++++++++++++++++++++++++++ app/processing/search.py | 21 ++++++++------ app/routes/files.py | 36 ++++++++++++++++++++++-- app/routes/search.py | 27 +----------------- app/services/indexer.py | 55 ++++++++++++++++++++++++++++++++++--- docker-compose.yml | 14 ++++++++++ 7 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 app/processing/expansion.py diff --git a/app/models/relationship.py b/app/models/relationship.py index 7692777..31a2c3c 100644 --- a/app/models/relationship.py +++ b/app/models/relationship.py @@ -1,21 +1,21 @@ import uuid -from sqlalchemy import Float, String, ForeignKey, UniqueConstraint -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import func -from sqlalchemy.orm import Mapped, mapped_column - +from sqlalchemy import Float, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base class FileRelationship(Base): __tablename__ = "file_relationships" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, server_default=func.gen_random_uuid()) + source_file_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("files.id", ondelete="CASCADE")) target_file_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("files.id", ondelete="CASCADE")) similarity_score: Mapped[float] = mapped_column(Float, nullable=False) - relation_type: Mapped[str] = mapped_column(String(50), server_default='semantic_similarity') + # Preventing the duplicate relationship pairs __table_args__ = ( UniqueConstraint('source_file_id', 'target_file_id', name='uq_file_relationship'), ) \ No newline at end of file diff --git a/app/processing/expansion.py b/app/processing/expansion.py new file mode 100644 index 0000000..8d2ada1 --- /dev/null +++ b/app/processing/expansion.py @@ -0,0 +1,54 @@ +import json +import urllib.request +import logging + +logger = logging.getLogger(__name__) + +# Ollama's url +OLLAMA_URL = "http://ollama:11434/api/generate" + +MODEL_NAME = "qwen2:0.5b" + +def generate_hypothetical_document(query: str) -> str: + """ + Uses a local LLM via Ollama to generate a hypothetical textbook answer + for the user's query. This is the core of the HyDE algorithm. + """ + # The system prompt forces the LLM to write like a document in your database, + # rather than acting like a chat assistant. + prompt = ( + "You are an expert technical writer. Write a single, concise, factual, " + "and highly technical textbook paragraph that directly answers the following " + "search query. Do not include conversational filler, introductions, or conclusions. " + f"Query: {query}" + ) + + payload = { + "model": MODEL_NAME, + "prompt": prompt, + "stream": False, + "options": { + "temperature": 0.3 # Low temperature forces deterministic, factual generation + } + } + + try: + logger.info(f"[expansion] Requesting hypothetical document for: '{query}'") + + # Zero-dependency HTTP request using standard Python + req = urllib.request.Request( + OLLAMA_URL, + data=json.dumps(payload).encode('utf-8'), + headers={'Content-Type': 'application/json'} + ) + + with urllib.request.urlopen(req, timeout=120) as response: + result = json.loads(response.read().decode('utf-8')) + hypothetical_doc = result.get("response", "").strip() + + logger.info(f"[expansion] HyDE Generated: {hypothetical_doc[:100]}...") + return hypothetical_doc + + except Exception as e: + logger.warning(f"[expansion] Local LLM failed: {e}. Falling back to original query.") + return query \ No newline at end of file diff --git a/app/processing/search.py b/app/processing/search.py index 942150d..0ec54a9 100644 --- a/app/processing/search.py +++ b/app/processing/search.py @@ -35,6 +35,7 @@ from typing import Any, Literal from .embeddings import _get_model +from .expansion import generate_hypothetical_document logger = logging.getLogger(__name__) @@ -284,10 +285,17 @@ def build_query( """ logger.info("[search] Building %s query: %r (top_k=%d)", mode, query, top_k) - # 1. Embed - vector = embed_query(query) + # ---> NEW: Intercept and Expand <--- + # Only generate a hypothetical document if we are doing vector math + if mode in ("vector", "hybrid"): + expanded_text_for_vector = generate_hypothetical_document(query) + else: + expanded_text_for_vector = query - # 2. Build SQL variants (pre-build all three so the API can expose them) + # 1. Embed (Using the HYPOTHETICAL document) + vector = embed_query(expanded_text_for_vector) + + # 2. Build SQL variants has_filter = file_filter is not None sql = { "vector": _sql_vector(top_k, has_filter), @@ -295,16 +303,13 @@ def build_query( "hybrid": _sql_hybrid(top_k, has_filter, rrf_k=rrf_k), } - # 3. Build bind parameters for the chosen mode + # 3. Build bind parameters params: dict[str, Any] = {"top_k": top_k} needs_vec = mode in ("vector", "hybrid") needs_text = mode in ("keyword", "hybrid") if needs_vec: - # pgvector accepts the vector as a Python list; SQLAlchemy passes it - # through. If you hit type errors, cast explicitly: - # str(vector) → "[0.12, -0.34, …]" (pgvector's text literal) params["query_vec"] = str(vector) if needs_text: @@ -315,6 +320,7 @@ def build_query( return SearchPayload( query=query, + expanded_query=expanded_text_for_vector, vector=vector, mode=mode, top_k=top_k, @@ -322,7 +328,6 @@ def build_query( params=params, ) - # --------------------------------------------------------------------------- # Convenience: just get the vector (for callers that run their own SQL) # --------------------------------------------------------------------------- diff --git a/app/routes/files.py b/app/routes/files.py index 6f98cb1..2f9997d 100644 --- a/app/routes/files.py +++ b/app/routes/files.py @@ -6,7 +6,7 @@ from sqlalchemy import select from pydantic import BaseModel from typing import List, Optional - +from app.models.relationship import FileRelationship from app.database import get_db from app.models import File @@ -66,4 +66,36 @@ def delete_file(file_id: uuid.UUID, db: Session = Depends(get_db)): db.delete(db_file) db.commit() - return {"status": "success", "message": f"File and vectors wiped successfully."} \ No newline at end of file + return {"status": "success", "message": f"File and vectors wiped successfully."} + +# 4. File relationship +@router.get("/{file_id}/related") +def get_related_files(file_id: uuid.UUID, db: Session = Depends(get_db)): + """Returns a list of files mathematically related to the requested file.""" + + # Check if the file exists + db_file = db.scalar(select(File).where(File.id == file_id)) + if not db_file: + raise HTTPException(status_code=404, detail="File not found") + + # Fetch relationships where this file is the source + relationships = db.execute( + select(FileRelationship, File) + .join(File, FileRelationship.target_file_id == File.id) + .where(FileRelationship.source_file_id == file_id) + .order_by(FileRelationship.similarity_score.desc()) + ).all() + + return { + "status": "success", + "file_id": str(file_id), + "related_files": [ + { + "target_file_id": str(rel.FileRelationship.target_file_id), + "similarity_score": round(rel.FileRelationship.similarity_score, 4), + "file_path": rel.File.file_path, + "mime_type": rel.File.mime_type + } + for rel in relationships + ] + } \ No newline at end of file diff --git a/app/routes/search.py b/app/routes/search.py index 35f0a2b..9d315db 100644 --- a/app/routes/search.py +++ b/app/routes/search.py @@ -37,6 +37,7 @@ def search_documents( return { "status": "success", "query": query, + "expanded_query": payload.get("expanded_query"), "mode": mode, "results": [dict(row) for row in raw_results] } @@ -45,29 +46,3 @@ def search_documents( raise HTTPException(status_code=500, detail=f"Search Engine Error: {str(e)}") -''' - - -# THIS WAS DONE TO TEST WHETHER EVERYTHING WORKS PERFECTLY OR NOT - -from fastapi import BackgroundTasks -from app.services.indexer import background_index_file - -@router.post("/test-indexer/") -def trigger_dummy_index(background_tasks: BackgroundTasks): - """ - TEMPORARY ROUTE: Creates a fake syllabus file and feeds it to the background indexer. - Delete this once Aryan finishes the real upload route! - """ - dummy_path = "/tmp/dummy_syllabus.txt" - - # 1. Create a fake document on the server - with open(dummy_path, "w") as f: - f.write("Welcome to the Computer Science program. The core syllabus for first-year students includes Python programming, Data Structures, and Algorithms. The exact course code for Data Structures is CSE1001. Good luck with your studies!") - - # 2. Hand it to your engine - background_tasks.add_task(background_index_file, dummy_path) - - return {"status": "Cheat code activated. Dummy file sent to indexer!"} - -''' \ No newline at end of file diff --git a/app/services/indexer.py b/app/services/indexer.py index 8e24576..432d605 100644 --- a/app/services/indexer.py +++ b/app/services/indexer.py @@ -3,15 +3,57 @@ from app.database import SessionLocal from app.models import File, FileContent from app.processing import process +from sqlalchemy import text logger = logging.getLogger(__name__) +def generate_file_relationships(source_file_id: str, db, threshold: float = 0.50): + """ + Cross-references a newly indexed file's vectors against the entire database + using pgvector's native cosine distance operator (<=>). + """ + try: + logger.info(f"Calculating vector relationships for file: {source_file_id}") + + sql_query = text(""" + INSERT INTO file_relationships (source_file_id, target_file_id, similarity_score) + SELECT + :source_id AS source_file_id, + target_contents.file_id AS target_file_id, + MAX(1 - (source_contents.embedding <=> target_contents.embedding)) AS similarity_score + FROM + file_content AS source_contents + CROSS JOIN + file_content AS target_contents + WHERE + source_contents.file_id = :source_id + AND target_contents.file_id != :source_id + GROUP BY + target_contents.file_id + HAVING + MAX(1 - (source_contents.embedding <=> target_contents.embedding)) >= :threshold + ON CONFLICT (source_file_id, target_file_id) + DO UPDATE SET similarity_score = EXCLUDED.similarity_score; + """) + + db.execute(sql_query, { + "source_id": str(source_file_id), + "threshold": threshold + }) + db.commit() + logger.info("Successfully mapped relationships.") + + except Exception as e: + db.rollback() + logger.error(f"Failed to generate relationships: {str(e)}") + def background_index_file(temp_file_path: str): """ Background task that processes a document, extracts AI embeddings, saves them to pgvector, and cleans up the temporary file. """ db = SessionLocal() + file_obj = None # <-- 1. SAFE INITIALIZATION try: logger.info(f"Starting background indexing for: {temp_file_path}") @@ -38,19 +80,24 @@ def background_index_file(temp_file_path: str): embedding=cr["embedding"] )) - # permanently saving the vectors + # Permanently saving the vectors db.commit() logger.info(f"Successfully indexed {len(file_rows)} file(s) and {len(content_rows)} chunks.") + + # <-- 2. SAFE CHECK: Only map relationships if we successfully extracted content + if file_obj is not None: + generate_file_relationships(source_file_id=file_obj.id, db=db) + else: + logger.warning("No content extracted; skipping relationship generation.") except Exception as e: - # undo the changes if anything fails + # Undoing the changes if anything fails db.rollback() logger.error(f"Failed to index file {temp_file_path}: {str(e)}") finally: db.close() - - # Server Cleanup,deleting the temp files + # Deleting the temporary files if os.path.exists(temp_file_path): os.remove(temp_file_path) logger.info(f"Cleaned up temporary file: {temp_file_path}") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 5b0bee0..0ffc93b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,11 +21,25 @@ services: container_name: wheretf-backend ports: - "8000:8000" + dns: + - 8.8.8.8 + - 8.8.4.4 depends_on: db: condition: service_healthy environment: DATABASE_URL: postgresql+psycopg://postgres:postgres@db:5432/wheretf +# Added Ollama For Query Expansion + ollama: + image: ollama/ollama:latest + container_name: wheretf-ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + restart: unless-stopped + volumes: postgres_data: + ollama_data: \ No newline at end of file From 13a51ed729dc1f3beb3d1b147623af33943b856b Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Fri, 10 Jul 2026 23:20:01 +0530 Subject: [PATCH 2/6] chore: added alembic migration for pgvector and relationship tables --- ...627272a4_add_ai_and_relationship_tables.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py diff --git a/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py b/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py new file mode 100644 index 0000000..5d6c8e5 --- /dev/null +++ b/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py @@ -0,0 +1,57 @@ +"""add_ai_and_relationship_tables + +Revision ID: 05f9627272a4 +Revises: +Create Date: 2026-07-10 17:47:02.157605 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import pgvector.sqlalchemy +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '05f9627272a4' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('file_content', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('file_id', sa.UUID(), nullable=False), + sa.Column('chunk_index', sa.Integer(), nullable=False), + sa.Column('content_text', sa.Text(), nullable=False), + sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=False), + sa.Column('keyword_tokens', postgresql.TSVECTOR(), sa.Computed("to_tsvector('english', content_text)", persisted=True), nullable=True), + sa.ForeignKeyConstraint(['file_id'], ['files.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('content_embedding_idx', 'file_content', ['embedding'], unique=False, postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) + op.create_index('content_fts_idx', 'file_content', ['keyword_tokens'], unique=False, postgresql_using='gin') + op.create_table('file_relationships', + sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), + sa.Column('source_file_id', sa.UUID(), nullable=False), + sa.Column('target_file_id', sa.UUID(), nullable=False), + sa.Column('similarity_score', sa.Float(), nullable=False), + sa.ForeignKeyConstraint(['source_file_id'], ['files.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['target_file_id'], ['files.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('source_file_id', 'target_file_id', name='uq_file_relationship') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('file_relationships') + op.drop_index('content_fts_idx', table_name='file_content', postgresql_using='gin') + op.drop_index('content_embedding_idx', table_name='file_content', postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) + op.drop_table('file_content') + # ### end Alembic commands ### From cf5c5bea39ced9226e632f9237388f2abac751bc Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Sat, 11 Jul 2026 22:52:57 +0530 Subject: [PATCH 3/6] fix: removed duplicate migration file to prevent table recreation --- ...627272a4_add_ai_and_relationship_tables.py | 57 ------------------- 1 file changed, 57 deletions(-) delete mode 100644 alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py diff --git a/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py b/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py deleted file mode 100644 index 5d6c8e5..0000000 --- a/alembic/versions/05f9627272a4_add_ai_and_relationship_tables.py +++ /dev/null @@ -1,57 +0,0 @@ -"""add_ai_and_relationship_tables - -Revision ID: 05f9627272a4 -Revises: -Create Date: 2026-07-10 17:47:02.157605 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import pgvector.sqlalchemy -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision: str = '05f9627272a4' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('file_content', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('file_id', sa.UUID(), nullable=False), - sa.Column('chunk_index', sa.Integer(), nullable=False), - sa.Column('content_text', sa.Text(), nullable=False), - sa.Column('embedding', pgvector.sqlalchemy.vector.VECTOR(dim=384), nullable=False), - sa.Column('keyword_tokens', postgresql.TSVECTOR(), sa.Computed("to_tsvector('english', content_text)", persisted=True), nullable=True), - sa.ForeignKeyConstraint(['file_id'], ['files.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - op.create_index('content_embedding_idx', 'file_content', ['embedding'], unique=False, postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) - op.create_index('content_fts_idx', 'file_content', ['keyword_tokens'], unique=False, postgresql_using='gin') - op.create_table('file_relationships', - sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), nullable=False), - sa.Column('source_file_id', sa.UUID(), nullable=False), - sa.Column('target_file_id', sa.UUID(), nullable=False), - sa.Column('similarity_score', sa.Float(), nullable=False), - sa.ForeignKeyConstraint(['source_file_id'], ['files.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['target_file_id'], ['files.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('source_file_id', 'target_file_id', name='uq_file_relationship') - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('file_relationships') - op.drop_index('content_fts_idx', table_name='file_content', postgresql_using='gin') - op.drop_index('content_embedding_idx', table_name='file_content', postgresql_using='hnsw', postgresql_with={'m': 16, 'ef_construction': 64}, postgresql_ops={'embedding': 'vector_cosine_ops'}) - op.drop_table('file_content') - # ### end Alembic commands ### From cfe265d6b12d5a89b9e3bc10c986f16987650615 Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Sun, 12 Jul 2026 01:09:24 +0530 Subject: [PATCH 4/6] perf: implemented singleton model cache to eliminate indexing cold-start bottleneck --- app/main.py | 29 +++++++++++++++++++++++------ app/processing/cache.py | 35 +++++++++++++++++++++++++++++++++++ app/processing/embeddings.py | 29 +++++------------------------ app/processing/ocr.py | 4 +++- app/processing/search.py | 4 ++-- 5 files changed, 68 insertions(+), 33 deletions(-) create mode 100644 app/processing/cache.py diff --git a/app/main.py b/app/main.py index fbd693a..085041c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,30 @@ from fastapi import FastAPI +from contextlib import asynccontextmanager +import logging + from app.routes import search from app.routes import upload from app.routes import files +from app.processing.cache import ModelCache # <-- Import the cache + +logger = logging.getLogger(__name__) + +# --- THE PRE-WARMER --- +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info(" Server booting up. Pre-loading AI models into RAM...") + ModelCache.get_encoder() + ModelCache.get_ocr_reader() + logger.info(" Models loaded successfully. Ready for instant bulk ingestion.") + + yield # The server handles actual user requests here + + logger.info("Shutting down server and clearing RAM.") +# ---------------------- app = FastAPI( - title="WhereTF Backend" + title="WhereTF Backend", + lifespan=lifespan # <-- Attach the hook here ) app.include_router(search.router) @@ -13,11 +33,8 @@ @app.get("/health", tags=["System"]) def health_check(): - """ - Health check endpoint to verify the API is running. - """ return { "status": "healthy", "service": "WhereTF Backend", - "database_connected": True # If the API booted, the Docker depends_on guarantees this is true - } + "database_connected": True + } \ No newline at end of file diff --git a/app/processing/cache.py b/app/processing/cache.py new file mode 100644 index 0000000..3c55e6c --- /dev/null +++ b/app/processing/cache.py @@ -0,0 +1,35 @@ +import logging + +logger = logging.getLogger(__name__) + +class ModelCache: + _encoder = None + _ocr_reader = None + + @classmethod + def get_encoder(cls): + if cls._encoder is None: + try: + from sentence_transformers import SentenceTransformer + logger.info("[System] Loading Sentence Transformer into RAM...") + # loading it exactly once here + cls._encoder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") + logger.info("[System] Sentence Transformer loaded successfully.") + except ImportError as exc: + raise RuntimeError("pip install sentence-transformers") from exc + return cls._encoder + + @classmethod + def get_ocr_reader(cls, languages=("en",)): + if cls._ocr_reader is None: + try: + import easyocr + import torch + # Safely check for GPU, otherwise fallback to CPU + gpu_available = torch.cuda.is_available() + logger.info(f"[System] Loading EasyOCR into RAM (gpu={gpu_available})...") + cls._ocr_reader = easyocr.Reader(list(languages), gpu=gpu_available) + logger.info("[System] EasyOCR loaded successfully.") + except ImportError as exc: + raise RuntimeError("pip install easyocr torch") from exc + return cls._ocr_reader \ No newline at end of file diff --git a/app/processing/embeddings.py b/app/processing/embeddings.py index cd9d027..2f1cc3b 100644 --- a/app/processing/embeddings.py +++ b/app/processing/embeddings.py @@ -14,9 +14,10 @@ from __future__ import annotations import logging -from functools import lru_cache from typing import TYPE_CHECKING +from .cache import ModelCache # <-- Importing our new Singleton + if TYPE_CHECKING: from .extractors import ChunkData @@ -26,27 +27,6 @@ DEFAULT_BATCH_SIZE = 64 -# --------------------------------------------------------------------------- -# Lazy model singleton -# --------------------------------------------------------------------------- - -@lru_cache(maxsize=1) -def _get_model(): - """Load and cache the SentenceTransformer model (once per process).""" - try: - from sentence_transformers import SentenceTransformer # type: ignore - except ImportError as exc: - raise RuntimeError( - "sentence-transformers is required. " - "Install it with: pip install sentence-transformers" - ) from exc - - logger.info("[embeddings] Loading model '%s' …", MODEL_NAME) - model = SentenceTransformer(MODEL_NAME) - logger.info("[embeddings] Model loaded.") - return model - - # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- @@ -83,7 +63,8 @@ def embed_chunks( if not pending_indices: return chunks - model = _get_model() + # grabs the model from RAM using singleton + model = ModelCache.get_encoder() texts = [chunks[i]["content_text"] for i in pending_indices] logger.info( @@ -106,4 +87,4 @@ def embed_chunks( chunks[chunk_idx]["embedding"] = vectors[list_pos].tolist() logger.info("[embeddings] Done.") - return chunks + return chunks \ No newline at end of file diff --git a/app/processing/ocr.py b/app/processing/ocr.py index 58c0d9f..108fc8e 100644 --- a/app/processing/ocr.py +++ b/app/processing/ocr.py @@ -17,6 +17,7 @@ import logging from functools import lru_cache from typing import Sequence +from .cache import ModelCache # added a new import logger = logging.getLogger(__name__) @@ -85,7 +86,8 @@ def ocr_image_bytes( import numpy as np # type: ignore from PIL import Image # type: ignore - reader = _get_reader(tuple(languages)) + #reader = _get_reader(tuple(languages)) + reader = ModelCache.get_ocr_reader(tuple(languages)) # Convert bytes → PIL → numpy (EasyOCR accepts numpy arrays natively) pil_img = Image.open(io.BytesIO(image_bytes)).convert("RGB") diff --git a/app/processing/search.py b/app/processing/search.py index 0ec54a9..af94431 100644 --- a/app/processing/search.py +++ b/app/processing/search.py @@ -34,7 +34,7 @@ import logging from typing import Any, Literal -from .embeddings import _get_model +from .cache import ModelCache from .expansion import generate_hypothetical_document logger = logging.getLogger(__name__) @@ -85,7 +85,7 @@ def embed_query(query: str) -> list[float]: 384-dimensional unit vector, ready to be cast to pgvector's ``vector`` type. """ - model = _get_model() + model = ModelCache.get_encoder() vec = model.encode( [query], convert_to_numpy=True, From 9f26515f4f31ef0005b398b94584fc7176bb7591 Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Tue, 14 Jul 2026 11:40:57 +0530 Subject: [PATCH 5/6] feat: updated LLM expansion prompt --- app/processing/expansion.py | 16 ++++++++++------ docker-compose.yml | 9 --------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/app/processing/expansion.py b/app/processing/expansion.py index 8d2ada1..82ce764 100644 --- a/app/processing/expansion.py +++ b/app/processing/expansion.py @@ -14,13 +14,17 @@ def generate_hypothetical_document(query: str) -> str: Uses a local LLM via Ollama to generate a hypothetical textbook answer for the user's query. This is the core of the HyDE algorithm. """ - # The system prompt forces the LLM to write like a document in your database, - # rather than acting like a chat assistant. + prompt = ( - "You are an expert technical writer. Write a single, concise, factual, " - "and highly technical textbook paragraph that directly answers the following " - "search query. Do not include conversational filler, introductions, or conclusions. " - f"Query: {query}" + "You are an expert search engine query expander. Your task is to take a short user query " + "and generate a highly detailed, technical and non technical extended description that represents what the ideal " + "target document would look like. \n\n" + "Rules:\n" + "1.If the query is technical,write a 2-3 sentence technical paragraph exlpaining the core concepts of the query. \n" + "2.If its non-technical , generate a description/intoduction of the query. \n" + "3. Naturally weave in relevant synonyms, alternate phrasing, and associated technical and non technical keywords.\n" + "4. Output ONLY the extended description. Do not include introductory phrases, labels, or conversational filler.\n\n" + f"User Query: {query}" ) payload = { diff --git a/docker-compose.yml b/docker-compose.yml index abcd1e9..412e280 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,15 +42,6 @@ services: - ollama_data:/root/.ollama restart: unless-stopped -# Added Ollama For Query Expansion - ollama: - image: ollama/ollama:latest - container_name: wheretf-ollama - ports: - - "11434:11434" - volumes: - - ollama_data:/root/.ollama - restart: unless-stopped volumes: postgres_data: From ab2782afb3e7c410aa1f1b26c4b131bf30fc992d Mon Sep 17 00:00:00 2001 From: Prakhar-Sethi012 Date: Thu, 6 Aug 2026 13:37:39 +0530 Subject: [PATCH 6/6] No change --- watchdog_client/watcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watchdog_client/watcher.py b/watchdog_client/watcher.py index f891cb2..3e9584e 100644 --- a/watchdog_client/watcher.py +++ b/watchdog_client/watcher.py @@ -1,4 +1,4 @@ -from .events import FileSystemEventHandler +from watchdog.events import FileSystemEventHandler from .api import upload, delete, modify, rename import time