mirror of
https://github.com/intari/search2_chatgpt.git
synced 2025-04-20 13:49:08 +00:00
116 lines
6.4 KiB
Python
116 lines
6.4 KiB
Python
from fastapi import FastAPI, HTTPException, Query, Depends
|
||
from fastapi.responses import FileResponse
|
||
import requests
|
||
import os
|
||
from typing import List, Dict, Any, Optional
|
||
from dotenv import load_dotenv
|
||
import logging
|
||
|
||
# Загрузка переменных окружения (например, из .env)
|
||
load_dotenv()
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Конфигурация
|
||
FILES_DIR: str = os.getenv("LOCAL_STORAGE_PATH", "/mnt/storage") # Путь монтирования в Docker
|
||
SEARCH_ENGINE_URL: str = os.getenv("MEILI_URL", "http://meilisearch:7700")
|
||
MEILI_API_KEY: Optional[str] = os.getenv("MEILI_MASTER_KEY") # Используйте Master Key или Search API Key
|
||
INDEX_NAME: str = "documents"
|
||
|
||
app = FastAPI(
|
||
title="Document Search API",
|
||
description="API для поиска по локальным документам и их получения",
|
||
version="0.2.0",
|
||
)
|
||
|
||
# Зависимость для получения HTTP-клиента (лучше использовать один клиент)
|
||
# В реальном приложении можно использовать httpx.AsyncClient
|
||
# Для простоты пока оставляем requests
|
||
def get_search_session() -> requests.Session:
|
||
"""Создает сессию requests с заголовками для Meilisearch."""
|
||
session = requests.Session()
|
||
headers = {}
|
||
if MEILI_API_KEY:
|
||
headers["Authorization"] = f"Bearer {MEILI_API_KEY}"
|
||
session.headers.update(headers)
|
||
return session
|
||
|
||
@app.get("/search", response_model=Dict[str, List[Dict[str, Any]]], summary="Поиск документов")
|
||
async def search(
|
||
q: str = Query(..., description="Поисковый запрос"),
|
||
limit: int = Query(20, ge=1, le=100, description="Максимальное количество результатов"),
|
||
session: requests.Session = Depends(get_search_session)
|
||
) -> Dict[str, List[Dict[str, Any]]]:
|
||
"""
|
||
Выполняет поиск документов в индексе Meilisearch.
|
||
"""
|
||
search_url = f"{SEARCH_ENGINE_URL}/indexes/{INDEX_NAME}/search"
|
||
params = {"q": q, "limit": limit, "attributesToHighlight": ["content"]} # Запрашиваем подсветку
|
||
try:
|
||
response = session.post(search_url, json=params) # Meilisearch рекомендует POST для поиска с параметрами
|
||
response.raise_for_status() # Вызовет исключение для кодов 4xx/5xx
|
||
results = response.json()
|
||
logger.info(f"Поиск по запросу '{q}' вернул {len(results.get('hits', []))} результатов")
|
||
# Возвращаем только нужные поля, включая _formatted для подсветки
|
||
hits = []
|
||
for hit in results.get("hits", []):
|
||
# Убираем полный content, если он большой, оставляем только id и _formatted
|
||
formatted_hit = hit.get("_formatted", {"id": hit.get("id", "N/A"), "content": "..."})
|
||
formatted_hit["id"] = hit.get("id", "N/A") # Убедимся, что id всегда есть
|
||
hits.append(formatted_hit)
|
||
|
||
return {"results": hits}
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
logger.error(f"Ошибка при обращении к Meilisearch ({search_url}): {e}")
|
||
raise HTTPException(status_code=503, detail="Сервис поиска временно недоступен")
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при поиске: {e}")
|
||
raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера при поиске")
|
||
|
||
|
||
@app.get("/files/{filename}", summary="Получение файла документа")
|
||
async def get_file(filename: str) -> FileResponse:
|
||
"""
|
||
Возвращает файл по его имени.
|
||
Используется для скачивания файлов, найденных через поиск.
|
||
"""
|
||
if not filename or ".." in filename or "/" in filename:
|
||
logger.warning(f"Попытка доступа к некорректному имени файла: {filename}")
|
||
raise HTTPException(status_code=400, detail="Некорректное имя файла")
|
||
|
||
file_path = os.path.join(FILES_DIR, filename)
|
||
# Проверка безопасности: убеждаемся, что путь действительно внутри FILES_DIR
|
||
if not os.path.abspath(file_path).startswith(os.path.abspath(FILES_DIR)):
|
||
logger.error(f"Попытка доступа за пределы разрешенной директории: {file_path}")
|
||
raise HTTPException(status_code=403, detail="Доступ запрещен")
|
||
|
||
if os.path.exists(file_path) and os.path.isfile(file_path):
|
||
logger.info(f"Отдаем файл: {filename}")
|
||
# media_type можно определять более точно, если нужно
|
||
return FileResponse(file_path, filename=filename)
|
||
else:
|
||
logger.warning(f"Запрошенный файл не найден: {filename} (путь {file_path})")
|
||
raise HTTPException(status_code=404, detail="Файл не найден")
|
||
|
||
# Можно добавить эндпоинт для статуса системы, проверки подключения к MeiliSearch и т.д.
|
||
@app.get("/health", summary="Проверка состояния сервиса")
|
||
async def health_check(session: requests.Session = Depends(get_search_session)) -> Dict[str, str]:
|
||
"""Проверяет доступность бэкенда и Meilisearch."""
|
||
meili_status = "недоступен"
|
||
try:
|
||
health_url = f"{SEARCH_ENGINE_URL}/health"
|
||
response = session.get(health_url)
|
||
response.raise_for_status()
|
||
if response.json().get("status") == "available":
|
||
meili_status = "доступен"
|
||
except requests.exceptions.RequestException:
|
||
pass # Статус останется "недоступен"
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при проверке здоровья Meilisearch: {e}")
|
||
|
||
|
||
return {"status": "ok", "meilisearch_status": meili_status}
|
||
|