use crate::db::Database;
use crate::indexer::{IndexConfig, Indexer};
use crate::models::downloader::{ensure_dense_model_available, DENSE_MODEL_ID, MODEL_ID};
use crate::search::{
read_metadata, write_metadata, AnnIndex, BM25Index, DenseRetriever, IndexMetadata,
};
use anyhow::{bail, Result};
use colored::Colorize;
use std::env;
use std::path::PathBuf;
pub fn run(path: Option<PathBuf>, force: bool) -> Result<()> {
let project_root = path.unwrap_or_else(|| env::current_dir().unwrap());
if !Database::minni_dir_exists(&project_root) {
println!(
"{} Minni not initialized. Run {} first.",
"Error:".red(),
"minni init".cyan()
);
println!(
" Tip: Run {} to diagnose setup issues.",
"minni doctor".cyan()
);
bail!("minni is not initialized in {}", project_root.display());
}
let db = Database::open(&project_root)?;
let config = IndexConfig {
force_reindex: force,
..Default::default()
};
let mut indexer = Indexer::new(&db, config);
println!("{} Indexing {}...", "→".blue(), project_root.display());
println!();
let (result, new_chunks) = indexer.index_directory(&project_root)?;
println!();
println!("{} Building BM25 search index...", "→".blue());
let bm25_index_path = project_root.join(".minni").join("bm25_index");
let meta_path = bm25_index_path.join("minni_meta.json");
let ann_index_path = project_root.join(".minni").join("ann_index.json");
let model_signature = format!("{}+{}", MODEL_ID, DENSE_MODEL_ID);
let expected_meta = IndexMetadata::expected(BM25Index::schema_signature()?, &model_signature);
let force_rebuild = force || {
match read_metadata(&meta_path)? {
None => true, Some(stored) => !stored.matches(&expected_meta),
}
};
let bm25_index = BM25Index::new(&bm25_index_path)?;
if force_rebuild {
bm25_index.clear()?;
let all_chunks = db.get_all_chunks()?;
bm25_index.index_chunks(&all_chunks)?;
} else {
bm25_index.delete_by_chunk_ids(&result.old_chunk_ids)?;
bm25_index.index_chunks(&new_chunks)?;
}
println!("{} Building dense embedding index...", "→".blue());
match ensure_dense_model_available() {
Ok(dense_model_path) => {
let mut dense = DenseRetriever::new(&dense_model_path)?;
if force_rebuild {
let all_chunks = db.get_all_chunks()?;
let texts: Vec<String> = all_chunks
.iter()
.map(|c| format!("{}\n{}", c.file_path, c.content))
.collect();
let vectors = dense.embed_texts(&texts)?;
db.clear_embeddings()?;
for (chunk, vector) in all_chunks.iter().zip(vectors.iter()) {
db.upsert_embedding(&chunk.id, vector)?;
}
} else {
db.delete_embeddings_by_chunk_ids(&result.old_chunk_ids)?;
if !new_chunks.is_empty() {
let texts: Vec<String> = new_chunks
.iter()
.map(|c| format!("{}\n{}", c.file_path, c.content))
.collect();
let vectors = dense.embed_texts(&texts)?;
for (chunk, vector) in new_chunks.iter().zip(vectors.iter()) {
db.upsert_embedding(&chunk.id, vector)?;
}
}
}
println!("{} Dense embedding index built!", "✓".green());
rebuild_ann_index(&db, &ann_index_path)?;
println!("{} ANN index built!", "✓".green());
}
Err(e) => {
println!(
"{} Dense model unavailable — continuing with BM25-only retrieval.",
"!".yellow()
);
println!(" Reason: {}", e);
println!(
" Tip: Run {} to check model and network status.",
"minni doctor".cyan()
);
db.clear_embeddings()?;
if ann_index_path.exists() {
std::fs::remove_file(&ann_index_path)?;
}
}
}
write_metadata(&meta_path, &expected_meta)?;
println!("{} BM25 index built!", "✓".green());
println!();
println!("{} Indexing complete!", "✓".green());
println!(
" Files indexed: {}",
result.files_indexed.to_string().cyan()
);
println!(
" Chunks created: {}",
result.chunks_created.to_string().cyan()
);
println!(
" Edges created: {}",
result.edges_created.to_string().cyan()
);
println!(
" Edges resolved: {}/{}",
result.edges_resolved.to_string().cyan(),
result.edges_total.to_string().cyan()
);
println!(" Files skipped: {}", result.skipped.to_string().yellow());
println!(
" Files deleted: {}",
result.files_deleted.to_string().yellow()
);
if !result.errors.is_empty() {
println!();
println!("{} Errors:", "!".yellow());
for error in &result.errors[..result.errors.len().min(10)] {
println!(" - {}", error);
}
if result.errors.len() > 10 {
println!(" ... and {} more", result.errors.len() - 10);
}
}
Ok(())
}
fn rebuild_ann_index(db: &Database, ann_index_path: &std::path::Path) -> Result<()> {
let embeddings = db.get_all_embeddings()?;
if let Some(index) = AnnIndex::build(&embeddings)? {
index.save(ann_index_path)?;
} else if ann_index_path.exists() {
std::fs::remove_file(ann_index_path)?;
}
Ok(())
}