Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Best-first crawling

A focused crawl reaches the pages most likely to matter first, by giving every link a priority: a score. It needs a frontier that orders by priority, InMemFrontier::priority() or a redb store’s, see The frontier. Scores come from the URL, in the frontier, and from the page, in the handler, and add up.

Scoring by URL

FrontierExt::score adds a scorer’s score to every seed pushed. A scorer is a Score, or a closure from &Seed to i32; Keywords counts keywords in the URL’s path and query, and ShallowFirst scores minus the depth. Stacked scorers add up:

use spire::frontier::score::{Keywords, ShallowFirst};
use spire::frontier::seed::Seed;
use spire::frontier::{FrontierExt, InMemFrontier};

let frontier = InMemFrontier::priority()
    .dedup()
    .score(Keywords::new(["laptop", "review"]).with_weight(10))
    .score(ShallowFirst)
    // Product pages over everything else.
    .score(|seed: &Seed| if seed.uri().path().starts_with("/p/") { 50 } else { 0 });

Scoring by page

A handler has the page a link was found on. FollowLinks scores the links it follows: score_page scores the page once, for every link on it, and score_links scores each link from its URI, text and rel. The two add up:

use spire::frontier::score::Keywords;
use spire::handler::FollowLinks;

let keywords = Keywords::new(["laptop", "review"]);
let page_keywords = keywords.clone();

let follow = FollowLinks::new("page")
    // Links on relevant pages first...
    .score_page(move |page| 10 * page_keywords.matches(&page.title().unwrap_or_default()))
    // ...then by their own text.
    .score_links(move |link| keywords.matches(link.text()));

Scoring in your own handler

When a link’s score needs more than the page, such as the router’s state or the request, a handler of your own scores the links: Html::anchors gives every link with its element, and Link::with_priority its priority.

use std::sync::Arc;

use spire::Result;
use spire::extract::State;
use spire::extract::html::{Html, Meta};
use spire::frontier::queue::{Link, Queue};
use spire::frontier::score::Keywords;

struct App {
    keywords: Keywords,
}

async fn page(html: Html, meta: Meta, State(app): State<Arc<App>>, queue: Queue) -> Result<()> {
    // Once per page: how relevant it is.
    let relevance = 10 * app.keywords.matches(&meta.title.unwrap_or_default());
    // Once per link: that, plus its own text.
    let links = html.anchors().map(|(href, anchor)| {
        let text: String = anchor.text().collect();
        Link::from(href).with_priority(relevance + app.keywords.matches(&text))
    });
    let links: Vec<Link> = links.collect();
    queue.follow_all(links).await?;
    Ok(())
}

How scores combine

A link’s priority is its base, which the frontier’s scorers add to. Seeds are scored once, when pushed: retries and revisits keep their priority. Scores only order a frontier that orders by priority; a FIFO or LIFO one ignores them.