mod data_processing;
mod index_ops;
mod vector_ops;
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;
#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
#[cfg(not(target_env = "msvc"))]
fn release_memory_to_os() {
use tikv_jemalloc_ctl::{epoch, stats};
let _ = epoch::advance();
unsafe {
if let Ok(purge) = tikv_jemalloc_ctl::raw::read::<bool>(b"opt.background_thread\0")
&& !purge
{
let _ = tikv_jemalloc_ctl::raw::write(b"arena.0.purge\0", ());
}
}
if let (Ok(allocated), Ok(resident)) = (stats::allocated::read(), stats::resident::read()) {
tracing::debug!(
"Memory: allocated={:.1} MB, resident={:.1} MB",
allocated as f64 / (1024.0 * 1024.0),
resident as f64 / (1024.0 * 1024.0)
);
}
}
#[cfg(target_env = "msvc")]
fn release_memory_to_os() {
}
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(name = "hermes-tool")]
#[command(version, about = "CLI for Hermes index management and data processing")]
#[command(after_help = "Use 'hermes-tool <command> --help' for more information.")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum OptimizationMode {
Adaptive,
Size,
Performance,
}
impl OptimizationMode {
fn to_index_optimization(self) -> hermes_core::structures::IndexOptimization {
match self {
Self::Adaptive => hermes_core::structures::IndexOptimization::Adaptive,
Self::Size => hermes_core::structures::IndexOptimization::SizeOptimized,
Self::Performance => hermes_core::structures::IndexOptimization::PerformanceOptimized,
}
}
}
#[derive(Subcommand)]
enum Commands {
Create {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long)]
schema: PathBuf,
},
Init {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long)]
sdl: String,
},
Index {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long, required_unless_present = "stdin")]
documents: Option<PathBuf>,
#[arg(long, default_value = "false")]
stdin: bool,
#[arg(short, long, default_value = "100000")]
progress: usize,
#[arg(short = 'm', long, default_value = "3072")]
max_indexing_memory_mb: usize,
#[arg(short = 'j', long)]
indexing_threads: Option<usize>,
#[arg(short = 'c', long)]
compression_threads: Option<usize>,
#[arg(short = 'O', long, default_value = "adaptive")]
optimization: OptimizationMode,
},
Commit {
#[arg(short, long)]
index: PathBuf,
},
Merge {
#[arg(short, long)]
index: PathBuf,
},
Reorder {
#[arg(short, long)]
index: PathBuf,
},
Info {
#[arg(short, long)]
index: PathBuf,
},
Heatmap {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long)]
field: Option<String>,
#[arg(short = 'W', long)]
width: Option<usize>,
#[arg(short = 'H', long)]
height: Option<usize>,
#[arg(short, long, default_value = "0")]
segment: usize,
},
Search {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long)]
query: String,
#[arg(short, long, default_value = "10")]
limit: usize,
#[arg(short, long, default_value = "0")]
offset: usize,
},
Warmup {
#[arg(short, long)]
index: PathBuf,
#[arg(short = 's', long, default_value = "67108864")]
cache_size: usize,
},
Simhash {
#[arg(short, long)]
field: String,
#[arg(short, long)]
output: Option<String>,
},
Sort {
#[arg(short, long)]
field: String,
#[arg(short = 'r', long, default_value = "false")]
reverse: bool,
#[arg(short = 'N', long, default_value = "false")]
numeric: bool,
#[arg(short = 'c', long, default_value = "100000")]
chunk_size: usize,
#[arg(short = 't', long)]
temp_dir: Option<PathBuf>,
},
#[command(name = "term-stats")]
TermStats {
#[arg(short, long, required = true)]
field: Vec<String>,
#[arg(short = 'F', long, default_value = "json")]
format: String,
#[arg(short = 'm', long, default_value = "1")]
min_df: u32,
#[arg(long, default_value = "1.2")]
bm25_k1: f32,
#[arg(long, default_value = "0.75")]
bm25_b: f32,
},
#[command(name = "train-centroids")]
TrainCentroids {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
field: String,
#[arg(short, long)]
output: PathBuf,
#[arg(short = 'k', long)]
clusters: Option<usize>,
#[arg(short = 'n', long, default_value = "20")]
max_iters: usize,
#[arg(short = 's', long)]
sample_size: Option<usize>,
#[arg(long, default_value = "42")]
seed: u64,
},
#[command(name = "retrain-centroids")]
RetrainCentroids {
#[arg(short, long)]
index: PathBuf,
#[arg(short, long)]
field: String,
#[arg(short = 'k', long)]
clusters: Option<usize>,
#[arg(short = 'n', long, default_value = "20")]
max_iters: usize,
#[arg(short = 's', long)]
sample_size: Option<usize>,
#[arg(long, default_value = "42")]
seed: u64,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("hermes_tool=info".parse()?),
)
.init();
let cli = Cli::parse();
match cli.command {
Commands::Create { index, schema } => {
index_ops::create_index(index, schema).await?;
}
Commands::Init { index, sdl } => {
index_ops::init_index_from_sdl(index, sdl).await?;
}
Commands::Index {
index,
documents,
stdin,
progress,
max_indexing_memory_mb,
indexing_threads,
compression_threads,
optimization,
} => {
index_ops::index_documents(
index,
documents,
stdin,
progress,
max_indexing_memory_mb,
indexing_threads,
compression_threads,
optimization.to_index_optimization(),
)
.await?;
}
Commands::Commit { index } => {
index_ops::commit_index(index).await?;
}
Commands::Merge { index } => {
index_ops::merge_index(index).await?;
}
Commands::Reorder { index } => {
index_ops::reorder_index(index).await?;
}
Commands::Info { index } => {
index_ops::show_info(index).await?;
}
Commands::Heatmap {
index,
field,
width,
height,
segment,
} => {
index_ops::heatmap_bmp_grid(index, field, width, height, segment).await?;
}
Commands::Search {
index,
query,
limit,
offset,
} => {
index_ops::search_index(index, &query, limit, offset).await?;
}
Commands::Warmup { index, cache_size } => {
index_ops::warmup_cache(index, cache_size).await?;
}
Commands::Simhash { field, output } => {
let output_field = output.unwrap_or_else(|| format!("{}_simhash", field));
data_processing::run_simhash(&field, &output_field)
.context("Failed to process simhash")?;
}
Commands::Sort {
field,
reverse,
numeric,
chunk_size,
temp_dir,
} => {
data_processing::run_sort(&field, reverse, numeric, chunk_size, temp_dir)
.context("Failed to sort documents")?;
}
Commands::TermStats {
field,
format,
min_df,
bm25_k1,
bm25_b,
} => {
data_processing::run_term_stats(&field, &format, min_df, bm25_k1, bm25_b)
.context("Failed to compute term statistics")?;
}
Commands::TrainCentroids {
input,
field,
output,
clusters,
max_iters,
sample_size,
seed,
} => {
vector_ops::train_centroids(
input,
field,
output,
clusters,
max_iters,
sample_size,
seed,
)
.context("Failed to train centroids")?;
}
Commands::RetrainCentroids {
index,
field,
clusters,
max_iters,
sample_size,
seed,
} => {
vector_ops::retrain_centroids(index, field, clusters, max_iters, sample_size, seed)
.await
.context("Failed to retrain centroids")?;
}
}
Ok(())
}