<commit_before>use crate::adapters::primary::common::settings::{BuildWeight, ImportanceQueryBoosts, StringQuery};
use geojson::Geometry;
use serde_json::json;

use super::coord::Coord;
use super::{filters, settings};

pub fn build_query(
    q: &str,
    filters: filters::Filters,
    lang: &str,
    settings: &settings::QuerySettings,
) -> serde_json::Value {
    let type_query = build_place_type_boost(&settings.type_query.boosts);
    let string_query = build_string_query(q, lang, &settings.string_query);
    let boosts = build_boosts(q, settings, &filters);

    let _filters_poi = build_filters(filters.shape, filters.poi_types, filters.zone_types);
    let filter_matching_condition = build_matching_condition(q);
    let _filter_house_number_condition = build_house_number_condition(q);

    json!({
        "query": {
            "bool": {
                "must": [ type_query, string_query ],
                "should": boosts,
                "filter": [filter_matching_condition]
            }
        },
        "_source": {
          "excludes": [ "boundary" ]
        },
    })
}

fn build_string_query(q: &str, lang: &str, settings: &StringQuery) -> serde_json::Value {
    json!({
        "bool": {
            "boost": settings.global,
            "should": [
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["name", format!("names.{}", lang)],
                        "boost": settings.boosts.name
                    }
                },
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["label", format!("label.{}", lang)],
                        "boost": settings.boosts.label
                    }
                },
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["label.prefix", format!("label.prefix.{}", lang)],
                        "boost": settings.boosts.label_prefix
                    }
                },
                {
                    "match": {
                        "zip_codes": {
                        "query": q,
                        "boost": settings.boosts.zip_codes
                        }
                    }

                },
                {
                    "match": {
                        "house_number": {
                         "query": q,
                         "boost": settings.boosts.house_number
                        }
                    }
                }
            ]
        }
    })
}

fn build_boosts(
    _q: &str,
    settings: &settings::QuerySettings,
    filters: &filters::Filters,
) -> Vec<serde_json::Value> {
    let mut boosts: Vec<Option<serde_json::Value>> = Vec::new();
    // TODO: in production, admins are boosted by their weight only in prefix mode.
    let admin_weight_boost = Some(build_admin_weight_query(
        &settings.importance_query,
        &filters.coord,
    ));
    boosts.push(admin_weight_boost);
    let mut decay = settings.importance_query.proximity.decay.clone();
    if let Some(proximity) = &filters.proximity {
        decay.scale = proximity.scale;
        decay.offset = proximity.offset;
        decay.decay = proximity.decay;
    }
    let proximity_boost = filters
        .coord
        .clone()
        .map(|coord| build_proximity_boost(coord, &decay));
    boosts.push(proximity_boost);
    boosts.into_iter().flatten().collect()
}

fn build_filters(
    shape: Option<(Geometry, Vec<String>)>,
    poi_types: Option<Vec<String>>,
    zone_types: Option<Vec<String>>,
) -> Vec<serde_json::Value> {
    let mut filters: Vec<serde_json::Value> = Vec::new();
    if let Some(geoshape_filter) = shape.map(|(geometry, scope)| build_shape_query(geometry, scope))
    {
        filters.push(geoshape_filter);
    };
    if let Some(poi_types_filter) = poi_types.map(build_poi_types_filter) {
        filters.push(poi_types_filter);
    }
    if let Some(zone_types_filter) = zone_types.map(build_zone_types_filter) {
        filters.push(zone_types_filter);
    }
    filters
}

