poi_model / app.py
Peersik's picture
Upload 25 files
412553b verified
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional, List, Dict, Any
import uvicorn
import logging
import time
from datetime import datetime
import os
from contextlib import asynccontextmanager
from search_engine import EnhancedPOISearchEngine
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Модели Pydantic
class Coordinates(BaseModel):
"""Модель координат"""
lat: float = Field(..., ge=-90, le=90, description="Широта (-90 до 90)")
lon: float = Field(..., ge=-180, le=180, description="Долгота (-180 до 180)")
@field_validator('lat', 'lon')
def validate_coordinates(cls, v):
if v == 0:
raise ValueError('Координаты не могут быть нулевыми')
return v
class RouteInfo(BaseModel):
"""Информация о маршруте"""
start: Optional[Coordinates] = Field(None, description="Начальная точка маршрута")
end: Optional[Coordinates] = Field(None, description="Конечная точка маршрута")
max_distance_km: float = Field(3.0, gt=0, le=50, description="Максимальное расстояние от маршрута в км")
class SearchRequest(BaseModel):
"""Запрос на поиск POI"""
query: str = Field(..., min_length=2, max_length=200, description="Текстовый запрос для поиска")
route: Optional[RouteInfo] = Field(None, description="Информация о маршруте (опционально)")
max_results: int = Field(20, ge=1, le=50, description="Максимальное количество результатов")
model_config = ConfigDict(
json_schema_extra={
"example": {
"query": "кафе и музей",
"route": {
"start": {"lat": 55.7539, "lon": 37.6208},
"end": {"lat": 55.7601, "lon": 37.6186},
"max_distance_km": 2.0
},
"max_results": 15
}
}
)
class POIResult(BaseModel):
"""Результат поиска точки интереса"""
id: int
name: str
category: str
type: str
lat: float
lon: float
score: float = Field(..., ge=0, le=1, description="Релевантность от 0 до 1")
distance_to_route: Optional[float] = Field(None, description="Расстояние до маршрута в км")
description: Optional[str] = None
class SearchResponse(BaseModel):
"""Ответ на запрос поиска"""
success: bool
query: str
results: List[POIResult]
count: int
processing_time_ms: float
categories_found: List[str]
timestamp: str
class HealthResponse(BaseModel):
"""Ответ проверки здоровья"""
status: str
service: str
version: str
model_loaded: bool
points_count: int
uptime_seconds: float
timestamp: str
# Инициализация приложения
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения"""
global search_engine
# Старт
logger.info("🚀 Запуск POI Search Service...")
try:
search_engine = EnhancedPOISearchEngine(model_path='model/enhanced')
if search_engine.load_model():
logger.info("✅ Модель успешно загружена")
else:
logger.error("❌ Не удалось загрузить модель")
except Exception as e:
logger.error(f"❌ Ошибка при инициализации: {e}")
yield
# Завершение
logger.info("👋 Остановка POI Search Service...")
search_engine = None
app = FastAPI(
title="POI Search API",
description="API для поиска точек интереса с учетом маршрута",
version="2.0.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan
)
# Настройка CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Глобальные переменные
search_engine = None
start_time = datetime.now()
@app.get("/", tags=["Root"])
async def root():
"""Корневой эндпоинт"""
return {
"service": "POI Search API",
"version": "2.0.0",
"description": "Поиск точек интереса с маршрутизацией",
"timestamp": datetime.now().isoformat()
}
@app.get("/api/v2/health", response_model=HealthResponse, tags=["Health"])
async def health_check():
"""Проверка здоровья сервиса"""
uptime = (datetime.now() - start_time).total_seconds()
model_loaded = search_engine is not None and search_engine.model is not None
points_count = len(search_engine.df) if search_engine and search_engine.df is not None else 0
return HealthResponse(
status="healthy" if model_loaded else "degraded",
service="POI Search API",
version="2.0.0",
model_loaded=model_loaded,
points_count=points_count,
uptime_seconds=uptime,
timestamp=datetime.now().isoformat()
)
@app.post("/api/v2/search", response_model=SearchResponse, tags=["Search"])
async def search_poi(request: SearchRequest):
"""Основной поиск точек интереса"""
if search_engine is None or search_engine.model is None:
raise HTTPException(status_code=503, detail="Модель не загружена")
start_timer = time.time()
try:
# Извлекаем параметры маршрута
start_coords = None
end_coords = None
max_distance = 5.0
if request.route and request.route.start and request.route.end:
start_coords = (request.route.start.lat, request.route.start.lon)
end_coords = (request.route.end.lat, request.route.end.lon)
max_distance = request.route.max_distance_km
# Выполняем поиск
results = search_engine.multi_category_search(
query=request.query,
start_coords=start_coords,
end_coords=end_coords,
max_distance_km=max_distance,
max_results=request.max_results
)
processing_time_ms = (time.time() - start_timer) * 1000
# Собираем найденные категории
categories_found = list(set(r['category'] for r in results if r.get('category')))
response = SearchResponse(
success=True,
query=request.query,
results=results,
count=len(results),
processing_time_ms=processing_time_ms,
categories_found=categories_found,
timestamp=datetime.now().isoformat()
)
logger.info(
f"🔍 Поиск '{request.query}': найдено {len(results)} точек ({len(categories_found)} категорий) за {processing_time_ms:.1f}ms")
return response
except Exception as e:
logger.error(f"❌ Ошибка при поиске '{request.query}': {e}")
raise HTTPException(status_code=500, detail=f"Ошибка при поиске: {str(e)}")
@app.post("/api/v2/search/fast", tags=["Search"])
async def fast_search(
query: str = Query(..., min_length=2),
max_results: int = Query(10, ge=1, le=50)
):
"""Быстрый поиск"""
if search_engine is None:
raise HTTPException(status_code=503, detail="Поисковый движок не загружен")
try:
results = search_engine.simple_search(query, max_results)
categories_found = list(set(r['category'] for r in results if r.get('category')))
return {
"success": True,
"query": query,
"results": results,
"count": len(results),
"categories_found": categories_found
}
except Exception as e:
logger.error(f"❌ Ошибка быстрого поиска: {e}")
raise HTTPException(status_code=500, detail=str(e))
MODEL_PATH = os.path.join(os.path.dirname(__file__), 'model/enhanced')
# В lifespan измените:
@asynccontextmanager
async def lifespan(app: FastAPI):
global search_engine
logger.info("🚀 Запуск POI Search Service на Hugging Face Spaces...")
try:
search_engine = EnhancedPOISearchEngine(model_path=MODEL_PATH)
if search_engine.load_model():
logger.info("✅ Модель успешно загружена")
else:
logger.warning("⚠️ Модель не загружена, но сервис работает в ограниченном режиме")
except Exception as e:
logger.error(f"❌ Ошибка при инициализации: {e}")
yield
logger.info("👋 Остановка POI Search Service...")
search_engine = None
# Middleware для логирования
@app.middleware("http")
async def log_requests(request, call_next):
"""Логирование всех запросов"""
start_time = time.time()
response = await call_next(request)
process_time = (time.time() - start_time) * 1000
logger.info(
f"{request.method} {request.url.path} - "
f"Status: {response.status_code} - "
f"Time: {process_time:.1f}ms"
)
return response
if __name__ == "__main__":
uvicorn.run(
"app:app",
host="0.0.0.0",
port=7860,
reload=False,
workers=1,
log_level="info"
)