""" HTTP API Server for Sparse Vector Search Engine Educational server with extensive logging and visualization """ from fastapi import FastAPI, HTTPException, Query from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from typing import List, Dict, Optional import uvicorn import logging import json from datetime import datetime from bm25_engine import SparseSearchEngine # Configure logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Initialize FastAPI app app = FastAPI( title="Educational Sparse Vector Search Engine", description="BM25-based search engine with inverted index for educational purposes", version="1.0.0" ) # Initialize search engine search_engine = SparseSearchEngine() # Pydantic models for request/response class IndexDocumentRequest(BaseModel): text: str = Field(..., description="Text content to index") metadata: Optional[Dict] = Field(None, description="Optional metadata") doc_id: Optional[str] = Field(None, description="Optional external document ID") class BatchIndexRequest(BaseModel): documents: List[Dict] = Field(..., description="List of documents to index") class SearchRequest(BaseModel): query: str = Field(..., description="Search query") top_k: int = Field(10, description="Number of results to return") class DocumentResponse(BaseModel): doc_id: str # Changed to str to support external IDs text: str metadata: Optional[Dict] score: Optional[float] = None debug: Optional[Dict] = None # Root endpoint with UI @app.get("/", response_class=HTMLResponse) async def root(): """Serve a simple HTML interface for the search engine""" html_content = """ Educational Sparse Vector Search Engine

🔍 Educational Sparse Vector Search Engine

Index Documents

Search

Index Statistics

Index Structure Visualization

""" return html_content @app.post("/index", response_model=Dict) async def index_document(request: IndexDocumentRequest): """Index a single document""" logger.info(f"Received index request for document of length {len(request.text)}") if request.doc_id: logger.info(f"External doc_id provided: {request.doc_id}") try: # Extract doc_id from metadata if not provided directly external_doc_id = request.doc_id if not external_doc_id and request.metadata and 'doc_id' in request.metadata: external_doc_id = request.metadata['doc_id'] doc_id = search_engine.index_document(request.text, request.metadata, external_doc_id) logger.info(f"Document indexed successfully with ID {doc_id}") return { "success": True, "doc_id": doc_id, "message": f"Document indexed successfully with ID {doc_id}" } except Exception as e: logger.error(f"Error indexing document: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/index/batch", response_model=Dict) async def index_batch(request: BatchIndexRequest): """Index multiple documents at once""" logger.info(f"Received batch index request for {len(request.documents)} documents") try: doc_ids = search_engine.index_batch(request.documents) logger.info(f"Batch indexing successful: {len(doc_ids)} documents indexed") return { "success": True, "doc_ids": doc_ids, "message": f"Successfully indexed {len(doc_ids)} documents" } except Exception as e: logger.error(f"Error in batch indexing: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/search", response_model=List[DocumentResponse]) async def search(request: SearchRequest): """Search for documents""" logger.info(f"Received search request: '{request.query}' (top_k={request.top_k})") try: results = search_engine.search(request.query, request.top_k) logger.info(f"Search completed, returning {len(results)} results") return results except Exception as e: logger.error(f"Error performing search: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/document/{doc_id}", response_model=DocumentResponse) async def get_document(doc_id: str): """Retrieve a specific document by ID""" logger.info(f"Retrieving document {doc_id}") document = search_engine.get_document(doc_id) if document is None: logger.warning(f"Document {doc_id} not found") raise HTTPException(status_code=404, detail=f"Document {doc_id} not found") logger.info(f"Document {doc_id} retrieved successfully") return document @app.get("/stats", response_model=Dict) async def get_statistics(): """Get index statistics""" logger.info("Retrieving index statistics") stats = search_engine.index.get_statistics() logger.info(f"Statistics retrieved: {stats['total_documents']} documents, " f"{stats['unique_terms']} unique terms") return stats @app.get("/index/structure", response_model=Dict) async def get_index_structure(): """Get detailed index structure for visualization""" logger.info("Retrieving index structure") info = search_engine.get_index_info() logger.info("Index structure retrieved successfully") return info @app.delete("/index", response_model=Dict) async def clear_index(): """Clear all indexed documents""" logger.warning("Clearing entire index") search_engine.clear_index() logger.info("Index cleared successfully") return { "success": True, "message": "Index cleared successfully" } @app.get("/logs", response_model=Dict) async def get_recent_logs(lines: int = Query(100, description="Number of log lines to retrieve")): """Get recent application logs for educational purposes""" # This is a simplified version - in production you'd read from a log file return { "message": "Logs are being written to console. Check terminal for detailed logs.", "log_level": "DEBUG", "description": "Educational logging is enabled. All indexing and search operations are logged." } if __name__ == "__main__": import sys # Allow overriding port from command line port = 4241 # Default to 4241 to avoid conflicts with common services if len(sys.argv) > 1: try: port = int(sys.argv[1]) except ValueError: pass logger.info("Starting Educational Sparse Vector Search Engine Server") logger.info(f"Server will run on http://localhost:{port}") logger.info(f"Visit http://localhost:{port} for the web interface") logger.info(f"API documentation available at http://localhost:{port}/docs") uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")