Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


SSL weather.rs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

use rusqlite::named_params;
use serde::Deserialize;
use sql_support::ConnExt;

use std::{cmp::Ordering, collections::HashSet};

use crate::{
    config::SuggestProviderConfig,
    db::{
        KeywordInsertStatement, KeywordMetricsInsertStatement, SuggestDao,
        SuggestionInsertStatement, DEFAULT_SUGGESTION_SCORE,
    },
    geoname::{GeonameMatch, GeonameType},
    metrics::MetricsContext,
    provider::SuggestionProvider,
    rs::{Client, Record, SuggestRecordId},
    store::SuggestStoreInner,
    suggestion::Suggestion,
    util::filter_map_chunks,
    Result, SuggestionQuery,
};

#[derive(Clone, Debug, Deserialize)]
pub(crate) struct DownloadedWeatherAttachment {
    /// Weather keywords.
    pub keywords: Vec<String>,
    /// Threshold for weather keyword prefix matching when a weather keyword is
    /// the first term in a query. `None` means prefix matching is disabled and
    /// weather keywords must be typed in full when they are first in the query.
    /// This threshold does not apply to city and region names. If there are
    /// multiple weather records, we use the `min_keyword_length` in the most
    /// recently ingested record.
    pub min_keyword_length: Option<i32>,
    /// Score for weather suggestions. If there are multiple weather records, we
    /// use the `score` from the most recently ingested record.
    pub score: Option<f64>,
    /// The max length of all keywords in the attachment. Used for keyword
    /// metrics. We pre-compute this to avoid doing duplicate work on all user's
    /// machines.
    pub max_keyword_length: u32,
    /// The max word count of all keywords in the attachment. Used for keyword
    /// metrics. We pre-compute this to avoid doing duplicate work on all user's
    /// machines.
    pub max_keyword_word_count: u32,
}

/// This data is used to service every query handled by the weather provider, so
/// we cache it from the DB.
#[derive(Debug, Default)]
pub struct WeatherCache {
    /// Cached value of the same name from `SuggestProviderConfig::Weather`.
    min_keyword_length: i32,
    /// Cached value of the same name from `SuggestProviderConfig::Weather`.
    score: f64,
    /// Max length of all weather keywords.
    max_keyword_length: usize,
    /// Max word count across all weather keywords.
    max_keyword_word_count: usize,
}

