"""Chunk-level lexical and hybrid contracts; the item-level baseline stays distinct.""" from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, Field, model_validator from app.schemas.catalog import Category, Input from app.schemas.indexing import IndexConfig from app.schemas.retrieval import RetrievalConfig from app.schemas.vectors import SpaceRead, VectorHit class ChunkKeywordRequest(Input): query: str = Field(min_length=1, max_length=500) mode: Literal["keyword_chunks"] = "keyword_chunks" top_k: int = Field(5, ge=1, le=20) category: Category | None = None class HybridRequest(ChunkKeywordRequest): mode: Literal["hybrid"] = "hybrid" candidate_k: int = Field(50, ge=1, le=200) @model_validator(mode="after") def sufficient_candidates(self): if self.candidate_k < self.top_k: raise ValueError("candidate_k 不得小于 top_k") return self class ChunkRetrievalConfig(BaseModel): ranking_unit: Literal["chunk"] = "chunk" lexical: RetrievalConfig chunking: IndexConfig tie_break: Literal["knowledge_id,chunk_ordinal,chunk_id"] = ( "knowledge_id,chunk_ordinal,chunk_id" ) class HybridConfig(ChunkRetrievalConfig): scorer: Literal["rrf-v1"] = "rrf-v1" rrf_k: Literal[60] = 60 weights: tuple[Literal[1], Literal[1]] = (1, 1) candidate_k: int vector_scorer: Literal["pgvector-cosine-exact-v1"] = "pgvector-cosine-exact-v1" space: SpaceRead class RankedCandidate(BaseModel): chunk_id: UUID rank: int score: float class HybridHit(VectorHit): lexical_rank: int | None lexical_score: float | None vector_rank: int | None vector_score: float | None lexical_contribution: float vector_contribution: float class ChunkKeywordResponse(BaseModel): run_id: UUID query: str mode: Literal["keyword_chunks"] = "keyword_chunks" status: Literal["ok", "no_evidence"] top_k: int category: str | None query_terms: list[str] text_index_version_id: UUID text_generation: int catalog_revision: int corpus_fingerprint: str corpus_size: int total_hits: int results: list[VectorHit] elapsed_ms: float created_at: datetime config: ChunkRetrievalConfig class HybridResponse(ChunkKeywordResponse): mode: Literal["hybrid"] = "hybrid" vector_version_id: UUID generation: int lexical_total_hits: int vector_total_hits: int lexical_candidates: list[RankedCandidate] vector_candidates: list[RankedCandidate] results: list[HybridHit] config: HybridConfig