LightRAG/lightrag/api/routers/search_routes.py
clssck 59e89772de refactor: consolidate to PostgreSQL-only backend and modernize stack
Remove legacy storage implementations and deprecated examples:
- Delete FAISS, JSON, Memgraph, Milvus, MongoDB, Nano Vector DB, Neo4j, NetworkX, Qdrant, Redis storage backends
- Remove Kubernetes deployment manifests and installation scripts
- Delete unofficial examples for deprecated backends and offline deployment docs
Streamline core infrastructure:
- Consolidate storage layer to PostgreSQL-only implementation
- Add full-text search caching with FTS cache module
- Implement metrics collection and monitoring pipeline
- Add explain and metrics API routes
Modernize frontend and tooling:
- Switch web UI to Bun with bun.lock, remove npm and pnpm lockfiles
- Update Dockerfile for PostgreSQL-only deployment
- Add Makefile for common development tasks
- Update environment and configuration examples
Enhance evaluation and testing capabilities:
- Add prompt optimization with DSPy and auto-tuning
- Implement ground truth regeneration and variant testing
- Add prompt debugging and response comparison utilities
- Expand test coverage with new integration scenarios
Simplify dependencies and configuration:
- Remove offline-specific requirement files
- Update pyproject.toml with streamlined dependencies
- Add Python version pinning with .python-version
- Create project guidelines in CLAUDE.md and AGENTS.md
2025-12-12 16:28:49 +01:00

165 lines
5.9 KiB
Python

"""
Search routes for BM25 full-text search.
This module provides a direct keyword search endpoint that bypasses the LLM
and returns matching chunks directly. This is complementary to the /query
endpoint which uses semantic RAG.
Use cases:
- /query (existing): Semantic RAG - "What causes X?" -> LLM-generated answer
- /search (this): Keyword search - "Find docs about X" -> Direct chunk results
"""
from typing import Annotated, Any, ClassVar
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from lightrag.api.utils_api import get_combined_auth_dependency
from lightrag.kg.postgres_impl import PostgreSQLDB
from lightrag.utils import logger
class SearchResult(BaseModel):
"""A single search result (chunk match)."""
id: str = Field(description='Chunk ID')
full_doc_id: str = Field(description='Parent document ID')
chunk_order_index: int = Field(description='Position in document')
tokens: int = Field(description='Token count')
content: str = Field(description='Chunk content')
file_path: str | None = Field(default=None, description='Source file path')
s3_key: str | None = Field(default=None, description='S3 key for source document')
char_start: int | None = Field(default=None, description='Character offset start in source document')
char_end: int | None = Field(default=None, description='Character offset end in source document')
score: float = Field(description='BM25 relevance score')
class SearchResponse(BaseModel):
"""Response model for search endpoint."""
query: str = Field(description='Original search query')
results: list[SearchResult] = Field(description='Matching chunks')
count: int = Field(description='Number of results returned')
workspace: str = Field(description='Workspace searched')
class Config:
json_schema_extra: ClassVar[dict[str, Any]] = {
'example': {
'query': 'machine learning algorithms',
'results': [
{
'id': 'chunk-abc123',
'full_doc_id': 'doc-xyz789',
'chunk_order_index': 5,
'tokens': 250,
'content': 'Machine learning algorithms can be categorized into...',
'file_path': 's3://lightrag/archive/default/doc-xyz789/report.pdf',
's3_key': 'archive/default/doc-xyz789/report.pdf',
'score': 0.85,
}
],
'count': 1,
'workspace': 'default',
}
}
def create_search_routes(
kv_storage: Any,
api_key: str | None = None,
) -> APIRouter:
"""
Create search routes for BM25 full-text search.
Args:
kv_storage: PGKVStorage instance (db accessed lazily at request time)
api_key: Optional API key for authentication
Returns:
FastAPI router with search endpoints
"""
router = APIRouter(
prefix='/search',
tags=['search'],
)
optional_api_key = get_combined_auth_dependency(api_key)
def get_db() -> PostgreSQLDB:
"""Get db lazily - initialized after app startup."""
db = getattr(kv_storage, 'db', None)
if db is None:
raise HTTPException(status_code=503, detail='Database not yet initialized')
return db
@router.get(
'',
response_model=SearchResponse,
summary='BM25 keyword search',
description="""
Perform BM25-style full-text search on document chunks.
This endpoint provides direct keyword search without LLM processing.
It's faster than /query for simple keyword lookups and returns
matching chunks directly.
The search uses PostgreSQL's native full-text search with ts_rank
for relevance scoring.
**Use cases:**
- Quick keyword lookups
- Finding specific terms or phrases
- Browsing chunks containing specific content
- Export/citation workflows where you need exact matches
**Differences from /query:**
- /query: Semantic search + LLM generation -> Natural language answer
- /search: Keyword search -> Direct chunk results (no LLM)
""",
)
async def search(
q: Annotated[str, Query(description='Search query', min_length=1)],
limit: Annotated[int, Query(description='Max results to return', ge=1, le=100)] = 10,
workspace: Annotated[str, Query(description='Workspace to search')] = 'default',
_: Annotated[bool, Depends(optional_api_key)] = True,
) -> SearchResponse:
"""Perform BM25 full-text search on chunks."""
try:
db = get_db()
results = await db.full_text_search(
query=q,
workspace=workspace,
limit=limit,
)
search_results = [
SearchResult(
id=r.get('id', ''),
full_doc_id=r.get('full_doc_id', ''),
chunk_order_index=r.get('chunk_order_index', 0),
tokens=r.get('tokens', 0),
content=r.get('content', ''),
file_path=r.get('file_path'),
s3_key=r.get('s3_key'),
char_start=r.get('char_start'),
char_end=r.get('char_end'),
score=float(r.get('score', 0)),
)
for r in results
]
return SearchResponse(
query=q,
results=search_results,
count=len(search_results),
workspace=workspace,
)
except Exception as e:
if isinstance(e, HTTPException):
raise
logger.error('Search failed: %s', e)
raise HTTPException(status_code=500, detail=f'Search failed: {e}') from e
return router