impl SuggestDao<'_> {
    /// Fetches weather suggestions.
    pub fn fetch_weather_suggestions(&self, query: &SuggestionQuery) -> Result<Vec<Suggestion>> {
        // We'll just stipulate we won't support tiny queries in order to avoid
        // a bunch of work when the user starts typing a query.
        if query.keyword.len() < 3 {
            return Ok(vec![]);
        }

        // The first step in parsing the query is lowercasing and splitting it
        // into words. We want to avoid that work for strings that are so long
        // they can't possibly match. The longest possible weather query is two
        // geonames + one weather keyword + at least two spaces between those
        // three components, say, 10 extra characters total for spaces and
        // punctuation. There's no point in an analogous min length check since
        // weather suggestions can be matched on city alone and many city names
        // are only a few characters long ("nyc").
        let g_cache = self.geoname_cache();
        let w_cache = self.weather_cache();
        let max_query_len = 2 * g_cache.max_name_length + w_cache.max_keyword_length + 10;
        if max_query_len < query.keyword.len() {
            return Ok(vec![]);
        }

        let max_chunk_size =
            std::cmp::max(g_cache.max_name_word_count, w_cache.max_keyword_word_count);

        // Lowercase, strip punctuation, and split the query into words.
        let kw_lower = query.keyword.to_lowercase();
        let words: Vec<_> = kw_lower
            .split_whitespace()
            .flat_map(|w| {
                w.split(|c| !char::is_alphabetic(c))
                    .filter(|s| !s.is_empty())
            })
            .collect();

        let mut matches =
            // Step 2: Parse the query words into a list of token paths.
            filter_map_chunks::<Token>(&words, max_chunk_size, |chunk, chunk_i, path| {
                // Match the chunk to token types that haven't already been matched
                // in this path. `all_tokens` will remain `None` until a token is
                // matched.
                let mut all_tokens: Option<Vec<Token>> = None;
                for tt in [
                    TokenType::City,
                    TokenType::Region,
                    TokenType::WeatherKeyword,
                ] {
                    if !path.iter().any(|t| t.token_type() == tt) {
                        let mut tokens = self.match_weather_tokens(tt, path, chunk, chunk_i == 0)?;
                        if !tokens.is_empty() {
                            let mut ts = all_tokens.take().unwrap_or_default();
                            ts.append(&mut tokens);
                            all_tokens.replace(ts);
                        }
                    }
                }
                // If no tokens were matched, `all_tokens` will be `None`.
                Ok(all_tokens)
            })?
            .into_iter()
            // Step 3: Map each token path to a city-region-keyword tuple (each
            // optional). Paths are vecs, so they're ordered, so we may end up
            // with duplicate tuples after this step. e.g., the paths
            // `[<Waterloo IA>, <IA>]` and `[<IA>, <Waterloo IA>]` map to the
            // same `(<Waterloo IA>, <IA>, None)` tuple.
            .map(|path| {
                path.into_iter()
                    .fold((None, None, None), |mut match_tuple, token| {
                        match token {
                            Token::City(c) => {
                                match_tuple.0 = Some(c);
                            }
                            Token::Region(r) => {
                                match_tuple.1 = Some(r);
                            }
                            Token::WeatherKeyword(kw) => {
                                match_tuple.2 = Some(kw);
                            }
                        }
                        match_tuple
                    })
            })
            // Step 4: Discard tuples that don't have the right combination of
            // tokens or that are otherwise invalid. Along with step 2, this is
            // the core of the matching logic. In general, allow a tuple if it
            // has (a) a city name typed in full or (b) a weather keyword at
            // least as long as the config's min keyword length, since that
            // indicates a weather intent.
            .filter(|(city_match, region_match, kw_match)| {
                match (city_match, region_match, kw_match) {
                    (None, None, Some(_)) => true,
                    (None, _, None) | (None, Some(_), Some(_)) => false,
                    (Some(city), region, kw) => {
                        (city.match_type.is_name() && !city.prefix)
                            // Allow city abbreviations without a weather
                            // keyword but only if the region was typed in full.
                            || (city.match_type.is_abbreviation()
                                && !city.prefix
                                && region.as_ref().map(|r| !r.prefix).unwrap_or(false))
                            || kw.as_ref().map(|k| k.is_min_keyword_length).unwrap_or(false)
                    }
                }
            })
            // Step 5: Map each tuple to a city-region tuple: Convert geoname
            // matches to their `Geoname` values and discard keywords.
            // Discarding keywords is important because we'll collect the tuples
            // in a set in the next step in order to dedupe city-regions.
            .map(|(city, region, _)| {
                (city.map(|c| c.geoname), region.map(|r| r.geoname))
            })
            // Step 6: Dedupe the city-regions by collecting them in a set.
            .collect::<HashSet<_>>()
            .into_iter()
            .collect::<Vec<_>>();

        // Sort the matches so cities with larger populations are first.
        matches.sort_by(
            |(city1, region1), (city2, region2)| match (&city1, &city2) {
                (Some(_), None) => Ordering::Less,
                (None, Some(_)) => Ordering::Greater,
                (Some(c1), Some(c2)) => c2.population.cmp(&c1.population),
                (None, None) => match (®ion1, ®ion2) {
                    (Some(_), None) => Ordering::Less,
                    (None, Some(_)) => Ordering::Greater,
                    (Some(r1), Some(r2)) => r2.population.cmp(&r1.population),
                    (None, None) => Ordering::Equal,
                },
            },
        );

        // Finally, map matches to suggestions.
        Ok(matches
            .iter()
            .map(|(city, _)| Suggestion::Weather {
                city: city.as_ref().map(|c| c.name.clone()),
                region: city.as_ref().map(|c| c.admin1_code.clone()),
                country: city.as_ref().map(|c| c.country_code.clone()),
                latitude: city.as_ref().map(|c| c.latitude),
                longitude: city.as_ref().map(|c| c.longitude),
                score: w_cache.score,
            })
            .collect())
    }

    fn match_weather_tokens(
        &self,
        token_type: TokenType,
        path: &[Token],
        candidate: &str,
        is_first_chunk: bool,
    ) -> Result<Vec<Token>> {
        match token_type {
            TokenType::City => {
                // Fetch matching cities, and filter them to regions we've
                // already matched in this path.
                let regions: Vec<_> = path
                    .iter()
                    .filter_map(|t| t.region().map(|m| &m.geoname))
                    .collect();
                Ok(self
                    .fetch_geonames(
                        candidate,
                        !is_first_chunk,
                        Some(GeonameType::City),
                        if regions.is_empty() {
                            None
                        } else {
                            Some(regions)
                        },
                    )?
                    .into_iter()
                    .map(Token::City)
                    .collect())
            }
            TokenType::Region => {
                // Fetch matching regions, and filter them to cities we've
                // already matched in this patch.
                let cities: Vec<_> = path
                    .iter()
                    .filter_map(|t| t.city().map(|m| &m.geoname))
                    .collect();
                Ok(self
                    .fetch_geonames(
                        candidate,
                        !is_first_chunk,
                        Some(GeonameType::Region),
                        if cities.is_empty() {
                            None
                        } else {
                            Some(cities)
                        },
                    )?
                    .into_iter()
                    .map(Token::Region)
                    .collect())
            }
            TokenType::WeatherKeyword => {
                // Fetch matching keywords. `min_keyword_length == 0` in the
                // config means that the config doesn't allow prefix matching.
                // `min_keyword_length > 0` means that the keyword must be at
                // least that long when there's not already a city name present
                // in the query.
                let len = self.weather_cache().min_keyword_length;
                if is_first_chunk && (candidate.len() as i32) < len {
                    // The candidate is the first term in the query and it's too
                    // short.
                    Ok(vec![])
                } else {
                    // Do arbitrary prefix matching if the candidate isn't the
                    // first term in the query or if the config allows prefix
                    // matching.
                    Ok(self
                        .match_weather_keywords(candidate, !is_first_chunk || len > 0)?
                        .into_iter()
                        .map(|keyword| {
                            Token::WeatherKeyword(WeatherKeywordMatch {
                                keyword,
                                is_min_keyword_length: (len as usize) <= candidate.len(),
                            })
                        })
                        .collect())
                }
            }
        }
    }

    fn match_weather_keywords(&self, candidate: &str, prefix: bool) -> Result<Vec<String>> {
        self.conn.query_rows_and_then_cached(
            r#"
            SELECT
                k.keyword,
                s.score,
                k.keyword != :keyword AS matched_prefix
            FROM
                suggestions s
            JOIN
                keywords k
                ON k.suggestion_id = s.id
            WHERE
                s.provider = :provider
                AND (
                    CASE :prefix WHEN FALSE THEN k.keyword = :keyword
                    ELSE (k.keyword BETWEEN :keyword AND :keyword || X'FFFF') END
                )
             "#,
            named_params! {
                ":prefix": prefix,
                ":keyword": candidate,
                ":provider": SuggestionProvider::Weather
            },
            |row| -> Result<String> { Ok(row.get("keyword")?) },
        )
    }

    /// Inserts weather suggestions data into the database.
    fn insert_weather_data(
        &mut self,
        record_id: &SuggestRecordId,
        attachments: &[DownloadedWeatherAttachment],
    ) -> Result<()> {
        self.scope.err_if_interrupted()?;
        let mut suggestion_insert = SuggestionInsertStatement::new(self.conn)?;
        let mut keyword_insert = KeywordInsertStatement::new(self.conn)?;
        let mut keyword_metrics_insert = KeywordMetricsInsertStatement::new(self.conn)?;
        let mut max_len = 0;
        let mut max_word_count = 0;
        for attach in attachments {
            let suggestion_id = suggestion_insert.execute(
                record_id,
                "",
                "",
                attach.score.unwrap_or(DEFAULT_SUGGESTION_SCORE),
                SuggestionProvider::Weather,
            )?;
            for (i, keyword) in attach.keywords.iter().enumerate() {
                keyword_insert.execute(suggestion_id, keyword, None, i)?;
            }
            self.put_provider_config(SuggestionProvider::Weather, &attach.into())?;
            max_len = std::cmp::max(max_len, attach.max_keyword_length as usize);
            max_word_count = std::cmp::max(max_word_count, attach.max_keyword_word_count as usize);
        }

        // Update keyword metrics.
        keyword_metrics_insert.execute(
            record_id,
            SuggestionProvider::Weather,
            max_len,
            max_word_count,
        )?;

        // We just made some insertions that might invalidate the data in the
        // cache. Clear it so it's repopulated the next time it's accessed.
        self.weather_cache.take();

        Ok(())
    }

    fn weather_cache(&self) -> &WeatherCache {
        self.weather_cache.get_or_init(|| {
            let mut cache = WeatherCache::default();

            // keyword metrics
            if let Ok((len, word_count)) = self.conn.query_row_and_then(
                r#"
                SELECT
                    max(max_length) AS len, max(max_word_count) AS word_count
                FROM
                    keywords_metrics
                WHERE
                    provider = :provider
                "#,
                named_params! {
                    ":provider": SuggestionProvider::Weather
                },
                |row| -> Result<(usize, usize)> { Ok((row.get("len")?, row.get("word_count")?)) },
            ) {
                cache.max_keyword_length = len;
                cache.max_keyword_word_count = word_count;
            }

            // provider config
            if let Ok(Some(SuggestProviderConfig::Weather {
                score,
                min_keyword_length,
            })) = self.get_provider_config(SuggestionProvider::Weather)
            {
                cache.min_keyword_length = min_keyword_length;
                cache.score = score;
            }

            cache
        })
    }
}

