Build and Push / build (release) Successful in 1m2s
- Add /rag/search endpoint for web, news, and image search via SearXNG - Add /content/extract and /content/extract/batch endpoints - Add ContentExtractor client using Trafilatura for content extraction - Enhance HybridRAG web search with full content extraction - Add Redis caching for search results - Add new configuration options for search and extraction timeouts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
88 lines
2.5 KiB
Python
88 lines
2.5 KiB
Python
"""
|
|
RAG search router for Library Desk API.
|
|
|
|
Endpoints for web, news, and image search with content extraction.
|
|
"""
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException, Depends
|
|
import logging
|
|
|
|
from src.models.rag_search import RAGSearchRequest, RAGSearchResponse
|
|
from src.services.rag_search_service import RAGSearchService
|
|
from src.core.dependencies import verify_api_key
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/rag", tags=["RAG Search"])
|
|
|
|
|
|
# Lazy import to avoid circular dependency
|
|
def get_rag_search_service() -> RAGSearchService:
|
|
"""Get RAG search service instance."""
|
|
from src.core.dependencies import get_rag_search_service as _get_service
|
|
return _get_service()
|
|
|
|
|
|
@router.post("/search", response_model=RAGSearchResponse)
|
|
async def search(
|
|
request: RAGSearchRequest,
|
|
api_key: str = Depends(verify_api_key)
|
|
):
|
|
"""
|
|
Execute RAG-optimized web search with content extraction.
|
|
|
|
Searches via SearXNG and extracts full content from results using
|
|
Trafilatura. Results are cached in Redis for efficiency.
|
|
|
|
**Search Types:**
|
|
- `web`: General web search (default)
|
|
- `news`: News articles with recency filtering
|
|
- `images`: Image search results
|
|
|
|
**Example Request:**
|
|
```json
|
|
{
|
|
"query": "Python async programming best practices",
|
|
"search_type": "web",
|
|
"limit": 10,
|
|
"user": "default"
|
|
}
|
|
```
|
|
|
|
**Response includes:**
|
|
- Full extracted text content per result
|
|
- Original search snippets
|
|
- Source domain names
|
|
- Markdown sources summary for LLM consumption
|
|
|
|
**Error Codes:**
|
|
- 400: Invalid query (empty or too long)
|
|
- 502: Search provider (SearXNG) error
|
|
- 504: Search timeout
|
|
"""
|
|
try:
|
|
service = get_rag_search_service()
|
|
response = await service.search(
|
|
query=request.query,
|
|
search_type=request.search_type,
|
|
limit=request.limit,
|
|
user=request.user
|
|
)
|
|
return response
|
|
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error(f"Search timed out for query: {request.query}")
|
|
raise HTTPException(status_code=504, detail="Search timed out")
|
|
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"Search provider error: {e}")
|
|
raise HTTPException(status_code=502, detail="Search provider error")
|
|
|
|
except Exception as e:
|
|
logger.error(f"RAG search failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Search failed")
|