Computes Lempel & Moran's SALSA hub and authority scores over the same cleaned, redirect/canonical-folded, domain-filtered, deduplicated link graph that [pagerank()] builds, so node identities line up across the centrality measures. SALSA is a stochastic variant of HITS: it runs HITS-style mutual reinforcement as PageRank-style random walks on the bipartite hub/authority graph, yielding stationary-distribution scores instead of dominant eigenvectors.
Usage
salsa(
edge_list_df,
redirects_df = NULL,
clean_edge_urls = TRUE,
clean_redirect_urls = TRUE,
rurl_params = list(),
self_loops = c("drop", "keep"),
drop_isolates_flag = TRUE,
duplicate_edge_policy = c("collapse", "aggregate", "count_instances"),
edge_from_col = "from",
edge_to_col = "to",
redirect_from_col = "from",
redirect_to_col = "to",
duplicate_from_policy = c("strict", "first_wins", "last_wins", "most_frequent",
"prune_source", "resolve_if_consistent"),
loop_handling = c("error", "prune_loop", "break_arrow"),
canonicals_df = NULL,
canonical_from_col = "from",
canonical_to_col = "to",
clean_canonical_urls = TRUE,
canonical_duplicate_from_policy = c("strict", "first_wins", "last_wins",
"most_frequent", "prune_source", "resolve_if_consistent"),
canonical_loop_handling = c("error", "prune_loop", "break_arrow"),
canonical_conflict_policy = c("redirect_wins", "error", "canonical_wins"),
keep_domains = NULL,
exclude_domains = NULL,
keep_hosts = NULL,
exclude_hosts = NULL,
...
)Arguments
- edge_list_df
A data frame representing the edge list, typically with columns like "from" and "to".
- redirects_df
An optional data frame for redirect rules, typically with "from" and "to" columns. Defaults to NULL.
- clean_edge_urls
Logical, whether to clean URLs in the edge list. Defaults to TRUE.
- clean_redirect_urls
Logical, whether to clean URLs in the redirect list. Defaults to TRUE. Only effective if `redirects_df` is provided.
- rurl_params
A list of parameters to pass to `rurl::clean_url`. Defaults to an empty list. `protocol_handling` defaults to `"keep"` and `case_handling` to `"lower_host"` for cross-project canonicalization consistency. If you set `host_encoding` (`"idna"` or `"unicode"`) to fold internationalized (IDN) hosts, that same value is also passed to the domain-filtering step so its comparisons stay consistent with the cleaned node keys. (Registrable-domain matching is encoding-independent, so this only matters if host-level filtering is involved.)
- self_loops
A character string specifying how to handle self-loops. Either "drop" (default) or "keep".
- drop_isolates_flag
Logical, whether to drop isolated nodes before PageRank computation. Defaults to TRUE.
- duplicate_edge_policy
How repeated `from -> to` rows are represented after URL cleaning, redirect/canonical folding, and domain filtering. One of:
- `"collapse"`
(default) Destination-level surfer: repeated rows collapse to one unweighted destination edge, preserving legacy `get_unique_edges()` behavior and the common binary PageRank convention.
- `"aggregate"`
Collapse each `from -> to` pair with [aggregate_edges()] semantics. Numeric columns, including `weight_col`, are summed; logical columns such as `nofollow` use the default `"any"` conflict policy.
- `"count_instances"`
Link-slot / edge-level surfer: repeated rows increase transition probability. With no `weight_col`, each surviving `from -> to` pair receives an internal weight equal to its duplicate-row count. With `weight_col`, weights are summed and an `instance_count` audit column is retained.
- edge_from_col, edge_to_col
Names of from/to columns in `edge_list_df`.
- redirect_from_col, redirect_to_col
Names of from/to columns in `redirects_df`.
- duplicate_from_policy
How to handle conflicting redirects in `redirects_df`. Passed through to [resolve_redirects()]. Default `"strict"` (error on conflicts). See [resolve_redirects()] for all available policies.
- loop_handling
How to handle redirect cycles. Passed through to [resolve_redirects()]. Default `"error"`. See [resolve_redirects()] for all available policies.
- canonicals_df
An optional data frame of declared `rel=canonical` links, with `from`/`to` columns (or as set by `canonical_from_col` / `canonical_to_col`) pairing a source URL with the canonical it declares. Default `NULL` (opt-in; the default preserves current behavior). Canonicals are a **distinct, advisory** signal from enforced 3xx `redirects_df`: they are tracked separately and audited via [audit_canonicals()] / [audit_fold()], then folded into the same composed map as redirects (see [build_fold_map()]). Self-canonicals drop as no-ops.
- canonical_from_col, canonical_to_col
From/to columns in `canonicals_df`. Default `"from"` / `"to"`.
- clean_canonical_urls
Logical, whether to clean URLs in `canonicals_df` using the same resolved `rurl_params` profile as edge and redirect cleaning. Default `TRUE`. Only effective when `canonicals_df` is provided.
- canonical_duplicate_from_policy
How to handle a canonical source that declares multiple distinct canonicals. Reuses the `duplicate_from_policy` enum (see [resolve_redirects()]). Default `"strict"`.
- canonical_loop_handling
How to handle cycles among declared canonicals. Reuses the `loop_handling` enum. Default `"error"`.
- canonical_conflict_policy
How to resolve a redirect-vs-canonical disagreement on the **same source** URL. One of `"redirect_wins"` (default; the 3xx wins and the canonical on a redirecting source is ignored and flagged), `"error"` (error on genuine disagreement), or `"canonical_wins"` (the declared canonical wins for that source, still flagged). See [build_fold_map()].
- keep_domains
Optional character vector of domains to keep. When provided, edges are filtered via [filter_links_by_domain()] so that only links where both endpoints belong to one of the specified domains are included. Useful for restricting to internal links. Default `NULL` (no domain filtering).
**Ordering:** filtering runs *after* redirect/canonical folding, so it scopes the post-fold (canonical) namespace, not the crawled input. If an out-of-scope canonical/redirect rewrites the crawled domain onto a different one, filtering on the crawled domain matches nothing (an empty graph). To scope the INPUT you crawled, run [filter_links_by_domain()] on the edge list *before* calling `pagerank()`.
- exclude_domains
Optional character vector of domains to exclude. Edges where either endpoint belongs to one of these domains are removed. Like `keep_domains`, this filters the post-fold namespace (see the ordering note above). Default `NULL` (no exclusion).
- keep_hosts
Optional character vector of exact hosts to keep (e.g. `"www.example.com"`), as opposed to registrable domains. Matched on the exact host using the same canonicalization profile as cleaning, so IDN folding (`host_encoding` in `rurl_params`) applies consistently. Default `NULL`.
- exclude_hosts
Optional character vector of exact hosts to exclude. Edges where either endpoint matches one of these hosts are removed. Ignore rules override keep rules. Default `NULL`.
- ...
Additional arguments forwarded to [compute_salsa()].
Value
A data frame with one row per node and columns `node_name`, `hub`, and `authority` (column names configurable via `...`). Hub and authority each sum to `1` over their non-`NA` entries.
Details
## Relationship to the PageRank pipeline
`salsa()` reuses the exact identity-forming steps of [pagerank()] — URL canonicalization (the same resolved `rurl` profile), the same composed redirect + canonical fold map, the same domain/host filtering, the same `duplicate_edge_policy` deduplication, and the same self-loop / isolate handling. The resulting vertex set therefore matches `pagerank()` run with the same arguments, so hub, authority, and PageRank can be joined on `node_name` without re-canonicalizing.
The PageRank-specific, *forward-flow* modeling devices have **no SALSA analogue and are intentionally not exposed**: nofollow evaporation, the indexability (noindex / robots.txt) transforms, the TIPR teleport prior, and the `reverse` flag. SALSA already computes both directions of authority flow (hub is the outflow-oriented score, authority the inflow-oriented one).
## SALSA versus HITS, and the pagerankr adaptation
Where [hits()] takes the dominant eigenvectors of \(A^\top A\) and \(A A^\top\), SALSA replaces the mutual-reinforcement iteration with two *stochastic* Markov chains on the bipartite hub/authority graph; their stationary distributions are the scores, which on a connected graph reduce to a simple in-/out-degree closed form (see [compute_salsa()]). Because the chains are stochastic, SALSA is far less prone than HITS to the "tightly-knit community" effect, where a dense cluster of mutually linking pages dominates the top scores.
Lempel & Moran's original SALSA ran on a query-focused base set. `salsa()` instead runs on the **full (or user-filtered) site graph** that `pagerankr` assembles — a documented site-graph adaptation of the focused-subgraph algorithm. Treat the scores as site-wide structural centralities, not query-relevance scores. Coverage differs from PageRank: a node's `hub` is `NA` when it has no outlinks and its `authority` is `NA` when it has no inlinks (see [compute_salsa()]).
References
Lempel, R. & Moran, S. (2001). SALSA: The Stochastic Approach for Link-Structure Analysis. *ACM Transactions on Information Systems*, 19(2), 131-160.
See also
[compute_salsa()] for the computational core, [hits()] for the HITS analogue, and [pagerank()] for the PageRank analogue sharing this identity pipeline.
Examples
edges <- data.frame(
from = c("http://A.com/", "http://A.com/", "B.com"),
to = c("B.com", "C.com", "C.com")
)
salsa(edges)
#> node_name hub authority
#> 1 http://a.com/ 0.6666667 NA
#> 2 http://b.com/ 0.3333333 0.3333333
#> 3 http://c.com/ NA 0.6666667
# Hub vs authority: a pure outflow page tops hub, a pure inflow page tops
# authority.
s <- salsa(edges)
s[which.max(s$hub), ]
#> node_name hub authority
#> 1 http://a.com/ 0.6666667 NA
s[which.max(s$authority), ]
#> node_name hub authority
#> 3 http://c.com/ NA 0.6666667