///|
/// A page of ranked search results for API pagination.
pub(all) struct ResultPage {
results : Array[SearchResult]
offset : Int
limit : Int
total : Int
has_more : Bool
}
///|
pub impl Show for ResultPage with fn output(self, logger) {
logger.write_string(
"ResultPage{offset: " +
self.offset.to_string() +
", limit: " +
self.limit.to_string() +
", total: " +
self.total.to_string() +
", has_more: " +
self.has_more.to_string() +
"}",
)
}
///|
/// Convert a ranked result list into a bounded page.
pub fn paginate_results(
results : Array[SearchResult],
offset : Int,
limit : Int,
) -> ResultPage {
let safe_offset = if offset < 0 { 0 } else { offset }
let safe_limit = if limit < 0 { 0 } else { limit }
let total = results.length()
let start = if safe_offset < total { safe_offset } else { total }
let end_candidate = start + safe_limit
let end = if end_candidate < total { end_candidate } else { total }
let page = []
for i = start; i < end; i = i + 1 {
page.push(results[i])
}
{
results: page,
offset: start,
limit: safe_limit,
total,
has_more: end < total,
}
}
///|
/// Merge result lists by id, retaining the best score for each document.
pub fn merge_results(
lists : Array[Array[SearchResult]],
metric : DistanceMetric,
) -> Array[SearchResult] {
let merged = Map([])
for list in lists {
for result in list {
match merged.get(result.id) {
None => merged.set(result.id, result)
Some(previous) => {
let keep = match metric {
Euclidean | Manhattan => result.score < previous.score
Cosine | DotProduct => result.score > previous.score
}
if keep {
merged.set(result.id, result)
}
}
}
}
}
let result = []
for entry in merged {
result.push(entry.1)
}
let ascending = match metric {
Euclidean | Manhattan => true
_ => false
}
sort_results(result, ascending)
result
}
///|
/// Remove duplicate ids while preserving the first occurrence.
pub fn unique_results(results : Array[SearchResult]) -> Array[SearchResult] {
let seen = Map([])
let unique = []
for result in results {
if !seen.contains(result.id) {
seen.set(result.id, true)
unique.push(result)
}
}
unique
}
///|
/// Keep at most one result per metadata value, useful for diversified feeds.
pub fn diversify_results(
results : Array[SearchResult],
metadata_key : String,
per_value : Int,
) -> Array[SearchResult] {
if per_value <= 0 {
return []
}
let counts = Map([])
let selected = []
for result in results {
let value = metadata_value(result.metadata, metadata_key)
let current = match counts.get(value) {
Some(count) => count
None => 0
}
if current < per_value {
counts.set(value, current + 1)
selected.push(result)
}
}
selected
}
///|
fn metadata_value(metadata : Array[(String, String)], key : String) -> String {
for pair in metadata {
if pair.0 == key {
return pair.1
}
}
""
}
///|
/// Return a score-normalized copy in the range [0, 1].
pub fn normalize_scores(
results : Array[SearchResult],
metric : DistanceMetric,
) -> Array[SearchResult] {
if results.length() == 0 {
return []
}
let mut min_score = results[0].score
let mut max_score = results[0].score
for result in results {
if result.score < min_score {
min_score = result.score
}
if result.score > max_score {
max_score = result.score
}
}
let span = max_score - min_score
let normalized = []
for result in results {
let score = if span == 0.0 {
1.0
} else if (match metric {
Euclidean | Manhattan => true
_ => false
}) {
(max_score - result.score) / span
} else {
(result.score - min_score) / span
}
normalized.push({ id: result.id, score, metadata: result.metadata })
}
normalized
}
///|
/// Select the highest-scoring result from a non-empty result list.
pub fn first_result(results : Array[SearchResult]) -> SearchResult? {
results.get(0)
}
///|
/// Return result ids in ranking order.
pub fn result_ids(results : Array[SearchResult]) -> Array[String] {
let ids = []
for result in results {
ids.push(result.id)
}
ids
}