relevancy/
interest.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::Error;
6use rusqlite::{types::ToSqlOutput, ToSql};
7
8/// Different kinds of interest vectors that we store for the user
9///
10/// The aspiration is to add more kinds and store more kinds of interest vectors, e.g.:
11///   - Full history existence -- does a URL appear anywhere in the user's history?
12///   - Open tabs -- does a URL appear in the user's open tabs?
13///   - Bookmarks -- does a URL appear in the user's bookmarks
14#[derive(Debug, Clone, Copy)]
15#[repr(u32)]
16pub enum InterestVectorKind {
17    // Calculated by checking the URLs in the user's frecency list against the topsite domains,
18    // categorized using Tranco.
19    Frecency = 1,
20}
21
22impl InterestVectorKind {
23    pub fn as_raw(&self) -> u32 {
24        *self as u32
25    }
26}
27
28impl ToSql for InterestVectorKind {
29    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
30        Ok(ToSqlOutput::from(self.as_raw()))
31    }
32}
33
34/// List of possible interests for a domain.  Domains can have be associated with one or multiple
35/// interests.  `Inconclusive` is used for domains in the user's top sites that we can't classify
36/// because there's no corresponding entry in the interest database.
37#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, uniffi::Enum)]
38#[repr(u32)]
39pub enum Interest {
40    // Note: if you change these codes, make sure to update the `TryFrom<u32>` implementation and
41    // the `test_interest_code_conversion` test.
42    Inconclusive = 0,
43    Animals = 1,
44    Arts = 2,
45    Autos = 3,
46    Business = 4,
47    Career = 5,
48    Education = 6,
49    Fashion = 7,
50    Finance = 8,
51    Food = 9,
52    Government = 10,
53    //Disable this per policy consultation
54    // Health = 11,
55    Hobbies = 12,
56    Home = 13,
57    News = 14,
58    RealEstate = 15,
59    Society = 16,
60    Sports = 17,
61    Tech = 18,
62    Travel = 19,
63}
64
65impl From<Interest> for u32 {
66    fn from(interest: Interest) -> Self {
67        interest as u32
68    }
69}
70
71impl From<Interest> for usize {
72    fn from(interest: Interest) -> Self {
73        interest as usize
74    }
75}
76
77impl TryFrom<u32> for Interest {
78    // On error, return the invalid code back
79    type Error = Error;
80
81    fn try_from(code: u32) -> Result<Self, Self::Error> {
82        match code {
83            0 => Ok(Self::Inconclusive),
84            1 => Ok(Self::Animals),
85            2 => Ok(Self::Arts),
86            3 => Ok(Self::Autos),
87            4 => Ok(Self::Business),
88            5 => Ok(Self::Career),
89            6 => Ok(Self::Education),
90            7 => Ok(Self::Fashion),
91            8 => Ok(Self::Finance),
92            9 => Ok(Self::Food),
93            10 => Ok(Self::Government),
94            //Disable this per policy consultation
95            // 11 => Ok(Self::Health),
96            12 => Ok(Self::Hobbies),
97            13 => Ok(Self::Home),
98            14 => Ok(Self::News),
99            15 => Ok(Self::RealEstate),
100            16 => Ok(Self::Society),
101            17 => Ok(Self::Sports),
102            18 => Ok(Self::Tech),
103            19 => Ok(Self::Travel),
104            n => Err(Error::InvalidInterestCode(n)),
105        }
106    }
107}
108
109impl Interest {
110    const COUNT: usize = 19;
111
112    pub fn all() -> [Interest; Self::COUNT] {
113        [
114            Self::Inconclusive,
115            Self::Animals,
116            Self::Arts,
117            Self::Autos,
118            Self::Business,
119            Self::Career,
120            Self::Education,
121            Self::Fashion,
122            Self::Finance,
123            Self::Food,
124            Self::Government,
125            // Self::Health,
126            Self::Hobbies,
127            Self::Home,
128            Self::News,
129            Self::RealEstate,
130            Self::Society,
131            Self::Sports,
132            Self::Tech,
133            Self::Travel,
134        ]
135    }
136
137    pub fn as_raw(&self) -> u32 {
138        *self as u32
139    }
140}
141
142impl ToSql for Interest {
143    fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
144        Ok(ToSqlOutput::from(self.as_raw()))
145    }
146}
147
148/// Vector storing a count value for each interest
149///
150/// Here "vector" refers to the mathematical object, not a Rust `Vec`.  It always has a fixed
151/// number of elements.
152#[derive(Debug, Default, PartialEq, Eq, uniffi::Record)]
153pub struct InterestVector {
154    pub inconclusive: u32,
155    pub animals: u32,
156    pub arts: u32,
157    pub autos: u32,
158    pub business: u32,
159    pub career: u32,
160    pub education: u32,
161    pub fashion: u32,
162    pub finance: u32,
163    pub food: u32,
164    pub government: u32,
165    // pub health: u32,
166    pub hobbies: u32,
167    pub home: u32,
168    pub news: u32,
169    pub real_estate: u32,
170    pub society: u32,
171    pub sports: u32,
172    pub tech: u32,
173    pub travel: u32,
174}
175
176impl InterestVector {
177    pub fn as_vec(&self) -> Vec<(Interest, u32)> {
178        vec![
179            (Interest::Inconclusive, self.inconclusive),
180            (Interest::Animals, self.animals),
181            (Interest::Arts, self.arts),
182            (Interest::Autos, self.autos),
183            (Interest::Business, self.business),
184            (Interest::Career, self.career),
185            (Interest::Education, self.education),
186            (Interest::Fashion, self.fashion),
187            (Interest::Finance, self.finance),
188            (Interest::Food, self.food),
189            (Interest::Government, self.government),
190            //(Interest::Health, self.health),
191            (Interest::Hobbies, self.hobbies),
192            (Interest::Home, self.home),
193            (Interest::News, self.news),
194            (Interest::RealEstate, self.real_estate),
195            (Interest::Society, self.society),
196            (Interest::Sports, self.sports),
197            (Interest::Tech, self.tech),
198            (Interest::Travel, self.travel),
199        ]
200    }
201
202    pub fn set(&mut self, interest: Interest, count: u32) {
203        match interest {
204            Interest::Inconclusive => {
205                self.inconclusive = count;
206            }
207            Interest::Animals => {
208                self.animals = count;
209            }
210            Interest::Arts => {
211                self.arts = count;
212            }
213            Interest::Autos => {
214                self.autos = count;
215            }
216            Interest::Business => {
217                self.business = count;
218            }
219            Interest::Career => {
220                self.career = count;
221            }
222            Interest::Education => {
223                self.education = count;
224            }
225            Interest::Fashion => {
226                self.fashion = count;
227            }
228            Interest::Finance => {
229                self.finance = count;
230            }
231            Interest::Food => {
232                self.food = count;
233            }
234            Interest::Government => {
235                self.government = count;
236            }
237            Interest::Hobbies => {
238                self.hobbies = count;
239            }
240            Interest::Home => {
241                self.home = count;
242            }
243            Interest::News => {
244                self.news = count;
245            }
246            Interest::RealEstate => {
247                self.real_estate = count;
248            }
249            Interest::Society => {
250                self.society = count;
251            }
252            Interest::Sports => {
253                self.sports = count;
254            }
255            Interest::Tech => {
256                self.tech = count;
257            }
258            Interest::Travel => {
259                self.travel = count;
260            }
261        }
262    }
263
264    pub fn summary(&self) -> String {
265        let mut interests: Vec<_> = self
266            .as_vec()
267            .into_iter()
268            .filter(|(_, count)| *count > 0)
269            .collect();
270        if interests.is_empty() {
271            return "<inconclusive>".to_string();
272        }
273        interests.sort_by(|a, b| b.1.cmp(&a.1));
274        let counts = interests
275            .into_iter()
276            .map(|(interest, count)| format!("{interest:?}: {count}"))
277            .collect::<Vec<_>>()
278            .join(", ");
279        format!("<interests: {counts}>")
280    }
281
282    pub fn print_all_counts(&self) {
283        let mut counts = self.as_vec();
284        counts.sort_by(|a, b| b.1.cmp(&a.1));
285        for (interest, count) in counts {
286            println!("{interest:?}: {count}")
287        }
288    }
289}
290
291impl std::ops::Add for InterestVector {
292    type Output = Self;
293
294    fn add(self, other: Self) -> Self {
295        Self {
296            inconclusive: self.inconclusive + other.inconclusive,
297            animals: self.animals + other.animals,
298            arts: self.arts + other.arts,
299            autos: self.autos + other.autos,
300            business: self.business + other.business,
301            career: self.career + other.career,
302            education: self.education + other.education,
303            fashion: self.fashion + other.fashion,
304            finance: self.finance + other.finance,
305            food: self.food + other.food,
306            government: self.government + other.government,
307            hobbies: self.hobbies + other.hobbies,
308            home: self.home + other.home,
309            news: self.news + other.news,
310            real_estate: self.real_estate + other.real_estate,
311            society: self.society + other.society,
312            sports: self.sports + other.sports,
313            tech: self.tech + other.tech,
314            travel: self.travel + other.travel,
315        }
316    }
317}
318
319impl std::ops::Index<Interest> for InterestVector {
320    type Output = u32;
321
322    fn index(&self, index: Interest) -> &u32 {
323        match index {
324            Interest::Inconclusive => &self.inconclusive,
325            Interest::Animals => &self.animals,
326            Interest::Arts => &self.arts,
327            Interest::Autos => &self.autos,
328            Interest::Business => &self.business,
329            Interest::Career => &self.career,
330            Interest::Education => &self.education,
331            Interest::Fashion => &self.fashion,
332            Interest::Finance => &self.finance,
333            Interest::Food => &self.food,
334            Interest::Government => &self.government,
335            // Interest::Health => &self.health,
336            Interest::Hobbies => &self.hobbies,
337            Interest::Home => &self.home,
338            Interest::News => &self.news,
339            Interest::RealEstate => &self.real_estate,
340            Interest::Society => &self.society,
341            Interest::Sports => &self.sports,
342            Interest::Tech => &self.tech,
343            Interest::Travel => &self.travel,
344        }
345    }
346}
347
348impl std::ops::IndexMut<Interest> for InterestVector {
349    fn index_mut(&mut self, index: Interest) -> &mut u32 {
350        match index {
351            Interest::Inconclusive => &mut self.inconclusive,
352            Interest::Animals => &mut self.animals,
353            Interest::Arts => &mut self.arts,
354            Interest::Autos => &mut self.autos,
355            Interest::Business => &mut self.business,
356            Interest::Career => &mut self.career,
357            Interest::Education => &mut self.education,
358            Interest::Fashion => &mut self.fashion,
359            Interest::Finance => &mut self.finance,
360            Interest::Food => &mut self.food,
361            Interest::Government => &mut self.government,
362            // Interest::Health => &mut self.health,
363            Interest::Hobbies => &mut self.hobbies,
364            Interest::Home => &mut self.home,
365            Interest::News => &mut self.news,
366            Interest::RealEstate => &mut self.real_estate,
367            Interest::Society => &mut self.society,
368            Interest::Sports => &mut self.sports,
369            Interest::Tech => &mut self.tech,
370            Interest::Travel => &mut self.travel,
371        }
372    }
373}
374
375#[cfg(test)]
376mod test {
377    use super::*;
378
379    #[test]
380    fn test_interest_code_conversion() {
381        for interest in Interest::all() {
382            assert_eq!(Interest::try_from(u32::from(interest)).unwrap(), interest)
383        }
384        // try_from() for out of bounds codes should return an error
385        assert!(matches!(
386            Interest::try_from(20),
387            Err(Error::InvalidInterestCode(20))
388        ));
389        assert!(matches!(
390            Interest::try_from(100),
391            Err(Error::InvalidInterestCode(100))
392        ));
393        // Health is currently disabled, so it's code should return None for now
394        assert!(matches!(
395            Interest::try_from(11),
396            Err(Error::InvalidInterestCode(11))
397        ));
398    }
399}