use uqa_core::memory::BudgetedVec;
use super::super::encoding::{put_u32, put_varint};
use super::super::scores::ScoreEncoder;
use super::super::{
cluster_id, corrupt, read_varint, DocId, PostingScore, StorageBackendResult, TokenOccurrence,
HEADER_LEN, OCCURRENCE_FORMAT_VERSION, POSITIONS_MAGIC,
};
use crate::read_control::StorageReadControl;
pub struct OccurrenceClusterBuilder {
scores: ScoreEncoder,
payload: BudgetedVec<u8>,
offsets: BudgetedVec<usize>,
cluster: Option<u64>,
}
impl OccurrenceClusterBuilder {
pub fn new(control: &StorageReadControl) -> StorageBackendResult<Self> {
let mut offsets = BudgetedVec::new(control.memory());
offsets.push(0_usize)?;
Ok(Self {
scores: ScoreEncoder::new(control),
payload: BudgetedVec::new(control.memory()),
offsets,
cluster: None,
})
}
pub fn is_empty(&self) -> bool {
self.scores.is_empty()
}
pub fn reserve(&mut self, count: usize) -> StorageBackendResult<()> {
self.scores.reserve(count)?;
self.offsets.reserve(count)?;
Ok(())
}
pub fn push(
&mut self,
doc_id: DocId,
doc_length: u64,
occurrences: &[TokenOccurrence],
control: &StorageReadControl,
) -> StorageBackendResult<()> {
self.enter(doc_id)?;
encode_occurrences(occurrences, &mut self.payload, control)?;
self.close(doc_id, occurrences.len() as u64, doc_length)
}
pub(crate) fn push_encoded(
&mut self,
doc_id: DocId,
mut posting: &[u8],
) -> StorageBackendResult<()> {
let doc_length = read_varint(&mut posting)?;
let term_freq = read_varint(&mut posting)?;
self.enter(doc_id)?;
self.payload.extend_from_slice(posting)?;
self.close(doc_id, term_freq, doc_length)
}
fn enter(&mut self, doc_id: DocId) -> StorageBackendResult<()> {
let cluster = cluster_id(doc_id);
if self.cluster.is_some_and(|current| current != cluster) {
return Err(corrupt("one occurrence value spans multiple clusters"));
}
self.cluster = Some(cluster);
Ok(())
}
fn close(
&mut self,
doc_id: DocId,
term_freq: u64,
doc_length: u64,
) -> StorageBackendResult<()> {
self.scores.push(PostingScore {
doc_id,
term_freq,
doc_length,
})?;
self.offsets.push(self.payload.len())?;
Ok(())
}
pub fn finish(
self,
control: &StorageReadControl,
) -> StorageBackendResult<(BudgetedVec<u8>, BudgetedVec<u8>)> {
control.cancellation().check()?;
if self.scores.is_empty() {
return Err(corrupt("cannot encode an empty occurrence cluster"));
}
let count = self.scores.len();
let score_blob = self.scores.finish(OCCURRENCE_FORMAT_VERSION, control)?;
let mut blob = BudgetedVec::new(control.memory());
let blob_len = self
.offsets
.len()
.checked_mul(size_of::<u32>())
.and_then(|directory| HEADER_LEN.checked_add(directory))
.and_then(|header| header.checked_add(self.payload.len()))
.ok_or(uqa_core::memory::MemoryError::SizeOverflow)?;
blob.reserve(blob_len)?;
blob.extend_from_slice(POSITIONS_MAGIC)?;
blob.extend_from_slice(&[OCCURRENCE_FORMAT_VERSION, 0, 0, 0])?;
put_u32(&mut blob, count, "occurrence posting count")?;
put_u32(&mut blob, self.offsets.len(), "occurrence offset count")?;
for &offset in self.offsets.iter() {
put_u32(&mut blob, offset, "occurrence payload offset")?;
}
blob.extend_from_slice(&self.payload)?;
Ok((score_blob, blob))
}
}
pub(crate) fn encode_posting(
doc_length: u64,
occurrences: &[TokenOccurrence],
output: &mut BudgetedVec<u8>,
control: &StorageReadControl,
) -> StorageBackendResult<()> {
put_varint(output, doc_length)?;
put_varint(output, occurrences.len() as u64)?;
encode_occurrences(occurrences, output, control)
}
fn encode_occurrences(
occurrences: &[TokenOccurrence],
payload: &mut BudgetedVec<u8>,
control: &StorageReadControl,
) -> StorageBackendResult<()> {
let mut previous = 0_u32;
for occurrence in occurrences {
control.cancellation().check()?;
occurrence
.validate()
.map_err(|error| corrupt(error.to_string()))?;
let delta = occurrence
.position
.checked_sub(previous)
.ok_or_else(|| corrupt("occurrence positions are not ordered"))?;
put_varint(payload, u64::from(delta))?;
put_varint(payload, u64::from(occurrence.position_length))?;
payload.push(u8::from(occurrence.offsets.is_some()))?;
if let Some(offsets) = occurrence.offsets {
put_varint(payload, offsets.start_utf8)?;
put_varint(payload, offsets.end_utf8 - offsets.start_utf8)?;
put_varint(payload, offsets.start_utf16)?;
put_varint(payload, offsets.end_utf16 - offsets.start_utf16)?;
}
previous = occurrence.position;
}
Ok(())
}
#[cfg(test)]
mod tests;