elasticsearch:
    enabled: "%env(bool:SHOPWARE_ES_ENABLED)%"
    indexing_enabled: "%env(bool:SHOPWARE_ES_INDEXING_ENABLED)%"
    indexing_batch_size: "%env(int:SHOPWARE_ES_INDEXING_BATCH_SIZE)%"
    hosts: "%env(string:OPENSEARCH_URL)%"
    index_prefix: "%env(string:SHOPWARE_ES_INDEX_PREFIX)%"
    throw_exception: "%env(string:SHOPWARE_ES_THROW_EXCEPTION)%"
    # Injects sw_dimension_normalize into the technical-term chains so
    # "5 x 70"/"5×70" collapse to one token. See SEARCH_ARCHITECTURE.md §4.
    # Toggling requires a full reindex.
    dimension_normalize: "%env(bool:SHOPWARE_ES_DIMENSION_NORMALIZE)%"
    search:
        timeout: 5s
        term_max_length: 300
        search_type: "query_then_fetch"
        # Optional: improves grouped Storefront product count accuracy in OpenSearch, but higher values also increase memory usage.
        # precision_threshold: 40000
        # Controls how much non-best-matching clauses contribute to the dis_max query score.
        # Elasticsearch default is 0.0 (only the best match counts). A value of 0.2 means other
        # matching clauses contribute 20% of their score, improving ranking for documents that
        # match across multiple fields or language variants without diluting the primary match.
        # Valid range: 0.0 (pure dis_max) to 1.0 (effectively a bool/should sum).
        dismax_tie_breaker: 0.2
    administration:
        hosts: "%env(string:ADMIN_OPENSEARCH_URL)%"
        enabled: "%env(bool:SHOPWARE_ADMIN_ES_ENABLED)%"
        indexing_batch_size: "%env(int:SHOPWARE_ADMIN_ES_INDEXING_BATCH_SIZE)%"
        refresh_indices: "%env(bool:SHOPWARE_ADMIN_ES_REFRESH_INDICES)%"
        index_prefix: "%env(string:SHOPWARE_ADMIN_ES_INDEX_PREFIX)%"
        throw_exception: "%env(string:SHOPWARE_ADMIN_ES_THROW_EXCEPTION)%"
        search:
            timeout: 5s
            term_max_length: 300
            search_type: "query_then_fetch"
        index_settings:
            number_of_shards: '%env(int-or-null:SHOPWARE_ADMIN_ES_NUMBER_OF_SHARDS)%'
            number_of_replicas: '%env(int-or-null:SHOPWARE_ADMIN_ES_NUMBER_OF_REPLICAS)%'
            'mapping.total_fields.limit': 50000
            'mapping.nested_fields.limit': 500
            'mapping.nested_objects.limit': 1000000
            max_result_window: 500000
            # Disable BM25 length normalization on admin indices so short
            # entity names (e.g. "Bag") don't outrank descriptive ones
            # ("Travel Bag XL") when both match the same query terms.
            similarity:
                default:
                    type: BM25
                    b: 0
                sw_length_norm:
                    type: BM25
                    b: 0.75
        analysis:
            normalizer:
                sw_lowercase_normalizer:
                    type: custom
                    filter: [ 'lowercase' ]
            char_filter:
                # German "3,3" → "3.3" so comma- and dot-decimal forms produce
                # identical tokens. Applied to the admin completion chain
                # unconditionally — admin names rarely use comma as a
                # thousands separator, and DACH product names with decimal
                # commas are common.
                sw_decimal_normalize:
                    type: pattern_replace
                    pattern: '(\d),(\d)'
                    replacement: '$1.$2'
                # Bridges quantity + unit ("100 ml" → "100ml", "3.3 mm" →
                # "3.3mm") before tokenization so glued and spaced forms
                # share tokens. See storefront `elasticsearch.yaml` for the
                # full pattern rationale.
                sw_unit_glue:
                    type: pattern_replace
                    pattern: '(^|\s)(\d+(?:[./,''\-]\d+)*)\s+([^\d\s])'
                    replacement: '$1$2$3'
            analyzer:
                sw_whitespace_analyzer:
                    type: custom
                    tokenizer: whitespace
                    filter: [ 'lowercase' ]
                sw_ngram_analyzer:
                    type: custom
                    tokenizer: whitespace
                    filter: [ 'lowercase', 'sw_ngram_filter' ]
                sw_admin_completion_index_analyzer:
                    type: custom
                    tokenizer: whitespace
                    char_filter: [ 'sw_decimal_normalize', 'sw_unit_glue' ]
                    filter: [ 'sw_word_delimiter_filter', 'flatten_graph', 'lowercase', 'sw_length_min', 'remove_duplicates' ]
                sw_admin_completion_search_analyzer:
                    type: custom
                    tokenizer: whitespace
                    char_filter: [ 'sw_decimal_normalize', 'sw_unit_glue' ]
                    filter: [ 'sw_word_delimiter_filter', 'lowercase', 'sw_length_min', 'remove_duplicates', 'sw_unique_filter' ]
            filter:
                sw_ngram_filter:
                    type: ngram
                    min_gram: "%env(int:SHOPWARE_ES_NGRAM_MIN_GRAM)%"
                    max_gram: "%env(int:SHOPWARE_ES_NGRAM_MAX_GRAM)%"
                sw_word_delimiter_filter:
                    type: word_delimiter_graph
                    preserve_original: true
                    catenate_all: true
                    catenate_words: true
                    catenate_numbers: true
                    split_on_case_change: true
                    generate_word_parts: true
                    split_on_numerics: true
                sw_length_min:
                    type: length
                    min: 2
                sw_unique_filter:
                    type: unique
                    only_on_same_position: false
        dynamic_templates:
            - keywords:
                match_mapping_type: string
                mapping:
                    type: keyword
                    normalizer: sw_lowercase_normalizer
                    fields:
                        text:
                            type: text
    product:
        custom_fields_mapping:
        exclude_source: "%env(bool:SHOPWARE_ES_EXCLUDE_SOURCE)%"
    ssl:
        verify_server_cert: true
    refresh_after_bulk: false
    index_settings:
        number_of_shards: '%env(int-or-null:SHOPWARE_ES_NUMBER_OF_SHARDS)%'
        number_of_replicas: '%env(int-or-null:SHOPWARE_ES_NUMBER_OF_REPLICAS)%'
        'mapping.total_fields.limit': 50000
        'mapping.nested_fields.limit': 500
        'mapping.nested_objects.limit': 1000000
        max_result_window: 10000
        # BM25 similarity configuration.
        # 'default' overrides Elasticsearch's built-in BM25 (b=0.75) to disable field-length normalization (b=0),
        # preventing short product names from ranking unfairly above descriptive ones.
        # 'sw_length_norm' preserves standard normalization for long-form text fields (description, metaDescription)
        # where document length IS a relevance signal.
        similarity:
            default:
                type: BM25
                b: 0
            sw_length_norm:
                type: BM25
                b: 0.75
    analysis:
        normalizer:
            sw_lowercase_normalizer:
                type: custom
                filter: ['lowercase']
        char_filter:
            # German "3,3" → "3.3" so comma- and dot-decimal forms share
            # tokens. German chain only — elsewhere `,` is a thousands
            # separator.
            sw_decimal_normalize:
                type: pattern_replace
                pattern: '(\d),(\d)'
                replacement: '$1.$2'
            # Dimensional notation → single token: "5 x 70"/"5×70" → "5x70".
            # Off by default; IndexCreator injects it into the technical-term
            # chains when SHOPWARE_ES_DIMENSION_NORMALIZE is true.
            sw_dimension_normalize:
                type: pattern_replace
                pattern: '(\d)\s*[xX×]\s*(\d)'
                replacement: '$1x$2'
            # Glues a numeric quantity to an adjacent unit/symbol before
            # tokenization: "100 ml" → "100ml", "5 €" → "5€", "1/2 inch" →
            # "1/2inch". Unit-agnostic. Guards against over-gluing:
            # `(^|\s)` anchor skips digits inside identifiers ("Gr49"),
            # and the `[^\d\s]` tail skips separate numerics ("5 of 10").
            # word_delimiter_graph then re-splits, so both glued and spaced
            # forms get indexed. Always on, all technical-term chains.
            sw_unit_glue:
                type: pattern_replace
                pattern: '(^|\s)(\d+(?:[./,''\-]\d+)*)\s+([^\d\s])'
                replacement: '$1$2$3'
        analyzer:
            sw_whitespace_analyzer:
                type: custom
                tokenizer: whitespace
                filter: ['lowercase']
            sw_whitespace_technical_term_index_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'flatten_graph', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates']
            sw_whitespace_technical_term_search_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates', 'sw_unique_filter']
            sw_ngram_analyzer:
                type: custom
                tokenizer: whitespace
                filter: ['lowercase', 'sw_ngram_filter']
            sw_english_analyzer:
                type: custom
                tokenizer: whitespace
                filter: ['lowercase', 'sw_english_stop_filter']
            sw_english_technical_term_index_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'flatten_graph', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates', 'sw_english_stop_filter']
            sw_english_technical_term_search_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates', 'sw_english_stop_filter', 'sw_unique_filter']
            sw_german_analyzer:
                type: custom
                tokenizer: whitespace
                filter: ['lowercase', 'sw_german_stop_filter']
            sw_german_technical_term_index_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_decimal_normalize', 'sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'flatten_graph', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates', 'sw_german_stop_filter']
            sw_german_technical_term_search_analyzer:
                type: custom
                tokenizer: whitespace
                char_filter: ['sw_decimal_normalize', 'sw_unit_glue']
                filter: ['sw_word_delimiter_filter', 'lowercase', 'sw_length_min', 'sw_decimal_normalize_token', 'remove_duplicates', 'sw_german_stop_filter', 'sw_unique_filter']
        filter:
            sw_ngram_filter:
                type: ngram
                min_gram: "%env(int:SHOPWARE_ES_NGRAM_MIN_GRAM)%"
                max_gram: "%env(int:SHOPWARE_ES_NGRAM_MAX_GRAM)%"
            sw_word_delimiter_filter:
                type: word_delimiter_graph
                preserve_original: true
                catenate_all: true
                catenate_words: true
                catenate_numbers: true
                split_on_case_change: true
                generate_word_parts: true
                split_on_numerics: true
            sw_length_min:
                type: length
                min: 2
            # Normalizes trailing-zero decimals so precision variants
            # cross-match: "5.0"/"5.00" → "5", "3.30" → "3.3". Emits the
            # canonical form as a sibling token (preserve_original keeps the
            # verbatim one). Mixed fractions ("5.5", "5.15") are untouched,
            # so distinct values stay distinct. Runs after sw_length_min so
            # short captures survive the length floor.
            sw_decimal_normalize_token:
                type: pattern_capture
                preserve_original: true
                patterns:
                    # Pure-zero fraction → integer: "5.0"/"5.00" → "5"
                    - '(\d+)\.0+(?=\D|$)'
                    # Trailing zeros after non-zero digit → trimmed:
                    # "3.30" → "3.3", "5.150" → "5.15"
                    - '(\d+\.\d*[1-9])0+(?=\D|$)'
            sw_unique_filter:
                type: unique
                only_on_same_position: false
            sw_english_stop_filter:
                type: 'stop'
                stopwords: '_english_'
            sw_german_stop_filter:
                type: 'stop'
                stopwords: '_german_'
    language_analyzer_mapping:
        en: sw_english_analyzer
        de: sw_german_analyzer
        gsw: sw_german_analyzer
        nds: sw_german_analyzer
    use_language_analyzer: "%env(bool:SHOPWARE_ES_USE_LANGUAGE_ANALYZER)%"
    dynamic_templates:
        - keywords:
            match_mapping_type: string
            mapping:
                type: keyword
                normalizer: sw_lowercase_normalizer
                fields:
                    text:
                        type: text

