275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# Ranking
|
|
|
|
Before results appear in the UrlbarView, they are fetched from providers.
|
|
|
|
Each [UrlbarProvider](https://firefox-source-docs.mozilla.org/browser/urlbar/overview.html#urlbarprovider)
|
|
implements its own internal ranking and returns sorted results.
|
|
|
|
Externally all the results are ranked by the {searchfox}`UrlbarMuxer <browser/components/urlbar/UrlbarMuxerStandard.sys.mjs>`
|
|
according to an hardcoded list of groups and sub-groups.
|
|
|
|
:::{NOTE}
|
|
Preferences can influence the groups order, for example by putting
|
|
Firefox Suggest before Search Suggestions.
|
|
:::
|
|
|
|
The Places provider returns history and bookmark results using an internal
|
|
ranking algorithm called **Frecency**.
|
|
|
|
## Frecency implementation
|
|
|
|
Frecency is a term derived from `frequency` and `recency`, its scope is to
|
|
provide a ranking algorithm that gives importance both to how often a page is
|
|
accessed and when it was last visited. Additionally, it accounts for the type of
|
|
each visit through a bonus system.
|
|
|
|
Frecency uses a double exponential decay.
|
|
|
|
Given a url, we identify a reference time, t{sub}`ref`, that will be either
|
|
the last visit date, or the last bookmark addition date (for pages not having
|
|
visits).
|
|
|
|
All the dates we use will be expressed as the number of days from the epoch.
|
|
|
|
Since calculating a score from all of the visits every time a new visit is added
|
|
would be expensive, a sample of the last
|
|
`places.frecency.pages.numSampledVisits` visits is used.
|
|
|
|
For each visit a *weight* is determined out of four buckets:
|
|
|
|
1. Very High (`places.frecency.pages.veryHighWeight`): This is used when a
|
|
page is in the high bucket and has interesting interaction data.
|
|
2. High (`places.frecency.pages.highWeight`): This is for bookmarked or typed
|
|
pages. Due to the current definition of typed, this includes URLs clicked on
|
|
the Firefox UI (not the web), except searches.
|
|
3. Medium (`places.frecency.pages.mediumWeight`): This is for normal browsing,
|
|
like clicking links on pages or downloads.
|
|
4. Low (`places.frecency.pages.lowWeight`): This is for non-typed redirect
|
|
sources, visits across HTML frames (non top level) or reloads.
|
|
|
|
The score for the single visit is then calculated as:
|
|
|
|
$$
|
|
score_{visit} = weight_{visit} * e^{-\lambda(t_{ref}-t_{visit})}
|
|
$$
|
|
|
|
With half-life defined as:
|
|
|
|
$$
|
|
\lambda = \frac{\ln 2}{30}
|
|
$$
|
|
|
|
The total score is the average score, multiplied by the total number of visits:
|
|
|
|
$$
|
|
score_{total} = \frac{\sum score_{visit}}{samplecount} * visitcount
|
|
$$
|
|
|
|
To keep into account recent, this score is decayed daily using the already
|
|
defined half-life. To avoid having to multiply each score daily by an
|
|
exponential, we instead store in the database (`moz_places`) the number of days
|
|
from epoch after which the calculated score would become 1, by decaying it every
|
|
day.
|
|
|
|
$$
|
|
frecency = t_{ref} + \frac{\ln(score)}{\lambda}
|
|
$$
|
|
|
|
### Interactions
|
|
|
|
Page view duration, keypress count, and scroll distance are tracked when a page
|
|
loads. All data is stored locally and never transmitted.
|
|
|
|
A page visit is classified as **interesting** if either criterion is met:
|
|
|
|
- Page view time ≥ `places.frecency.pages.interactions.viewTimeSeconds`.
|
|
- Page view time ≥ `places.frecency.pages.interactions.viewTimeIfManyKeypressesSeconds`
|
|
AND keypress count ≥ `places.frecency.pages.interactions.manyKeypresses`.
|
|
|
|
When a visit is classified as interesting, its scoring bucket is promoted. For
|
|
example, a visit initially categorized in the medium bucket is promoted to the
|
|
high bucket if it has an interesting interaction.
|
|
|
|
Interactions are stored separately from visits. To associate them, the query
|
|
checks whether a visit was recorded within `places.frecency.pages.interactions.maxVisitGapSeconds`
|
|
of a recorded interaction. If a matching visit exists within this time window,
|
|
the interaction is paired with it. Otherwise, the interaction becomes a
|
|
**virtual visit**.
|
|
|
|
A single visit can generate multiple interactions. When a user switches away
|
|
from a tab, the current interaction ends if the user does not return within
|
|
`browser.places.interactions.breakupIfNoUpdatesForSeconds`. Returning to the
|
|
tab after this threshold creates a new interaction, even though the original
|
|
visit remains the same.
|
|
|
|
### How frecency for a page is calculated
|
|
|
|
```{mermaid}
|
|
:align: center
|
|
:caption: Frecency calculation flow
|
|
|
|
flowchart TD
|
|
start([Page URL Input])
|
|
subgraph gather ["Gather"]
|
|
visits[Gather recent visits<br/>up to sample limit]
|
|
interactions[Gather interesting<br/>interactions]
|
|
pair[Match interactions<br/>with visits by timestamp]
|
|
virtual{Unpaired<br/>interaction?}
|
|
generate[Create virtual visit<br/>from interaction]
|
|
combine[Combine visits<br/>with virtual visits]
|
|
hasVisits{Any visits<br/>collected?}
|
|
isBookmarked{Is URL<br/>bookmarked?}
|
|
bookmarkFallback[High<br/>Bucket]
|
|
noData[frecency = 0]
|
|
end
|
|
subgraph classify ["Classify"]
|
|
visitType{Visit<br/>Type?}
|
|
bookmark_typed[Bookmark/Typed]
|
|
regular_click[Regular Link]
|
|
redirect_auto[Redirect/Sponsored]
|
|
bookmarkInteraction{Interesting<br/>interaction?}
|
|
regularInteraction{Interesting<br/>interaction?}
|
|
score1[Very High<br/>Bucket]
|
|
score2[High<br/>Bucket]
|
|
score3[Medium<br/>Bucket]
|
|
score4[Low<br/>Bucket]
|
|
end
|
|
calcDecay[Apply exponential decay<br/>based on visit age]
|
|
aggregate[Sum decayed scores and<br/> normalize by visit count]
|
|
project[frecency = days until<br/>score decays to 1]
|
|
%% Main
|
|
start --> visits & interactions
|
|
visits --> pair
|
|
interactions --> pair
|
|
pair --> virtual
|
|
virtual -->|Yes| generate
|
|
virtual -->|No| combine
|
|
generate --> combine
|
|
combine --> hasVisits
|
|
hasVisits -->|Yes| visitType
|
|
hasVisits -->|No| isBookmarked
|
|
isBookmarked -->|Yes| bookmarkFallback
|
|
isBookmarked -->|No| noData
|
|
bookmarkFallback --> calcDecay
|
|
%% Classification
|
|
visitType -->|Bookmark/Typed| bookmark_typed
|
|
visitType -->|Regular| regular_click
|
|
visitType -->|Redirect| redirect_auto
|
|
bookmark_typed --> bookmarkInteraction
|
|
regular_click --> regularInteraction
|
|
redirect_auto --> score4
|
|
bookmarkInteraction -->|Yes| score1
|
|
bookmarkInteraction -->|No| score2
|
|
regularInteraction -->|Yes| score2
|
|
regularInteraction -->|No| score3
|
|
%% Decay
|
|
score1 & score2 & score3 & score4 --> calcDecay
|
|
calcDecay --> aggregate
|
|
%% Final
|
|
aggregate --> project
|
|
%% Styling
|
|
style score1 fill:#4caf50,stroke:#2e7d32,stroke-width:3px,color:#fff
|
|
style score2 fill:#8bc34a,stroke:#558b2f,stroke-width:2px
|
|
style score3 fill:#ffc107,stroke:#f57f17,stroke-width:2px
|
|
style bookmarkFallback fill:#8bc34a,stroke:#558b2f,stroke-width:2px
|
|
style score4 fill:#ff9800,stroke:#e65100,stroke-width:2px
|
|
style project fill:#2196f3,stroke:#0d47a1,stroke-width:3px,color:#fff
|
|
style noData fill:#2196f3,stroke:#0d47a1,stroke-width:3px,color:#fff
|
|
```
|
|
|
|
1. **Gather visits and interactions:** For a given URL, collect recent visits
|
|
(up to the sample limit) and any interesting interactions.
|
|
2. **Match and create virtual visits:** Pair each interesting interaction with
|
|
its corresponding visit by timestamp. If an interaction has no paired visit,
|
|
create a "virtual visit" from that interaction. Combine all actual visits
|
|
with these virtual visits for scoring.
|
|
3. **Check visits exist:** If no virtual or non-virtual visits were collected,
|
|
check whether the URL is bookmarked. If bookmarked, assign it to the High
|
|
bucket, otherwise set frecency to 0.
|
|
4. **Classify visits into buckets:** Categorize each visit based on its type and
|
|
whether it has an interesting interaction.
|
|
5. **Apply exponential decay:** Reduce each visit's score based on its age
|
|
using the time difference between the most recent visit in the sample vs.
|
|
the visit timestamp.
|
|
6. **Aggregate scores:** Sum all decayed scores and normalize by dividing by the
|
|
total visit count to get an average decayed score.
|
|
7. **Project to frecency value:** Convert the aggregated score into a timestamp
|
|
representing the number of days from the Unix epoch until the score would
|
|
decay to 1. This final value is the store frecency value.
|
|
|
|
### When frecency for a page is calculated
|
|
|
|
Operations that may influence the frecency score are:
|
|
|
|
- Adding visits
|
|
- Removing visits
|
|
- Adding bookmarks
|
|
- Removing bookmarks
|
|
- Changing the url of a bookmark
|
|
- An interesting interaction is recorded
|
|
|
|
Frecency is recalculated:
|
|
|
|
- Immediately, when a new visit is added. The user expectation here is that the
|
|
page appears in search results after being visited. This is also valid for
|
|
any History API that allows to add visits.
|
|
- In background on idle times, in any other case. In most cases having a
|
|
temporarily stale value is not a problem, the main concern would be privacy
|
|
when removing history of a page, but removing whole history will either
|
|
completely remove the page or, if it's bookmarked, it will still be relevant.
|
|
In this case, when a change influencing frecency happens, the `recalc_frecency`
|
|
database field for the page is set to `1`.
|
|
|
|
Recalculation is done by the {searchfox}`PlacesFrecencyRecalculator <toolkit/components/places/PlacesFrecencyRecalculator.sys.mjs>` module.
|
|
The Recalculator is notified when `PlacesUtils.history.shouldStartFrecencyRecalculation`
|
|
value changes from false to true, that means there's values to recalculate.
|
|
A DeferredTask is armed, that will look for a user idle opportunity
|
|
in the next 5 minutes, otherwise it will run when that time elapses.
|
|
Once all the outdated values have been recalculated
|
|
`PlacesUtils.history.shouldStartFrecencyRecalculation` is set back to false
|
|
until the next operation invalidating a frecency.
|
|
The recalculation task is also armed on the `idle-daily` notification.
|
|
|
|
When the task is executed, it recalculates frecency of a chunk of pages. If
|
|
there are more pages left to recalculate, the task is re-armed. After frecency
|
|
of a page is recalculated, its `recalc_frecency` field is set back to `0`.
|
|
|
|
## Adaptive Input History
|
|
|
|
Input History (also known as Adaptive History) is a feature that allows to
|
|
find urls that the user previously picked. To do so, it associates search strings
|
|
with picked urls.
|
|
|
|
Adaptive results are usually presented before frecency derived results, making
|
|
them appear as having an infinite frecency.
|
|
|
|
When the user types a given string, and picks a result from the address bar, that
|
|
relation is stored and increases a use_count field for the given string.
|
|
The use_count field asymptotically approaches a max of `10` (the update is
|
|
done as `use_count * .9 + 1`).
|
|
|
|
On querying, all the search strings that start with the input string are matched,
|
|
a rank is calculated per each page as `ROUND(MAX(use_count) * (1 + (input = :search_string)), 1)`,
|
|
so that results perfectly matching the search string appear at the top.
|
|
Results with the same rank are additionally sorted by descending frecency.
|
|
|
|
On daily idles, when frecency is decayed, also input history gets decayed, in
|
|
particular the use_count field is multiplied by a decay rate of `0.975`.
|
|
After decaying, any entry that has a `use_count < 0.975^90 (= 0.1)` is removed,
|
|
thus entries are removed if unused for 90 days.
|
|
|
|
## Why legacy frecency scoring was replaced
|
|
|
|
{doc}`ranking-legacy` had the following issues:
|
|
|
|
1. The score depended on the time at which it is calculated, making the user
|
|
experience more unstable. For example removing a bookmark (affects the score
|
|
negatively) will recalculate the score to a value higher than another
|
|
untouched bookmarked url, just because it was recalculated later.
|
|
2. All the scores must be decayed every day to keep recency into account. It is
|
|
an expensive operation.
|
|
3. It has far too many coefficients, making it impossible to study for
|
|
optimizing those coefficients.
|
|
4. Changing the coefficients doesn't recalculate all the scores, making
|
|
experimentation more complicated.
|