mod adaptive;
mod context;
mod facet;
pub mod furniture;
mod harvest;
mod model;
mod queue;
mod url_shape;
pub use url_shape::shape as url_shape;
mod weights;
pub use adaptive::{Adaptive, MIN_RELEVANCE};
pub use context::link_contexts;
use context::prose_words;
pub use harvest::{Harvest, subtree};
pub use queue::{BestFirst, Offer, cluster_key};
use facet::{FacetStats, Facets};
use model::LinkModel;
use pagesift_extract::Query;
use url::Url;
use url_shape::{Section, foreign_language_segment, is_english, language_portal, locator, section};
use weights::TermWeights;
const W_ANCHOR: f32 = 2.0;
const W_LOCATOR: f32 = 1.5;
const W_CONTEXT: f32 = 1.0;
const W_PARENT: f32 = 0.5;
const PHRASE_BONUS: f32 = 0.5;
const DEPTH_DECAY: f32 = 0.9;
const LANGUAGE_PORTAL_SCORE: f32 = 0.6;
const SECTION_PRIOR: f32 = furniture::FURNITURE_FACTOR * (W_ANCHOR * 0.5 + PHRASE_BONUS);
pub fn is_blind(score: f32) -> bool {
score <= SECTION_PRIOR
}
const SUBSITE_PRIOR: f32 = SECTION_PRIOR * 0.5;
const FOREIGN_LANGUAGE_FACTOR: f32 = furniture::FURNITURE_FACTOR;
pub const LIST_FREE: usize = 8;
const RESCORE_DELTA: f32 = 0.05;
#[derive(Clone, Copy, Debug, Default)]
pub struct PageSignal {
title: u64,
body: u64,
facets: Facets,
factor: f32,
fixed: f32,
foreign: bool,
}
impl PageSignal {
pub fn fixed(relevance: f32) -> Self {
Self {
fixed: relevance,
..Self::default()
}
}
}
#[derive(Clone, Copy, Debug)]
enum Shape {
Plain,
Factor(f32),
Floor(f32),
Fixed(f32),
}
#[derive(Clone, Copy, Debug)]
pub struct LinkSignal {
anchor: u64,
locator: u64,
context: u64,
parent: PageSignal,
shape: Shape,
depth: u8,
factor: f32,
}
impl LinkSignal {
pub fn fixed(score: f32) -> Self {
Self {
anchor: 0,
locator: 0,
context: 0,
parent: PageSignal::default(),
shape: Shape::Fixed(score),
depth: 0,
factor: 1.0,
}
}
pub fn scale(&mut self, factor: f32) {
self.factor *= factor;
}
}
pub struct Focus {
pub query: Query,
weights: TermWeights,
facets: FacetStats,
host: Option<String>,
english_home: bool,
scored_with: Vec<f32>,
english_seed: bool,
model: LinkModel,
}
impl Focus {
pub fn new(query: &str) -> Self {
let query = Query::new(query);
let weights = TermWeights::new(query.len());
let facets = FacetStats::new(query.len());
Self {
scored_with: weights
.current()
.iter()
.copied()
.chain(facets.shares())
.collect(),
query,
weights,
facets,
host: None,
english_home: false,
english_seed: false,
model: LinkModel::new([W_ANCHOR, W_LOCATOR, W_CONTEXT, W_PARENT, PHRASE_BONUS]),
}
}
pub fn note_seed_language(&mut self, lang: Option<&str>) {
self.english_seed = lang.is_some_and(is_english);
}
fn seeking_english(&self) -> bool {
self.english() && !self.english_home && !self.english_seed
}
fn english_target(&self) -> bool {
self.english() && (self.english_home || self.english_seed)
}
pub fn found_english_home(&mut self) {
self.english_home = true;
}
pub fn with_host(mut self, host: &str) -> Self {
self.host = Some(host.trim_start_matches("www.").to_ascii_lowercase());
self
}
fn same_host(&self, url: &Url) -> bool {
match (&self.host, url.host_str()) {
(Some(h), Some(u)) => u.trim_start_matches("www.").eq_ignore_ascii_case(h),
_ => true,
}
}
fn english_entry(&self, url: &Url, anchor: Option<&str>) -> bool {
if self.same_host(url) {
return language_portal(url, anchor);
}
let host = url.host_str().unwrap_or_default().to_ascii_lowercase();
let same_site = self
.host
.as_deref()
.is_none_or(|h| pagesift_content::site_key(h) == pagesift_content::site_key(&host));
let english_label = matches!(
host.split('.').next().unwrap_or_default(),
"en" | "eng" | "english"
);
let root = url.path().split('/').filter(|s| !s.is_empty()).count() == 0;
same_site && english_label && root
}
pub fn observe_page(&mut self, page: &PageSignal) {
self.weights.observe(page.body);
self.facets.observe(&page.facets);
}
#[cfg(test)]
pub fn page_relevance(
&self,
url: &Url,
title: Option<&str>,
markdown: &str,
links: usize,
) -> f32 {
self.relevance(&self.page_signal(url, title, markdown, links, None))
}
pub fn page_signal(
&self,
url: &Url,
title: Option<&str>,
markdown: &str,
links: usize,
lang: Option<&str>,
) -> PageSignal {
let mut factor = 1.0;
if furniture::is_hub(links, prose_words(markdown)) {
factor *= furniture::HUB_FACTOR;
}
if furniture::is_furniture_url(url, &self.wanted()) {
factor *= furniture::FURNITURE_FACTOR;
}
let title = title.unwrap_or_default();
PageSignal {
title: self.query.hit_mask(title),
body: self.query.hit_mask(markdown),
facets: Facets::of(&self.query, title, &locator(url), markdown),
factor,
fixed: 0.0,
foreign: lang.is_some_and(|l| !is_english(l)),
}
}
pub fn relevance(&self, p: &PageSignal) -> f32 {
if p.title == 0 && p.body == 0 && p.facets.is_empty() {
return p.fixed;
}
p.fixed + p.factor * self.facets.relevance(&p.facets)
}
pub fn weights_moved(&mut self) -> bool {
let now: Vec<f32> = self
.weights
.current()
.iter()
.copied()
.chain(self.facets.shares())
.collect();
let moved = now
.iter()
.zip(&self.scored_with)
.any(|(a, b)| (a - b).abs() > RESCORE_DELTA);
if moved {
self.scored_with = now;
}
moved
}
fn wanted(&self) -> impl Fn(&str) -> bool {
let query = &self.query;
move |w: &str| query.hit_mask(w) != 0
}
pub fn link_score(
&self,
url: &Url,
anchor: Option<&str>,
context: Option<&str>,
parent: f32,
depth: usize,
) -> f32 {
self.score(&self.link_signal(url, anchor, context, PageSignal::fixed(parent), depth))
}
pub fn link_signal(
&self,
url: &Url,
anchor: Option<&str>,
context: Option<&str>,
parent: PageSignal,
depth: usize,
) -> LinkSignal {
let wanted = self.wanted();
let furniture = furniture::is_furniture_url(url, &wanted)
|| anchor.is_some_and(|a| furniture::is_furniture_anchor(a, &wanted));
let shape = if furniture::is_dated_url(url) {
Shape::Factor(furniture::DATED_FACTOR)
} else if furniture {
Shape::Factor(furniture::FURNITURE_FACTOR)
} else if self.english() && foreign_language_segment(url) {
Shape::Factor(FOREIGN_LANGUAGE_FACTOR)
} else if self.seeking_english() && self.english_entry(url, anchor) {
Shape::Floor(LANGUAGE_PORTAL_SCORE)
} else {
match section(url, anchor) {
Some(Section::SameHost) if self.same_host(url) => Shape::Floor(SECTION_PRIOR),
Some(_) => Shape::Floor(SUBSITE_PRIOR),
None => Shape::Plain,
}
};
LinkSignal {
anchor: self.query.hit_mask(anchor.unwrap_or_default()),
locator: self.query.hit_mask(&locator(url)),
context: self.query.hit_mask(context.unwrap_or_default()),
parent,
shape,
depth: depth.min(64) as u8,
factor: 1.0,
}
}
pub fn score(&self, s: &LinkSignal) -> f32 {
let raw: f32 = self
.features(s)
.iter()
.zip(self.model.weights())
.map(|(x, w)| x * w)
.sum();
let raw = match s.shape {
Shape::Plain => raw,
Shape::Factor(f) => raw * f,
Shape::Floor(_) if s.parent.foreign && self.english_target() => raw,
Shape::Floor(f) => raw.max(f),
Shape::Fixed(v) => return v * s.factor,
};
raw * DEPTH_DECAY.powi(i32::from(s.depth)) * s.factor
}
fn features(&self, s: &LinkSignal) -> [f32; model::FEATURES] {
[
self.weights.coverage(s.anchor),
self.weights.coverage(s.locator),
self.weights.coverage(s.context),
self.relevance(&s.parent),
if s.anchor.count_ones() >= 2 { 1.0 } else { 0.0 },
]
}
pub fn learn(&mut self, s: &LinkSignal, hit: bool) -> bool {
if matches!(s.shape, Shape::Fixed(_)) {
return false;
}
let x = self.features(s);
self.model.observe(&x, hit)
}
pub fn is_language_portal(&self, url: &Url) -> bool {
self.seeking_english() && self.english_entry(url, None)
}
pub fn alternate_score(&self, lang: &str, depth: usize) -> Option<f32> {
let english = lang == "en" || lang.starts_with("en-");
(self.seeking_english() && english && depth == 0).then_some(LANGUAGE_PORTAL_SCORE)
}
fn english(&self) -> bool {
self.query.terms().iter().all(|t| t.is_ascii())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn u(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn relevant_links_score_higher_and_depth_decays() {
let f = Focus::new("refund policy");
let good = f.link_score(
&u("https://e.com/help/refund-policy"),
Some("Refund policy"),
None,
0.0,
1,
);
let ctx = f.link_score(
&u("https://e.com/help/x"),
Some("here"),
Some("Read our refund policy here"),
0.0,
1,
);
let bad = f.link_score(&u("https://e.com/team/x"), Some("Our team"), None, 0.0, 1);
assert!(good > ctx && ctx > bad, "{good} {ctx} {bad}");
assert_eq!(bad, 0.0);
let section = f.link_score(&u("https://e.com/docs/"), Some("Docs"), None, 0.0, 1);
let dated = f.link_score(
&u("https://e.com/blog/2026/04/refund-story/"),
Some("A refund story"),
None,
0.0,
1,
);
assert!(section > dated && dated > 0.0, "{section} {dated}");
assert!(section < ctx, "{section} {ctx}");
let subsite = f.link_score(&u("https://inf.e.com/"), Some("Informatics"), None, 0.0, 1);
let german = f.link_score(&u("https://e.com/de/"), Some("Deutsch"), None, 0.0, 1);
assert!(
subsite < section && subsite > german,
"{section} {subsite} {german}"
);
assert!(f.alternate_score("en-gb", 0).is_some());
assert!(f.alternate_score("de", 0).is_none());
assert!(
f.alternate_score("en", 1).is_none(),
"only the seed's alternate"
);
let en_section = f.link_score(
&u("https://e.com/en/studies/"),
Some("Studies"),
None,
0.0,
1,
);
assert!(
(en_section - section).abs() < 1e-6,
"{en_section} {section}"
);
let english_section = f.link_score(
&u("https://e.com/english/research/"),
Some("Research"),
None,
0.0,
1,
);
assert!(
(english_section - section).abs() < 1e-6,
"{english_section} {section}"
);
assert!(f.is_language_portal(&u("https://e.com/en/")));
assert!(f.is_language_portal(&u("https://e.com/en.html")));
assert!(!f.is_language_portal(&u("https://e.com/en/docs/")));
let g = Focus::new("api authentication").with_host("www.e.com");
let own = g.link_score(&u("https://e.com/en/"), Some("English"), None, 0.0, 1);
let dept = g.link_score(&u("https://cs.e.com/en/"), Some("English"), None, 0.0, 1);
assert!(own > dept && dept < section, "{own} {dept} {section}");
assert!(!g.is_language_portal(&u("https://cs.e.com/en/")));
assert!(g.is_language_portal(&u("https://en.e.com/")));
assert!(!g.is_language_portal(&u("https://en.other.com/")));
assert!(!g.is_language_portal(&u("https://en.e.com/news/")));
let mut g = g;
g.found_english_home();
let later = g.link_score(
&u("https://e.com/staff/en.html"),
Some("English"),
None,
0.0,
1,
);
assert!(later < own, "{later} {own}");
assert!(!g.is_language_portal(&u("https://e.com/en/")));
let api = Focus::new("api authentication");
let reference = api.link_score(
&u("https://e.com/docs/api/"),
Some("API reference"),
None,
0.0,
2,
);
let blog = api.link_score(
&u("https://e.com/blog/api-news/"),
Some("API news"),
None,
0.0,
2,
);
assert!(reference > 2.0 * blog, "{reference} {blog}");
let deep = f.link_score(
&u("https://e.com/help/refund-policy"),
Some("Refund policy"),
None,
0.0,
3,
);
assert!(deep < good);
let parent = f.link_score(&u("https://e.com/team/x"), Some("Our team"), None, 1.0, 1);
assert!(parent > bad);
let home = u("https://e.com/");
assert!(
f.page_relevance(&home, Some("Refund policy"), "refund", 3)
> f.page_relevance(&home, None, "refund", 3)
);
}
#[test]
fn furniture_and_hubs_are_demoted() {
let f = Focus::new("international students admission");
let content = f.link_score(
&u("https://e.edu/admissions/international/"),
Some("International students"),
None,
0.2,
2,
);
let news = f.link_score(
&u("https://e.edu/news/2026/05/story-1/"),
Some("International students celebrate the festival"),
None,
0.2,
1,
);
assert!(content > news, "{content} {news}");
let about = f.link_score(&u("https://e.edu/about/"), Some("About"), None, 0.9, 1);
let study = f.link_score(&u("https://e.edu/study/"), Some("Study"), None, 0.9, 1);
assert!(about < study);
let home = u("https://e.edu/");
let text = "international students admission";
let article = f.page_relevance(&home, None, text, 3);
let hub = f.page_relevance(&home, None, text, 40);
let list = "- [International students admission](https://e.edu/a)\n- [International students admission](https://e.edu/b)\n- [International students admission](https://e.edu/c)\n- [International students admission](https://e.edu/d)\n- [International students admission](https://e.edu/e)\n- [International students admission](https://e.edu/f)\n- [International students admission](https://e.edu/g)\n- [International students admission](https://e.edu/h)\n";
assert!(f.page_relevance(&home, None, list, 8) < f.page_relevance(&home, None, list, 0));
let dated = f.page_relevance(&u("https://e.edu/news/2026/"), None, text, 3);
assert!(hub < article && dated < article, "{article} {hub} {dated}");
}
#[test]
fn host_labels_count_and_common_terms_fade() {
let mut f = Focus::new("international students visa");
let intl = f.link_score(&u("https://international.e.edu/"), None, None, 0.0, 1);
let plain = f.link_score(&u("https://www.e.edu/"), None, None, 0.0, 1);
assert!(intl > plain && plain == 0.0, "{intl} {plain}");
for i in 0..6 {
let text = if i == 0 {
"students visa"
} else {
"students and more students"
};
let page = f.page_signal(&u("https://e.edu/p"), None, text, 0, None);
f.observe_page(&page);
}
let students = f.link_score(&u("https://e.edu/x"), Some("students"), None, 0.0, 1);
let visa = f.link_score(&u("https://e.edu/y"), Some("visa"), None, 0.0, 1);
assert!(visa > students, "{visa} {students}");
}
}