// Stract is an open source web search engine. // Copyright (C) 2024 Stract ApS // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see use std::{ collections::BinaryHeap, io::{BufWriter, Write}, ops::RangeBounds, path::{Path, PathBuf}, }; use file_store::Peekable; use super::{ blob_id_index::{BlobIdIndex, BlobIdIndexWriter}, blob_index::{BlobIndex, BlobIndexWriter}, blob_store::{BlobStore, BlobStoreWriter}, Serialized, SerializedRef, }; use crate::Result; pub struct Writers where W: Write, { pub id_index: BlobIdIndexWriter, pub blob_index: BlobIndexWriter, pub store: BlobStoreWriter, pub bloom: W, } pub struct SegmentWriter where W: Write, { writers: Writers, bloom: bloom::BytesBloomFilter>, } impl SegmentWriter where W: Write, { pub fn new(num_items: usize, writers: Writers) -> Self { let bloom = bloom::BytesBloomFilter::new(num_items as u64, 0.01); Self { writers, bloom } } } impl SegmentWriter where W: Write, { fn insert(&mut self, key: SerializedRef<'_, K>, value: SerializedRef<'_, V>) -> Result<()> { let ptr = self.writers.store.write(key, value)?; let id = self.writers.blob_index.write(&ptr)?; self.writers.id_index.insert(key.as_bytes(), &id)?; self.bloom.insert_raw(key.as_bytes()); Ok(()) } fn finish(self) -> Result<()> { self.writers.id_index.finish()?; self.writers.blob_index.finish()?; self.writers.store.finish()?; let mut wrt = BufWriter::new(self.writers.bloom); bincode::encode_into_std_write(self.bloom, &mut wrt, common::bincode_config())?; wrt.flush()?; Ok(()) } pub fn write_sorted_it<'a, I>(mut self, it: I) -> Result<()> where I: Iterator, SerializedRef<'a, V>)>, { for (key, value) in it { self.insert(key, value)?; } self.finish() } } pub struct Segment { id_index: BlobIdIndex, blob_index: BlobIndex, store: BlobStore, bloom: bloom::BytesBloomFilter>, folder: PathBuf, uuid: uuid::Uuid, } impl Segment { pub fn open>(uuid: uuid::Uuid, folder: P) -> Result { let id_index = BlobIdIndex::open(folder.as_ref().join(BlobIdIndex::::file_name(uuid)))?; let blob_index = BlobIndex::open(folder.as_ref().join(BlobIndex::file_name(uuid)))?; let store = BlobStore::open(folder.as_ref().join(BlobStore::::file_name(uuid)))?; let mut bloom = std::fs::OpenOptions::new() .read(true) .open(folder.as_ref().join(Segment::::bloom_file_name(uuid)))?; let bloom = bincode::decode_from_std_read(&mut bloom, common::bincode_config())?; Ok(Self { uuid, id_index, blob_index, store, bloom, folder: folder.as_ref().to_path_buf(), }) } pub fn bloom_file_name(uuid: uuid::Uuid) -> String { format!("{}.blm", uuid) } pub fn merge>( segments: Vec>, folder: P, ) -> Result>> { if segments.is_empty() { return Ok(None); } if segments.len() == 1 { return Ok(Some(segments.into_iter().next().unwrap())); } let uuid = uuid::Uuid::new_v4(); let it = SortedSegments::new( segments .iter() .map(|s| Peekable::new(s.iter_raw())) .collect(), ); let id_index = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(folder.as_ref().join(BlobIdIndex::::file_name(uuid)))?; let blob_index = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(folder.as_ref().join(BlobIndex::file_name(uuid)))?; let store = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(folder.as_ref().join(BlobStore::::file_name(uuid)))?; let bloom = std::fs::OpenOptions::new() .write(true) .create(true) .truncate(true) .open(folder.as_ref().join(Segment::::bloom_file_name(uuid)))?; let writers = Writers { id_index: BlobIdIndexWriter::new(id_index)?, blob_index: BlobIndexWriter::new(blob_index), store: BlobStoreWriter::new(store), bloom, }; let num_items: usize = segments.iter().map(|s| s.len()).sum(); SegmentWriter::new(num_items, writers).write_sorted_it(it)?; // cleanup old segments for segment in segments { std::fs::remove_file(segment.blob_index.path())?; std::fs::remove_file(segment.id_index.path())?; std::fs::remove_file(segment.store.path())?; std::fs::remove_file(segment.bloom_path())?; } Ok(Some(Segment::open(uuid, folder)?)) } pub fn iter_raw( &self, ) -> impl Iterator, SerializedRef<'_, V>)> + '_ { self.blob_index.iter().map(|ptr| { let blob = self.store.get_raw(&ptr).unwrap(); (blob.key, blob.value) }) } pub fn iter_raw_with_offset( &self, offset: u64, ) -> impl Iterator, SerializedRef<'_, V>)> + '_ { self.blob_index.iter_with_offset(offset).map(|ptr| { let blob = self.store.get_raw(&ptr).unwrap(); (blob.key, blob.value) }) } pub fn get_raw(&self, key: &[u8]) -> Result>> { if !self.bloom.contains_raw(key) { return Ok(None); } if let Some(id) = self.id_index.get(key) { let ptr = self.blob_index.get(id); let blob = self.store.get_raw(&ptr)?; Ok(Some(blob.value)) } else { Ok(None) } } pub fn search_raw<'a, A>( &'a self, query: A, ) -> impl Iterator, SerializedRef<'a, V>)> + 'a where A: fst::Automaton + 'a, { self.id_index.search(query).map(move |(_, id)| { let ptr = self.blob_index.get(id); let blob = self.store.get_raw(&ptr).unwrap(); (blob.key, blob.value) }) } pub fn range_raw<'a, R>( &'a self, range: R, ) -> impl Iterator, SerializedRef<'a, V>)> + 'a where R: RangeBounds>, { self.id_index.range(range).map(move |(_, id)| { let ptr = self.blob_index.get(id); let blob = self.store.get_raw(&ptr).unwrap(); (blob.key, blob.value) }) } pub fn uuid(&self) -> uuid::Uuid { self.uuid } pub fn len(&self) -> usize { self.id_index.len() } pub fn is_empty(&self) -> bool { self.id_index.is_empty() } fn bloom_path(&self) -> PathBuf { self.folder.join(Self::bloom_file_name(self.uuid)) } pub fn move_to>(&mut self, new_folder: P) -> Result<()> { if !new_folder.as_ref().exists() { std::fs::create_dir_all(&new_folder)?; } std::fs::rename( self.blob_index.path(), new_folder .as_ref() .join(self.blob_index.path().file_name().unwrap()), )?; std::fs::rename( self.id_index.path(), new_folder .as_ref() .join(self.id_index.path().file_name().unwrap()), )?; std::fs::rename( self.store.path(), new_folder .as_ref() .join(self.store.path().file_name().unwrap()), )?; std::fs::rename( self.bloom_path(), new_folder.as_ref().join(Self::bloom_file_name(self.uuid)), )?; *self = Segment::open(self.uuid, new_folder)?; Ok(()) } } struct SortedPeekable<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { segment_ord: usize, iter: Peekable, } impl<'a, K, V, I> PartialOrd for SortedPeekable<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a, K, V, I> Ord for SortedPeekable<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { fn cmp(&self, other: &Self) -> std::cmp::Ordering { match (self.iter.peek(), other.iter.peek()) { (Some((a, _)), Some((b, _))) => a .cmp(b) .reverse() .then_with(|| self.segment_ord.cmp(&other.segment_ord)), (Some(_), None) => std::cmp::Ordering::Greater, (None, Some(_)) => std::cmp::Ordering::Less, (None, None) => std::cmp::Ordering::Equal, } } } impl<'a, K, V, I> PartialEq for SortedPeekable<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { fn eq(&self, other: &Self) -> bool { self.iter == other.iter && self.segment_ord == other.segment_ord } } impl<'a, K, V, I> Eq for SortedPeekable<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)> { } pub struct SortedSegments<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { segments: BinaryHeap>, } impl<'a, K, V, I> SortedSegments<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { pub fn new(segments: Vec>) -> Self { let mut heap = BinaryHeap::new(); for item in segments .into_iter() .enumerate() .map(|(segment_ord, iter)| SortedPeekable { segment_ord, iter }) { heap.push(item); } Self { segments: heap } } } impl<'a, K, V, I> Iterator for SortedSegments<'a, K, V, I> where I: Iterator, SerializedRef<'a, V>)>, { type Item = I::Item; fn next(&mut self) -> Option { let (key, value) = { let mut min = self.segments.peek_mut()?; min.iter.next()? }; // advance all segments with the same key while let Some(mut peek) = self.segments.peek_mut() { if peek.iter.peek().map(|(k, _)| k) != Some(&key) { break; } peek.iter.next(); } Some((key, value)) } }