uio mmap posting (#8721)

* [AI] update docstrings

* move support strucutres from mmap_posting.rs

* rename and simplify: MmapPostingValue -> PersistedPostingValue -> ZerocopyPostingValue

* implement `with_view` method for UniversalPostings

* WIP: RawPOstingList + AsPostingListView

* wip: bathcing and pre-collection of posting lists

* wip: batch reading in UniversalPostings

* [AI] implement with_existing_postings

* fmt

* pre-collect in check_compressed_postings_phrase

* batch pre-collect for check_any and check_intersection

* get rid of iter_postings which is collected anyway

* all_postings in UniversalPostings

* missing unique for tokens in phrases

* [AI] replace MmapPostings with UniversalPostings

* mising file

* cfg(test)

* unnecessary Cow

---------

Co-authored-by: Luis Cossío <luis.cossio@outlook.com>
This commit is contained in:
Andrey Vasnetsov
2026-04-21 00:20:02 +02:00
committed by timvisee
parent 519b253bb2
commit 5fcd74c0e5
23 changed files with 943 additions and 570 deletions

View File

@@ -12,6 +12,9 @@ pub enum UniversalIoError {
#[error("Bytemuck cast error: {0:?}")]
BytemuckCast(bytemuck::PodCastError),
#[error("Zerocopy size error: {0:?}")]
ZerocopySize(String),
#[error(transparent)]
IoUringNotSupported(io::Error),
@@ -59,3 +62,9 @@ impl From<bytemuck::PodCastError> for UniversalIoError {
Self::BytemuckCast(err)
}
}
impl<Src, Dst: ?Sized> From<zerocopy::SizeError<Src, Dst>> for UniversalIoError {
fn from(err: zerocopy::SizeError<Src, Dst>) -> Self {
Self::ZerocopySize(format!("{err:?}"))
}
}

View File

@@ -12,7 +12,7 @@ use bitpacking::BitPacker;
type BitPackerImpl = bitpacking::BitPacker4x;
/// How many elements are packed in a single chunk.
const CHUNK_LEN: usize = 128;
pub const CHUNK_LEN: usize = 128;
const _: () = assert!(128 == BitPackerImpl::BLOCK_LEN);
pub trait SizedValue: Sized + Copy + std::fmt::Debug {}

View File

@@ -12,6 +12,7 @@ use crate::visitor::PostingVisitor;
use crate::{BitPackerImpl, CHUNK_LEN, PostingChunk, PostingList, SizedTypeFor};
/// A non-owning view of [`PostingList`].
#[derive(Clone)]
pub struct PostingListView<'a, V: PostingValue> {
pub(crate) id_data: &'a [u8],
chunks: &'a [PostingChunk<SizedTypeFor<V>>],

View File

@@ -162,10 +162,29 @@ impl From<UniversalIoError> for OperationError {
| UniversalIoError::InvalidFileIndex { .. } => Self::service_error(err.to_string()),
UniversalIoError::BytemuckCast(_) => Self::service_error(err.to_string()),
UniversalIoError::Uninitialized { .. } => Self::service_error(err.to_string()),
UniversalIoError::ZerocopySize(_) => Self::service_error(err.to_string()),
}
}
}
impl<Src, Dst: ?Sized> From<zerocopy::SizeError<Src, Dst>> for OperationError
where
zerocopy::SizeError<Src, Dst>: std::fmt::Display,
{
fn from(err: zerocopy::SizeError<Src, Dst>) -> Self {
Self::service_error(format!("Zerocopy size error: {err}"))
}
}
impl<A, S, V> From<zerocopy::ConvertError<A, S, V>> for OperationError
where
zerocopy::ConvertError<A, S, V>: std::fmt::Display,
{
fn from(err: zerocopy::ConvertError<A, S, V>) -> Self {
Self::service_error(format!("Zerocopy convert error: {err}"))
}
}
impl From<serde_cbor::Error> for OperationError {
fn from(err: serde_cbor::Error) -> Self {
Self::service_error(format!("Failed to parse data: {err}"))

View File

@@ -22,8 +22,8 @@ pub(super) enum Storage {
impl ImmutableFullTextIndex {
/// Open and load immutable full text index from mmap storage
pub fn open_mmap(index: MmapFullTextIndex) -> Self {
let inverted_index = ImmutableInvertedIndex::from(&index.inverted_index);
pub fn open_mmap(index: MmapFullTextIndex) -> OperationResult<Self> {
let inverted_index = ImmutableInvertedIndex::try_from(&index.inverted_index)?;
// Index is now loaded into memory, clear cache of backing mmap storage
if let Err(err) = index.clear_cache() {
@@ -36,7 +36,7 @@ impl ImmutableFullTextIndex {
cached_ram_usage_bytes: 0,
};
result.cached_ram_usage_bytes = result.inverted_index.ram_usage_bytes();
result
Ok(result)
}
pub fn remove_point(&mut self, id: PointOffsetType) {

View File

@@ -21,6 +21,22 @@ use crate::index::field_index::full_text_index::inverted_index::postings_iterato
check_compressed_postings_phrase, intersect_compressed_postings_phrase_iterator,
};
/// Collect posting-list views for every token in `token_ids`.
/// Returns `None` as soon as any token id is out of range.
fn get_all_or_none<'a, V: PostingValue>(
postings: &'a [PostingList<V>],
token_ids: &[TokenId],
) -> Option<Vec<(TokenId, PostingListView<'a, V>)>> {
token_ids
.iter()
.map(|&token_id| {
postings
.get(token_id as usize)
.map(|list| (token_id, list.view()))
})
.collect()
}
#[cfg_attr(test, derive(Clone))]
#[derive(Debug)]
pub struct ImmutableInvertedIndex {
@@ -188,11 +204,19 @@ impl ImmutableInvertedIndex {
match &self.postings {
ImmutablePostings::WithPositions(postings) => {
Either::Right(intersect_compressed_postings_phrase_iterator(
phrase,
|token_id| postings.get(*token_id as usize).map(PostingList::view),
is_active,
))
// Deduplicate phrase tokens: repeated tokens (e.g. "zn zn") must
// not fetch the same posting list twice, otherwise positions get
// added twice in `phrase_in_all_postings`.
let unique_tokens = phrase.to_token_set();
if let Some(selected_postings) = get_all_or_none(postings, unique_tokens.tokens()) {
Either::Right(intersect_compressed_postings_phrase_iterator(
phrase,
selected_postings,
is_active,
))
} else {
Either::Left(std::iter::empty())
}
}
// cannot do phrase matching if there's no positional information
ImmutablePostings::Ids(_postings) => Either::Left(std::iter::empty()),
@@ -212,9 +236,13 @@ impl ImmutableInvertedIndex {
match &self.postings {
ImmutablePostings::WithPositions(postings) => {
check_compressed_postings_phrase(phrase, point_id, |token_id| {
postings.get(*token_id as usize).map(PostingList::view)
})
let unique_tokens = phrase.to_token_set();
let Some(selected_postings) = get_all_or_none(postings, unique_tokens.tokens())
else {
return false;
};
check_compressed_postings_phrase(phrase, point_id, selected_postings)
}
// cannot do phrase matching if there's no positional information
ImmutablePostings::Ids(_postings) => false,
@@ -270,24 +298,35 @@ impl InvertedIndex for ImmutableInvertedIndex {
}
}
fn get_posting_len(&self, token_id: TokenId, _: &HardwareCounterCell) -> Option<usize> {
self.postings.posting_len(token_id)
fn get_posting_len(
&self,
token_id: TokenId,
_: &HardwareCounterCell,
) -> OperationResult<Option<usize>> {
Ok(self.postings.posting_len(token_id))
}
fn vocab_with_postings_len_iter(&self) -> impl Iterator<Item = (&str, usize)> + '_ {
fn vocab_with_postings_len_iter(
&self,
) -> impl Iterator<Item = OperationResult<(&str, usize)>> + '_ {
self.vocab.iter().filter_map(|(token, &token_id)| {
self.postings
.posting_len(token_id)
.map(|len| (token.as_str(), len))
.map(|len| Ok((token.as_str(), len)))
})
}
fn check_match(&self, parsed_query: &ParsedQuery, point_id: PointOffsetType) -> bool {
match parsed_query {
fn check_match(
&self,
parsed_query: &ParsedQuery,
point_id: PointOffsetType,
) -> OperationResult<bool> {
let matched = match parsed_query {
ParsedQuery::AllTokens(tokens) => self.check_has_subset(tokens, point_id),
ParsedQuery::Phrase(phrase) => self.check_has_phrase(phrase, point_id),
ParsedQuery::AnyTokens(tokens) => self.check_has_any(tokens, point_id),
}
};
Ok(matched)
}
fn values_is_empty(&self, point_id: PointOffsetType) -> bool {
@@ -446,21 +485,15 @@ fn create_compressed_postings_with_positions(
.collect()
}
impl From<&MmapInvertedIndex> for ImmutableInvertedIndex {
fn from(index: &MmapInvertedIndex) -> Self {
impl TryFrom<&MmapInvertedIndex> for ImmutableInvertedIndex {
type Error = OperationError;
fn try_from(index: &MmapInvertedIndex) -> OperationResult<Self> {
let postings = match &index.storage.postings {
MmapPostingsEnum::Ids(postings) => ImmutablePostings::Ids(
postings
.iter_postings()
.map(PostingListView::to_owned)
.collect(),
),
MmapPostingsEnum::WithPositions(postings) => ImmutablePostings::WithPositions(
postings
.iter_postings()
.map(PostingListView::to_owned)
.collect(),
),
MmapPostingsEnum::Ids(postings) => ImmutablePostings::Ids(postings.all_postings()?),
MmapPostingsEnum::WithPositions(postings) => {
ImmutablePostings::WithPositions(postings.all_postings()?)
}
};
let vocab: HashMap<String, TokenId> = index
@@ -475,12 +508,12 @@ impl From<&MmapInvertedIndex> for ImmutableInvertedIndex {
"postings and vocab must be the same size",
);
ImmutableInvertedIndex {
Ok(ImmutableInvertedIndex {
postings,
vocab,
point_to_tokens_count: index.storage.point_to_tokens_count.to_vec(),
points_count: index.points_count(),
}
})
}
}

View File

@@ -0,0 +1,169 @@
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use common::zeros::WriteZerosExt;
use fs_err::File;
use posting_list::{PostingList, PostingListComponents};
use zerocopy::IntoBytes;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::types::{
ALIGNMENT, PostingListHeader, PostingsHeader, ZerocopyPostingValue,
};
/// Write a vector of compressed posting lists to `path` in the format expected
/// by [`UniversalPostings`](super::uio_postings::UniversalPostings).
///
/// On-disk layout:
///
/// ```text
/// offset 0
/// ┌────────────────────────────────────────────────┐
/// │ PostingsHeader │ size_of::<PostingsHeader>()
/// │ posting_count: usize (one per term) │ = 8 + 32 = 40 bytes
/// │ _reserved: [u8; 32] │
/// ├────────────────────────────────────────────────┤
/// │ PostingListHeader[0] ── token_id 0 │ each
/// │ PostingListHeader[1] ── token_id 1 │ size_of::<PostingListHeader>()
/// │ ... │ = 24 bytes
/// │ PostingListHeader[N-1] ── token_id N-1 │
/// ├────────────────────────────────────────────────┤
/// │ CompressedPostingList[0] │ variable
/// │ CompressedPostingList[1] │ size; located via
/// │ ... │ header.offset
/// │ CompressedPostingList[N-1] │
/// └────────────────────────────────────────────────┘
/// ```
///
/// One `CompressedPostingList`:
///
/// ```text
/// header.offset ─►
/// ┌──────────────────────────────────────────────┐
/// │ last_doc_id : PointOffsetType (u32) │ 4 B
/// ├──────────────────────────────────────────────┤
/// │ chunks: [PostingChunk<Sized<V>>; │ chunks_count *
/// │ chunks_count] │ size_of::<PostingChunk>
/// │ each chunk = (initial_id, sized_value, │
/// │ offset into id_data) │
/// ├──────────────────────────────────────────────┤
/// │ id_data: [u8; ids_data_bytes_count] │ bit-packed deltas for
/// │ │ CHUNK_LEN ids per chunk
/// ├──────────────────────────────────────────────┤
/// │ var_size_data: [u8; var_size_data_bytes] │ optional (Positions);
/// │ │ empty for V = ()
/// ├──────────────────────────────────────────────┤
/// │ alignment: [0u8; alignment_bytes_count] │ pad so next region is
/// │ │ 4-byte aligned
/// ├──────────────────────────────────────────────┤
/// │ remainders:[RemainderPosting<Sized<V>>; │ tail < CHUNK_LEN ids
/// │ remainder_count] │ stored uncompressed
/// └──────────────────────────────────────────────┘
/// ```
///
/// The alignment slot exists because `id_data` + `var_size_data` are byte
/// streams of arbitrary length, but `RemainderPosting` is `#[repr(C)]` and
/// read via zerocopy, so its start needs 4-byte alignment.
///
/// Two flavors of `V`:
/// - `V = ()` — ids-only postings; `var_size_data` is empty and
/// `SizedTypeFor<V>` is zero-sized.
/// - `V = Positions` — phrase index; per-doc token positions live in
/// `var_size_data`, with each `PostingChunk` / `RemainderPosting` carrying
/// a `Sized` handle (offset/len) into that blob.
pub fn create_postings_file<V: ZerocopyPostingValue>(
path: PathBuf,
compressed_postings: &[PostingList<V>],
) -> io::Result<()> {
// Create a new empty file, where we will write the compressed posting lists and the header
let (file, temp_path) = tempfile::Builder::new()
.prefix(path.file_name().ok_or(io::ErrorKind::InvalidInput)?)
.tempfile_in(path.parent().ok_or(io::ErrorKind::InvalidInput)?)?
.into_parts();
let file = File::from_parts::<&Path>(file, temp_path.as_ref());
let mut bufw = io::BufWriter::new(&file);
let postings_header = PostingsHeader {
posting_count: compressed_postings.len(),
_reserved: [0; 32],
};
bufw.write_all(postings_header.as_bytes())?;
let postings_lists_headers_size = compressed_postings.len() * size_of::<PostingListHeader>();
let mut posting_offset = size_of::<PostingsHeader>() + postings_lists_headers_size;
for compressed_posting in compressed_postings {
let view = compressed_posting.view();
let PostingListComponents {
id_data,
chunks,
var_size_data,
remainders,
last_id: _, // not used for the header
} = view.components();
let id_data_len = id_data.len();
let var_size_data_len = var_size_data.len();
let data_len = id_data_len + var_size_data_len;
let alignment_len = data_len.next_multiple_of(ALIGNMENT) - data_len;
let posting_list_header = PostingListHeader {
offset: posting_offset as u64,
chunks_count: chunks.len() as u32,
ids_data_bytes_count: id_data_len as u32,
var_size_data_bytes_count: var_size_data_len as u32,
alignment_bytes_count: alignment_len as u8,
remainder_count: remainders.len() as u8,
_reserved: [0; 2],
};
bufw.write_all(posting_list_header.as_bytes())?;
posting_offset += posting_list_header.posting_size::<V>();
}
for compressed_posting in compressed_postings {
let view = compressed_posting.view();
let PostingListComponents {
id_data,
chunks,
var_size_data,
remainders,
last_id,
} = view.components();
bufw.write_all(
last_id
.expect("posting must have at least one element")
.as_bytes(),
)?;
for chunk in chunks {
bufw.write_all(chunk.as_bytes())?;
}
bufw.write_all(id_data)?;
if !var_size_data.is_empty() {
bufw.write_all(var_size_data)?;
}
// alignment padding so the next `RemainderPosting` is 4-byte aligned
let data_len = id_data.len() + var_size_data.len();
bufw.write_zeros(data_len.next_multiple_of(ALIGNMENT) - data_len)?;
for element in remainders {
bufw.write_all(element.as_bytes())?;
}
}
bufw.flush()?;
drop(bufw);
file.sync_all()?;
drop(file);
temp_path.persist(path)?;
Ok(())
}

View File

@@ -1,304 +0,0 @@
use std::fmt::Debug;
use std::io;
use std::io::Write;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use common::mmap::{Advice, AdviceSetting, Madviseable, open_read_mmap};
use common::types::PointOffsetType;
use common::zeros::WriteZerosExt;
use fs_err::File;
use memmap2::Mmap;
use posting_list::{
PostingChunk, PostingList, PostingListComponents, PostingListView, PostingValue,
RemainderPosting, SizedTypeFor, ValueHandler,
};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::index::field_index::full_text_index::inverted_index::TokenId;
use crate::index::field_index::full_text_index::inverted_index::positions::Positions;
const ALIGNMENT: usize = 4;
/// Trait marker to enrich [`posting_list::PostingValue`] for handling mmap files with the posting list.
pub(in crate::index::field_index::full_text_index) trait MmapPostingValue:
PostingValue<
Handler: ValueHandler<Sized: FromBytes + Immutable + IntoBytes + KnownLayout + Unaligned>
+ Clone
+ Debug,
>
{
}
impl MmapPostingValue for () {}
impl MmapPostingValue for Positions {}
#[derive(Debug, Default, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
struct PostingsHeader {
/// Number of posting lists. One posting list per term
pub posting_count: usize,
_reserved: [u8; 32],
}
/// This data structure should contain all the necessary information to
/// construct `PostingListView<V>` from the mmap file.
#[derive(Debug, Default, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
pub(in crate::index::field_index::full_text_index) struct PostingListHeader {
/// Offset in bytes from the start of the mmap file
/// where the posting list data starts
offset: u64,
/// Amount of chunks in compressed posting list
chunks_count: u32,
/// Length in bytes for the compressed postings data
ids_data_bytes_count: u32,
/// Length in bytes for the alignment bytes
alignment_bytes_count: u8,
/// Length in bytes for the remainder postings
remainder_count: u8,
_reserved: [u8; 2],
/// Length in bytes for the var-sized data. Add-on for phrase matching, otherwise 0
var_size_data_bytes_count: u32,
}
impl PostingListHeader {
fn posting_size<V: PostingValue>(&self) -> usize {
self.ids_data_bytes_count as usize
+ self.var_size_data_bytes_count as usize
+ self.alignment_bytes_count as usize
+ self.remainder_count as usize * size_of::<RemainderPosting<SizedTypeFor<V>>>()
+ self.chunks_count as usize * size_of::<PostingChunk<SizedTypeFor<V>>>()
+ size_of::<PointOffsetType>() // last_doc_id
}
}
/// MmapPostings Structure on disk:
///
///
/// `| PostingsHeader |
/// [ PostingListHeader, PostingListHeader, ... ] |
/// [ CompressedMmapPostingList, CompressedMmapPostingList, ... ] |`
pub struct MmapPostings<V: MmapPostingValue> {
_path: PathBuf,
mmap: Mmap,
header: PostingsHeader,
_value_type: PhantomData<V>,
}
impl<V: MmapPostingValue> MmapPostings<V> {
fn get_header(&self, token_id: TokenId) -> Option<&PostingListHeader> {
if self.header.posting_count <= token_id as usize {
return None;
}
let header_offset =
size_of::<PostingsHeader>() + token_id as usize * size_of::<PostingListHeader>();
PostingListHeader::ref_from_prefix(self.mmap.get(header_offset..)?)
.ok()
.map(|(header, _)| header)
}
/// Create PostingListView<V> from the given header
///
/// Assume the following layout:
///
/// ```ignore
/// last_doc_id: &'a PointOffsetType,
/// chunks_index: &'a [PostingChunk<()>],
/// data: &'a [u8],
/// var_size_data: &'a [u8], // might be empty in case of only ids
/// _alignment: &'a [u8], // 0-3 extra bytes to align the data
/// remainder_postings: &'a [PointOffsetType],
/// ```
fn get_view<'a>(&'a self, header: &'a PostingListHeader) -> Option<PostingListView<'a, V>> {
let bytes = self.mmap.get(header.offset as usize..)?;
let (last_doc_id, bytes) = PointOffsetType::read_from_prefix(bytes).ok()?;
let (chunks, bytes) = <[PostingChunk<SizedTypeFor<V>>]>::ref_from_prefix_with_elems(
bytes,
header.chunks_count as usize,
)
.ok()?;
let (id_data, bytes) = bytes.split_at(header.ids_data_bytes_count as usize);
let (var_size_data, bytes) = bytes.split_at(header.var_size_data_bytes_count as usize);
// skip padding
let bytes = bytes.get(header.alignment_bytes_count as usize..)?;
let (remainder_postings, _) =
<[RemainderPosting<SizedTypeFor<V>>]>::ref_from_prefix_with_elems(
bytes,
header.remainder_count as usize,
)
.ok()?;
Some(PostingListView::from_components(
id_data,
chunks,
var_size_data,
remainder_postings,
Some(last_doc_id),
))
}
pub fn get<'a>(&'a self, token_id: TokenId) -> Option<PostingListView<'a, V>> {
let header = self.get_header(token_id)?;
self.get_view(header)
}
/// Given a vector of compressed posting lists, this function writes them to the `path` file.
/// The format of the file is compatible with the `MmapPostings` structure.
pub fn create(path: PathBuf, compressed_postings: &[PostingList<V>]) -> io::Result<()> {
// Create a new empty file, where we will write the compressed posting lists and the header
let (file, temp_path) = tempfile::Builder::new()
.prefix(path.file_name().ok_or(io::ErrorKind::InvalidInput)?)
.tempfile_in(path.parent().ok_or(io::ErrorKind::InvalidInput)?)?
.into_parts();
let file = File::from_parts::<&Path>(file, temp_path.as_ref());
let mut bufw = io::BufWriter::new(&file);
let postings_header = PostingsHeader {
posting_count: compressed_postings.len(),
_reserved: [0; 32],
};
// Write the header to the buffer
bufw.write_all(postings_header.as_bytes())?;
let postings_lists_headers_size =
compressed_postings.len() * size_of::<PostingListHeader>();
let mut posting_offset = size_of::<PostingsHeader>() + postings_lists_headers_size;
for compressed_posting in compressed_postings {
let view = compressed_posting.view();
let PostingListComponents {
id_data,
chunks,
var_size_data,
remainders,
last_id: _, // not used for the header
} = view.components();
let id_data_len = id_data.len();
let var_size_data_len = var_size_data.len();
let data_len = id_data_len + var_size_data_len;
let alignment_len = data_len.next_multiple_of(ALIGNMENT) - data_len;
let posting_list_header = PostingListHeader {
offset: posting_offset as u64,
chunks_count: chunks.len() as u32,
ids_data_bytes_count: id_data_len as u32,
var_size_data_bytes_count: var_size_data_len as u32,
alignment_bytes_count: alignment_len as u8,
remainder_count: remainders.len() as u8,
_reserved: [0; 2],
};
// Write the posting list header to the buffer
bufw.write_all(posting_list_header.as_bytes())?;
posting_offset += posting_list_header.posting_size::<V>();
}
for compressed_posting in compressed_postings {
let view = compressed_posting.view();
let PostingListComponents {
id_data,
chunks,
var_size_data, // not used with just ids postings
remainders,
last_id,
} = view.components();
bufw.write_all(
last_id
.expect("posting must have at least one element")
.as_bytes(),
)?;
for chunk in chunks {
bufw.write_all(chunk.as_bytes())?;
}
// write all unaligned data together
bufw.write_all(id_data)?;
// write var_size_data if it exists
if !var_size_data.is_empty() {
bufw.write_all(var_size_data)?;
}
// write alignment padding
// Example:
// For data size = 5, alignment = 3 as (5 + 3 = 8)
// alignment = 8 - 5 = 3
let data_len = id_data.len() + var_size_data.len();
bufw.write_zeros(data_len.next_multiple_of(ALIGNMENT) - data_len)?;
for element in remainders {
bufw.write_all(element.as_bytes())?;
}
}
// Explicitly flush write buffer so we can catch IO errors
bufw.flush()?;
drop(bufw);
file.sync_all()?;
drop(file);
temp_path.persist(path)?;
Ok(())
}
pub fn open(path: impl Into<PathBuf>, populate: bool) -> io::Result<Self> {
let path = path.into();
let mmap = open_read_mmap(&path, AdviceSetting::Advice(Advice::Normal), populate)?;
let (header, _) = PostingsHeader::read_from_prefix(&mmap).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid header deserialization in {}", path.display()),
)
})?;
Ok(Self {
_path: path,
mmap,
header,
_value_type: PhantomData,
})
}
/// Populate all pages in the mmap.
/// Block until all pages are populated.
pub fn populate(&self) {
self.mmap.populate();
}
/// Hint to the OS that pages backing this mmap can be reclaimed.
pub fn clear_cache(&self) {
let Self {
_path,
mmap,
header: _,
_value_type,
} = self;
mmap.clear_cache();
}
/// Iterate over posting lists, returning a view for each
pub fn iter_postings<'a>(&'a self) -> impl Iterator<Item = PostingListView<'a, V>> {
(0..self.header.posting_count as u32)
// we are iterating over existing posting lists, all of them should return `Some`
.filter_map(|posting_idx| self.get(posting_idx))
}
}

View File

@@ -1,36 +1,36 @@
#[cfg(test)]
use common::types::PointOffsetType;
use common::universal_io::MmapFile;
use crate::index::field_index::full_text_index::inverted_index::TokenId;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::mmap_postings::MmapPostings;
use super::super::positions::Positions;
use crate::common::operation_error::OperationResult;
use crate::index::field_index::full_text_index::inverted_index::TokenId;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::uio_postings::UniversalPostings;
pub enum MmapPostingsEnum {
Ids(MmapPostings<()>),
WithPositions(MmapPostings<Positions>),
Ids(UniversalPostings<(), MmapFile>),
WithPositions(UniversalPostings<Positions, MmapFile>),
}
impl MmapPostingsEnum {
pub fn populate(&self) {
pub fn populate(&self) -> OperationResult<()> {
match self {
MmapPostingsEnum::Ids(postings) => postings.populate(),
MmapPostingsEnum::WithPositions(postings) => postings.populate(),
}
}
pub fn clear_cache(&self) {
pub fn clear_cache(&self) -> OperationResult<()> {
match self {
MmapPostingsEnum::Ids(postings) => postings.clear_cache(),
MmapPostingsEnum::WithPositions(postings) => postings.clear_cache(),
}
}
pub fn posting_len(&self, token_id: TokenId) -> Option<usize> {
pub fn posting_len(&self, token_id: TokenId) -> OperationResult<Option<usize>> {
match self {
MmapPostingsEnum::Ids(postings) => postings.get(token_id).map(|view| view.len()),
MmapPostingsEnum::WithPositions(postings) => {
postings.get(token_id).map(|view| view.len())
}
MmapPostingsEnum::Ids(postings) => postings.posting_len(token_id),
MmapPostingsEnum::WithPositions(postings) => postings.posting_len(token_id),
}
}
@@ -39,15 +39,20 @@ impl MmapPostingsEnum {
&'a self,
token_id: TokenId,
) -> Option<Box<dyn Iterator<Item = PointOffsetType> + 'a>> {
match self {
MmapPostingsEnum::Ids(postings) => postings.get(token_id).map(|view| {
Box::new(view.into_iter().map(|elem| elem.id))
as Box<dyn Iterator<Item = PointOffsetType>>
}),
MmapPostingsEnum::WithPositions(postings) => postings.get(token_id).map(|view| {
Box::new(view.into_iter().map(|elem| elem.id))
as Box<dyn Iterator<Item = PointOffsetType>>
}),
}
// Collect ids upfront so the borrowed `RawPostingList` bytes don't have
// to outlive this call. Acceptable because UniversalPostings is on disk.
let ids: Vec<PointOffsetType> = match self {
MmapPostingsEnum::Ids(postings) => {
let raw = postings.get(token_id).unwrap()?;
let view = raw.as_view::<()>().unwrap();
view.into_iter().map(|elem| elem.id).collect()
}
MmapPostingsEnum::WithPositions(postings) => {
let raw = postings.get(token_id).unwrap()?;
let view = raw.as_view::<Positions>().unwrap();
view.into_iter().map(|elem| elem.id).collect()
}
};
Some(Box::new(ids.into_iter()))
}
}

View File

@@ -3,13 +3,14 @@ use std::path::PathBuf;
use common::bitvec::BitVec;
use common::counter::hardware_counter::HardwareCounterCell;
use common::mmap::{self, AdviceSetting, MmapSlice, create_and_ensure_length};
use common::mmap::{self, Advice, AdviceSetting, MmapSlice, create_and_ensure_length};
use common::mmap_hashmap::{MmapHashMap, READ_ENTRY_OVERHEAD};
use common::types::PointOffsetType;
use common::universal_io::{MmapFile, OpenOptions};
use itertools::Either;
use mmap_postings::{MmapPostingValue, MmapPostings};
use types::ZerocopyPostingValue;
use uio_postings::UniversalPostings;
use self::create_postings::create_postings_file;
use super::immutable_inverted_index::ImmutableInvertedIndex;
use super::immutable_postings_enum::ImmutablePostings;
use super::mmap_inverted_index::mmap_postings_enum::MmapPostingsEnum;
@@ -27,8 +28,11 @@ use crate::index::field_index::full_text_index::inverted_index::postings_iterato
check_compressed_postings_phrase, intersect_compressed_postings_phrase_iterator,
};
pub(super) mod mmap_postings;
mod create_postings;
pub mod mmap_postings_enum;
mod raw_posting_list;
pub(in crate::index::field_index::full_text_index) mod types;
mod uio_postings;
const POSTINGS_FILE: &str = "postings.dat";
const VOCAB_FILE: &str = "vocab.dat";
@@ -68,9 +72,9 @@ impl MmapInvertedIndex {
let deleted_points_path = path.join(DELETED_POINTS_FILE);
match postings {
ImmutablePostings::Ids(postings) => MmapPostings::create(postings_path, postings)?,
ImmutablePostings::Ids(postings) => create_postings_file(postings_path, postings)?,
ImmutablePostings::WithPositions(postings) => {
MmapPostings::create(postings_path, postings)?
create_postings_file(postings_path, postings)?
}
}
@@ -126,12 +130,25 @@ impl MmapInvertedIndex {
return Ok(None);
}
let postings_open_options = OpenOptions {
writeable: false,
need_sequential: false,
disk_parallel: None,
populate: Some(populate),
advice: Some(AdviceSetting::Advice(Advice::Normal)),
prevent_caching: None,
};
let postings = match has_positions {
false => MmapPostingsEnum::Ids(MmapPostings::<()>::open(&postings_path, populate)?),
true => MmapPostingsEnum::WithPositions(MmapPostings::<Positions>::open(
false => MmapPostingsEnum::Ids(UniversalPostings::<(), MmapFile>::open(
&postings_path,
populate,
postings_open_options,
)?),
true => {
MmapPostingsEnum::WithPositions(UniversalPostings::<Positions, MmapFile>::open(
&postings_path,
postings_open_options,
)?)
}
};
let vocab = MmapHashMap::<str, TokenId>::open(&vocab_path, false)?;
@@ -185,39 +202,33 @@ impl MmapInvertedIndex {
!is_deleted
}
/// Iterate over point ids whose documents contain all given tokens
pub fn filter_has_all<'a>(
&'a self,
tokens: TokenSet,
) -> Box<dyn Iterator<Item = PointOffsetType> + 'a> {
/// Iterate over point ids whose documents contain all given tokens.
///
/// Pre-collected upfront because [`UniversalPostings`] exposes posting
/// views via a `FnOnce` callback. Acceptable since this index lives
/// on disk.
pub fn filter_has_all(&self, tokens: TokenSet) -> OperationResult<Vec<PointOffsetType>> {
// in case of mmap immutable index, deleted points are still in the postings
let filter = move |idx| self.is_active(idx);
fn intersection<'a, V: MmapPostingValue>(
postings: &'a MmapPostings<V>,
fn intersection<V: ZerocopyPostingValue>(
postings: &UniversalPostings<V, MmapFile>,
tokens: TokenSet,
filter: impl Fn(u32) -> bool + 'a,
) -> Box<dyn Iterator<Item = PointOffsetType> + 'a> {
let postings_opt: Option<Vec<_>> = tokens
.tokens()
.iter()
.map(|&token_id| postings.get(token_id))
.collect();
let Some(posting_readers) = postings_opt else {
// There are unseen tokens -> no matches
return Box::new(std::iter::empty());
};
if posting_readers.is_empty() {
// Empty request -> no matches
return Box::new(std::iter::empty());
}
Box::new(intersect_compressed_postings_iterator(
posting_readers,
filter,
))
filter: impl Fn(PointOffsetType) -> bool,
) -> OperationResult<Vec<PointOffsetType>> {
let result =
postings.with_all_or_none_postings(tokens.tokens(), |posting_readers| {
if posting_readers.is_empty() {
return Ok(Vec::new());
}
let posting_readers = posting_readers
.into_iter()
.map(|(_token_id, posting_list_view)| posting_list_view)
.collect();
Ok(intersect_compressed_postings_iterator(posting_readers, filter).collect())
})?;
// Some token has no posting list -> no matches
Ok(result.unwrap_or_default())
}
match &self.storage.postings {
@@ -227,65 +238,60 @@ impl MmapInvertedIndex {
}
/// Iterate over point ids whose documents contain at least one of the given tokens
fn filter_has_any<'a>(
&'a self,
tokens: TokenSet,
) -> impl Iterator<Item = PointOffsetType> + 'a {
fn filter_has_any(&self, tokens: TokenSet) -> OperationResult<Vec<PointOffsetType>> {
// in case of immutable index, deleted documents are still in the postings
let is_active = move |idx| self.is_active(idx);
fn merge<'a, V: MmapPostingValue>(
postings: &'a MmapPostings<V>,
fn merge<V: ZerocopyPostingValue>(
postings: &UniversalPostings<V, MmapFile>,
tokens: TokenSet,
is_active: impl Fn(PointOffsetType) -> bool + 'a,
) -> impl Iterator<Item = PointOffsetType> + 'a {
let postings: Vec<_> = tokens
.tokens()
.iter()
.filter_map(|&token_id| postings.get(token_id))
.collect();
// Query must not be empty
if postings.is_empty() {
return Either::Left(std::iter::empty());
};
Either::Right(merge_compressed_postings_iterator(postings, is_active))
is_active: impl Fn(PointOffsetType) -> bool,
) -> OperationResult<Vec<PointOffsetType>> {
postings.with_existing_postings(tokens.tokens(), |posting_readers| {
if posting_readers.is_empty() {
return Ok(Vec::new());
}
let posting_readers = posting_readers
.into_iter()
.map(|(_token_id, posting_list_view)| posting_list_view)
.collect();
Ok(merge_compressed_postings_iterator(posting_readers, is_active).collect())
})
}
match &self.storage.postings {
MmapPostingsEnum::Ids(postings) => Either::Left(merge(postings, tokens, is_active)),
MmapPostingsEnum::WithPositions(postings) => {
Either::Right(merge(postings, tokens, is_active))
}
MmapPostingsEnum::Ids(postings) => merge(postings, tokens, is_active),
MmapPostingsEnum::WithPositions(postings) => merge(postings, tokens, is_active),
}
}
fn check_has_subset(&self, tokens: &TokenSet, point_id: PointOffsetType) -> bool {
fn check_has_subset(
&self,
tokens: &TokenSet,
point_id: PointOffsetType,
) -> OperationResult<bool> {
// check non-empty query
if tokens.is_empty() {
return false;
return Ok(false);
}
// check presence of the document
if self.values_is_empty(point_id) {
return false;
return Ok(false);
}
fn check_intersection<V: MmapPostingValue>(
postings: &MmapPostings<V>,
fn check_intersection<V: ZerocopyPostingValue>(
postings: &UniversalPostings<V, MmapFile>,
tokens: &TokenSet,
point_id: PointOffsetType,
) -> bool {
// Check that all tokens are in document
tokens.tokens().iter().all(|query_token| {
postings
.get(*query_token)
// unwrap safety: all tokens exist in the vocabulary, otherwise there'd be no query tokens
.unwrap()
.visitor()
.contains(point_id)
})
) -> OperationResult<bool> {
let result = postings.with_all_or_none_postings(tokens.tokens(), |all_postings| {
Ok(all_postings
.into_iter()
.all(|(_token_id, posting)| posting.visitor().contains(point_id)))
})?;
// Some token has no posting list -> no match
Ok(result.unwrap_or(false))
}
match &self.storage.postings {
@@ -296,25 +302,25 @@ impl MmapInvertedIndex {
}
}
fn check_has_any(&self, tokens: &TokenSet, point_id: PointOffsetType) -> bool {
fn check_has_any(&self, tokens: &TokenSet, point_id: PointOffsetType) -> OperationResult<bool> {
if tokens.is_empty() {
return false;
return Ok(false);
}
// check presence of the document
if self.values_is_empty(point_id) {
return false;
return Ok(false);
}
fn check_any<V: MmapPostingValue>(
postings: &MmapPostings<V>,
fn check_any<V: ZerocopyPostingValue>(
postings: &UniversalPostings<V, MmapFile>,
tokens: &TokenSet,
point_id: PointOffsetType,
) -> bool {
// Check that at least one token is in document
tokens.tokens().iter().any(|token_id| {
let posting_list = postings.get(*token_id).unwrap();
posting_list.visitor().contains(point_id)
) -> OperationResult<bool> {
postings.with_existing_postings(tokens.tokens(), |all_postings| {
Ok(all_postings
.into_iter()
.any(|(_token_id, posting)| posting.visitor().contains(point_id)))
})
}
@@ -325,40 +331,63 @@ impl MmapInvertedIndex {
}
/// Iterate over point ids whose documents contain all given tokens in the same order they are provided
pub fn filter_has_phrase<'a>(
&'a self,
phrase: Document,
) -> impl Iterator<Item = PointOffsetType> + 'a {
pub fn filter_has_phrase(&self, phrase: Document) -> OperationResult<Vec<PointOffsetType>> {
// in case of mmap immutable index, deleted points are still in the postings
let is_active = move |idx| self.is_active(idx);
match &self.storage.postings {
MmapPostingsEnum::WithPositions(postings) => {
Either::Right(intersect_compressed_postings_phrase_iterator(
phrase,
|token_id| postings.get(*token_id),
is_active,
))
// Deduplicate phrase tokens: repeated tokens (e.g. "zn zn") must
// not fetch the same posting list twice, otherwise positions get
// added twice in `phrase_in_all_postings`.
let unique_tokens = phrase.to_token_set();
let result = postings.with_all_or_none_postings(
unique_tokens.tokens(),
|selected_postings| {
Ok(intersect_compressed_postings_phrase_iterator(
phrase,
selected_postings,
is_active,
)
.collect())
},
)?;
// Some token has no posting list -> no matches
Ok(result.unwrap_or_default())
}
// cannot do phrase matching if there's no positional information
MmapPostingsEnum::Ids(_postings) => Either::Left(std::iter::empty()),
MmapPostingsEnum::Ids(_postings) => Ok(Vec::new()),
}
}
pub fn check_has_phrase(&self, phrase: &Document, point_id: PointOffsetType) -> bool {
pub fn check_has_phrase(
&self,
phrase: &Document,
point_id: PointOffsetType,
) -> OperationResult<bool> {
// in case of mmap immutable index, deleted points are still in the postings
if !self.is_active(point_id) {
return false;
return Ok(false);
}
match &self.storage.postings {
MmapPostingsEnum::WithPositions(postings) => {
check_compressed_postings_phrase(phrase, point_id, |token_id| {
postings.get(*token_id)
})
let unique_tokens = phrase.to_token_set();
let result = postings.with_all_or_none_postings(
unique_tokens.tokens(),
|selected_postings| {
Ok(check_compressed_postings_phrase(
phrase,
point_id,
selected_postings,
))
},
)?;
// Some token has no posting list -> no match
Ok(result.unwrap_or(false))
}
// cannot do phrase matching if there's no positional information
MmapPostingsEnum::Ids(_postings) => false,
MmapPostingsEnum::Ids(_postings) => Ok(false),
}
}
@@ -386,7 +415,7 @@ impl MmapInvertedIndex {
/// Populate all pages in the mmap.
/// Block until all pages are populated.
pub fn populate(&self) -> OperationResult<()> {
self.storage.postings.populate();
self.storage.postings.populate()?;
self.storage.vocab.populate()?;
self.storage.point_to_tokens_count.populate()?;
Ok(())
@@ -406,7 +435,7 @@ impl MmapInvertedIndex {
point_to_tokens_count,
deleted_points,
} = storage;
postings.clear_cache();
postings.clear_cache()?;
vocab.clear_cache()?;
point_to_tokens_count.clear_cache()?;
deleted_points.clear_cache()?;
@@ -467,31 +496,41 @@ impl InvertedIndex for MmapInvertedIndex {
query: ParsedQuery,
_hw_counter: &HardwareCounterCell,
) -> OperationResult<Box<dyn Iterator<Item = PointOffsetType> + 'a>> {
match query {
ParsedQuery::AllTokens(tokens) => Ok(self.filter_has_all(tokens)),
ParsedQuery::Phrase(phrase) => Ok(Box::new(self.filter_has_phrase(phrase))),
ParsedQuery::AnyTokens(tokens) => Ok(Box::new(self.filter_has_any(tokens))),
}
let ids = match query {
ParsedQuery::AllTokens(tokens) => self.filter_has_all(tokens)?,
ParsedQuery::Phrase(phrase) => self.filter_has_phrase(phrase)?,
ParsedQuery::AnyTokens(tokens) => self.filter_has_any(tokens)?,
};
Ok(Box::new(ids.into_iter()))
}
fn get_posting_len(
&self,
token_id: TokenId,
_hw_counter: &HardwareCounterCell,
) -> Option<usize> {
) -> OperationResult<Option<usize>> {
self.storage.postings.posting_len(token_id)
}
fn vocab_with_postings_len_iter(&self) -> impl Iterator<Item = (&str, usize)> + '_ {
fn vocab_with_postings_len_iter(
&self,
) -> impl Iterator<Item = OperationResult<(&str, usize)>> + '_ {
self.iter_vocab().filter_map(move |(token, &token_id)| {
self.storage
.postings
.posting_len(token_id)
.map(|posting_len| (token, posting_len))
// Surface read errors as iterator items; drop tokens with no
// posting list silently (same as the in-memory variants).
match self.storage.postings.posting_len(token_id) {
Ok(Some(posting_len)) => Some(Ok((token, posting_len))),
Ok(None) => None,
Err(err) => Some(Err(err)),
}
})
}
fn check_match(&self, parsed_query: &ParsedQuery, point_id: PointOffsetType) -> bool {
fn check_match(
&self,
parsed_query: &ParsedQuery,
point_id: PointOffsetType,
) -> OperationResult<bool> {
match parsed_query {
ParsedQuery::AllTokens(tokens) => self.check_has_subset(tokens, point_id),
ParsedQuery::Phrase(phrase) => self.check_has_phrase(phrase, point_id),

View File

@@ -0,0 +1,56 @@
use std::borrow::Cow;
use common::types::PointOffsetType;
use posting_list::{PostingChunk, PostingListView, RemainderPosting, SizedTypeFor};
use zerocopy::FromBytes;
use crate::common::operation_error::OperationResult;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::types::{
PostingListHeader, ZerocopyPostingValue,
};
/// Raw byte representation of posting list, which can be converted into [`PostingListView`]
pub struct RawPostingList<'a> {
bytes: Cow<'a, [u8]>,
header: PostingListHeader,
}
impl<'a> RawPostingList<'a> {
pub fn new(bytes: Cow<'a, [u8]>, header: PostingListHeader) -> RawPostingList<'a> {
RawPostingList { bytes, header }
}
}
impl<'a> RawPostingList<'a> {
pub fn as_view<V: ZerocopyPostingValue>(&'a self) -> OperationResult<PostingListView<'a, V>> {
let (last_doc_id, bytes) = PointOffsetType::read_from_prefix(self.bytes.as_ref())?;
let (chunks, bytes) = <[PostingChunk<SizedTypeFor<V>>]>::ref_from_prefix_with_elems(
bytes,
self.header.chunks_count as usize,
)?;
let (id_data, bytes) = bytes.split_at(self.header.ids_data_bytes_count as usize);
let (var_size_data, bytes) = bytes.split_at(self.header.var_size_data_bytes_count as usize);
// skip padding
let bytes = &bytes[self.header.alignment_bytes_count as usize..];
let (remainder_postings, _) =
<[RemainderPosting<SizedTypeFor<V>>]>::ref_from_prefix_with_elems(
bytes,
self.header.remainder_count as usize,
)?;
let posting_list_view = PostingListView::from_components(
id_data,
chunks,
var_size_data,
remainder_postings,
Some(last_doc_id),
);
Ok(posting_list_view)
}
}

View File

@@ -0,0 +1,68 @@
use common::types::PointOffsetType;
use posting_list::{
CHUNK_LEN, PostingChunk, PostingValue, RemainderPosting, SizedTypeFor, ValueHandler,
};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::index::field_index::full_text_index::inverted_index::positions::Positions;
pub const ALIGNMENT: usize = 4;
/// A [`PostingValue`] whose sized payload can be zerocopy-read from an mmap.
pub(in crate::index::field_index::full_text_index) trait ZerocopyPostingValue:
PostingValue<
Handler: ValueHandler<Sized: FromBytes + IntoBytes + Immutable + KnownLayout + Unaligned>,
>
{
}
impl ZerocopyPostingValue for () {}
impl ZerocopyPostingValue for Positions {}
#[derive(Debug, Default, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
pub(in crate::index::field_index::full_text_index) struct PostingsHeader {
/// Number of posting lists. One posting list per term
pub posting_count: usize,
pub _reserved: [u8; 32],
}
/// This data structure should contain all the necessary information to
/// construct `PostingListView<V>` from the mmap file.
#[derive(Debug, Default, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
#[repr(C)]
pub(in crate::index::field_index::full_text_index) struct PostingListHeader {
/// Offset in bytes from the start of the mmap file
/// where the posting list data starts
pub offset: u64,
/// Amount of chunks in compressed posting list
pub chunks_count: u32,
/// Length in bytes for the compressed postings data
pub ids_data_bytes_count: u32,
/// Length in bytes for the alignment bytes
pub alignment_bytes_count: u8,
/// Number of `RemainderPosting` elements (tail that doesn't fill a chunk)
pub remainder_count: u8,
pub _reserved: [u8; 2],
/// Length in bytes for the var-sized data. Add-on for phrase matching, otherwise 0
pub var_size_data_bytes_count: u32,
}
impl PostingListHeader {
pub fn posting_size<V: PostingValue>(&self) -> usize {
self.ids_data_bytes_count as usize
+ self.var_size_data_bytes_count as usize
+ self.alignment_bytes_count as usize
+ self.remainder_count as usize * size_of::<RemainderPosting<SizedTypeFor<V>>>()
+ self.chunks_count as usize * size_of::<PostingChunk<SizedTypeFor<V>>>()
+ size_of::<PointOffsetType>() // last_doc_id
}
/// Number of elements in the posting list. Matches `PostingListView::len`.
pub fn posting_len(&self) -> usize {
self.chunks_count as usize * CHUNK_LEN + self.remainder_count as usize
}
}

View File

@@ -0,0 +1,268 @@
use std::cell::Cell;
use std::marker::PhantomData;
use std::path::PathBuf;
use common::generic_consts::{Random, Sequential};
use common::universal_io::{OpenOptions, ReadRange, UniversalIoError, UniversalRead};
use posting_list::{PostingList, PostingListView};
use zerocopy::FromBytes;
use crate::common::operation_error::OperationResult;
use crate::index::field_index::full_text_index::inverted_index::TokenId;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::raw_posting_list::RawPostingList;
use crate::index::field_index::full_text_index::inverted_index::mmap_inverted_index::types::{
PostingListHeader, PostingsHeader, ZerocopyPostingValue,
};
/// Posting lists stored on disk and accessed via the [`UniversalRead`]
/// abstraction. The on-disk layout is produced by
/// [`create_postings_file`](super::create_postings::create_postings_file)
/// and matches the layout documented there.
///
/// `get_header(token_id)` indexes the per-token header array directly:
/// `size_of::<PostingsHeader>() + token_id * size_of::<PostingListHeader>()`.
/// Each [`PostingListHeader`] then points (via absolute `offset`) into the
/// posting-data region.
pub struct UniversalPostings<V: ZerocopyPostingValue, S: UniversalRead<u8>> {
_path: PathBuf,
storage: S,
header: PostingsHeader,
_value_type: PhantomData<V>,
}
type HeaderResult = Result<(TokenId, PostingListHeader), UniversalIoError>;
/// Output of [`UniversalPostings::headers_iter`]: a lazy iterator over the
/// headers that are in range, paired with the pre-filtered list of token ids
/// that were outside the valid range.
struct HeadersBatch<'a> {
iter: Box<dyn Iterator<Item = HeaderResult> + 'a>,
missing: Vec<TokenId>,
}
impl<V: ZerocopyPostingValue, S: UniversalRead<u8>> UniversalPostings<V, S> {
/// Open the postings file at `path` via the `S` storage backend.
pub fn open(path: impl Into<PathBuf>, options: OpenOptions) -> OperationResult<Self> {
let path = path.into();
let storage = S::open(&path, options)?;
let header_bytes = storage.read::<Sequential>(ReadRange {
byte_offset: 0,
length: size_of::<PostingsHeader>() as u64,
})?;
let (header, _) = PostingsHeader::read_from_prefix(header_bytes.as_ref())?;
Ok(Self {
_path: path,
storage,
header,
_value_type: PhantomData,
})
}
/// Hint the storage backend to populate any RAM cache backing this file.
/// For mmap-backed storage this is `madvise(MADV_POPULATE_READ)` and blocks
/// until pages are populated.
pub fn populate(&self) -> OperationResult<()> {
self.storage.populate()?;
Ok(())
}
/// Hint the storage backend to drop any RAM cache backing this file.
/// For mmap-backed storage this is `madvise(MADV_PAGEOUT)`.
pub fn clear_cache(&self) -> OperationResult<()> {
self.storage.clear_ram_cache()?;
Ok(())
}
/// Stream posting list headers for a batch of token ids.
///
/// The returned [`HeadersBatch`] pairs a lazy iterator over the in-range
/// `(token_id, header)` pairs with the list of token ids that were
/// out-of-range. Iterator order is not guaranteed to match the input.
fn headers_iter(&self, token_ids: &[TokenId]) -> Result<HeadersBatch<'_>, UniversalIoError> {
let header_length = size_of::<PostingListHeader>() as u64;
let posting_count = self.header.posting_count;
let mut valid_ranges: Vec<(TokenId, ReadRange)> = Vec::with_capacity(token_ids.len());
let mut filtered_out: Vec<TokenId> = Vec::new();
for &token_id in token_ids {
if posting_count <= token_id as usize {
filtered_out.push(token_id);
continue;
}
let header_offset =
size_of::<PostingsHeader>() as u64 + u64::from(token_id) * header_length;
valid_ranges.push((
token_id,
ReadRange {
byte_offset: header_offset,
length: header_length,
},
));
}
let valid_iter = self
.storage
.read_iter::<Random, _>(valid_ranges)?
.map(|res| {
let (token_id, bytes) = res?;
let (header, _) = PostingListHeader::read_from_prefix(bytes.as_ref())?;
Ok((token_id, header))
});
Ok(HeadersBatch {
iter: Box::new(valid_iter),
missing: filtered_out,
})
}
fn get_header(&self, token_id: TokenId) -> OperationResult<Option<PostingListHeader>> {
let HeadersBatch { mut iter, .. } = self.headers_iter(&[token_id])?;
let Some(entry) = iter.next() else {
return Ok(None);
};
let (_, header) = entry?;
Ok(Some(header))
}
/// Create PostingListView<V> from the given header
///
/// Assume the following layout:
///
/// ```ignore
/// last_doc_id: &'a PointOffsetType,
/// chunks_index: &'a [PostingChunk<()>],
/// data: &'a [u8],
/// var_size_data: &'a [u8], // might be empty in case of only ids
/// _alignment: &'a [u8], // 0-3 extra bytes to align the data
/// remainder_postings: &'a [PointOffsetType],
/// ```
#[cfg(test)]
fn raw_posting<'a>(
&'a self,
header: PostingListHeader,
) -> Result<RawPostingList<'a>, UniversalIoError> {
let read_range = ReadRange {
byte_offset: header.offset,
length: header.posting_size::<V>() as u64,
};
let bytes = self.storage.read::<Sequential>(read_range)?;
let result = RawPostingList::new(bytes, header);
Ok(result)
}
#[cfg(test)]
pub fn get(&self, token_id: TokenId) -> OperationResult<Option<RawPostingList<'_>>> {
let header = self.get_header(token_id)?;
if let Some(header) = header {
let posting = self.raw_posting(header).map(Some)?;
return Ok(posting);
}
Ok(None)
}
/// Number of elements in the posting list for `token_id`. Reads only the
/// per-token header, not the posting bytes.
pub fn posting_len(&self, token_id: TokenId) -> OperationResult<Option<usize>> {
Ok(self.get_header(token_id)?.map(|h| h.posting_len()))
}
/// Read the posting lists for every header yielded by `header_iter` and
/// hand the resulting views to `callback`. Header iteration is pipelined
/// into the posting reads - submissions are scheduled lazily as headers
/// arrive.
fn with_posting_views<T>(
&self,
header_iter: Box<dyn Iterator<Item = HeaderResult> + '_>,
expected_capacity: usize,
callback: impl FnOnce(Vec<(TokenId, PostingListView<'_, V>)>) -> OperationResult<T>,
) -> OperationResult<T> {
let header_err: Cell<Option<UniversalIoError>> = Cell::new(None);
let range_iter = header_iter.filter_map(|header_res| match header_res {
Ok((token_id, header)) => {
let range = ReadRange {
byte_offset: header.offset,
length: header.posting_size::<V>() as u64,
};
Some(((token_id, header), range))
}
Err(err) => {
header_err.set(Some(err));
None
}
});
let mut raw_postings: Vec<(TokenId, RawPostingList<'_>)> =
Vec::with_capacity(expected_capacity);
for entry in self.storage.read_iter::<Sequential, _>(range_iter)? {
let ((token_id, header), bytes) = entry?;
raw_postings.push((token_id, RawPostingList::new(bytes, header)));
}
if let Some(err) = header_err.take() {
return Err(err.into());
}
let views = raw_postings
.iter()
.map(|(token_id, raw)| raw.as_view::<V>().map(|view| (*token_id, view)))
.collect::<OperationResult<Vec<_>>>()?;
callback(views)
}
/// Fetch posting lists for the given token ids and hand them to `callback`.
/// If any of posting lists is not found - return `None`.
pub fn with_all_or_none_postings<T>(
&self,
token_ids: &[TokenId],
callback: impl FnOnce(Vec<(TokenId, PostingListView<'_, V>)>) -> OperationResult<T>,
) -> OperationResult<Option<T>> {
let HeadersBatch {
iter: header_iter,
missing,
} = self.headers_iter(token_ids)?;
if !missing.is_empty() {
return Ok(None);
}
self.with_posting_views(header_iter, token_ids.len(), callback)
.map(Some)
}
/// Fetch all existing posting lists for the given token ids and hand them
/// to `callback`. Token ids without a posting list are silently ignored.
pub fn with_existing_postings<T>(
&self,
token_ids: &[TokenId],
callback: impl FnOnce(Vec<(TokenId, PostingListView<'_, V>)>) -> OperationResult<T>,
) -> OperationResult<T> {
let HeadersBatch {
iter: header_iter, ..
} = self.headers_iter(token_ids)?;
self.with_posting_views(header_iter, token_ids.len(), callback)
}
pub fn all_postings(&self) -> OperationResult<Vec<PostingList<V>>> {
let mut result = Vec::new();
let all_tokens = (0..self.header.posting_count as TokenId).collect::<Vec<_>>();
self.with_existing_postings(&all_tokens, |views| {
for (_, view) in views {
let posting_list = view.to_owned();
result.push(posting_list);
}
Ok(())
})?;
Ok(result)
}
}

View File

@@ -230,15 +230,18 @@ pub trait InvertedIndex {
hw_counter: &'a HardwareCounterCell,
) -> OperationResult<Box<dyn Iterator<Item = PointOffsetType> + 'a>>;
fn get_posting_len(&self, token_id: TokenId, hw_counter: &HardwareCounterCell)
-> Option<usize>;
fn get_posting_len(
&self,
token_id: TokenId,
hw_counter: &HardwareCounterCell,
) -> OperationResult<Option<usize>>;
fn estimate_cardinality(
&self,
query: &ParsedQuery,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> CardinalityEstimation {
) -> OperationResult<CardinalityEstimation> {
match query {
ParsedQuery::AllTokens(tokens) => {
self.estimate_has_subset_cardinality(tokens, condition, hw_counter)
@@ -257,31 +260,31 @@ pub trait InvertedIndex {
tokens: &TokenSet,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> CardinalityEstimation {
) -> OperationResult<CardinalityEstimation> {
let points_count = self.points_count();
let posting_lengths: Option<Vec<usize>> = tokens
.tokens()
.iter()
.map(|&vocab_idx| self.get_posting_len(vocab_idx, hw_counter))
.collect();
.collect::<OperationResult<Option<Vec<usize>>>>()?;
if posting_lengths.is_none() || points_count == 0 {
// There are unseen tokens -> no matches
return CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
let postings = posting_lengths.unwrap();
if postings.is_empty() {
// Empty request -> no matches
return CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
// Smallest posting is the largest possible cardinality
let smallest_posting = postings.iter().min().copied().unwrap();
if postings.len() == 1 {
return CardinalityEstimation::exact(smallest_posting)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(smallest_posting)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
let expected_frac: f64 = postings
@@ -289,12 +292,12 @@ pub trait InvertedIndex {
.map(|posting| *posting as f64 / points_count as f64)
.product();
let exp = (expected_frac * points_count as f64) as usize;
CardinalityEstimation {
Ok(CardinalityEstimation {
primary_clauses: vec![PrimaryCondition::Condition(Box::new(condition.clone()))],
min: 0, // ToDo: make better estimation
exp,
max: smallest_posting,
}
})
}
fn estimate_has_any_cardinality(
@@ -302,39 +305,39 @@ pub trait InvertedIndex {
tokens: &TokenSet,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> CardinalityEstimation {
) -> OperationResult<CardinalityEstimation> {
let points_count = self.points_count();
let posting_lengths: Vec<_> = tokens
let posting_lengths: Vec<usize> = tokens
.tokens()
.iter()
.filter_map(|&vocab_idx| self.get_posting_len(vocab_idx, hw_counter))
.collect();
.filter_map(|&vocab_idx| self.get_posting_len(vocab_idx, hw_counter).transpose())
.collect::<OperationResult<Vec<usize>>>()?;
if posting_lengths.is_empty() {
// Empty request -> no matches
return CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
// At least one posting is the largest possible cardinality
let largest_posting = posting_lengths.iter().max().copied().unwrap();
if posting_lengths.len() == 1 {
return CardinalityEstimation::exact(largest_posting)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(largest_posting)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
let sum: usize = posting_lengths.iter().sum();
let exp = expected_should_estimation(posting_lengths.into_iter(), points_count);
CardinalityEstimation {
Ok(CardinalityEstimation {
primary_clauses: vec![PrimaryCondition::Condition(Box::new(condition.clone()))],
min: largest_posting,
exp,
max: min(sum, points_count),
}
})
}
fn estimate_has_phrase_cardinality(
@@ -342,44 +345,46 @@ pub trait InvertedIndex {
phrase: &Document,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> CardinalityEstimation {
) -> OperationResult<CardinalityEstimation> {
if phrase.is_empty() {
return CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone())));
return Ok(CardinalityEstimation::exact(0)
.with_primary_clause(PrimaryCondition::Condition(Box::new(condition.clone()))));
}
// Start with same cardinality estimation as has_subset
let tokenset = phrase.to_token_set();
let subset_estimation =
self.estimate_has_subset_cardinality(&tokenset, condition, hw_counter);
self.estimate_has_subset_cardinality(&tokenset, condition, hw_counter)?;
// But we can restrict it by considering the phrase length
let phrase_sq = phrase.len() * phrase.len();
CardinalityEstimation {
Ok(CardinalityEstimation {
primary_clauses: vec![PrimaryCondition::Condition(Box::new(condition.clone()))],
min: subset_estimation.min / phrase_sq,
exp: subset_estimation.exp / phrase_sq,
max: subset_estimation.max / phrase_sq,
}
})
}
fn vocab_with_postings_len_iter(&self) -> impl Iterator<Item = (&str, usize)> + '_;
fn vocab_with_postings_len_iter(
&self,
) -> impl Iterator<Item = OperationResult<(&str, usize)>> + '_;
fn payload_blocks(
&self,
threshold: usize,
key: PayloadKeyType,
) -> impl Iterator<Item = OperationResult<PayloadBlockCondition>> + '_ {
let map_filter_condition = move |(token, postings_len): (&str, usize)| {
if postings_len >= threshold {
let map_filter_condition = move |item: OperationResult<(&str, usize)>| match item {
Ok((token, postings_len)) if postings_len >= threshold => {
Some(Ok(PayloadBlockCondition {
condition: FieldCondition::new_match(key.clone(), Match::new_text(token)),
cardinality: postings_len,
}))
} else {
None
}
Ok(_) => None,
Err(err) => Some(Err(err)),
};
// It might be very hard to predict possible combinations of conditions,
@@ -388,7 +393,11 @@ pub trait InvertedIndex {
.filter_map(map_filter_condition)
}
fn check_match(&self, parsed_query: &ParsedQuery, point_id: PointOffsetType) -> bool;
fn check_match(
&self,
parsed_query: &ParsedQuery,
point_id: PointOffsetType,
) -> OperationResult<bool>;
fn values_is_empty(&self, point_id: PointOffsetType) -> bool;
@@ -544,7 +553,7 @@ mod tests {
.unwrap()
.unwrap();
let imm_mmap = ImmutableInvertedIndex::from(&mmap);
let imm_mmap = ImmutableInvertedIndex::try_from(&mmap).unwrap();
// Check same vocabulary
for (token, token_id) in &immutable.vocab {
@@ -611,7 +620,7 @@ mod tests {
.unwrap()
.unwrap();
let mut imm_mmap_index = ImmutableInvertedIndex::from(&mmap_index);
let mut imm_mmap_index = ImmutableInvertedIndex::try_from(&mmap_index).unwrap();
let queries: Vec<_> = (0..100).map(|_| generate_query()).collect();

View File

@@ -222,23 +222,33 @@ impl InvertedIndex for MutableInvertedIndex {
}
}
fn get_posting_len(&self, token_id: TokenId, _: &HardwareCounterCell) -> Option<usize> {
self.postings.get(token_id as usize).map(|x| x.len())
fn get_posting_len(
&self,
token_id: TokenId,
_: &HardwareCounterCell,
) -> OperationResult<Option<usize>> {
Ok(self.postings.get(token_id as usize).map(|x| x.len()))
}
fn vocab_with_postings_len_iter(&self) -> impl Iterator<Item = (&str, usize)> + '_ {
fn vocab_with_postings_len_iter(
&self,
) -> impl Iterator<Item = OperationResult<(&str, usize)>> + '_ {
self.vocab.iter().filter_map(|(token, &posting_idx)| {
self.postings
.get(posting_idx as usize)
.map(|postings| (token.as_str(), postings.len()))
.map(|postings| Ok((token.as_str(), postings.len())))
})
}
fn check_match(&self, parsed_query: &ParsedQuery, point_id: PointOffsetType) -> bool {
match parsed_query {
fn check_match(
&self,
parsed_query: &ParsedQuery,
point_id: PointOffsetType,
) -> OperationResult<bool> {
let matched = match parsed_query {
ParsedQuery::AllTokens(query) => {
let Some(doc) = self.get_tokens(point_id) else {
return false;
return Ok(false);
};
// Check that all tokens are in document
@@ -246,7 +256,7 @@ impl InvertedIndex for MutableInvertedIndex {
}
ParsedQuery::Phrase(document) => {
let Some(doc) = self.get_document(point_id) else {
return false;
return Ok(false);
};
// Check that all tokens are in document, in order
@@ -254,13 +264,14 @@ impl InvertedIndex for MutableInvertedIndex {
}
ParsedQuery::AnyTokens(query) => {
let Some(doc) = self.get_tokens(point_id) else {
return false;
return Ok(false);
};
// Check that at least one token is in document
doc.has_any(query)
}
}
};
Ok(matched)
}
fn values_is_empty(&self, point_id: PointOffsetType) -> bool {

View File

@@ -87,7 +87,7 @@ pub fn merge_compressed_postings_iterator<'a, V: PostingValue + 'a>(
/// Returns an iterator over the points that match the given phrase query.
pub fn intersect_compressed_postings_phrase_iterator<'a>(
phrase: Document,
token_to_posting: impl Fn(&TokenId) -> Option<PostingListView<'a, Positions>>,
mut postings: Vec<(TokenId, PostingListView<'a, Positions>)>,
is_active: impl Fn(PointOffsetType) -> bool + 'a,
) -> impl Iterator<Item = PointOffsetType> + 'a {
if phrase.is_empty() {
@@ -95,18 +95,6 @@ pub fn intersect_compressed_postings_phrase_iterator<'a>(
return Either::Left(std::iter::empty());
}
let postings_opt: Option<Vec<_>> = phrase
.to_token_set()
.tokens()
.iter()
.map(|token_id| token_to_posting(token_id).map(|posting| (*token_id, posting)))
.collect();
let Some(mut postings) = postings_opt else {
// There are unseen tokens -> no matches
return Either::Left(std::iter::empty());
};
let smallest_posting_idx = postings
.iter()
.enumerate()
@@ -174,21 +162,15 @@ fn phrase_in_all_postings<'a>(
PartialDocument::new(tokens_positions).has_phrase(phrase)
}
pub fn check_compressed_postings_phrase<'a>(
pub fn check_compressed_postings_phrase(
phrase: &Document,
point_id: PointOffsetType,
token_to_posting: impl Fn(&TokenId) -> Option<PostingListView<'a, Positions>>,
token_to_posting: Vec<(TokenId, PostingListView<'_, Positions>)>,
) -> bool {
let Some(mut posting_iterators): Option<Vec<_>> = phrase
.to_token_set()
.tokens()
.iter()
.map(|token_id| token_to_posting(token_id).map(|posting| (*token_id, posting.into_iter())))
.collect()
else {
// not all tokens are present in the index
return false;
};
let mut posting_iterators = token_to_posting
.into_iter()
.map(|(token_id, posting)| (token_id, posting.into_iter()))
.collect::<Vec<_>>();
phrase_in_all_postings(point_id, phrase, Vec::new(), &mut posting_iterators)
}

View File

@@ -209,7 +209,7 @@ impl FieldIndexBuilderTrait for FullTextMmapIndexBuilder {
let text_index = if is_on_disk {
FullTextIndex::Mmap(Box::new(mmap_index))
} else {
FullTextIndex::Immutable(ImmutableFullTextIndex::open_mmap(mmap_index))
FullTextIndex::Immutable(ImmutableFullTextIndex::open_mmap(mmap_index)?)
};
Ok(text_index)

View File

@@ -188,7 +188,7 @@ fn test_prefix_search() {
let query = index.parse_text_query("ROBO", &hw_counter).unwrap();
for idx in res.iter().copied() {
assert!(index.check_match(&query, idx));
assert!(index.check_match(&query, idx).unwrap());
}
assert_eq!(res.len(), 3);
@@ -253,9 +253,9 @@ fn test_phrase_matching() {
let text_query = index
.parse_text_query("quick brown fox", &hw_counter)
.unwrap();
assert!(index.check_match(&text_query, 0));
assert!(index.check_match(&text_query, 1));
assert!(index.check_match(&text_query, 2));
assert!(index.check_match(&text_query, 0).unwrap());
assert!(index.check_match(&text_query, 1).unwrap());
assert!(index.check_match(&text_query, 2).unwrap());
let text_results: Vec<_> = index
.filter_query(text_query, &hw_counter)
@@ -272,8 +272,8 @@ fn test_phrase_matching() {
let phrase_query = index
.parse_phrase_query("quick brown fox", &hw_counter)
.unwrap();
assert!(index.check_match(&phrase_query, 0));
assert!(index.check_match(&phrase_query, 2));
assert!(index.check_match(&phrase_query, 0).unwrap());
assert!(index.check_match(&phrase_query, 2).unwrap());
let phrase_results: Vec<_> = index
.filter_query(phrase_query, &hw_counter)
@@ -307,7 +307,7 @@ fn test_phrase_matching() {
let phrase_query = index
.parse_phrase_query("brown brown fox", &hw_counter)
.unwrap();
assert!(index.check_match(&phrase_query, 4));
assert!(index.check_match(&phrase_query, 4).unwrap());
// Should only match document 4
let filter_results: Vec<_> = index
@@ -378,7 +378,7 @@ fn test_ascii_folding_in_full_text_index_word() {
// ASCII-only queries should match only when folding is enabled
let query_enabled = index_enabled.parse_text_query("acao", &hw_counter).unwrap();
assert!(index_enabled.check_match(&query_enabled, 0));
assert!(index_enabled.check_match(&query_enabled, 0).unwrap());
let results_enabled: Vec<_> = index_enabled
.filter_query(query_enabled, &hw_counter)
@@ -398,7 +398,7 @@ fn test_ascii_folding_in_full_text_index_word() {
// Non-folded query must work in both
let query_acento = index_enabled.parse_text_query("ação", &hw_counter).unwrap();
assert!(index_enabled.check_match(&query_acento, 0));
assert!(index_enabled.check_match(&query_acento, 0).unwrap());
let results_acento: Vec<_> = index_enabled
.filter_query(query_acento, &hw_counter)
.unwrap()

View File

@@ -344,8 +344,8 @@ fn test_congruence(
for point_id in 0..POINT_COUNT as PointOffsetType {
assert_eq!(
index_a.check_match(&parsed_query_a, point_id),
index_b.check_match(&parsed_query_b, point_id),
index_a.check_match(&parsed_query_a, point_id).unwrap(),
index_b.check_match(&parsed_query_b, point_id).unwrap(),
);
}
@@ -387,8 +387,8 @@ fn test_congruence(
for point_id in 0..POINT_COUNT as PointOffsetType {
assert_eq!(
index_a.check_match(&parsed_query_a, point_id),
index_b.check_match(&parsed_query_b, point_id),
index_a.check_match(&parsed_query_a, point_id).unwrap(),
index_b.check_match(&parsed_query_b, point_id).unwrap(),
);
}
@@ -453,7 +453,7 @@ fn check_phrase<const KEYWORD_COUNT: usize>(
let parsed_query = parse_query(phrase, phrase_matching, index);
assert!(index.check_match(&parsed_query, *exp_id));
assert!(index.check_match(&parsed_query, *exp_id).unwrap());
let result = index
.filter_query(parsed_query, &hw_counter)

View File

@@ -54,7 +54,7 @@ impl FullTextIndex {
// Load into RAM, use mmap as backing storage
Some(Self::Immutable(ImmutableFullTextIndex::open_mmap(
mmap_index,
)))
)?))
};
Ok(index)
}
@@ -156,7 +156,7 @@ impl FullTextIndex {
query: &ParsedQuery,
condition: &FieldCondition,
hw_counter: &HardwareCounterCell,
) -> CardinalityEstimation {
) -> OperationResult<CardinalityEstimation> {
match self {
Self::Mutable(index) => index
.inverted_index
@@ -170,7 +170,11 @@ impl FullTextIndex {
}
}
pub fn check_match(&self, query: &ParsedQuery, point_id: PointOffsetType) -> bool {
pub fn check_match(
&self,
query: &ParsedQuery,
point_id: PointOffsetType,
) -> OperationResult<bool> {
match self {
Self::Mutable(index) => index.inverted_index.check_match(query, point_id),
Self::Immutable(index) => index.inverted_index.check_match(query, point_id),
@@ -522,7 +526,7 @@ impl PayloadFieldIndex for FullTextIndex {
&parsed_query,
condition,
hw_counter,
)))
)?))
}
fn payload_blocks(

View File

@@ -294,7 +294,7 @@ impl<S: StoredGeoMapIndexStorage> StoredGeoMapIndex<S> {
.point_to_values
.check_values_any(idx, |v| check_fn(v), &hw_counter)
})
.map(|r| r.unwrap_or(false))
.map(|r| r.unwrap_or(false)) // FIXME: don't silently ignore error
.unwrap_or(false)
}

View File

@@ -281,7 +281,10 @@ fn get_match_text_checker(
};
Some(Box::new(move |point_id: PointOffsetType| {
full_text_index.check_match(&parsed_query, point_id)
// FIXME: don't silently ignore errors. Log error? Update ConditionCheckerFn?
full_text_index
.check_match(&parsed_query, point_id)
.unwrap_or(false)
}))
}
FieldIndex::BoolIndex(_)

View File

@@ -212,5 +212,6 @@ pub fn io_error_to_status(e: UniversalIoError) -> Status {
Status::internal(format!("Uninitialized: {description}"))
}
UniversalIoError::BytemuckCast(e) => Status::internal(format!("Bytemuck cast error: {e}")),
UniversalIoError::ZerocopySize(e) => Status::internal(e),
}
}