///|
/// Precomputed searchable representations for one caller-labelled value.
pub struct MatchCandidate {
label_text : String
full_key_value : IndexKey
initials_key_value : IndexKey
} derive(Eq, Debug)
///|
/// Score components and fuzzy-distance evidence for one candidate.
pub struct MatchExplanation {
query_text : String
full_key_text : String
initials_key_text : String
exact_value : Double
prefix_value : Double
initials_value : Double
fuzzy_value : Double
final_value : Double
distance_value : DistanceResult
} derive(Eq, Debug)
///|
/// One ranked result retaining the caller's original position.
pub struct RankedCandidate {
candidate_value : MatchCandidate
explanation_value : MatchExplanation
caller_position : Int
} derive(Eq, Debug)
///|
fn string_starts_with(value : String, prefix : String) -> Bool {
if prefix.length() > value.length() {
return false
}
for index = 0; index < prefix.length(); index = index + 1 {
if value[index] != prefix[index] {
return false
}
}
true
}
///|
fn compact_query(value : String) -> String {
normalize_literal(value)
}
///|
fn maximum_int(left : Int, right : Int) -> Int {
if left > right {
left
} else {
right
}
}
///|
fn bounded_unit(value : Double) -> Double {
if value < 0.0 {
0.0
} else if value > 1.0 {
1.0
} else {
value
}
}
///|
/// Builds search representations once for repeated queries.
pub fn match_candidate(
label : String,
converted : ConversionResult,
) -> MatchCandidate {
{
label_text: label,
full_key_value: generate_index_key(converted, Compact),
initials_key_value: generate_index_key(converted, Initials),
}
}
///|
/// Scores one candidate and returns independently inspectable components.
pub fn score_match(
query_input : String,
candidate : MatchCandidate,
profile : FuzzyProfile,
) -> MatchExplanation {
let query = compact_query(query_input)
let full = compact_query(candidate.full_key_value.key())
let initials = compact_query(candidate.initials_key_value.key())
let distance = weighted_pinyin_distance(query, full, profile)
let exact = if query.length() > 0 && query == full { 0.55 } else { 0.0 }
let prefix = if query.length() > 0 && string_starts_with(full, query) {
0.20
} else {
0.0
}
let initials_score = if query.length() > 0 && query == initials {
0.20
} else {
0.0
}
let length_scale = maximum_int(query.length(), full.length())
let fuzzy_ratio = if length_scale == 0 {
1.0
} else {
bounded_unit(1.0 - distance.cost() / length_scale.to_double())
}
let fuzzy_score = 0.25 * fuzzy_ratio
let final_score = bounded_unit(exact + prefix + initials_score + fuzzy_score)
{
query_text: query,
full_key_text: full,
initials_key_text: initials,
exact_value: exact,
prefix_value: prefix,
initials_value: initials_score,
fuzzy_value: fuzzy_score,
final_value: final_score,
distance_value: distance,
}
}
///|
/// Ranks candidates by descending score and preserves caller order for ties.
pub fn rank_matches(
query : String,
candidates : Array[MatchCandidate],
profile : FuzzyProfile,
) -> Array[RankedCandidate] {
let ranked : Array[RankedCandidate] = []
for index = 0; index < candidates.length(); index = index + 1 {
ranked.push({
candidate_value: candidates[index],
explanation_value: score_match(query, candidates[index], profile),
caller_position: index,
})
}
ranked.sort_by((left, right) => {
let score_order = right.explanation_value.final_value.compare(
left.explanation_value.final_value,
)
if score_order != 0 {
score_order
} else {
left.caller_position - right.caller_position
}
})
ranked
}
///|
pub fn MatchCandidate::label(self : MatchCandidate) -> String {
self.label_text
}
///|
pub fn MatchCandidate::full_key(self : MatchCandidate) -> IndexKey {
self.full_key_value
}
///|
pub fn MatchCandidate::initials_key(self : MatchCandidate) -> IndexKey {
self.initials_key_value
}
///|
pub fn MatchExplanation::query(self : MatchExplanation) -> String {
self.query_text
}
///|
pub fn MatchExplanation::full_key(self : MatchExplanation) -> String {
self.full_key_text
}
///|
pub fn MatchExplanation::initials_key(self : MatchExplanation) -> String {
self.initials_key_text
}
///|
pub fn MatchExplanation::exact_component(self : MatchExplanation) -> Double {
self.exact_value
}
///|
pub fn MatchExplanation::prefix_component(self : MatchExplanation) -> Double {
self.prefix_value
}
///|
pub fn MatchExplanation::initials_component(self : MatchExplanation) -> Double {
self.initials_value
}
///|
pub fn MatchExplanation::fuzzy_component(self : MatchExplanation) -> Double {
self.fuzzy_value
}
///|
pub fn MatchExplanation::score(self : MatchExplanation) -> Double {
self.final_value
}
///|
pub fn MatchExplanation::distance(self : MatchExplanation) -> DistanceResult {
self.distance_value
}
///|
pub fn RankedCandidate::label(self : RankedCandidate) -> String {
self.candidate_value.label()
}
///|
pub fn RankedCandidate::score(self : RankedCandidate) -> Double {
self.explanation_value.score()
}
///|
pub fn RankedCandidate::explanation(self : RankedCandidate) -> MatchExplanation {
self.explanation_value
}
///|
pub fn RankedCandidate::original_position(self : RankedCandidate) -> Int {
self.caller_position
}