///|
pub struct SearchHit {
code : String
title : String
score : Int
} derive(Debug, Eq, ToJson)
///|
pub fn Catalog::ranked_search(
self : Catalog,
query : String,
limit : Int,
) -> Array[SearchHit] {
if limit <= 0 {
return []
}
let normalized_query = query.trim().to_owned().to_lower()
if normalized_query.is_empty() {
return []
}
let tokens = search_tokens(normalized_query)
let ranked = []
for entry in self.entries {
let score = score_entry(entry, normalized_query, tokens)
if score > 0 {
insert_search_hit(ranked, { code: entry.code, title: entry.title, score })
}
}
if ranked.length() > limit {
ranked[:limit].to_owned()
} else {
ranked
}
}
///|
fn search_tokens(query : String) -> Array[String] {
let tokens = []
let mut current = ""
for c in query {
if c == ' ' || c == '\t' || c == '\n' {
if !current.is_empty() {
tokens.push(current)
current = ""
}
} else {
current = current + c.to_string()
}
}
if !current.is_empty() {
tokens.push(current)
}
tokens
}
///|
fn score_entry(entry : IcdEntry, query : String, tokens : Array[String]) -> Int {
let title = entry.title.to_lower()
let code = entry.code.to_lower()
let mut score = 0
if title.contains(query) {
score += 10
}
if code == query {
score += 20
} else if code.has_prefix(query) {
score += 6
}
for token in tokens {
if title.contains(token) {
score += 3
}
if code.contains(token) {
score += 1
}
}
score
}
///|
fn insert_search_hit(ranked : Array[SearchHit], hit : SearchHit) -> Unit {
let next = []
let mut inserted = false
for current in ranked {
if !inserted &&
(
hit.score > current.score ||
(hit.score == current.score && hit.code < current.code)
) {
next.push(hit)
inserted = true
}
next.push(current)
}
if !inserted {
next.push(hit)
}
ranked.clear()
for current in next {
ranked.push(current)
}
}