///|
struct SearchDocument {
identifier_value : String
source_value : String
candidate_value : MatchCandidate
}
///|
/// Mutable validated builder for a reusable search index.
pub struct SearchIndexBuilder {
document_values : Array[SearchDocument]
}
///|
/// Immutable collection of precomputed Pinyin match candidates.
pub struct SearchIndex {
document_values : Array[SearchDocument]
}
///|
/// Query limits and minimum accepted score.
pub struct SearchOptions {
limit_value : Int
minimum_score_value : Double
} derive(Eq, Debug)
///|
/// One ranked hit with source text and score evidence.
pub struct SearchHit {
identifier_value : String
source_value : String
explanation_value : MatchExplanation
source_position : Int
} derive(Eq, Debug)
///|
fn copy_search_documents(
values : Array[SearchDocument],
) -> Array[SearchDocument] {
let output : Array[SearchDocument] = []
for value in values {
output.push(value)
}
output
}
///|
fn valid_search_identifier(value : String) -> Bool {
if value.length() == 0 {
return false
}
for code in text_code_points(value) {
if code == 10 || code == 13 {
return false
}
}
true
}
///|
pub fn SearchIndexBuilder::new() -> SearchIndexBuilder {
{ document_values: [] }
}
///|
/// Converts and precomputes one document before inserting it.
pub fn SearchIndexBuilder::add(
self : SearchIndexBuilder,
identifier : String,
source : String,
lexicon : Lexicon,
options : ConvertOptions,
) -> Result[Unit, PinyinError] {
if !valid_search_identifier(identifier) {
let reason = if identifier.length() == 0 {
"empty_identifier"
} else {
"line_break_in_identifier"
}
return Err(InvalidSearchDocument(identifier, reason))
}
for document in self.document_values {
if document.identifier_value == identifier {
return Err(InvalidSearchDocument(identifier, "duplicate_identifier"))
}
}
let converted = match convert_text(source, lexicon, options) {
Err(error) => return Err(error)
Ok(value) => value
}
self.document_values.push({
identifier_value: identifier,
source_value: source,
candidate_value: match_candidate(identifier, converted),
})
Ok(())
}
///|
pub fn SearchIndexBuilder::length(self : SearchIndexBuilder) -> Int {
self.document_values.length()
}
///|
/// Freezes the current insertion order into an immutable index.
pub fn SearchIndexBuilder::build(self : SearchIndexBuilder) -> SearchIndex {
{ document_values: copy_search_documents(self.document_values) }
}
///|
pub fn search_options(
limit : Int,
minimum_score : Double,
) -> Result[SearchOptions, PinyinError] {
if limit <= 0 {
return Err(InvalidSearchOptions("limit", limit.to_string()))
}
if minimum_score.is_nan() || minimum_score.is_inf() {
return Err(InvalidSearchOptions("minimum_score", "not_finite"))
}
if minimum_score < 0.0 || minimum_score > 1.0 {
return Err(InvalidSearchOptions("minimum_score", "out_of_range"))
}
Ok({ limit_value: limit, minimum_score_value: minimum_score })
}
///|
pub fn default_search_options() -> SearchOptions {
search_options(20, 0.0).unwrap()
}
///|
/// Queries precomputed candidates with stable caller-order tie breaking.
pub fn SearchIndex::search(
self : SearchIndex,
query : String,
options : SearchOptions,
profile : FuzzyProfile,
) -> Result[Array[SearchHit], PinyinError] {
if normalize_literal(query).length() == 0 {
return Err(InvalidSearchQuery("empty_normalized_query"))
}
let candidates : Array[MatchCandidate] = []
for document in self.document_values {
candidates.push(document.candidate_value)
}
let ranked = rank_matches(query, candidates, profile)
let hits : Array[SearchHit] = []
for ranked_item in ranked {
if hits.length() >= options.limit_value {
break
}
if ranked_item.score() < options.minimum_score_value {
continue
}
let position = ranked_item.original_position()
let document = self.document_values[position]
hits.push({
identifier_value: document.identifier_value,
source_value: document.source_value,
explanation_value: ranked_item.explanation(),
source_position: position,
})
}
Ok(hits)
}
///|
pub fn SearchIndex::length(self : SearchIndex) -> Int {
self.document_values.length()
}
///|
pub fn SearchOptions::limit(self : SearchOptions) -> Int {
self.limit_value
}
///|
pub fn SearchOptions::minimum_score(self : SearchOptions) -> Double {
self.minimum_score_value
}
///|
pub fn SearchHit::identifier(self : SearchHit) -> String {
self.identifier_value
}
///|
pub fn SearchHit::source(self : SearchHit) -> String {
self.source_value
}
///|
pub fn SearchHit::score(self : SearchHit) -> Double {
self.explanation_value.score()
}
///|
pub fn SearchHit::explanation(self : SearchHit) -> MatchExplanation {
self.explanation_value
}
///|
pub fn SearchHit::original_position(self : SearchHit) -> Int {
self.source_position
}