# coding: utf-8

"""
    Mixpeek API

    This is the Mixpeek API, providing access to various endpoints for data processing and retrieval.

    The version of the OpenAPI document: 1.3.0
    Contact: info@mixpeek.com
    Generated by OpenAPI Generator (https://openapi-generator.tech)

    Do not edit the class manually.
"""  # noqa: E501


from __future__ import annotations
import pprint
import re  # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing_extensions import Annotated
from mixpeek.models.facet_field_config import FacetFieldConfig
from mixpeek.models.feature_search_config import FeatureSearchConfig
from mixpeek.models.feature_search_group_by import FeatureSearchGroupBy
from mixpeek.models.fusion_strategy import FusionStrategy
from mixpeek.models.learned_fusion_config import LearnedFusionConfig
from mixpeek.models.stage_cache_behavior import StageCacheBehavior
from typing import Optional, Set
from typing_extensions import Self

class StageParamsFeatureSearch(BaseModel):
    """
    User configuration for the feature_filter stage.  **Stage Category**: FILTER  **Transformation**: 0 documents → ≤final_top_k documents  **Purpose**: Unified stage for semantic and hybrid search across N feature URIs. Performs vector similarity search on one or more embedding features and fuses results using configurable strategies. Leverages Qdrant's native multi-vector search and fusion capabilities for optimal performance.  **When to Use**:     - As the initial stage to find candidate documents from collections     - Single feature URI (N=1): Pure semantic/KNN search     - Multiple feature URIs (N>1): Hybrid/multimodal search with fusion     - Multimodal search: Text + image + video embeddings combined     - Lexical + semantic: Sparse + dense vectors for best-of-both     - Any combination of dense vector features     - **Filter-only mode (N=0)**: Pure attribute/text filtering without embeddings       by providing only pre_filters (uses Qdrant's native full-text search)  **When NOT to Use**:     - For filtering in-memory results (use attribute_filter or llm_filter)     - For reordering results (use SORT stages)     - For enriching documents (use APPLY stages)  **Operational Behavior**:     - **With searches**: Queries vector databases (Qdrant) for each feature URI       in parallel, generates embeddings via inference service, performs multi-vector       search with Qdrant native fusion, returns fused and scored results     - **Filter-only mode**: Uses Qdrant's scroll API with native filtering (no embeddings,       no vector search). Supports full-text search via TEXT operator and all other       filter operators. Returns documents matching filters without relevance scores.     - Moderate performance (depends on number of features and top_k)     - Output schema = Collection document schema (no schema changes)  **Common Pipeline Position**: FILTER (this stage) → ENRICH → SORT → REDUCE  **Configuration vs Execution**:     Configuration time (defining the retriever):     - Specify searches: List of features to search with parameters     - Use TEMPLATES for query inputs: {{INPUT.field_name}}     - Configure fusion strategy, weights, final_top_k      Execution time (running the retriever):     - Only pass input values (e.g., {\"user_query\": \"search text\"})     - System resolves templates using your input values     - Generates embeddings and performs searches automatically  Requirements:     - searches: REQUIRED, at least one feature search configuration     - final_top_k: OPTIONAL, defaults to 25 results after fusion     - fusion: OPTIONAL, defaults to RRF for multi-feature searches     - cache_behavior: OPTIONAL, defaults to 'auto' for caching (inherited)  Use Cases:     - Single-modal search: One feature URI (text OR image OR video)     - Multimodal search: Text + image + video features combined     - Hybrid search: Dense + sparse vectors for best-of-both     - Multi-index search: Search across multiple feature types simultaneously  Example Stage Configuration (Multimodal):     ```json     {         \"stage_name\": \"multimodal_search\",         \"stage_type\": \"filter\",         \"config\": {             \"stage_id\": \"feature_search\",             \"parameters\": {                 \"searches\": [                     {                         \"feature_uri\": \"mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1\",                         \"query\": {\"input_mode\": \"text\", \"value\": \"{{INPUT.user_query}}\"},                         \"top_k\": 100,                         \"weight\": 0.6                     },                     {                         \"feature_uri\": \"mixpeek://clip_extractor@v1/image_embedding\",                         \"query\": {\"input_mode\": \"text\", \"value\": \"{{INPUT.user_query}}\"},                         \"top_k\": 50,                         \"weight\": 0.4                     }                 ],                 \"final_top_k\": 25,                 \"fusion\": \"weighted\"             }         }     }     ```  Example Execution Request:     ```json     {         \"inputs\": {             \"user_query\": \"red sports car with spoiler\"         }     }     ```
    """ # noqa: E501
    cache_behavior: Optional[StageCacheBehavior] = Field(default=None, description="Controls internal caching behavior for this stage. OPTIONAL - defaults to 'auto' for transparent performance.   'auto' (default): Automatic caching for deterministic operations. Stage intelligently caches results based on inputs and parameters. Use for transformations, parsing, formatting, stable API calls. Cache invalidates automatically when parameters change. Recommended for 95% of use cases.   'disabled': Skip all internal caching. Every execution runs fresh without cache lookup. Use for templates with now(), random(), or external APIs that must be called every time (real-time data). No performance benefit but guarantees fresh execution.   'aggressive': Cache even non-deterministic operations. Use ONLY when you fully understand caching implications. May cache time-sensitive or random data. Generally not recommended - prefer 'auto' or 'disabled'.   Note: This controls internal stage caching. Retriever-level caching (cache_config.cache_stage_names) is separate and caches complete stage outputs.")
    cache_ttl_seconds: Optional[Annotated[int, Field(strict=True, ge=0)]] = Field(default=null, description="Time-to-live for cache entries in seconds. OPTIONAL - defaults to None (LRU eviction only).   When None (default, recommended): Cache uses Redis LRU eviction policy. Most frequently used items stay cached automatically. No manual TTL management needed. Memory bounded by Redis maxmemory setting.   When specified: Cache entries expire after this duration regardless of usage. Useful for data that becomes stale after specific time periods. Lower values for frequently changing external data. Higher values for stable transformations.   Examples: - None: LRU-based eviction (recommended for most cases) - 300: 5 minutes (for semi-static external data) - 3600: 1 hour (for stable transformations) - 86400: 24 hours (for rarely changing operations)   Performance Note: TTL adds minimal overhead (<1ms) but forces eviction even for frequently accessed items. Use None unless you have specific staleness requirements.")
    searches: Optional[Annotated[List[FeatureSearchConfig], Field(max_length=10)]] = Field(default=None, description="List of feature searches to perform and fuse. Can be empty for filter-only mode (requires pre_filters). For single-modal search: Provide 1 feature search (pure KNN/semantic). For hybrid/multimodal: Provide 2+ feature searches (fusion applied). Each feature search specifies: feature URI, query input, top_k, score threshold. Searches execute in parallel and results are fused using fusion strategy.   **Filter-only mode**: Leave empty and provide pre_filters to use Qdrant's native filtering (including full-text search) without vector embeddings. This enables pure attribute/text search without semantic similarity.")
    final_top_k: Optional[Annotated[int, Field(le=500, strict=True, ge=1)]] = Field(default=25, description="OPTIONAL. Maximum number of documents to return after fusion. Defaults to 25. This is applied AFTER all feature searches are fused together. Must be ≤ minimum top_k across all searches.Higher values: More comprehensive results but slower. Common values: 10 (fast), 25 (balanced), 50-100 (comprehensive).")
    fusion: Optional[FusionStrategy] = Field(default=None, description="OPTIONAL. Score fusion strategy for combining multiple feature searches. Defaults to 'rrf' (Reciprocal Rank Fusion). Only relevant when searches has 2+ entries. Ignored for single feature search (no fusion needed).   ┌──────────┬──────────────┬────────────────────────────────────────┐ │ Strategy │ Qdrant Native│ Description                            │ ├──────────┼──────────────┼────────────────────────────────────────┤ │ rrf      │ ✅ Yes       │ Rank-based, robust default             │ │ dbsf     │ ✅ Yes       │ Score-based with normalization         │ │ weighted │ ❌ No        │ Manual score-weighted fusion           │ │ max      │ ❌ No        │ Best match from any feature            │ │ learned  │ ❌ No        │ Bandit-learned, personalized weights   │ └──────────┴──────────────┴────────────────────────────────────────┘   'rrf' (default): Rank-based fusion, robust and simple. Single Qdrant call, no tuning needed.   'dbsf': Score-based fusion with statistical normalization. Single Qdrant call, handles different score scales.   'weighted': Manual weight fusion using FeatureSearchConfig.weight. N separate queries, merged client-side.   'max': Maximum score across features (OR semantics). N separate queries, merged client-side.   'learned': Bandit-learned weights from user feedback. Requires learning_config. Enables personalization. N separate queries for per-feature score tracking.")
    learning_config: Optional[LearnedFusionConfig] = Field(default=None, description="OPTIONAL. Configuration for learned fusion. REQUIRED when fusion='learned', ignored otherwise.   Enables personalized feature weighting by learning from user interactions. The system learns which features (text, image, audio) matter most for different users or contexts via Thompson Sampling bandit.   See LearnedFusionConfig for full documentation.")
    collection_identifiers: Optional[List[StrictStr]] = Field(default=None, description="OPTIONAL. Collection identifiers to search within this stage. Can be collection IDs or names. If NOT provided, uses retriever's default collections. Use this to target specific collections independent of retriever defaults.   Note: These identifiers are validated at retriever creation time to ensure the stage's feature_uris can be resolved against these collections.   Use cases: - Multi-tier pipelines where different stages search different collections - Stage-specific collection targeting - Override retriever defaults for this stage only")
    facets: Optional[List[FacetFieldConfig]] = Field(default=None, description="OPTIONAL. Fields to compute facet counts for during search. Facets run in PARALLEL with the search query using the same filters, providing value counts for the entire filtered result set (not just paginated results).   Use cases: - Build faceted search UIs (e.g., 'Filter by Category: Sports (45), Music (23)') - Show available filter options with document counts - Enable drill-down navigation in search results   Requirements: - Each faceted field MUST have a keyword index in Qdrant - Common auto-indexed fields: metadata.*, status, collection_id - For custom fields, ensure indexing is configured   Performance: - Facets execute in parallel with search (minimal latency impact) - Use approximate counts (exact=False) for fast UI responses - Limit facet count per field to reduce response size")
    group_by: Optional[FeatureSearchGroupBy] = Field(default=None, description="OPTIONAL. Database-level grouping configuration (uses Qdrant query_points_groups). When enabled, results are grouped by the specified field at the database level, which is more efficient than in-memory grouping for large result sets.   Use cases: - Decompose/recompose: Search chunks, return parent documents - Deduplication: One best result per product_id - Scene→Video grouping: Search frames, return parent videos   Output modes (mirrors group_by REDUCE stage for consistency): - 'first': Top doc per group (deduplication) - 'all': All docs with group structure preserved - 'flatten': All docs as flat list   Performance: - Grouping happens in Qdrant (database-level) - Much faster than fetching all results and grouping in memory - Use for decompose/recompose patterns at scale")
    __properties: ClassVar[List[str]] = ["cache_behavior", "cache_ttl_seconds", "searches", "final_top_k", "fusion", "learning_config", "collection_identifiers", "facets", "group_by"]

    model_config = ConfigDict(
        populate_by_name=True,
        validate_assignment=True,
        protected_namespaces=(),
    )


    def to_str(self) -> str:
        """Returns the string representation of the model using alias"""
        return pprint.pformat(self.model_dump(by_alias=True))

    def to_json(self) -> str:
        """Returns the JSON representation of the model using alias"""
        # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
        return json.dumps(self.to_dict())

    @classmethod
    def from_json(cls, json_str: str) -> Optional[Self]:
        """Create an instance of StageParamsFeatureSearch from a JSON string"""
        return cls.from_dict(json.loads(json_str))

    def to_dict(self) -> Dict[str, Any]:
        """Return the dictionary representation of the model using alias.

        This has the following differences from calling pydantic's
        `self.model_dump(by_alias=True)`:

        * `None` is only added to the output dict for nullable fields that
          were set at model initialization. Other fields with value `None`
          are ignored.
        """
        excluded_fields: Set[str] = set([
        ])

        _dict = self.model_dump(
            by_alias=True,
            exclude=excluded_fields,
            exclude_none=True,
        )
        # override the default output from pydantic by calling `to_dict()` of each item in searches (list)
        _items = []
        if self.searches:
            for _item_searches in self.searches:
                if _item_searches:
                    _items.append(_item_searches.to_dict())
            _dict['searches'] = _items
        # override the default output from pydantic by calling `to_dict()` of learning_config
        if self.learning_config:
            _dict['learning_config'] = self.learning_config.to_dict()
        # override the default output from pydantic by calling `to_dict()` of each item in facets (list)
        _items = []
        if self.facets:
            for _item_facets in self.facets:
                if _item_facets:
                    _items.append(_item_facets.to_dict())
            _dict['facets'] = _items
        # override the default output from pydantic by calling `to_dict()` of group_by
        if self.group_by:
            _dict['group_by'] = self.group_by.to_dict()
        return _dict

    @classmethod
    def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
        """Create an instance of StageParamsFeatureSearch from a dict"""
        if obj is None:
            return None

        if not isinstance(obj, dict):
            return cls.model_validate(obj)

        _obj = cls.model_validate({
            "cache_behavior": obj.get("cache_behavior"),
            "cache_ttl_seconds": obj.get("cache_ttl_seconds") if obj.get("cache_ttl_seconds") is not None else null,
            "searches": [FeatureSearchConfig.from_dict(_item) for _item in obj["searches"]] if obj.get("searches") is not None else None,
            "final_top_k": obj.get("final_top_k") if obj.get("final_top_k") is not None else 25,
            "fusion": obj.get("fusion"),
            "learning_config": LearnedFusionConfig.from_dict(obj["learning_config"]) if obj.get("learning_config") is not None else None,
            "collection_identifiers": obj.get("collection_identifiers"),
            "facets": [FacetFieldConfig.from_dict(_item) for _item in obj["facets"]] if obj.get("facets") is not None else None,
            "group_by": FeatureSearchGroupBy.from_dict(obj["group_by"]) if obj.get("group_by") is not None else None
        })
        return _obj


