models.py•6.68 kB
"""
Data Models for Elasticsearch Search Library
Type-safe dataclasses for configuration and search results.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Union
@dataclass(frozen=True)
class FieldConfig:
    """
    Configuration for a single search field.
    
    Attributes:
        name: Field name (e.g., 'user_name', 'impact_id')
        boost: Relevance boost multiplier (default: 1.0)
        enabled: Whether field is included in searches (default: True)
        fuzziness: Fuzzy matching tolerance (0, 1, 2, or 'AUTO')
    """
    name: str
    boost: float = 1.0
    enabled: bool = True
    fuzziness: Union[int, str] = 'AUTO'
    
    def __post_init__(self):
        """Validate field configuration."""
        if self.boost < 0:
            raise ValueError(f"Boost must be non-negative, got {self.boost}")
        
        if isinstance(self.fuzziness, int) and self.fuzziness < 0:
            raise ValueError(f"Fuzziness must be non-negative, got {self.fuzziness}")
        
        if isinstance(self.fuzziness, str) and self.fuzziness not in ('AUTO', '0', '1', '2'):
            raise ValueError(f"Invalid fuzziness value: {self.fuzziness}")
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'name': self.name,
            'boost': self.boost,
            'enabled': self.enabled,
            'fuzziness': self.fuzziness,
        }
@dataclass(frozen=True)
class EntityConfig:
    """
    Configuration for an entity type.
    
    Attributes:
        entity_type: Entity type name (e.g., 'user', 'impact')
        fuzziness: Default fuzziness for this entity
        default_limit: Default number of results to return
        max_limit: Maximum allowed results
        min_score: Minimum relevance score threshold
        fields: List of field configurations
    """
    entity_type: str
    fuzziness: Union[int, str] = 'AUTO'
    default_limit: int = 10
    max_limit: int = 100
    min_score: float = 0.0
    fields: List[FieldConfig] = field(default_factory=list)
    
    def __post_init__(self):
        """Validate entity configuration."""
        if self.default_limit < 1:
            raise ValueError(f"default_limit must be positive, got {self.default_limit}")
        
        if self.max_limit < self.default_limit:
            raise ValueError(f"max_limit ({self.max_limit}) must be >= default_limit ({self.default_limit})")
        
        if self.min_score < 0:
            raise ValueError(f"min_score must be non-negative, got {self.min_score}")
    
    def get_enabled_fields(self) -> List[FieldConfig]:
        """Get only enabled fields."""
        return [f for f in self.fields if f.enabled]
    
    def get_field(self, field_name: str) -> Optional[FieldConfig]:
        """Get field configuration by name."""
        for f in self.fields:
            if f.name == field_name:
                return f
        return None
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'entity_type': self.entity_type,
            'fuzziness': self.fuzziness,
            'default_limit': self.default_limit,
            'max_limit': self.max_limit,
            'min_score': self.min_score,
            'fields': [f.to_dict() for f in self.fields],
        }
@dataclass(frozen=True)
class SearchResult:
    """
    Single search result item.
    
    Attributes:
        data: Source data from Elasticsearch document
        score: Relevance score
        index: Index name where document was found
        id: Document ID
    """
    data: Dict[str, Any]
    score: float
    index: str
    id: str
    
    def get(self, key: str, default: Any = None) -> Any:
        """Get value from data dictionary."""
        return self.data.get(key, default)
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            'data': self.data,
            'score': self.score,
            'index': self.index,
            'id': self.id,
        }
@dataclass
class SearchResponse:
    """
    Complete search response.
    
    Attributes:
        success: Whether search was successful
        entity_type: Entity type that was searched
        query: Search query string
        total_hits: Total number of matching documents
        returned_count: Number of results in this response
        items: List of search result items
        index_name: Elasticsearch index that was searched
        error: Error message if search failed
    """
    success: bool
    entity_type: str
    query: str
    total_hits: int = 0
    returned_count: int = 0
    items: List[SearchResult] = field(default_factory=list)
    index_name: str = ""
    error: Optional[str] = None
    
    @classmethod
    def success_response(
        cls,
        entity_type: str,
        query: str,
        total_hits: int,
        items: List[SearchResult],
        index_name: str,
    ) -> "SearchResponse":
        """Create a successful search response."""
        return cls(
            success=True,
            entity_type=entity_type,
            query=query,
            total_hits=total_hits,
            returned_count=len(items),
            items=items,
            index_name=index_name,
            error=None,
        )
    
    @classmethod
    def error_response(
        cls,
        entity_type: str,
        query: str,
        error: str,
    ) -> "SearchResponse":
        """Create an error search response."""
        return cls(
            success=False,
            entity_type=entity_type,
            query=query,
            total_hits=0,
            returned_count=0,
            items=[],
            index_name="",
            error=error,
        )
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        result = {
            'success': self.success,
            'entity_type': self.entity_type,
            'query': self.query,
            'total_hits': self.total_hits,
            'returned_count': self.returned_count,
            'results': [item.to_dict() for item in self.items],
            'index_name': self.index_name,
        }
        
        if self.error:
            result['error'] = self.error
        
        return result
    
    def __len__(self) -> int:
        """Return number of results."""
        return len(self.items)
    
    def __iter__(self):
        """Iterate over result items."""
        return iter(self.items)
    
    def __getitem__(self, index: int) -> SearchResult:
        """Get result by index."""
        return self.items[index]