use std::num::{NonZeroU32, NonZeroUsize};
use haematite::db::ReadOnlyDatabase;
use haematite_query::handle::Error;
use haematite_query::page::{Origin, Record, RecordSource};
use super::parser::{self, Question};
use super::shaped_source::ShapedSource;
use super::source::AuthorizedSource;
use crate::serve::access::Access;
use crate::serve::protocol::{Metric, Selection, VectorSearch};
use crate::serve::service::{Service, Words, not_configured};
use crate::serve::storage::vectors::{self, Hit};
use crate::serve::storage::{Captured, Failure};
const RECORD_MISSING: &str = "query_search_record_missing";
pub struct Search {
pub field: String,
pub question: Question,
pub measure: Metric,
pub limit: NonZeroU32,
}
impl From<parser::Search> for Search {
fn from(search: parser::Search) -> Self {
Self {
field: search.field,
question: Question::Record(search.record),
measure: search.measure,
limit: search.limit,
}
}
}
pub struct Read<'a> {
pub access: &'a Access,
pub reader: &'a ReadOnlyDatabase,
pub service: Option<&'a Service>,
}
pub enum Records<'a> {
Scan(AuthorizedSource<'a>),
Shaped(Box<ShapedSource<'a>>),
Search(Hits<'a>),
}
impl Records<'_> {
pub const fn compared(&self) -> Option<u64> {
match self {
Self::Scan(_) | Self::Shaped(_) => None,
Self::Search(hits) => Some(hits.compared),
}
}
}
impl RecordSource for Records<'_> {
fn next_record(&mut self) -> Result<Option<Record>, Error> {
match self {
Self::Scan(source) => source.next_record(),
Self::Shaped(source) => source.next_record(),
Self::Search(hits) => hits.next_record(),
}
}
fn passed(&self) -> Option<Origin> {
match self {
Self::Scan(source) => source.passed(),
Self::Shaped(source) => source.passed(),
Self::Search(_) => None,
}
}
}
pub struct Hits<'a> {
source: AuthorizedSource<'a>,
collection: String,
remaining: std::vec::IntoIter<Hit>,
compared: u64,
}
pub struct Reach {
pub bound: u64,
pub skip: u64,
}
impl RecordSource for Hits<'_> {
fn next_record(&mut self) -> Result<Option<Record>, Error> {
for hit in self.remaining.by_ref() {
let found = self
.source
.found(&self.collection, &hit.document_id, hit.score)?;
if found.is_some() {
return Ok(found);
}
}
Ok(None)
}
}
pub fn hits<'a>(
source: AuthorizedSource<'a>,
read: &Read<'_>,
at: &Captured,
collection: &str,
search: &Search,
reach: &Reach,
) -> Result<Hits<'a>, Failure> {
let k = NonZeroUsize::try_from(search.limit)
.map_err(|_| Failure::new("page_limit", "the limit does not fit this machine"))?;
let (mut found, compared) = match &search.question {
Question::Record(record) => {
let question = vectors::get(
read.access,
read.reader,
at,
collection,
&search.field,
record,
)?
.ok_or_else(|| missing(&search.field))?;
let request = VectorSearch {
selection: Selection {
branch: at.branch.clone(),
version: Some(at.version),
},
collection: collection.to_owned(),
field: search.field.clone(),
query: question,
k: search.limit,
metric: search.measure,
};
vectors::search_within(read.access, read.reader, at, &request, k, Some(reach.bound))?
}
Question::Text(text) => {
let service = read.service.ok_or_else(not_configured)?;
let (found, _) = service.search_words(
read.access,
at,
&Words {
collection,
field: &search.field,
text,
measure: Some(search.measure),
k,
bound: reach.bound,
},
)?;
let hits = found
.hits
.into_iter()
.map(|hit| Hit {
document_id: hit.id,
score: hit.score,
})
.collect();
(hits, found.compared)
}
};
let given = usize::try_from(reach.skip).map_or(found.len(), |given| given.min(found.len()));
found.drain(..given);
Ok(Hits {
source,
collection: collection.to_owned(),
remaining: found.into_iter(),
compared,
})
}
pub(super) fn missing(field: &str) -> Failure {
Failure::new(
RECORD_MISSING,
format!("the record named has no vector in .{field} that this request may read"),
)
}
#[cfg(test)]
#[path = "search_tests.rs"]
mod tests;
#[cfg(all(test, unix))]
#[path = "search_text_tests.rs"]
mod text_tests;