parameters:
    default_elasticsearch_prefix: "sw"
    default_whitespace: " "
    env(SHOPWARE_ES_ENABLED): ""
    env(SHOPWARE_ES_INDEXING_ENABLED): ""
    env(OPENSEARCH_URL): ""
    env(SHOPWARE_ES_INDEX_PREFIX): "sw"
    env(SHOPWARE_ES_THROW_EXCEPTION): "1"
    env(SHOPWARE_ADMIN_ES_ENABLED): ""
    env(ADMIN_OPENSEARCH_URL): ""
    env(SHOPWARE_ADMIN_ES_INDEX_PREFIX): "sw-admin"
    env(SHOPWARE_ADMIN_ES_REFRESH_INDICES): ""
    env(SHOPWARE_ADMIN_ES_THROW_EXCEPTION): "1"
    env(SHOPWARE_ES_INDEXING_BATCH_SIZE): "100"
    env(SHOPWARE_ADMIN_ES_INDEXING_BATCH_SIZE): "1000"
    env(SHOPWARE_ES_EXCLUDE_SOURCE): "0"
    env(SHOPWARE_ES_NGRAM_MIN_GRAM): "4"
    env(SHOPWARE_ES_NGRAM_MAX_GRAM): "5"
    env(SHOPWARE_ES_USE_LANGUAGE_ANALYZER): "1"
    env(SHOPWARE_ES_DIMENSION_NORMALIZE): "0"
    env(SHOPWARE_ES_NUMBER_OF_SHARDS): ""
    env(SHOPWARE_ES_NUMBER_OF_REPLICAS): ""
    # @deprecated tag:v6.8.0 - change SHOPWARE_ADMIN_ES_NUMBER_OF_SHARDS and SHOPWARE_ADMIN_ES_NUMBER_OF_REPLICAS to empty string to match regular Elasticsearch of Storefront config
    env(SHOPWARE_ADMIN_ES_NUMBER_OF_SHARDS): "3"
    env(SHOPWARE_ADMIN_ES_NUMBER_OF_REPLICAS): "3"