fn build_weight_depending_on_radius(
    importance_query_settings: &ImportanceQueryBoosts,
    coord: &Option<Coord>,
) -> BuildWeight {
    let settings_weight = importance_query_settings.clone().weights;

    // Weights for minimal radius
    let min_weights = settings_weight.clone().min_radius_prefix;

    // Weights for maximal radius
    let max_weights = settings_weight.clone().max_radius;

    // Compute a linear combination of `min_weights` and `max_weights` depending of
    // the level of zoom.
    let zoom_ratio = match coord {
        None => 1.,
        Some(_) => {
            let (min_radius, max_radius) = settings_weight.radius_range;
            let curve = importance_query_settings.clone().proximity.decay;
            let radius = (curve.offset + curve.scale).min(max_radius).max(min_radius);
            (radius.ln_1p() - min_radius.ln_1p()) / (max_radius.ln_1p() - min_radius.ln_1p())
        }
    };

    BuildWeight {
        admin: (1. - zoom_ratio) * min_weights.admin + zoom_ratio * max_weights.admin,
        factor: (1. - zoom_ratio) * min_weights.factor + zoom_ratio * max_weights.factor,
        missing: (1. - zoom_ratio) * min_weights.missing + zoom_ratio * max_weights.missing,
    }
}

fn build_house_number_condition(q: &str) -> serde_json::Value {
    if q.split_whitespace().count() > 1 {
        // Filter to handle house number.
        // We either want:
        // * to exactly match the document house_number
        // * or that the document has no house_number
        json!({
            "bool": {
                "should": [
                {
                    "bool": {
                        "must_not": {
                            "exists": {
                              "field": "house_number"
                            }
                        },
                    }
                },
                {
                    "match": {
                        "house_number": {
                            "query": q
                        }
                    }
                }
                ]
            }
        })
    } else {
        // If the query contains a single word, we don't exact any house number in the result.
        json!({
            "bool": {
                "must_not": {
                    "exists": {
                        "field": "house_number"
                    }
                },
            }
        })
    }
}

fn build_matching_condition(q: &str) -> serde_json::Value {
    // Filter to handle house number.
    // We either want:
    // * to exactly match the document house_number
    // * or that the document has no house_number<commit_msg>other fix<commit_after>use crate::adapters::primary::common::settings::{BuildWeight, ImportanceQueryBoosts, StringQuery};
use geojson::Geometry;
use serde_json::json;

use super::coord::Coord;
use super::{filters, settings};

pub fn build_query(
    q: &str,
    filters: filters::Filters,
    lang: &str,
    settings: &settings::QuerySettings,
) -> serde_json::Value {
    let type_query = build_place_type_boost(&settings.type_query.boosts);
    let string_query = build_string_query(q, lang, &settings.string_query);
    let boosts = build_boosts(q, settings, &filters);

    let mut filters = build_filters(filters.shape, filters.poi_types, filters.zone_types);
    filters.push(build_matching_condition(q));
    filters.push(build_house_number_condition(q));

    json!({
        "query": {
            "bool": {
                "must": [ type_query, string_query ],
                "should": boosts,
                "filter": filters
            }
        },
        "_source": {
          "excludes": [ "boundary" ]
        },
    })
}

fn build_string_query(q: &str, lang: &str, settings: &StringQuery) -> serde_json::Value {
    json!({
        "bool": {
            "boost": settings.global,
            "should": [
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["name", format!("names.{}", lang)],
                        "boost": settings.boosts.name
                    }
                },
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["label", format!("label.{}", lang)],
                        "boost": settings.boosts.label
                    }
                },
                {
                    "multi_match": {
                        "query": q,
                        "fields": ["label.prefix", format!("label.prefix.{}", lang)],
                        "boost": settings.boosts.label_prefix
                    }
                },
                {
                    "match": {
                        "zip_codes": {
                        "query": q,
                        "boost": settings.boosts.zip_codes
                        }
                    }

                },
                {
                    "match": {
                        "house_number": {
                         "query": q,
                         "boost": settings.boosts.house_number
                        }
                    }
                }
            ]
        }
    })
}

