///|
/// Human-readable relevance evidence for one search result.
pub struct ResultExplanation {
doc_id : String
score : Double
matched_terms : Array[String]
missing_terms : Array[String]
matched_required : Int
total_required : Int
token_overlap : Double
} derive(Debug)
///|
fn unique_query_tokens(query : String, tokenizer : Tokenizer) -> Array[String] {
let out : Array[String] = []
for token in tokenizer.tokenize(query) {
if !out.contains(token) {
out.push(token)
}
}
out
}
///|
pub fn explain_result(
query : String,
result : SearchResult,
tokenizer? : Tokenizer = Tokenizer::new(),
) -> ResultExplanation {
let query_tokens = unique_query_tokens(query, tokenizer)
let text_tokens = unique_query_tokens(result.text, tokenizer)
let matched : Array[String] = []
let missing : Array[String] = []
for token in query_tokens {
if text_tokens.contains(token) {
matched.push(token)
} else {
missing.push(token)
}
}
{
doc_id: result.doc_id,
score: result.score,
matched_terms: matched,
missing_terms: missing,
matched_required: matched.length(),
total_required: query_tokens.length(),
token_overlap: token_jaccard(query, result.text, tokenizer~),
}
}
///|
pub fn explain_results(
query : String,
results : Array[SearchResult],
tokenizer? : Tokenizer = Tokenizer::new(),
) -> Array[ResultExplanation] {
let out : Array[ResultExplanation] = []
for result in results {
out.push(explain_result(query, result, tokenizer~))
}
out
}
///|
pub fn explanation_summary(explanation : ResultExplanation) -> String {
let builder = StringBuilder()
builder.write_string(explanation.doc_id)
builder.write_string(": matched ")
builder.write_string(explanation.matched_required.to_string())
builder.write_string("/")
builder.write_string(explanation.total_required.to_string())
if !explanation.matched_terms.is_empty() {
builder.write_string(" [")
for i = 0; i < explanation.matched_terms.length(); i = i + 1 {
if i > 0 {
builder.write_string(", ")
}
builder.write_string(explanation.matched_terms[i])
}
builder.write_string("]")
}
builder.to_string()
}