impl<S> SuggestStoreInner<S>
where
    S: Client,
{
    /// Inserts a weather record into the database.
    pub fn process_weather_record(
        &self,
        dao: &mut SuggestDao,
        record: &Record,
        context: &mut MetricsContext,
    ) -> Result<()> {
        self.download_attachment(dao, record, context, |dao, record_id, data| {
            dao.insert_weather_data(record_id, data)
        })
    }
}

impl From<&DownloadedWeatherAttachment> for SuggestProviderConfig {
    fn from(a: &DownloadedWeatherAttachment) -> Self {
        Self::Weather {
            score: a.score.unwrap_or(DEFAULT_SUGGESTION_SCORE),
            min_keyword_length: a.min_keyword_length.unwrap_or(0),
        }
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum TokenType {
    City,
    Region,
    WeatherKeyword,
}

#[derive(Clone, Debug)]
enum Token {
    City(GeonameMatch),
    Region(GeonameMatch),
    WeatherKeyword(WeatherKeywordMatch),
}

impl Token {
    fn city(&self) -> Option<&GeonameMatch> {
        match self {
            Self::City(g) => Some(g),
            _ => None,
        }
    }

    fn region(&self) -> Option<&GeonameMatch> {
        match self {
            Self::Region(g) => Some(g),
            _ => None,
        }
    }

    fn token_type(&self) -> TokenType {
        match self {
            Self::City(_) => TokenType::City,
            Self::Region(_) => TokenType::Region,
            Self::WeatherKeyword(_) => TokenType::WeatherKeyword,
        }
    }
}

#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
struct WeatherKeywordMatch {
    keyword: String,
    is_min_keyword_length: bool,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        geoname, geoname::Geoname, store::tests::TestStore, testing::*, SuggestIngestionConstraints,
    };

    impl From<Geoname> for Suggestion {
        fn from(g: Geoname) -> Self {
            Suggestion::Weather {
                city: Some(g.name),
                region: Some(g.admin1_code),
                country: Some(g.country_code),
                latitude: Some(g.latitude),
                longitude: Some(g.longitude),
                score: 0.24,
            }
        }
    }

    #[test]
    fn weather_provider_config() -> anyhow::Result<()> {
        before_each();
        let store = TestStore::new(MockRemoteSettingsClient::default().with_record(
            "weather",
            "weather-1",
            json!({
                "min_keyword_length": 3,
                "keywords": ["ab", "xyz", "weather"],
                "max_keyword_length": "weather".len(),
                "max_keyword_word_count": 1,
                "score": 0.24
            }),
        ));
        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });
        assert_eq!(
            store.fetch_provider_config(SuggestionProvider::Weather),
            Some(SuggestProviderConfig::Weather {
                score: 0.24,
                min_keyword_length: 3,
            })
        );
        Ok(())
    }

    #[test]
    fn weather_keywords_prefixes_allowed() -> anyhow::Result<()> {
        before_each();

        let store = TestStore::new(MockRemoteSettingsClient::default().with_record(
            "weather",
            "weather-1",
            json!({
                // min_keyword_length > 0 means prefixes are allowed.
                "min_keyword_length": 5,
                "keywords": ["ab", "xyz", "cdefg", "weather"],
                "max_keyword_length": "weather".len(),
                "max_keyword_word_count": 1,
                "score": 0.24
            }),
        ));

        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });

        let no_matches = [
            // doesn't match any keyword
            "ab123",
            "123ab",
            "xyz12",
            "12xyz",
            "xcdefg",
            "cdefgx",
            "x cdefg",
            "cdefg x",
            "weatherx",
            "xweather",
            "xweat",
            "weatx",
            "x   weather",
            "   weather x",
            "weather foo",
            "foo weather",
            // too short
            "ab",
            "xyz",
            "cdef",
            "we",
            "wea",
            "weat",
        ];
        for q in no_matches {
            assert_eq!(store.fetch_suggestions(SuggestionQuery::weather(q)), vec![]);
        }

        let matches = [
            "cdefg",
            "weath",
            "weathe",
            "weather",
            "WeAtHeR",
            "  weather  ",
        ];
        for q in matches {
            assert_eq!(
                store.fetch_suggestions(SuggestionQuery::weather(q)),
                vec![Suggestion::Weather {
                    score: 0.24,
                    city: None,
                    region: None,
                    country: None,
                    latitude: None,
                    longitude: None,
                },]
            );
        }

        Ok(())
    }

    #[test]
    fn weather_keywords_prefixes_not_allowed() -> anyhow::Result<()> {
        before_each();

        let store = TestStore::new(MockRemoteSettingsClient::default().with_record(
            "weather",
            "weather-1",
            json!({
                // min_keyword_length == 0 means prefixes are not allowed.
                "min_keyword_length": 0,
                "keywords": ["weather"],
                "max_keyword_length": "weather".len(),
                "max_keyword_word_count": 1,
                "score": 0.24
            }),
        ));

        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });

        let no_matches = ["wea", "weat", "weath", "weathe"];
        for q in no_matches {
            assert_eq!(store.fetch_suggestions(SuggestionQuery::weather(q)), vec![]);
        }

        let matches = ["weather", "WeAtHeR", "  weather  "];
        for q in matches {
            assert_eq!(
                store.fetch_suggestions(SuggestionQuery::weather(q)),
                vec![Suggestion::Weather {
                    score: 0.24,
                    city: None,
                    region: None,
                    country: None,
                    latitude: None,
                    longitude: None,
                },]
            );
        }

        Ok(())
    }

    #[test]
    fn cities_and_regions() -> anyhow::Result<()> {
        before_each();

        let mut store = geoname::tests::new_test_store();
        store.client_mut().add_record(
            "weather",
            "weather-1",
            json!({
                // Include a keyword that's a prefix of another keyword --
                // "weather" and "weather near me" -- so that when a test
                // matches both we can verify only one suggestion is returned,
                // not two.
                "keywords": ["ab", "xyz", "weather", "weather near me"],
                "min_keyword_length": 5,
                "max_keyword_length": "weather".len(),
                "max_keyword_word_count": 1,
                "score": 0.24
            }),
        );

        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });

        let tests: &[(&str, Vec<Suggestion>)] = &[
            (
                "act",
                vec![],
            ),
            (
                "act w",
                vec![],
            ),
            (
                "act we",
                vec![],
            ),
            (
                "act wea",
                vec![],
            ),
            (
                "act weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "act weath",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act weathe",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act weather",
                vec![geoname::tests::waco().into()],
            ),
            (
                "weather a",
                // The made-up long-name city starts with A.
                vec![geoname::tests::long_name_city().into()],
            ),
            (
                "weather ac",
                vec![],
            ),
            (
                "weather act",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act t",
                vec![],
            ),
            (
                "act tx",
                vec![],
            ),
            (
                "act tx w",
                vec![],
            ),
            (
                "act tx we",
                vec![],
            ),
            (
                "act tx wea",
                vec![],
            ),
            (
                "act tx weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "act tx weath",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act tx weathe",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act tx weather",
                vec![geoname::tests::waco().into()],
            ),
            (
                "tx a",
                vec![],
            ),
            (
                "tx ac",
                vec![],
            ),
            (
                "tx act",
                vec![],
            ),
            (
                "tx act w",
                vec![],
            ),
            (
                "tx act we",
                vec![],
            ),
            (
                "tx act wea",
                vec![],
            ),
            (
                "tx act weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "tx act weath",
                vec![geoname::tests::waco().into()],
            ),
            (
                "tx act weathe",
                vec![geoname::tests::waco().into()],
            ),
            (
                "tx act weather",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act te",
                vec![],
            ),
            (
                "act tex",
                vec![],
            ),
            (
                "act texa",
                vec![],
            ),
            (
                "act texas",
                vec![],
            ),
            (
                "act texas w",
                vec![],
            ),
            (
                "act texas we",
                vec![],
            ),
            (
                "act texas wea",
                vec![],
            ),
            (
                "act texas weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "act texas weath",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act texas weathe",
                vec![geoname::tests::waco().into()],
            ),
            (
                "act texas weather",
                vec![geoname::tests::waco().into()],
            ),
            (
                "texas a",
                vec![],
            ),
            (
                "texas ac",
                vec![],
            ),
            (
                "texas act",
                vec![],
            ),
            (
                "texas act w",
                vec![],
            ),
            (
                "texas act we",
                vec![],
            ),
            (
                "texas act wea",
                vec![],
            ),
            (
                "texas act weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "texas act weath",
                vec![geoname::tests::waco().into()],
            ),
            (
                "texas act weathe",
                vec![geoname::tests::waco().into()],
            ),
            (
                "texas act weather",
                vec![geoname::tests::waco().into()],
            ),
            (
                "ia w",
                vec![],
            ),
            (
                "ia wa",
                vec![],
            ),
            (
                "ia wat",
                vec![],
            ),
            (
                "ia wate",
                vec![],
            ),
            (
                "ia water",
                vec![],
            ),
            (
                "ia waterl",
                vec![],
            ),
            (
                "ia waterlo",
                vec![],
            ),
            (
                "waterloo",
                vec![
                    // Waterloo, IA should be first since its population is
                    // larger than Waterloo, AL.
                    geoname::tests::waterloo_ia().into(),
                    geoname::tests::waterloo_al().into(),
                ],
            ),
            (
                "waterloo i",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo ia",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo io",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo iow",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo iowa",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "ia waterloo",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo al",
                vec![geoname::tests::waterloo_al().into()],
            ),
            (
                "al waterloo",
                vec![geoname::tests::waterloo_al().into()],
            ),
            ("waterloo ia al", vec![]),
            ("waterloo ny", vec![]),
            (
                "ia",
                vec![],
            ),
            (
                "iowa",
                vec![],
            ),
            (
                "al",
                vec![],
            ),
            (
                "alabama",
                vec![],
            ),
            (
                "new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny ny",
                vec![geoname::tests::nyc().into()],
            ),
            ("ny ny ny", vec![]),
            (
                "ny n",
                vec![],
            ),
            (
                "ny ne",
                vec![],
            ),
            (
                "ny new",
                vec![],
            ),
            (
                "ny new ",
                vec![],
            ),
            (
                "ny new y",
                vec![],
            ),
            (
                "ny new yo",
                vec![],
            ),
            (
                "ny new yor",
                vec![],
            ),
            (
                "ny new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny w",
                vec![],
            ),
            (
                "ny we",
                vec![],
            ),
            (
                "ny wea",
                vec![],
            ),
            (
                "ny weat",
                vec![],
            ),
            (
                // `min_keyword_length` = 5, so there should be a match.
                "ny weath",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny weathe",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather ny ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny weather ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "rochester ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "ny rochester",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester weather ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester ny weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather ny rochester",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "ny weather rochester",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "ny rochester weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new weather york",
                vec![],
            ),
            (
                "new york weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather new york new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york weather new york",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york new york weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather water",
                vec![
                    geoname::tests::waterloo_ia().into(),
                    geoname::tests::waterloo_al().into(),
                ],
            ),
            (
                "waterloo w",
                vec![
                    geoname::tests::waterloo_ia().into(),
                    geoname::tests::waterloo_al().into(),
                ],
            ),
            ("weather w w", vec![]),
            ("weather w water", vec![]),
            ("weather w waterloo", vec![]),
            ("weather water w", vec![]),
            ("weather waterloo water", vec![]),
            ("weather water water", vec![]),
            ("weather water waterloo", vec![]),
            ("waterloo foo", vec![]),
            ("waterloo weather foo", vec![]),
            ("foo waterloo", vec![]),
            ("foo waterloo weather", vec![]),
            (
                "ny",
                vec![],
            ),
            (
                "nyc",
                vec![],
            ),
            (
                "roc",
                vec![],
            ),
            (
                "nyc ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "ny nyc",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "roc ny",
                vec![],
            ),
            (
                "ny roc",
                vec![],
            ),
            (
                "nyc weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather nyc",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "roc weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather roc",
                vec![geoname::tests::rochester().into()],
            ),
            (
                geoname::tests::LONG_NAME,
                vec![geoname::tests::long_name_city().into()],
            ),
            (
                "   WaTeRlOo   ",
                vec![
                    geoname::tests::waterloo_ia().into(),
                    geoname::tests::waterloo_al().into(),
                ],
            ),
            (
                "     waterloo ia",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo     ia",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "waterloo ia     ",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "  waterloo   ia    ",
                vec![geoname::tests::waterloo_ia().into()],
            ),
            (
                "     new york weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new     york weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york     weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york weather     ",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "rochester,",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester ,",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester , ",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester,ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester, ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester ,ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester , ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester,",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester, ",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester , ",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester,ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester, ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester ,ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "weather rochester , ny",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester,weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester, weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester ,weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester , weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester,ny weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester, ny weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester ,ny weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "rochester , ny weather",
                vec![geoname::tests::rochester().into()],
            ),
            (
                "new york,",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york ,",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york , ",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york,ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york, ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york ,ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york , ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather new york,ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather new york, ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather new york ,ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "weather new york , ny",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york,weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york, weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york ,weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york , weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york,ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york, ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york ,ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                "new york , ny weather",
                vec![geoname::tests::nyc().into()],
            ),
            (
                &format!("{} weather", geoname::tests::LONG_NAME),
                vec![geoname::tests::long_name_city().into()],
            ),
            (
                &format!("weather {}", geoname::tests::LONG_NAME),
                vec![geoname::tests::long_name_city().into()],
            ),
            (
                &format!("{} and some other words that don't match anything but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and some other words that don't match anything {} but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and some other words that don't match anything but that is neither here nor there {}", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("weather {} and some other words that don't match anything but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("{} weather and some other words that don't match anything but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("{} and some other words that don't match anything weather but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("{} and some other words that don't match anything but that is neither here nor there weather", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("weather and some other words that don't match anything {} but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("weather and some other words that don't match anything but that is neither here nor there {}", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and some other words that don't match anything weather {} but that is neither here nor there", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and some other words that don't match anything but that is neither here nor there weather {}", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("{} weather and then this also doesn't match anything down here", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("{} and then this also doesn't match anything down here weather", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and then this also doesn't match anything down here {} weather", geoname::tests::LONG_NAME),
                vec![]
            ),
            (
                &format!("and then this also doesn't match anything down here weather {}", geoname::tests::LONG_NAME),
                vec![]
            ),
        ];

        for (query, expected_suggestions) in tests {
            assert_eq!(
                &store.fetch_suggestions(SuggestionQuery::weather(query)),
                expected_suggestions,
                "Query: {:?}",
                query
            );
        }

        Ok(())
    }

    #[test]
    fn keywords_metrics() -> anyhow::Result<()> {
        before_each();

        // Add a couple of records with different metrics. We're just testing
        // metrics so the other values don't matter.
        let mut store = TestStore::new(
            MockRemoteSettingsClient::default()
                .with_record(
                    "weather",
                    "weather-0",
                    json!({
                        "max_keyword_length": 10,
                        "max_keyword_word_count": 5,
                        "min_keyword_length": 3,
                        "score": 0.24,
                        "keywords": []
                    }),
                )
                .with_record(
                    "weather",
                    "weather-1",
                    json!({
                        "max_keyword_length": 20,
                        "max_keyword_word_count": 2,
                        "min_keyword_length": 3,
                        "score": 0.24,
                        "keywords": []
                    }),
                ),
        );

        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });

        store.read(|dao| {
            let cache = dao.weather_cache();
            assert_eq!(cache.max_keyword_length, 20);
            assert_eq!(cache.max_keyword_word_count, 5);
            Ok(())
        })?;

        // Delete the first record. The metrics should change.
        store
            .client_mut()
            .delete_record("quicksuggest", "weather-0");
        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });
        store.read(|dao| {
            let cache = dao.weather_cache();
            assert_eq!(cache.max_keyword_length, 20);
            assert_eq!(cache.max_keyword_word_count, 2);
            Ok(())
        })?;

        // Add a new record. The metrics should change again.
        store.client_mut().add_record(
            "weather",
            "weather-3",
            json!({
                "max_keyword_length": 15,
                "max_keyword_word_count": 3,
                "min_keyword_length": 3,
                "score": 0.24,
                "keywords": []
            }),
        );
        store.ingest(SuggestIngestionConstraints {
            providers: Some(vec![SuggestionProvider::Weather]),
            ..SuggestIngestionConstraints::all_providers()
        });
        store.read(|dao| {
            let cache = dao.weather_cache();
            assert_eq!(cache.max_keyword_length, 20);
            assert_eq!(cache.max_keyword_word_count, 3);
            Ok(())
        })?;

        Ok(())
    }
}

[ Verzeichnis aufwärts0.55unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge