use std::{cmp, path::Path};
const EMPTY_COMPOSITE: &[u8] = include_bytes!("empty/composite.bin");
const EMPTY_FAST: &[u8] = include_bytes!("empty/fast.bin");
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Empty {
Composite,
Fast,
}
impl Empty {
pub fn max_len() -> usize {
cmp::max(EMPTY_COMPOSITE.len(), EMPTY_FAST.len())
}
pub fn bytes(self) -> &'static [u8] {
match self {
Empty::Composite => EMPTY_COMPOSITE,
Empty::Fast => EMPTY_FAST,
}
}
pub fn for_path(path: &Path) -> Option<Empty> {
match path.extension()?.to_str()? {
"idx" | "pos" | "term" | "fieldnorm" => Some(Empty::Composite),
"fast" => Some(Empty::Fast),
_ => None,
}
}
pub fn detect(path: &Path, content: &[u8]) -> Option<Empty> {
let empty = Empty::for_path(path)?;
(content == empty.bytes()).then_some(empty)
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, env, fs, path::Path};
use eyre::Result;
use tantivy::{
Index, IndexSettings, TantivyDocument,
directory::MmapDirectory,
doc,
schema::{FAST, INDEXED, Schema, SchemaBuilder, TEXT},
};
use super::*;
fn segment_files<T>(
label: &str,
build: impl FnOnce(&mut SchemaBuilder) -> T,
doc: impl FnOnce(&Schema) -> Result<TantivyDocument>,
) -> Result<BTreeMap<String, Vec<u8>>> {
let tmp = env::temp_dir().join(format!("tantivy_remote_empty_{label}"));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp)?;
let mut builder = SchemaBuilder::new();
build(&mut builder);
let schema = builder.build();
let dir = MmapDirectory::open(&tmp)?;
let index = Index::create(dir, schema.clone(), IndexSettings::default())?;
let mut writer = index.writer(15_000_000)?;
let document = doc(&schema)?;
writer.add_document(document)?;
writer.commit()?;
let mut files = BTreeMap::new();
for entry in fs::read_dir(&tmp)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if let Some((_, ext)) = name.rsplit_once('.') {
if matches!(ext, "json" | "lock") {
continue;
}
let file = fs::read(entry.path())?;
files.insert(ext.to_string(), file);
}
}
Ok(files)
}
#[test]
fn captured_constants_match_tantivy() -> Result<()> {
let files = segment_files(
"fast_only",
|sb| sb.add_u64_field("n", FAST),
|s| Ok(doc!(s.get_field("n")? => 42u64)),
)?;
for ext in ["idx", "pos", "term", "fieldnorm"] {
assert_eq!(
files[ext].as_slice(),
EMPTY_COMPOSITE,
"empty `.{ext}` no longer matches the captured composite template",
);
}
let files = segment_files(
"indexed_only",
|sb| sb.add_u64_field("n", INDEXED),
|s| Ok(doc!(s.get_field("n")? => 42u64)),
)?;
assert_eq!(
files["fast"].as_slice(),
EMPTY_FAST,
"empty `.fast` no longer matches the captured fast-field template",
);
Ok(())
}
#[test]
fn populated_components_are_not_empty() -> Result<()> {
let files = segment_files(
"text",
|sb| sb.add_text_field("body", TEXT),
|s| Ok(doc!(s.get_field("body")? => "hello world hello")),
)?;
for ext in ["idx", "pos", "term", "fieldnorm", "store"] {
let path = format!("seg.{ext}");
assert_eq!(
Empty::detect(Path::new(&path), &files[ext]),
None,
"populated `.{ext}` was wrongly detected as empty",
);
}
assert_eq!(
Empty::detect(Path::new("seg.fast"), &files["fast"]),
Some(Empty::Fast),
"`.fast` with no fast field should be logically empty",
);
Ok(())
}
#[test]
fn detect_matches_only_the_right_extension() {
assert_eq!(
Empty::detect(Path::new("seg.idx"), Empty::Composite.bytes()),
Some(Empty::Composite),
);
assert_eq!(
Empty::detect(Path::new("seg.fast"), Empty::Fast.bytes()),
Some(Empty::Fast),
);
assert_eq!(
Empty::detect(Path::new("seg.idx"), Empty::Fast.bytes()),
None,
);
assert_eq!(
Empty::detect(Path::new("seg.store"), Empty::Composite.bytes()),
None,
);
assert_eq!(Empty::detect(Path::new("seg.0.del"), &[]), None);
}
}