gravel_core/
scoring.rs

1use abi_stable::traits::IntoReprRust;
2use fuzzy_matcher::FuzzyMatcher;
3use fuzzy_matcher::skim::SkimMatcherV2;
4use gravel_ffi::{ArcDynHit, ScoredHit};
5use itertools::Itertools;
6use lazy_static::lazy_static;
7use std::cmp::Ordering;
8
9lazy_static! {
10	static ref MATCHER: SkimMatcherV2 = SkimMatcherV2::default();
11}
12
13/// Like [`to_scored`], but skips the actual scoring step, defaulting to 0
14pub fn to_unscored(hits: impl IntoIterator<Item = ArcDynHit>) -> Vec<ScoredHit> {
15	hits.into_iter()
16		.map(|hit| {
17			let score = hit.override_score().unwrap_or(0);
18			ScoredHit::from(hit, score)
19		})
20		.sorted_by(compare_hits)
21		.collect()
22}
23
24/// Assigns each hit a score based on how closely its title matches the query,
25/// discards non-matching hits and orders them highest to lowest.
26pub fn to_scored(hits: Vec<ArcDynHit>, query: &str) -> Vec<ScoredHit> {
27	hits.into_iter()
28		.filter_map(|h| scored(h, query))
29		.sorted_by(compare_hits)
30		.collect()
31}
32
33fn scored(hit: ArcDynHit, query: &str) -> Option<ScoredHit> {
34	let override_score = hit.override_score().into_rust();
35
36	let score = override_score.or_else(|| score(&hit, query))?;
37
38	Some(ScoredHit::from(hit, score))
39}
40
41fn score(hit: &ArcDynHit, query: &str) -> Option<u32> {
42	let title = hit.title().into_rust();
43	MATCHER.fuzzy_match(title, query).map(|s| s as u32)
44}
45
46fn compare_hits(a: &ScoredHit, b: &ScoredHit) -> Ordering {
47	match b.score.cmp(&a.score) {
48		Ordering::Equal => a.hit.title().cmp(&b.hit.title()),
49		ordering => ordering,
50	}
51}