mod matcher;
use crate::{Result, SemanticError, SemanticVector};
use matcher::validate_candidate_text;
use weavatrix_graph::NodeId;
#[derive(Debug, Clone, PartialEq)]
pub struct AnchorConfig {
model: String,
min_similarity: f64,
max_suggestions_per_link: usize,
}
impl AnchorConfig {
#[must_use]
pub fn new(
model: impl Into<String>,
min_similarity: f64,
max_suggestions_per_link: usize,
) -> Self {
Self {
model: model.into(),
min_similarity,
max_suggestions_per_link,
}
}
#[must_use]
pub fn model(&self) -> &str {
&self.model
}
#[must_use]
pub const fn min_similarity(&self) -> f64 {
self.min_similarity
}
#[must_use]
pub const fn max_suggestions_per_link(&self) -> usize {
self.max_suggestions_per_link
}
fn validate(&self) -> Result<()> {
if self.model.is_empty() {
return Err(SemanticError::EmptyAnchorModel);
}
if self.model.trim() != self.model {
return Err(SemanticError::AnchorModelHasSurroundingWhitespace);
}
if !self.min_similarity.is_finite() || !(0.0..=1.0).contains(&self.min_similarity) {
return Err(SemanticError::InvalidAnchorSimilarityThreshold);
}
if self.max_suggestions_per_link == 0 {
return Err(SemanticError::ZeroAnchorSuggestions);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnchorCandidate {
source: NodeId,
locator: String,
anchor_text: String,
context: String,
vector: SemanticVector,
}
impl AnchorCandidate {
pub fn new(
source: NodeId,
locator: impl Into<String>,
anchor_text: impl Into<String>,
context: impl Into<String>,
values: Vec<f32>,
) -> Result<Self> {
let locator = locator.into();
let anchor_text = anchor_text.into();
let context = context.into();
if locator.is_empty() {
return Err(SemanticError::EmptyAnchorLocator {
source: source.to_string(),
});
}
validate_candidate_text(&source, &locator, "locator", &locator)?;
if anchor_text.is_empty() {
return Err(SemanticError::EmptyAnchorText {
source: source.to_string(),
locator,
});
}
validate_candidate_text(&source, &locator, "anchor_text", &anchor_text)?;
if context.is_empty() {
return Err(SemanticError::EmptyAnchorContext {
source: source.to_string(),
locator,
});
}
validate_candidate_text(&source, &locator, "context", &context)?;
if !context.contains(&anchor_text) {
return Err(SemanticError::AnchorTextOutsideContext {
source: source.to_string(),
locator,
});
}
let vector = SemanticVector::new(source.to_string(), values)?;
Ok(Self {
source,
locator,
anchor_text,
context,
vector,
})
}
#[must_use]
pub const fn source(&self) -> &NodeId {
&self.source
}
#[must_use]
pub fn locator(&self) -> &str {
&self.locator
}
#[must_use]
pub fn anchor_text(&self) -> &str {
&self.anchor_text
}
#[must_use]
pub fn context(&self) -> &str {
&self.context
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnchorSuggestion {
locator: String,
anchor_text: String,
context: String,
similarity: f64,
}
impl AnchorSuggestion {
#[must_use]
pub fn locator(&self) -> &str {
&self.locator
}
#[must_use]
pub fn anchor_text(&self) -> &str {
&self.anchor_text
}
#[must_use]
pub fn context(&self) -> &str {
&self.context
}
#[must_use]
pub const fn similarity(&self) -> f64 {
self.similarity
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnchoredLink {
source: NodeId,
target: NodeId,
link_similarity: f64,
suggestions: Vec<AnchorSuggestion>,
}
impl AnchoredLink {
#[must_use]
pub const fn source(&self) -> &NodeId {
&self.source
}
#[must_use]
pub const fn target(&self) -> &NodeId {
&self.target
}
#[must_use]
pub const fn link_similarity(&self) -> f64 {
self.link_similarity
}
#[must_use]
pub fn suggestions(&self) -> &[AnchorSuggestion] {
&self.suggestions
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnchorMatchReport {
candidate_count: usize,
comparisons: u64,
links: Vec<AnchoredLink>,
}
impl AnchorMatchReport {
#[must_use]
pub const fn candidate_count(&self) -> usize {
self.candidate_count
}
#[must_use]
pub const fn comparisons(&self) -> u64 {
self.comparisons
}
#[must_use]
pub fn links(&self) -> &[AnchoredLink] {
&self.links
}
#[must_use]
pub fn matched_link_count(&self) -> usize {
self.links
.iter()
.filter(|link| !link.suggestions.is_empty())
.count()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnchorMatcher {
config: AnchorConfig,
}