use std::any::type_name;
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::sync::Arc;
use std::time::SystemTime;
use crate::error::{Error, Result};
use crate::query::Query;
use crate::representation::Representation;
pub type Requirements = BTreeMap<String, Representation>;
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DocumentKey {
corpus: Arc<str>,
id: Arc<str>,
}
impl DocumentKey {
pub fn new(corpus: impl Into<Arc<str>>, id: impl Into<Arc<str>>) -> Self {
Self {
corpus: corpus.into(),
id: id.into(),
}
}
pub fn corpus(&self) -> &str {
&self.corpus
}
pub fn id(&self) -> &str {
&self.id
}
}
impl fmt::Debug for DocumentKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{:?}/{:?}", self.corpus, self.id)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Candidate {
pub key: DocumentKey,
pub gather_score: f32,
pub gather_rank: usize,
pub provenance: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Subset {
ids: BTreeMap<Arc<str>, HashSet<Arc<str>>>,
}
impl Subset {
pub fn new() -> Self {
Self::default()
}
pub fn with<I>(mut self, corpus: impl Into<Arc<str>>, ids: I) -> Self
where
I: IntoIterator,
I::Item: Into<Arc<str>>,
{
self.ids
.insert(corpus.into(), ids.into_iter().map(Into::into).collect());
self
}
pub fn ids(&self, corpus: &str) -> Option<&HashSet<Arc<str>>> {
self.ids.get(corpus)
}
pub fn contains(&self, key: &DocumentKey) -> bool {
self.ids(key.corpus())
.is_some_and(|ids| ids.contains(key.id()))
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &HashSet<Arc<str>>)> {
self.ids.iter().map(|(corpus, ids)| (corpus.as_ref(), ids))
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Gathered {
pub candidates: Vec<Candidate>,
pub as_of: SystemTime,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Scored {
pub scores: Vec<Option<f32>>,
pub as_of: SystemTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResourceBudget {
max_batch_tokens: usize,
max_documents_per_batch: usize,
threads: Option<usize>,
}
impl Default for ResourceBudget {
fn default() -> Self {
Self {
max_batch_tokens: 131_072,
max_documents_per_batch: 256,
threads: None,
}
}
}
impl ResourceBudget {
pub fn new(
max_batch_tokens: usize,
max_documents_per_batch: usize,
threads: Option<usize>,
) -> Result<Self> {
if max_batch_tokens == 0 {
return Err(Error::invalid("max_batch_tokens must be positive"));
}
if max_documents_per_batch == 0 {
return Err(Error::invalid("max_documents_per_batch must be positive"));
}
if threads == Some(0) {
return Err(Error::invalid("threads must be positive"));
}
Ok(Self {
max_batch_tokens,
max_documents_per_batch,
threads,
})
}
pub fn max_batch_tokens(&self) -> usize {
self.max_batch_tokens
}
pub fn max_documents_per_batch(&self) -> usize {
self.max_documents_per_batch
}
pub fn threads(&self) -> Option<usize> {
self.threads
}
}
fn short_type_name<T: ?Sized>() -> &'static str {
let full = type_name::<T>();
let path = full.split('<').next().unwrap_or(full);
path.rsplit("::").next().unwrap_or(path)
}
pub trait CandidateGenerator: Send + Sync {
fn requires(&self) -> &Requirements;
fn score_semantics(&self) -> &str;
fn name(&self) -> &str {
short_type_name::<Self>()
}
fn gather(&self, query: &Query, limit: usize, subset: Option<&Subset>) -> Result<Gathered>;
}
pub trait Reranker: Send + Sync {
fn requires(&self) -> &Requirements;
fn score_semantics(&self) -> &str;
fn name(&self) -> &str {
short_type_name::<Self>()
}
fn rerank(
&self,
query: &Query,
candidates: &[Candidate],
budget: &ResourceBudget,
) -> Result<Scored>;
}