fn build_boosts(
    _q: &str,
    settings: &settings::QuerySettings,
    filters: &filters::Filters,
) -> Vec<serde_json::Value> {
    let mut boosts: Vec<Option<serde_json::Value>> = Vec::new();
    // TODO: in production, admins are boosted by their weight only in prefix mode.
    let admin_weight_boost = Some(build_admin_weight_query(
        &settings.importance_query,
        &filters.coord,
    ));
    boosts.push(admin_weight_boost);
    let mut decay = settings.importance_query.proximity.decay.clone();
    if let Some(proximity) = &filters.proximity {
        decay.scale = proximity.scale;
        decay.offset = proximity.offset;
        decay.decay = proximity.decay;
    }
    let proximity_boost = filters
        .coord
        .clone()
        .map(|coord| build_proximity_boost(coord, &decay));
    boosts.push(proximity_boost);
    boosts.into_iter().flatten().collect()
}

fn build_filters(
    shape: Option<(Geometry, Vec<String>)>,
    poi_types: Option<Vec<String>>,
    zone_types: Option<Vec<String>>,
) -> Vec<serde_json::Value> {
    let mut filters: Vec<serde_json::Value> = Vec::new();
    if let Some(geoshape_filter) = shape.map(|(geometry, scope)| build_shape_query(geometry, scope))
    {
        filters.push(geoshape_filter);
    };
    if let Some(poi_types_filter) = poi_types.map(build_poi_types_filter) {
        filters.push(poi_types_filter);
    }
    if let Some(zone_types_filter) = zone_types.map(build_zone_types_filter) {
        filters.push(zone_types_filter);
    }
    filters
}

fn build_weight_depending_on_radius(
    importance_query_settings: &ImportanceQueryBoosts,
    coord: &Option<Coord>,
) -> BuildWeight {
    let settings_weight = importance_query_settings.clone().weights;

    // Weights for minimal radius
    let min_weights = settings_weight.clone().min_radius_prefix;

    // Weights for maximal radius
    let max_weights = settings_weight.clone().max_radius;

    // Compute a linear combination of `min_weights` and `max_weights` depending of
    // the level of zoom.
    let zoom_ratio = match coord {
        None => 1.,
        Some(_) => {
            let (min_radius, max_radius) = settings_weight.radius_range;
            let curve = importance_query_settings.clone().proximity.decay;
            let radius = (curve.offset + curve.scale).min(max_radius).max(min_radius);
            (radius.ln_1p() - min_radius.ln_1p()) / (max_radius.ln_1p() - min_radius.ln_1p())
        }
    };

    BuildWeight {
        admin: (1. - zoom_ratio) * min_weights.admin + zoom_ratio * max_weights.admin,
        factor: (1. - zoom_ratio) * min_weights.factor + zoom_ratio * max_weights.factor,
        missing: (1. - zoom_ratio) * min_weights.missing + zoom_ratio * max_weights.missing,
    }
}

fn build_house_number_condition(q: &str) -> serde_json::Value {
    if q.split_whitespace().count() > 1 {
        // Filter to handle house number.
        // We either want:
        // * to exactly match the document house_number
        // * or that the document has no house_number
        json!({
            "bool": {
                "should": [
                {
                    "bool": {
                        "must_not": {
                            "exists": {
                              "field": "house_number"
                            }
                        },
                    }
                },
                {
                    "match": {
                        "house_number": {
                            "query": q
                        }
                    }
                }
                ]
            }
        })
    } else {
        // If the query contains a single word, we don't exact any house number in the result.
        json!({
            "bool": {
              "must_not": {
                "exists": {
                  "field": "house_number"
                }
              }
            }
        })
    }
}

fn build_matching_condition(q: &str) -> serde_json::Value {
    // Filter to handle house number.
    // We either want:
    // * to exactly match the document house_number
    // * or that the document has no house_number
{'fraction_non_alphanumeric': 0.11883159991148484, 'fraction_numerical': 0.0013277273733126797, 'mean_word_length': 1.5731637881951035, 'pattern_counts': {'":': 86, '<': 27, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5619728', 'n_tokens_mistral': 3886, 'n_tokens_neox': 3749, 'n_words': 741}