"""
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 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")