mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-23 11:11:00 -05:00
Migrate InvertedIndexCompressedMmap to UIO (#9144)
* Migrate InvertedIndexCompressedMmap to UniversalRead * Rehaul search_scratch.rs (was scores_memory_pool.rs) - Use `typed_arena::Arena` instead of `bumpalo::Bump`. Reason: bump don't drop. Drawback: `Arena::new()` is not free, and `Arena::clear()` doesn't exist; but this commit has workarounds. - Use names that make more sense. * Misc fixes * InvertedIndexCompressedMmap: explicit S type parameter
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -7530,6 +7530,7 @@ dependencies = [
|
||||
"sha2 0.11.0",
|
||||
"sparse",
|
||||
"tempfile",
|
||||
"typed-arena",
|
||||
"validator",
|
||||
"zerocopy",
|
||||
]
|
||||
@@ -8382,6 +8383,12 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-arena"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.3"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
|
||||
use aligned_vec::{AVec, Alignment, RuntimeAlign};
|
||||
use aligned_vec::Alignment;
|
||||
pub use aligned_vec::{AVec, RuntimeAlign};
|
||||
use bytemuck::PodCastError;
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::MmapFile;
|
||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||
use dataset::Dataset;
|
||||
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||
@@ -104,7 +105,7 @@ fn sparse_vector_index_search_benchmark_impl(
|
||||
let sparse_index_config =
|
||||
SparseIndexConfig::new(Some(FULL_SCAN_THRESHOLD), SparseIndexType::Mmap, None);
|
||||
let pb = progress("Indexing (2/2)", vectors_len);
|
||||
let sparse_vector_index_mmap: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
|
||||
let sparse_vector_index_mmap: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
|
||||
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
|
||||
config: sparse_index_config,
|
||||
id_tracker: sparse_vector_index.id_tracker().clone(),
|
||||
|
||||
@@ -118,9 +118,9 @@ macro_rules! fixture_for_all_indices {
|
||||
::sparse::index::inverted_index::inverted_index_compressed_immutable_ram::InvertedIndexCompressedImmutableRam<f32>
|
||||
>($($args)*);
|
||||
|
||||
eprintln!("InvertedIndexCompressedMmap<f32>");
|
||||
eprintln!("InvertedIndexCompressedMmap<f32, MmapFile>");
|
||||
$test::<
|
||||
::sparse::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexCompressedMmap<f32>
|
||||
::sparse::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexCompressedMmap<f32, MmapFile>
|
||||
>($($args)*);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::generic_consts::Random;
|
||||
use common::storage_version::StorageVersion as _;
|
||||
use fs_err as fs;
|
||||
use sparse::common::scores_memory_pool::ScoresMemoryPool;
|
||||
use sparse::SearchScratchPool;
|
||||
use sparse::common::sparse_vector::SparseVector;
|
||||
use sparse::index::inverted_index::InvertedIndex;
|
||||
use sparse::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
|
||||
@@ -35,7 +35,7 @@ pub struct SparseVectorIndex<TInvertedIndex: InvertedIndex> {
|
||||
inverted_index: TInvertedIndex,
|
||||
searches_telemetry: SparseSearchesTelemetry,
|
||||
indices_tracker: IndicesTracker,
|
||||
scores_memory_pool: ScoresMemoryPool,
|
||||
search_scratch_pool: SearchScratchPool,
|
||||
}
|
||||
|
||||
/// Getters for internals, used for testing.
|
||||
@@ -130,7 +130,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
|
||||
let searches_telemetry = SparseSearchesTelemetry::new();
|
||||
let path = path.to_path_buf();
|
||||
let scores_memory_pool = ScoresMemoryPool::new();
|
||||
let search_scratch_pool = SearchScratchPool::new();
|
||||
Ok(Self {
|
||||
config,
|
||||
id_tracker,
|
||||
@@ -140,7 +140,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
inverted_index,
|
||||
searches_telemetry,
|
||||
indices_tracker,
|
||||
scores_memory_pool,
|
||||
search_scratch_pool,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -227,12 +227,14 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
let hw_counter = HardwareCounterCell::disposable();
|
||||
|
||||
let mut unique_record_ids = std::collections::HashSet::new();
|
||||
let mut arena = sparse::SearchScratchArena::new_slow();
|
||||
for dim_id in &query_vector.indices {
|
||||
if let Some(dim_id) = self.indices_tracker.remap_index(*dim_id) {
|
||||
let posting_list_iter = self.inverted_index.get(dim_id, &hw_counter)?;
|
||||
let posting_list_iter = self.inverted_index.get(dim_id, &arena, &hw_counter)?;
|
||||
for element in posting_list_iter.into_std_iter() {
|
||||
unique_record_ids.insert(element.record_id);
|
||||
}
|
||||
arena.gc();
|
||||
}
|
||||
}
|
||||
Ok(unique_record_ids.len())
|
||||
|
||||
@@ -132,7 +132,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
.collect_vec();
|
||||
|
||||
let sparse_vector = self.indices_tracker.remap_vector(sparse_vector.clone());
|
||||
let memory_handle = self.scores_memory_pool.get();
|
||||
let mut scratch = self.search_scratch_pool.get();
|
||||
let mut hw_counter = vector_query_context.hardware_counter();
|
||||
let is_index_on_disk = self.config.index_type.is_on_disk();
|
||||
if is_index_on_disk {
|
||||
@@ -145,7 +145,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
sparse_vector,
|
||||
top,
|
||||
&self.inverted_index,
|
||||
memory_handle,
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hw_counter,
|
||||
)?;
|
||||
@@ -175,7 +175,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
let is_stopped = vector_query_context.is_stopped();
|
||||
|
||||
let sparse_vector = self.indices_tracker.remap_vector(sparse_vector.clone());
|
||||
let memory_handle = self.scores_memory_pool.get();
|
||||
let mut scratch = self.search_scratch_pool.get();
|
||||
let mut hw_counter = vector_query_context.hardware_counter();
|
||||
let is_index_on_disk = self.config.index_type.is_on_disk();
|
||||
if is_index_on_disk {
|
||||
@@ -188,7 +188,7 @@ impl<TInvertedIndex: InvertedIndex> SparseVectorIndex<TInvertedIndex> {
|
||||
sparse_vector,
|
||||
top,
|
||||
&self.inverted_index,
|
||||
memory_handle,
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hw_counter,
|
||||
)?;
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::PathBuf;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::types::{PointOffsetType, ScoredPointOffset, TelemetryDetail};
|
||||
use common::universal_io::MmapFile;
|
||||
use half::f16;
|
||||
use sparse::common::types::{DimId, QuantizedU8};
|
||||
use sparse::index::inverted_index::InvertedIndex;
|
||||
@@ -95,9 +96,9 @@ pub enum VectorIndexEnum {
|
||||
SparseCompressedImmutableRamU8(
|
||||
SparseVectorIndex<InvertedIndexCompressedImmutableRam<QuantizedU8>>,
|
||||
),
|
||||
SparseCompressedMmapF32(SparseVectorIndex<InvertedIndexCompressedMmap<f32>>),
|
||||
SparseCompressedMmapF16(SparseVectorIndex<InvertedIndexCompressedMmap<f16>>),
|
||||
SparseCompressedMmapU8(SparseVectorIndex<InvertedIndexCompressedMmap<QuantizedU8>>),
|
||||
SparseCompressedMmapF32(SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>>),
|
||||
SparseCompressedMmapF16(SparseVectorIndex<InvertedIndexCompressedMmap<f16, MmapFile>>),
|
||||
SparseCompressedMmapU8(SparseVectorIndex<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>),
|
||||
}
|
||||
|
||||
impl VectorIndexEnum {
|
||||
|
||||
@@ -6,6 +6,7 @@ use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::generic_consts::Random;
|
||||
use common::storage_version::VERSION_FILE;
|
||||
use common::types::{PointOffsetType, TelemetryDetail};
|
||||
use common::universal_io::MmapFile;
|
||||
use fs_err as fs;
|
||||
use itertools::Itertools;
|
||||
use rand::SeedableRng;
|
||||
@@ -170,9 +171,10 @@ fn check_index_storage_consistency<T: InvertedIndex>(sparse_vector_index: &Spars
|
||||
.iter()
|
||||
.zip(remapped_vector.values.iter())
|
||||
{
|
||||
let arena = sparse::SearchScratchArena::new_slow();
|
||||
let posting_list = sparse_vector_index
|
||||
.inverted_index()
|
||||
.get(*dim_id, &hw_counter)
|
||||
.get(*dim_id, &arena, &hw_counter)
|
||||
.unwrap();
|
||||
// assert posting list sorted by record id
|
||||
assert!(
|
||||
@@ -221,7 +223,7 @@ fn sparse_vector_index_consistent_with_storage() {
|
||||
// create mmap sparse vector index
|
||||
let mut sparse_index_config = sparse_vector_ram_index.config();
|
||||
sparse_index_config.index_type = SparseIndexType::Mmap;
|
||||
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
|
||||
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
|
||||
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
|
||||
config: sparse_index_config,
|
||||
id_tracker: sparse_vector_ram_index.id_tracker().clone(),
|
||||
@@ -247,7 +249,7 @@ fn sparse_vector_index_consistent_with_storage() {
|
||||
// load index from memmap file
|
||||
let mut sparse_index_config = sparse_vector_ram_index.config();
|
||||
sparse_index_config.index_type = SparseIndexType::Mmap;
|
||||
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32>> =
|
||||
let sparse_vector_mmap_index: SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>> =
|
||||
SparseVectorIndex::open(SparseVectorIndexOpenArgs {
|
||||
config: sparse_index_config,
|
||||
id_tracker: sparse_vector_ram_index.id_tracker().clone(),
|
||||
@@ -271,13 +273,14 @@ fn sparse_vector_index_consistent_with_storage() {
|
||||
#[test]
|
||||
fn sparse_vector_index_load_missing_mmap() {
|
||||
let data_dir = Builder::new().prefix("data_dir").tempdir().unwrap();
|
||||
let sparse_vector_index: OperationResult<SparseVectorIndex<InvertedIndexCompressedMmap<f32>>> =
|
||||
fixture_sparse_index_from_iter(
|
||||
data_dir.path(),
|
||||
[].iter().cloned(),
|
||||
10_000,
|
||||
SparseIndexType::Mmap,
|
||||
);
|
||||
let sparse_vector_index: OperationResult<
|
||||
SparseVectorIndex<InvertedIndexCompressedMmap<f32, MmapFile>>,
|
||||
> = fixture_sparse_index_from_iter(
|
||||
data_dir.path(),
|
||||
[].iter().cloned(),
|
||||
10_000,
|
||||
SparseIndexType::Mmap,
|
||||
);
|
||||
// absent configuration file for mmap are ignored
|
||||
// a new index is created
|
||||
assert!(sparse_vector_index.is_ok())
|
||||
|
||||
@@ -33,6 +33,7 @@ itertools = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
log = { workspace = true }
|
||||
zerocopy = { workspace = true }
|
||||
typed-arena = "2.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::MmapFile;
|
||||
use criterion::measurement::Measurement;
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use dataset::Dataset;
|
||||
@@ -13,7 +14,7 @@ use itertools::Itertools;
|
||||
use rand::SeedableRng as _;
|
||||
use rand::rngs::StdRng;
|
||||
use sha2::Digest;
|
||||
use sparse::common::scores_memory_pool::ScoresMemoryPool;
|
||||
use sparse::SearchScratchPool;
|
||||
use sparse::common::sparse_vector::{RemappedSparseVector, SparseVector};
|
||||
use sparse::common::sparse_vector_fixture::{random_positive_sparse_vector, random_sparse_vector};
|
||||
use sparse::common::types::{QuantizedU8, Weight};
|
||||
@@ -162,7 +163,7 @@ pub fn run_bench(
|
||||
|
||||
run_bench2(
|
||||
c.benchmark_group(format!("search/mmap_{}/{name}", $name)),
|
||||
&InvertedIndexCompressedMmap::<$type>::open(&index_path).unwrap(),
|
||||
&InvertedIndexCompressedMmap::<$type, MmapFile>::open(&index_path).unwrap(),
|
||||
query_vectors,
|
||||
&hottest_query_vectors,
|
||||
);
|
||||
@@ -181,7 +182,7 @@ fn run_bench2(
|
||||
query_vectors: &[SparseVector],
|
||||
hottest_query_vectors: &[RemappedSparseVector],
|
||||
) {
|
||||
let pool = ScoresMemoryPool::new();
|
||||
let pool = SearchScratchPool::new();
|
||||
let stopped = AtomicBool::new(false);
|
||||
|
||||
let mut it = query_vectors.iter().cycle();
|
||||
@@ -192,7 +193,8 @@ fn run_bench2(
|
||||
b.iter_batched(
|
||||
|| it.next().unwrap().clone().into_remapped(),
|
||||
|vec| {
|
||||
SearchContext::new(vec, TOP, index, pool.get(), &stopped, &hardware_counter)
|
||||
let mut scratch = pool.get();
|
||||
SearchContext::new(vec, TOP, index, &mut scratch, &stopped, &hardware_counter)
|
||||
.unwrap()
|
||||
.search(&|_| true)
|
||||
},
|
||||
@@ -207,7 +209,8 @@ fn run_bench2(
|
||||
b.iter_batched(
|
||||
|| it.next().unwrap().clone(),
|
||||
|vec| {
|
||||
SearchContext::new(vec, TOP, index, pool.get(), &stopped, &hardware_counter)
|
||||
let mut scratch = pool.get();
|
||||
SearchContext::new(vec, TOP, index, &mut scratch, &stopped, &hardware_counter)
|
||||
.unwrap()
|
||||
.search(&|_| true)
|
||||
},
|
||||
@@ -267,7 +270,8 @@ fn cached_compressed_index<W: Weight>(index: &InvertedIndexRam, name: &str) -> P
|
||||
fs::remove_dir_all(&tmp_path).unwrap();
|
||||
}
|
||||
fs::create_dir_all(&tmp_path).unwrap();
|
||||
InvertedIndexCompressedMmap::<W>::from_ram_index(Cow::Borrowed(index), &tmp_path).unwrap();
|
||||
InvertedIndexCompressedMmap::<W, MmapFile>::from_ram_index(Cow::Borrowed(index), &tmp_path)
|
||||
.unwrap();
|
||||
fs::rename(tmp_path, &path).unwrap();
|
||||
}
|
||||
eprintln!("Using cache: {path:?}");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod scores_memory_pool;
|
||||
pub mod sparse_vector;
|
||||
#[cfg(feature = "testing")]
|
||||
pub mod sparse_vector_fixture;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
use common::defaults::POOL_KEEP_LIMIT;
|
||||
use common::types::ScoreType;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
type PooledScores = Vec<ScoreType>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PooledScoresHandle<'a> {
|
||||
pool: &'a ScoresMemoryPool,
|
||||
pub scores: PooledScores,
|
||||
}
|
||||
|
||||
impl<'a> PooledScoresHandle<'a> {
|
||||
fn new(pool: &'a ScoresMemoryPool, scores: PooledScores) -> Self {
|
||||
PooledScoresHandle { pool, scores }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledScoresHandle<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.pool.return_back(std::mem::take(&mut self.scores));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScoresMemoryPool {
|
||||
pool: Mutex<Vec<PooledScores>>,
|
||||
}
|
||||
|
||||
impl ScoresMemoryPool {
|
||||
pub fn new() -> Self {
|
||||
ScoresMemoryPool {
|
||||
pool: Mutex::new(Vec::with_capacity(*POOL_KEEP_LIMIT)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> PooledScoresHandle<'_> {
|
||||
match self.pool.lock().pop() {
|
||||
None => PooledScoresHandle::new(self, vec![]),
|
||||
Some(data) => PooledScoresHandle::new(self, data),
|
||||
}
|
||||
}
|
||||
|
||||
fn return_back(&self, data: PooledScores) {
|
||||
let mut pool = self.pool.lock();
|
||||
if pool.len() < *POOL_KEEP_LIMIT {
|
||||
pool.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScoresMemoryPool {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ use crate::common::types::{DimWeight, Weight};
|
||||
type BitPackerImpl = bitpacking::BitPacker4x;
|
||||
|
||||
/// How many elements are packed in a single chunk.
|
||||
const CHUNK_SIZE: usize = BitPackerImpl::BLOCK_LEN;
|
||||
pub(super) const CHUNK_SIZE: usize = BitPackerImpl::BLOCK_LEN;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub struct CompressedPostingList<W: Weight> {
|
||||
|
||||
@@ -3,17 +3,18 @@ use std::path::Path;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::Result;
|
||||
use common::universal_io::{MmapFs, Result};
|
||||
|
||||
use super::inverted_index_compressed_mmap::InvertedIndexCompressedMmap;
|
||||
use super::inverted_index_ram::InvertedIndexRam;
|
||||
use super::{InvertedIndex, out_of_bounds};
|
||||
use crate::SearchScratchArena;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::common::types::{DimId, DimOffset, Weight};
|
||||
use crate::index::compressed_posting_list::{
|
||||
CompressedPostingBuilder, CompressedPostingList, CompressedPostingListIterator,
|
||||
CompressedPostingListView,
|
||||
};
|
||||
use crate::index::posting_list_common::PostingListIter as _;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct InvertedIndexCompressedImmutableRam<W: Weight> {
|
||||
@@ -22,17 +23,19 @@ pub struct InvertedIndexCompressedImmutableRam<W: Weight> {
|
||||
pub(super) total_sparse_size: usize,
|
||||
}
|
||||
|
||||
type Storage = common::universal_io::MmapFile;
|
||||
|
||||
impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
type Iter<'a> = CompressedPostingListIterator<'a, W>;
|
||||
|
||||
type Version = <InvertedIndexCompressedMmap<W> as InvertedIndex>::Version;
|
||||
type Version = <InvertedIndexCompressedMmap<W, Storage> as InvertedIndex>::Version;
|
||||
|
||||
fn is_on_disk(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn open(path: &Path) -> Result<Self> {
|
||||
let mmap_inverted_index = InvertedIndexCompressedMmap::load(path)?;
|
||||
let mmap_inverted_index = InvertedIndexCompressedMmap::<W, Storage>::load(&MmapFs, path)?;
|
||||
let mut inverted_index = InvertedIndexCompressedImmutableRam {
|
||||
postings: Vec::with_capacity(mmap_inverted_index.file_header.posting_count),
|
||||
vector_count: mmap_inverted_index.file_header.vector_count,
|
||||
@@ -41,9 +44,11 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
|
||||
let hw_counter = HardwareCounterCell::disposable();
|
||||
|
||||
let mut arena = SearchScratchArena::new_slow();
|
||||
for i in 0..mmap_inverted_index.file_header.posting_count as DimId {
|
||||
let posting_list = mmap_inverted_index.get(i, &hw_counter)?;
|
||||
let posting_list = mmap_inverted_index.get(i, &arena, &hw_counter)?;
|
||||
inverted_index.postings.push(posting_list.to_owned());
|
||||
arena.gc();
|
||||
}
|
||||
|
||||
mmap_inverted_index.clear_cache()?;
|
||||
@@ -51,21 +56,18 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
Ok(inverted_index)
|
||||
}
|
||||
|
||||
fn save(&self, path: &Path) -> std::io::Result<()> {
|
||||
InvertedIndexCompressedMmap::convert_and_save(self, path)?;
|
||||
fn save(&self, path: &Path) -> Result<()> {
|
||||
InvertedIndexCompressedMmap::<W, Storage>::convert_and_save(&MmapFs, self, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
id: DimOffset,
|
||||
_arena: &'a SearchScratchArena,
|
||||
hw_counter: &'a HardwareCounterCell, // Ignored for in-ram index
|
||||
) -> Result<Self::Iter<'a>> {
|
||||
let posting = self
|
||||
.postings
|
||||
.get(id as usize)
|
||||
.ok_or(out_of_bounds(id, self.len()))?;
|
||||
Ok(posting.iter(hw_counter))
|
||||
Ok(self.get(id, hw_counter)?.iter())
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
@@ -73,16 +75,16 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
}
|
||||
|
||||
fn posting_list_len(&self, id: DimOffset, hw_counter: &HardwareCounterCell) -> Result<usize> {
|
||||
Ok(self.get(id, hw_counter)?.len_to_end())
|
||||
Ok(self.get(id, hw_counter)?.len())
|
||||
}
|
||||
|
||||
fn files(path: &Path) -> Vec<std::path::PathBuf> {
|
||||
InvertedIndexCompressedMmap::<W>::files(path)
|
||||
InvertedIndexCompressedMmap::<W, Storage>::files(path)
|
||||
}
|
||||
|
||||
fn immutable_files(path: &Path) -> Vec<std::path::PathBuf> {
|
||||
// `InvertedIndexCompressedImmutableRam` is always immutable
|
||||
InvertedIndexCompressedMmap::<W>::immutable_files(path)
|
||||
InvertedIndexCompressedMmap::<W, Storage>::immutable_files(path)
|
||||
}
|
||||
|
||||
fn remove(&mut self, _id: PointOffsetType, _old_vector: RemappedSparseVector) {
|
||||
@@ -98,10 +100,7 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
panic!("Cannot upsert into a read-only RAM inverted index")
|
||||
}
|
||||
|
||||
fn from_ram_index<P: AsRef<Path>>(
|
||||
ram_index: Cow<InvertedIndexRam>,
|
||||
_path: P,
|
||||
) -> std::io::Result<Self> {
|
||||
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, _path: P) -> Result<Self> {
|
||||
let mut postings = Vec::with_capacity(ram_index.postings.len());
|
||||
for old_posting_list in &ram_index.postings {
|
||||
let mut new_posting_list = CompressedPostingBuilder::new();
|
||||
@@ -141,6 +140,20 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedImmutableRam<W> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Weight> InvertedIndexCompressedImmutableRam<W> {
|
||||
#[inline]
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
id: DimOffset,
|
||||
hw_counter: &'a HardwareCounterCell,
|
||||
) -> Result<CompressedPostingListView<'a, W>> {
|
||||
let Some(posting) = self.postings.get(id as usize) else {
|
||||
return Err(out_of_bounds(id, self.len()));
|
||||
};
|
||||
Ok(posting.view(hw_counter))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::Builder;
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use std::io::{BufWriter, Write as _};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::size_of;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::ext::aligned_vec::ACow;
|
||||
use common::fs::{atomic_save_json, read_json};
|
||||
use common::mmap::{Advice, AdviceSetting, Madviseable, create_and_ensure_length, open_read_mmap};
|
||||
use common::generic_consts::Random;
|
||||
use common::mmap::{Advice, AdviceSetting, create_and_ensure_length};
|
||||
#[expect(deprecated, reason = "legacy code")]
|
||||
use common::mmap::{transmute_to_u8, transmute_to_u8_slice};
|
||||
use common::storage_version::StorageVersion;
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::Result;
|
||||
use memmap2::Mmap;
|
||||
use common::universal_io::{OpenOptions, Populate, Result, UniversalRead, UniversalReadFs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zerocopy::{FromBytes, Immutable, KnownLayout};
|
||||
|
||||
use super::inverted_index_compressed_immutable_ram::InvertedIndexCompressedImmutableRam;
|
||||
use super::{INDEX_FILE_NAME, corrupted_index, out_of_bounds};
|
||||
use crate::SearchScratchArena;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::common::types::{DimId, DimOffset, Weight};
|
||||
use crate::index::compressed_posting_list::{
|
||||
CompressedPostingChunk, CompressedPostingListIterator, CompressedPostingListView,
|
||||
CHUNK_SIZE, CompressedPostingChunk, CompressedPostingListIterator, CompressedPostingListView,
|
||||
};
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
|
||||
@@ -53,14 +55,14 @@ pub struct InvertedIndexFileHeader {
|
||||
|
||||
/// Inverted flatten index from dimension id to posting list
|
||||
#[derive(Debug)]
|
||||
pub struct InvertedIndexCompressedMmap<W> {
|
||||
pub struct InvertedIndexCompressedMmap<W, S: UniversalRead> {
|
||||
path: PathBuf,
|
||||
mmap: Arc<Mmap>,
|
||||
storage: S,
|
||||
pub file_header: InvertedIndexFileHeader,
|
||||
_phantom: PhantomData<W>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, FromBytes, Immutable, KnownLayout)]
|
||||
#[derive(Debug, Default, Copy, Clone, FromBytes, Immutable, KnownLayout)]
|
||||
#[repr(C)]
|
||||
struct PostingListFileHeader<W: Weight> {
|
||||
pub ids_start: u64,
|
||||
@@ -73,7 +75,24 @@ struct PostingListFileHeader<W: Weight> {
|
||||
pub quantization_params: W::QuantizationParams,
|
||||
}
|
||||
|
||||
impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
impl<W: Weight> PostingListFileHeader<W> {
|
||||
fn postings_count(&self, remainders_end: u64) -> Option<usize> {
|
||||
let chunks_bytes = self.chunks_count as usize * size_of::<CompressedPostingChunk<W>>();
|
||||
let data_len = remainders_end.checked_sub(self.ids_start)? as usize;
|
||||
let remainders_bytes = data_len.checked_sub(self.ids_len as usize + chunks_bytes)?;
|
||||
let elem_size = size_of::<GenericPostingElement<W>>();
|
||||
if !remainders_bytes.is_multiple_of(elem_size) {
|
||||
return None;
|
||||
}
|
||||
let remainders_count = remainders_bytes / elem_size;
|
||||
Some(self.chunks_count as usize * CHUNK_SIZE + remainders_count)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Weight, S: UniversalRead + 'static> InvertedIndex for InvertedIndexCompressedMmap<W, S>
|
||||
where
|
||||
S::Fs: Default,
|
||||
{
|
||||
type Iter<'a> = CompressedPostingListIterator<'a, W>;
|
||||
|
||||
type Version = Version;
|
||||
@@ -83,10 +102,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
}
|
||||
|
||||
fn open(path: &Path) -> Result<Self> {
|
||||
Self::load(path)
|
||||
Self::load(&S::Fs::default(), path)
|
||||
}
|
||||
|
||||
fn save(&self, path: &Path) -> std::io::Result<()> {
|
||||
fn save(&self, path: &Path) -> Result<()> {
|
||||
debug_assert_eq!(path, self.path);
|
||||
|
||||
// If Self instance exists, it's either constructed by using `open()` (which reads index
|
||||
@@ -102,9 +121,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
id: DimOffset,
|
||||
arena: &'a SearchScratchArena,
|
||||
hw_counter: &'a HardwareCounterCell,
|
||||
) -> Result<CompressedPostingListIterator<'a, W>> {
|
||||
Ok(self.get(id, hw_counter)?.iter())
|
||||
Ok(self.get(id, arena, hw_counter)?.iter())
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
@@ -112,7 +132,10 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
}
|
||||
|
||||
fn posting_list_len(&self, id: DimOffset, hw_counter: &HardwareCounterCell) -> Result<usize> {
|
||||
Ok(self.get(id, hw_counter)?.len())
|
||||
let (header, remainders_end) = self.read_posting_header(id, hw_counter)?;
|
||||
header
|
||||
.postings_count(remainders_end)
|
||||
.ok_or_else(corrupted_index)
|
||||
}
|
||||
|
||||
fn files(path: &Path) -> Vec<PathBuf> {
|
||||
@@ -140,12 +163,9 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
panic!("Cannot upsert into a read-only Mmap inverted index")
|
||||
}
|
||||
|
||||
fn from_ram_index<P: AsRef<Path>>(
|
||||
ram_index: Cow<InvertedIndexRam>,
|
||||
path: P,
|
||||
) -> std::io::Result<Self> {
|
||||
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, path: P) -> Result<Self> {
|
||||
let index = InvertedIndexCompressedImmutableRam::from_ram_index(ram_index, &path)?;
|
||||
Self::convert_and_save(&index, path)
|
||||
Self::convert_and_save(&S::Fs::default(), &index, path)
|
||||
}
|
||||
|
||||
fn vector_count(&self) -> usize {
|
||||
@@ -168,7 +188,7 @@ impl<W: Weight> InvertedIndex for InvertedIndexCompressedMmap<W> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
impl<W: Weight, S: UniversalRead + Debug + 'static> InvertedIndexCompressedMmap<W, S> {
|
||||
const HEADER_SIZE: usize = size_of::<PostingListFileHeader<W>>();
|
||||
|
||||
pub fn index_file_path(path: &Path) -> PathBuf {
|
||||
@@ -182,42 +202,31 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
pub fn get<'a>(
|
||||
&'a self,
|
||||
id: DimId,
|
||||
arena: &'a SearchScratchArena,
|
||||
hw_counter: &'a HardwareCounterCell,
|
||||
) -> Result<CompressedPostingListView<'a, W>> {
|
||||
// check that the id is not out of bounds (posting_count includes the empty zeroth entry)
|
||||
if id >= self.file_header.posting_count as DimId {
|
||||
return Err(out_of_bounds(id, self.file_header.posting_count));
|
||||
}
|
||||
let (header, remainders_end) = self.read_posting_header(id, hw_counter)?;
|
||||
|
||||
let header = self.read_header(id)?.clone();
|
||||
|
||||
hw_counter.vector_io_read().incr_delta(Self::HEADER_SIZE);
|
||||
|
||||
let remainders_start = header.ids_start
|
||||
+ u64::from(header.ids_len)
|
||||
+ u64::from(header.chunks_count) * size_of::<CompressedPostingChunk<W>>() as u64;
|
||||
|
||||
let remainders_end = if id + 1 < self.file_header.posting_count as DimId {
|
||||
self.read_header(id + 1)?.ids_start
|
||||
} else {
|
||||
self.mmap.len() as u64
|
||||
let data_bytes = self.storage.read_bytes::<Random>(
|
||||
header.ids_start..remainders_end,
|
||||
align_of::<(CompressedPostingChunk<W>, GenericPostingElement<W>)>(),
|
||||
)?;
|
||||
let data = match data_bytes {
|
||||
ACow::Borrowed(b) => b,
|
||||
ACow::Owned(avec) => arena.alloc(avec),
|
||||
};
|
||||
|
||||
if remainders_end
|
||||
.checked_sub(remainders_start)
|
||||
.is_some_and(|len| len % size_of::<GenericPostingElement<W>>() as u64 != 0)
|
||||
{
|
||||
return Err(corrupted_index());
|
||||
}
|
||||
|
||||
let id_data = self.byte_slice(header.ids_start, u64::from(header.ids_len))?;
|
||||
let chunks = <[CompressedPostingChunk<W>]>::ref_from_bytes(self.byte_slice(
|
||||
header.ids_start + u64::from(header.ids_len),
|
||||
u64::from(header.chunks_count) * size_of::<CompressedPostingChunk<W>>() as u64,
|
||||
)?)
|
||||
let ids_len = header.ids_len as usize;
|
||||
let chunks_bytes = header.chunks_count as usize * size_of::<CompressedPostingChunk<W>>();
|
||||
let id_data = data.get(..ids_len).ok_or_else(corrupted_index)?;
|
||||
let chunks = <[CompressedPostingChunk<W>]>::ref_from_bytes(
|
||||
data.get(ids_len..ids_len + chunks_bytes)
|
||||
.ok_or_else(corrupted_index)?,
|
||||
)
|
||||
.map_err(|_| corrupted_index())?;
|
||||
let remainders = <[GenericPostingElement<W>]>::ref_from_bytes(
|
||||
&self.mmap[remainders_start as usize..remainders_end as usize],
|
||||
data.get(ids_len + chunks_bytes..)
|
||||
.ok_or_else(corrupted_index)?,
|
||||
)
|
||||
.map_err(|_| corrupted_index())?;
|
||||
|
||||
@@ -231,25 +240,42 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
))
|
||||
}
|
||||
|
||||
fn read_header(&self, id: DimId) -> Result<&PostingListFileHeader<W>> {
|
||||
let start = id as usize * Self::HEADER_SIZE;
|
||||
let end = start + Self::HEADER_SIZE;
|
||||
let bytes = self.mmap.get(start..end).ok_or_else(corrupted_index)?;
|
||||
PostingListFileHeader::<W>::ref_from_bytes(bytes).map_err(|_| corrupted_index())
|
||||
}
|
||||
/// Read the header for `id`, plus the next header's `ids_start`
|
||||
/// (which doubles as this entry's remainders end).
|
||||
fn read_posting_header(
|
||||
&self,
|
||||
id: DimId,
|
||||
hw_counter: &HardwareCounterCell,
|
||||
) -> Result<(PostingListFileHeader<W>, u64)> {
|
||||
if id >= self.file_header.posting_count as DimId {
|
||||
return Err(out_of_bounds(id, self.file_header.posting_count));
|
||||
}
|
||||
|
||||
fn byte_slice(&self, start: u64, len: u64) -> Result<&[u8]> {
|
||||
let start = start as usize;
|
||||
let end = start
|
||||
.checked_add(len as usize)
|
||||
.ok_or_else(corrupted_index)?;
|
||||
self.mmap.get(start..end).ok_or_else(corrupted_index)
|
||||
let header_start = u64::from(id) * Self::HEADER_SIZE as u64;
|
||||
let has_next = id + 1 < self.file_header.posting_count as DimId;
|
||||
let read_size = Self::HEADER_SIZE + if has_next { size_of::<u64>() } else { 0 };
|
||||
let header_bytes = self.storage.read_bytes::<Random>(
|
||||
header_start..header_start + read_size as u64,
|
||||
align_of::<PostingListFileHeader<W>>(),
|
||||
)?;
|
||||
let (&header, rest) = PostingListFileHeader::<W>::ref_from_prefix(&header_bytes)
|
||||
.map_err(|_| corrupted_index())?;
|
||||
let remainders_end = if has_next {
|
||||
*u64::ref_from_bytes(rest).map_err(|_| corrupted_index())?
|
||||
} else {
|
||||
self.storage.len::<u8>()?
|
||||
};
|
||||
|
||||
hw_counter.vector_io_read().incr_delta(read_size);
|
||||
|
||||
Ok((header, remainders_end))
|
||||
}
|
||||
|
||||
pub fn convert_and_save<P: AsRef<Path>>(
|
||||
fs: &S::Fs,
|
||||
index: &InvertedIndexCompressedImmutableRam<W>,
|
||||
path: P,
|
||||
) -> std::io::Result<Self> {
|
||||
) -> Result<Self> {
|
||||
let total_posting_headers_size =
|
||||
index.postings.as_slice().len() * size_of::<PostingListFileHeader<W>>();
|
||||
|
||||
@@ -264,6 +290,7 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
.map(|p| p.view(&hw_counter).store_size().total)
|
||||
.sum::<usize>();
|
||||
let file_path = Self::index_file_path(path.as_ref());
|
||||
// TODO(uio): use UIO for writing as well.
|
||||
let file = create_and_ensure_length(file_path.as_ref(), file_length)?;
|
||||
|
||||
let mut buf = BufWriter::new(file);
|
||||
@@ -312,34 +339,46 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
|
||||
atomic_save_json(&Self::index_config_file_path(path.as_ref()), &file_header)?;
|
||||
|
||||
let storage = fs.open(
|
||||
&file_path,
|
||||
OpenOptions {
|
||||
writeable: false,
|
||||
need_sequential: false,
|
||||
populate: Populate::No,
|
||||
advice: AdviceSetting::Global,
|
||||
},
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
path: path.as_ref().to_owned(),
|
||||
mmap: Arc::new(open_read_mmap(
|
||||
file_path.as_ref(),
|
||||
AdviceSetting::Global,
|
||||
false,
|
||||
)?),
|
||||
storage,
|
||||
file_header,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
pub fn load<P: AsRef<Path>>(fs: &S::Fs, path: P) -> Result<Self> {
|
||||
// read index config file
|
||||
let config_file_path = Self::index_config_file_path(path.as_ref());
|
||||
// if the file header does not exist, the index is malformed
|
||||
let file_header: InvertedIndexFileHeader = read_json(&config_file_path)?;
|
||||
// read index data into mmap
|
||||
// open index data via universal IO
|
||||
let file_path = Self::index_file_path(path.as_ref());
|
||||
let mmap = open_read_mmap(
|
||||
file_path.as_ref(),
|
||||
AdviceSetting::from(Advice::Normal),
|
||||
false,
|
||||
let storage = fs.open(
|
||||
&file_path,
|
||||
OpenOptions {
|
||||
writeable: false,
|
||||
need_sequential: false,
|
||||
populate: Populate::No,
|
||||
advice: AdviceSetting::Advice(Advice::Normal),
|
||||
},
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let mut index = Self {
|
||||
path: path.as_ref().to_owned(),
|
||||
mmap: Arc::new(mmap),
|
||||
storage,
|
||||
file_header,
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
@@ -348,63 +387,56 @@ impl<W: Weight> InvertedIndexCompressedMmap<W> {
|
||||
|
||||
if index.file_header.total_sparse_size.is_none() {
|
||||
index.file_header.total_sparse_size =
|
||||
Some(index.calculate_total_sparse_size(&hw_counter));
|
||||
Some(index.calculate_total_sparse_size(&hw_counter)?);
|
||||
atomic_save_json(&config_file_path, &index.file_header)?;
|
||||
}
|
||||
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
fn calculate_total_sparse_size(&self, hw_counter: &HardwareCounterCell) -> usize {
|
||||
(0..self.file_header.posting_count as DimId)
|
||||
.filter_map(|id| {
|
||||
self.get(id, hw_counter)
|
||||
.map(|posting| posting.store_size().total)
|
||||
.ok()
|
||||
})
|
||||
.sum()
|
||||
fn calculate_total_sparse_size(&self, hw_counter: &HardwareCounterCell) -> Result<usize> {
|
||||
let mut total = 0;
|
||||
let mut arena = SearchScratchArena::new_slow();
|
||||
for id in 0..self.file_header.posting_count as DimId {
|
||||
total += self.get(id, &arena, hw_counter)?.store_size().total;
|
||||
arena.gc();
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// Populate all pages in the mmap.
|
||||
/// Block until all pages are populated.
|
||||
pub fn populate(&self) -> std::io::Result<()> {
|
||||
self.mmap.populate();
|
||||
Ok(())
|
||||
/// Populate the underlying storage in RAM cache. Block until completed.
|
||||
pub fn populate(&self) -> Result<()> {
|
||||
self.storage.populate()
|
||||
}
|
||||
|
||||
/// Drop disk cache.
|
||||
pub fn clear_cache(&self) -> std::io::Result<()> {
|
||||
let Self {
|
||||
path: _,
|
||||
mmap,
|
||||
file_header: _,
|
||||
_phantom,
|
||||
} = self;
|
||||
mmap.clear_cache();
|
||||
Ok(())
|
||||
pub fn clear_cache(&self) -> Result<()> {
|
||||
self.storage.clear_ram_cache()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use common::universal_io::MmapFile;
|
||||
use tempfile::Builder;
|
||||
|
||||
use super::*;
|
||||
use crate::common::types::QuantizedU8;
|
||||
use crate::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
|
||||
|
||||
fn compare_indexes<W: Weight>(
|
||||
fn compare_indexes<S: UniversalRead + 'static, W: Weight>(
|
||||
inverted_index_ram: &InvertedIndexCompressedImmutableRam<W>,
|
||||
inverted_index_mmap: &InvertedIndexCompressedMmap<W>,
|
||||
inverted_index_mmap: &InvertedIndexCompressedMmap<W, S>,
|
||||
) {
|
||||
let hw_counter = HardwareCounterCell::new();
|
||||
let arena = SearchScratchArena::new_slow();
|
||||
for id in 0..inverted_index_ram.postings.len() as DimId {
|
||||
let posting_list_ram = inverted_index_ram
|
||||
.postings
|
||||
.get(id as usize)
|
||||
.unwrap()
|
||||
.view(&hw_counter);
|
||||
let posting_list_mmap = inverted_index_mmap.get(id, &hw_counter).unwrap();
|
||||
let posting_list_mmap = inverted_index_mmap.get(id, &arena, &hw_counter).unwrap();
|
||||
|
||||
let mmap_parts = posting_list_mmap.parts();
|
||||
let ram_parts = posting_list_ram.parts();
|
||||
@@ -415,13 +447,25 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_inverted_index_mmap() {
|
||||
check_inverted_index_mmap::<f32>();
|
||||
check_inverted_index_mmap::<half::f16>();
|
||||
check_inverted_index_mmap::<u8>();
|
||||
check_inverted_index_mmap::<QuantizedU8>();
|
||||
check_inverted_index_mmap::<MmapFile, f32>();
|
||||
check_inverted_index_mmap::<MmapFile, half::f16>();
|
||||
check_inverted_index_mmap::<MmapFile, u8>();
|
||||
check_inverted_index_mmap::<MmapFile, QuantizedU8>();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use common::universal_io::IoUringFile;
|
||||
check_inverted_index_mmap::<IoUringFile, f32>();
|
||||
check_inverted_index_mmap::<IoUringFile, half::f16>();
|
||||
check_inverted_index_mmap::<IoUringFile, u8>();
|
||||
check_inverted_index_mmap::<IoUringFile, QuantizedU8>();
|
||||
}
|
||||
}
|
||||
|
||||
fn check_inverted_index_mmap<W: Weight>() {
|
||||
fn check_inverted_index_mmap<S: UniversalRead + 'static, W: Weight>()
|
||||
where
|
||||
S::Fs: Default,
|
||||
{
|
||||
let hw_counter = HardwareCounterCell::new();
|
||||
|
||||
// skip 4th dimension
|
||||
@@ -446,7 +490,8 @@ mod tests {
|
||||
let tmp_dir_path = Builder::new().prefix("test_index_dir2").tempdir().unwrap();
|
||||
|
||||
{
|
||||
let inverted_index_mmap = InvertedIndexCompressedMmap::<W>::convert_and_save(
|
||||
let inverted_index_mmap = InvertedIndexCompressedMmap::<W, S>::convert_and_save(
|
||||
&S::Fs::default(),
|
||||
&inverted_index_ram,
|
||||
&tmp_dir_path,
|
||||
)
|
||||
@@ -454,22 +499,25 @@ mod tests {
|
||||
|
||||
compare_indexes(&inverted_index_ram, &inverted_index_mmap);
|
||||
}
|
||||
let inverted_index_mmap = InvertedIndexCompressedMmap::<W>::load(&tmp_dir_path).unwrap();
|
||||
let index =
|
||||
InvertedIndexCompressedMmap::<W, MmapFile>::load(&Default::default(), &tmp_dir_path)
|
||||
.unwrap();
|
||||
// posting_count: 0th entry is always empty + 1st + 2nd + 3rd + 4th empty + 5th
|
||||
assert_eq!(inverted_index_mmap.file_header.posting_count, 6);
|
||||
assert_eq!(inverted_index_mmap.file_header.vector_count, 9);
|
||||
assert_eq!(index.file_header.posting_count, 6);
|
||||
assert_eq!(index.file_header.vector_count, 9);
|
||||
|
||||
compare_indexes(&inverted_index_ram, &inverted_index_mmap);
|
||||
compare_indexes(&inverted_index_ram, &index);
|
||||
|
||||
assert!(inverted_index_mmap.get(0, &hw_counter).unwrap().is_empty()); // the first entry is always empty as dimension ids start at 1
|
||||
assert_eq!(inverted_index_mmap.get(1, &hw_counter).unwrap().len(), 9);
|
||||
assert_eq!(inverted_index_mmap.get(2, &hw_counter).unwrap().len(), 4);
|
||||
assert_eq!(inverted_index_mmap.get(3, &hw_counter).unwrap().len(), 3);
|
||||
assert!(inverted_index_mmap.get(4, &hw_counter).unwrap().is_empty()); // return empty posting list info for intermediary empty ids
|
||||
assert_eq!(inverted_index_mmap.get(5, &hw_counter).unwrap().len(), 2);
|
||||
// index after the last values are None
|
||||
assert!(inverted_index_mmap.get(6, &hw_counter).is_err());
|
||||
assert!(inverted_index_mmap.get(7, &hw_counter).is_err());
|
||||
assert!(inverted_index_mmap.get(100, &hw_counter).is_err());
|
||||
let arena = SearchScratchArena::new_slow();
|
||||
assert!(index.get(0, &arena, &hw_counter).unwrap().is_empty()); // the first entry is always empty as dimension ids start at 1
|
||||
assert_eq!(index.get(1, &arena, &hw_counter).unwrap().len(), 9);
|
||||
assert_eq!(index.get(2, &arena, &hw_counter).unwrap().len(), 4);
|
||||
assert_eq!(index.get(3, &arena, &hw_counter).unwrap().len(), 3);
|
||||
assert!(index.get(4, &arena, &hw_counter).unwrap().is_empty()); // return empty posting list info for intermediary empty ids
|
||||
assert_eq!(index.get(5, &arena, &hw_counter).unwrap().len(), 2);
|
||||
// index after the last values are errors (out of bounds)
|
||||
assert!(index.get(6, &arena, &hw_counter).is_err());
|
||||
assert!(index.get(7, &arena, &hw_counter).is_err());
|
||||
assert!(index.get(100, &arena, &hw_counter).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,13 +52,14 @@ impl InvertedIndex for InvertedIndexRam {
|
||||
panic!("InvertedIndexRam is not supposed to be loaded");
|
||||
}
|
||||
|
||||
fn save(&self, _path: &Path) -> std::io::Result<()> {
|
||||
fn save(&self, _path: &Path) -> Result<()> {
|
||||
panic!("InvertedIndexRam is not supposed to be saved");
|
||||
}
|
||||
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
id: DimOffset,
|
||||
_arena: &'a crate::SearchScratchArena,
|
||||
_hw_counter: &'a HardwareCounterCell,
|
||||
) -> Result<PostingListIterator<'a>> {
|
||||
Ok(self.get(id)?.iter())
|
||||
@@ -104,10 +105,7 @@ impl InvertedIndex for InvertedIndexRam {
|
||||
self.upsert(id, vector, old_vector);
|
||||
}
|
||||
|
||||
fn from_ram_index<P: AsRef<Path>>(
|
||||
ram_index: Cow<InvertedIndexRam>,
|
||||
_path: P,
|
||||
) -> std::io::Result<Self> {
|
||||
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, _path: P) -> Result<Self> {
|
||||
Ok(ram_index.into_owned())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use common::types::PointOffsetType;
|
||||
use common::universal_io::{Result, UniversalIoError};
|
||||
|
||||
use super::posting_list_common::PostingListIter;
|
||||
use crate::SearchScratchArena;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::common::types::DimOffset;
|
||||
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
|
||||
@@ -36,12 +37,13 @@ pub trait InvertedIndex: Sized + Debug + 'static {
|
||||
fn open(path: &Path) -> Result<Self>;
|
||||
|
||||
/// Save index
|
||||
fn save(&self, path: &Path) -> std::io::Result<()>;
|
||||
fn save(&self, path: &Path) -> Result<()>;
|
||||
|
||||
/// Get posting list for dimension id
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
id: DimOffset,
|
||||
arena: &'a SearchScratchArena,
|
||||
hw_counter: &'a HardwareCounterCell,
|
||||
) -> Result<Self::Iter<'a>>;
|
||||
|
||||
@@ -72,10 +74,7 @@ pub trait InvertedIndex: Sized + Debug + 'static {
|
||||
);
|
||||
|
||||
/// Create inverted index from ram index
|
||||
fn from_ram_index<P: AsRef<Path>>(
|
||||
ram_index: Cow<InvertedIndexRam>,
|
||||
path: P,
|
||||
) -> std::io::Result<Self>;
|
||||
fn from_ram_index<P: AsRef<Path>>(ram_index: Cow<InvertedIndexRam>, path: P) -> Result<Self>;
|
||||
|
||||
/// Number of indexed vectors
|
||||
fn vector_count(&self) -> usize;
|
||||
|
||||
@@ -4,11 +4,11 @@ use std::sync::atomic::Ordering::Relaxed;
|
||||
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::top_k::TopK;
|
||||
use common::types::{PointOffsetType, ScoredPointOffset};
|
||||
use common::types::{PointOffsetType, ScoreType, ScoredPointOffset};
|
||||
use common::universal_io::Result;
|
||||
|
||||
use super::posting_list_common::PostingListIter;
|
||||
use crate::common::scores_memory_pool::PooledScoresHandle;
|
||||
use crate::SearchScratch;
|
||||
use crate::common::sparse_vector::{RemappedSparseVector, score_vectors};
|
||||
use crate::common::types::{DimId, DimWeight};
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
@@ -24,7 +24,7 @@ pub struct IndexedPostingListIterator<T: PostingListIter> {
|
||||
/// Making this larger makes the search faster but uses more (pooled) memory
|
||||
const ADVANCE_BATCH_SIZE: usize = 10_000;
|
||||
|
||||
pub struct SearchContext<'a, 'b, T: PostingListIter = PostingListIterator<'a>> {
|
||||
pub struct SearchContext<'a, T: PostingListIter = PostingListIterator<'a>> {
|
||||
postings_iterators: Vec<IndexedPostingListIterator<T>>,
|
||||
query: RemappedSparseVector,
|
||||
top: usize,
|
||||
@@ -32,27 +32,28 @@ pub struct SearchContext<'a, 'b, T: PostingListIter = PostingListIterator<'a>> {
|
||||
top_results: TopK,
|
||||
min_record_id: Option<PointOffsetType>, // min_record_id ids across all posting lists
|
||||
max_record_id: PointOffsetType, // max_record_id ids across all posting lists
|
||||
pooled: PooledScoresHandle<'b>, // handle to pooled scores
|
||||
/// Scores buffer from [`SearchScratch`].
|
||||
scores: &'a mut Vec<ScoreType>,
|
||||
use_pruning: bool,
|
||||
hardware_counter: &'a HardwareCounterCell,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
|
||||
impl<'a, T: PostingListIter> SearchContext<'a, T> {
|
||||
pub fn new(
|
||||
query: RemappedSparseVector,
|
||||
top: usize,
|
||||
inverted_index: &'a impl InvertedIndex<Iter<'a> = T>,
|
||||
pooled: PooledScoresHandle<'b>,
|
||||
scratch: &'a mut SearchScratch<'_>,
|
||||
is_stopped: &'a AtomicBool,
|
||||
hardware_counter: &'a HardwareCounterCell,
|
||||
) -> Result<SearchContext<'a, 'b, T>> {
|
||||
) -> Result<SearchContext<'a, T>> {
|
||||
let mut postings_iterators = Vec::new();
|
||||
// track min and max record ids across all posting lists
|
||||
let mut max_record_id = 0;
|
||||
let mut min_record_id = u32::MAX;
|
||||
// iterate over query indices
|
||||
for (query_weight_offset, id) in query.indices.iter().enumerate() {
|
||||
let mut it = inverted_index.get(*id, hardware_counter)?;
|
||||
let mut it = inverted_index.get(*id, &scratch.arena, hardware_counter)?;
|
||||
if let (Some(first), Some(last_id)) = (it.peek(), it.last_id()) {
|
||||
// check if new min
|
||||
let min_record_id_posting = first.record_id;
|
||||
@@ -87,7 +88,7 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
|
||||
top_results,
|
||||
min_record_id,
|
||||
max_record_id,
|
||||
pooled,
|
||||
scores: &mut scratch.scores,
|
||||
use_pruning,
|
||||
hardware_counter,
|
||||
})
|
||||
@@ -158,13 +159,13 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
|
||||
) {
|
||||
// init batch scores
|
||||
let batch_len = batch_last_id - batch_start_id + 1;
|
||||
self.pooled.scores.clear(); // keep underlying allocated memory
|
||||
self.pooled.scores.resize(batch_len as usize, 0.0);
|
||||
self.scores.clear(); // keep underlying allocated memory
|
||||
self.scores.resize(batch_len as usize, 0.0);
|
||||
|
||||
for posting in self.postings_iterators.iter_mut() {
|
||||
posting.posting_list_iterator.for_each_till_id(
|
||||
batch_last_id,
|
||||
self.pooled.scores.as_mut_slice(),
|
||||
self.scores.as_mut_slice(),
|
||||
#[inline(always)]
|
||||
|scores, id, weight| {
|
||||
let element_score = weight * posting.query_weight;
|
||||
@@ -176,7 +177,7 @@ impl<'a, 'b, T: PostingListIter> SearchContext<'a, 'b, T> {
|
||||
);
|
||||
}
|
||||
|
||||
for (local_index, &score) in self.pooled.scores.iter().enumerate() {
|
||||
for (local_index, &score) in self.scores.iter().enumerate() {
|
||||
// publish only the non-zero scores above the current min to beat
|
||||
if score != 0.0 && score > self.top_results.threshold() {
|
||||
let real_id = batch_start_id + local_index as PointOffsetType;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use common::types::PointOffsetType;
|
||||
use common::universal_io::MmapFile;
|
||||
use rand::{RngExt, SeedableRng};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::common::scores_memory_pool::{PooledScoresHandle, ScoresMemoryPool};
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::common::types::{DimOffset, Weight};
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
@@ -13,14 +12,6 @@ use crate::index::inverted_index::inverted_index_compressed_mmap::InvertedIndexC
|
||||
use crate::index::inverted_index::inverted_index_ram::InvertedIndexRam;
|
||||
use crate::index::inverted_index::inverted_index_ram_builder::InvertedIndexBuilder;
|
||||
|
||||
static TEST_SCORES_POOL: OnceLock<ScoresMemoryPool> = OnceLock::new();
|
||||
|
||||
pub fn get_pooled_scores() -> PooledScoresHandle<'static> {
|
||||
TEST_SCORES_POOL
|
||||
.get_or_init(ScoresMemoryPool::default)
|
||||
.get()
|
||||
}
|
||||
|
||||
pub struct TestIndex<I: InvertedIndex> {
|
||||
pub index: I,
|
||||
_temp_dir: TempDir,
|
||||
@@ -78,7 +69,7 @@ pub fn generate_sparse_index<W, R>(
|
||||
density: usize,
|
||||
vocab1: usize,
|
||||
vocab2: usize,
|
||||
) -> TestIndex<InvertedIndexCompressedMmap<W>>
|
||||
) -> TestIndex<InvertedIndexCompressedMmap<W, MmapFile>>
|
||||
where
|
||||
W: Weight + 'static,
|
||||
R: rand::Rng,
|
||||
@@ -98,7 +89,7 @@ pub fn build_index<W>(
|
||||
density: usize,
|
||||
vocab1: usize,
|
||||
vocab2: usize,
|
||||
) -> TestIndex<InvertedIndexCompressedMmap<W>>
|
||||
) -> TestIndex<InvertedIndexCompressedMmap<W, MmapFile>>
|
||||
where
|
||||
W: Weight + 'static,
|
||||
{
|
||||
|
||||
@@ -4,21 +4,23 @@ use common::counter::hardware_accumulator::HwMeasurementAcc;
|
||||
use common::types::PointOffsetType;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::SearchScratch;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
use crate::index::search_context::SearchContext;
|
||||
use crate::index::tests::common::{build_index, get_pooled_scores, match_all};
|
||||
use crate::index::tests::common::{build_index, match_all};
|
||||
|
||||
fn do_search<I: InvertedIndex>(index: &I, query: RemappedSparseVector) -> HwMeasurementAcc {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let top = 10;
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
query,
|
||||
top,
|
||||
index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -41,11 +43,12 @@ fn do_plain_search<I: InvertedIndex>(
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let top = 10;
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
query,
|
||||
top,
|
||||
index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
|
||||
@@ -2,21 +2,23 @@ use std::sync::atomic::AtomicBool;
|
||||
|
||||
use common::counter::hardware_accumulator::HwMeasurementAcc;
|
||||
|
||||
use crate::SearchScratch;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
use crate::index::search_context::SearchContext;
|
||||
use crate::index::tests::common::{build_index, get_pooled_scores, match_all};
|
||||
use crate::index::tests::common::{build_index, match_all};
|
||||
|
||||
fn query<I: InvertedIndex>(index: &I, query: RemappedSparseVector) {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let top = 10;
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
query.clone(),
|
||||
top,
|
||||
index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -24,12 +26,13 @@ fn query<I: InvertedIndex>(index: &I, query: RemappedSparseVector) {
|
||||
|
||||
let result = search_context.search(&match_all);
|
||||
let docs: Vec<_> = result.iter().map(|x| x.idx).collect();
|
||||
drop(search_context);
|
||||
|
||||
let mut search_context = SearchContext::new(
|
||||
query,
|
||||
top,
|
||||
index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
mod tests {
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use common::counter::hardware_accumulator::HwMeasurementAcc;
|
||||
use common::counter::hardware_counter::HardwareCounterCell;
|
||||
use common::types::{PointOffsetType, ScoredPointOffset};
|
||||
#[cfg(target_os = "linux")]
|
||||
use common::universal_io::IoUringFile;
|
||||
use common::universal_io::MmapFile;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::common::scores_memory_pool::{PooledScoresHandle, ScoresMemoryPool};
|
||||
use crate::SearchScratch;
|
||||
use crate::common::sparse_vector::RemappedSparseVector;
|
||||
use crate::common::types::QuantizedU8;
|
||||
use crate::index::inverted_index::InvertedIndex;
|
||||
@@ -38,28 +40,36 @@ mod tests {
|
||||
#[instantiate_tests(<InvertedIndexCompressedImmutableRam<QuantizedU8>>)]
|
||||
mod iram_q8 {}
|
||||
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<f32>>)]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<f32, MmapFile>>)]
|
||||
mod mmap_f32 {}
|
||||
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16>>)]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16, MmapFile>>)]
|
||||
mod mmap_f16 {}
|
||||
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<u8>>)]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<u8, MmapFile>>)]
|
||||
mod mmap_u8 {}
|
||||
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8>>)]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>)]
|
||||
mod mmap_q8 {}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<f32, IoUringFile>>)]
|
||||
mod uring_f32 {}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<half::f16, IoUringFile>>)]
|
||||
mod uring_f16 {}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<u8, IoUringFile>>)]
|
||||
mod uring_u8 {}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[instantiate_tests(<InvertedIndexCompressedMmap<QuantizedU8, IoUringFile>>)]
|
||||
mod uring_q8 {}
|
||||
|
||||
// --- End of test instantiations ---
|
||||
|
||||
static TEST_SCORES_POOL: OnceLock<ScoresMemoryPool> = OnceLock::new();
|
||||
|
||||
fn get_pooled_scores() -> PooledScoresHandle<'static> {
|
||||
TEST_SCORES_POOL
|
||||
.get_or_init(ScoresMemoryPool::default)
|
||||
.get()
|
||||
}
|
||||
|
||||
/// Match all filter condition for testing
|
||||
fn match_all(_p: PointOffsetType) -> bool {
|
||||
true
|
||||
@@ -88,7 +98,9 @@ mod tests {
|
||||
fn round_scores<I: 'static>(mut scores: Vec<ScoredPointOffset>) -> Vec<ScoredPointOffset> {
|
||||
let errors_allowed_for = [
|
||||
TypeId::of::<InvertedIndexCompressedImmutableRam<QuantizedU8>>(),
|
||||
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8>>(),
|
||||
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8, MmapFile>>(),
|
||||
#[cfg(target_os = "linux")]
|
||||
TypeId::of::<InvertedIndexCompressedMmap<QuantizedU8, IoUringFile>>(),
|
||||
];
|
||||
if errors_allowed_for.contains(&TypeId::of::<I>()) {
|
||||
let precision = 0.25;
|
||||
@@ -108,11 +120,12 @@ mod tests {
|
||||
let hw_counter = HardwareCounterCell::disposable();
|
||||
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector::default(), // empty query vector
|
||||
10,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hw_counter,
|
||||
)
|
||||
@@ -133,6 +146,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -140,7 +154,7 @@ mod tests {
|
||||
},
|
||||
10,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -192,6 +206,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -199,7 +214,7 @@ mod tests {
|
||||
},
|
||||
10,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -235,6 +250,7 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -242,7 +258,7 @@ mod tests {
|
||||
},
|
||||
10,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -290,6 +306,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -297,7 +314,7 @@ mod tests {
|
||||
},
|
||||
3,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -332,6 +349,7 @@ mod tests {
|
||||
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -339,7 +357,7 @@ mod tests {
|
||||
},
|
||||
4,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -385,6 +403,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1],
|
||||
@@ -392,7 +411,7 @@ mod tests {
|
||||
},
|
||||
1,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -420,6 +439,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -427,7 +447,7 @@ mod tests {
|
||||
},
|
||||
1,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -460,6 +480,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -467,7 +488,7 @@ mod tests {
|
||||
},
|
||||
1,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -498,6 +519,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -505,7 +527,7 @@ mod tests {
|
||||
},
|
||||
3,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -531,6 +553,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 2, 3],
|
||||
@@ -538,7 +561,7 @@ mod tests {
|
||||
},
|
||||
3,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
@@ -586,6 +609,7 @@ mod tests {
|
||||
let is_stopped = AtomicBool::new(false);
|
||||
let accumulator = HwMeasurementAcc::new();
|
||||
let hardware_counter = accumulator.get_counter_cell();
|
||||
let mut scratch = SearchScratch::new_for_test();
|
||||
let mut search_context = SearchContext::new(
|
||||
RemappedSparseVector {
|
||||
indices: vec![1, 3],
|
||||
@@ -593,7 +617,7 @@ mod tests {
|
||||
},
|
||||
3,
|
||||
&index.index,
|
||||
get_pooled_scores(),
|
||||
&mut scratch,
|
||||
&is_stopped,
|
||||
&hardware_counter,
|
||||
)
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
pub mod common;
|
||||
pub mod index;
|
||||
pub(crate) mod search_scratch;
|
||||
|
||||
pub use search_scratch::{SearchScratch, SearchScratchArena, SearchScratchPool};
|
||||
|
||||
102
lib/sparse/src/search_scratch.rs
Normal file
102
lib/sparse/src/search_scratch.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use common::defaults::POOL_KEEP_LIMIT;
|
||||
use common::ext::aligned_vec::{AVec, RuntimeAlign};
|
||||
use common::types::ScoreType;
|
||||
use parking_lot::Mutex;
|
||||
use typed_arena::Arena;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SearchScratchPool {
|
||||
pool: Mutex<Vec<SearchScratchScores>>,
|
||||
}
|
||||
|
||||
impl SearchScratchPool {
|
||||
#[expect(clippy::new_without_default)]
|
||||
pub fn new() -> SearchScratchPool {
|
||||
SearchScratchPool {
|
||||
pool: Mutex::new(Vec::with_capacity(*POOL_KEEP_LIMIT)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a single [`SearchScratch`] from the pool.
|
||||
pub fn get(&self) -> SearchScratch<'_> {
|
||||
let scores = self.pool.lock().pop().unwrap_or_default();
|
||||
SearchScratch {
|
||||
pool: self,
|
||||
scores,
|
||||
arena: SearchScratchArena::new_slow(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scratch space for use by [`crate::index::search_context::SearchContext`].
|
||||
pub struct SearchScratch<'a> {
|
||||
/// The pool to return this scratch on drop.
|
||||
pool: &'a SearchScratchPool,
|
||||
/// Used for batched scoring.
|
||||
pub(crate) scores: SearchScratchScores,
|
||||
/// Used to own/store posting list bytes while reading them from the file.
|
||||
pub(crate) arena: SearchScratchArena,
|
||||
}
|
||||
|
||||
impl SearchScratch<'_> {
|
||||
#[cfg(test)]
|
||||
pub fn new_for_test() -> SearchScratch<'static> {
|
||||
use std::sync::OnceLock;
|
||||
static POOL: OnceLock<SearchScratchPool> = OnceLock::new();
|
||||
POOL.get_or_init(SearchScratchPool::new).get()
|
||||
}
|
||||
}
|
||||
|
||||
type SearchScratchScores = Vec<ScoreType>;
|
||||
|
||||
impl Drop for SearchScratch<'_> {
|
||||
fn drop(&mut self) {
|
||||
let SearchScratch {
|
||||
pool: SearchScratchPool { pool },
|
||||
scores,
|
||||
arena: _,
|
||||
} = self;
|
||||
let mut pool = pool.lock();
|
||||
if pool.len() < *POOL_KEEP_LIMIT {
|
||||
pool.push(std::mem::take(scores));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hacky wrapper around [`Arena`] with [`Self::gc`] method.
|
||||
///
|
||||
/// TODO: get rid of this struct once [`Arena`] gets `clear()` method, and use
|
||||
/// type alias instead.
|
||||
/// https://github.com/thomcc/rust-typed-arena/issues/64
|
||||
// pub type SearchScratchArena = Arena<AVec<u8, RuntimeAlign>>;
|
||||
pub struct SearchScratchArena(Arena<AVec<u8, RuntimeAlign>>);
|
||||
|
||||
impl SearchScratchArena {
|
||||
/// Same as [`Arena::new`].
|
||||
/// Renamed to `new_slow` to point out that [`Arena::new`] allocates.
|
||||
pub fn new_slow() -> SearchScratchArena {
|
||||
SearchScratchArena(Arena::new())
|
||||
}
|
||||
|
||||
/// Free memory if the arena got too big.
|
||||
/// Poor's man version of `Arena::clear()`.
|
||||
pub fn gc(&mut self) {
|
||||
// Too small => frequent reallocations
|
||||
// Too big => memory bloat
|
||||
const ARBITRARY_LIMIT: usize = 256;
|
||||
if self.0.len() > ARBITRARY_LIMIT {
|
||||
self.0 = Arena::new();
|
||||
}
|
||||
|
||||
for vec in self.0.iter_mut() {
|
||||
*vec = AVec::new(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for SearchScratchArena {
|
||||
type Target = Arena<AVec<u8, RuntimeAlign>>;
|
||||
fn deref(&self) -> &Arena<AVec<u8, RuntimeAlign>> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user