use std::path::Path;
use std::sync::Mutex;
use indicatif::{ProgressBar, ProgressStyle};
use rayon::prelude::*;
use crate::config::IndexConfig;
use crate::error::Result;
use crate::indexer::schema::DocRecord;
use crate::indexer::simhash::compute_simhash;
use crate::indexer::trigram::TrigramAccumulator as TrigramAcc;
use crate::parser::{self, file_hash};
use crate::parser::walker::walk_directory;
use crate::storage::mmap::write_postings;
use crate::storage::segment::{new_segment, write_simhash};
use crate::storage::store::DocStore;
pub struct IndexStats {
pub added: usize,
pub skipped: usize,
pub removed: usize,
pub errors: usize,
}
pub fn run_index(config: &IndexConfig, root: &Path, force_full: bool) -> Result<IndexStats> {
config.ensure_dirs()?;
let store = DocStore::open(&config.docstore_path)?;
eprintln!("Scanning {}...", root.display());
let entries = walk_directory(root)?;
eprintln!("Found {} indexable files.", entries.len());
let mut to_index = Vec::new();
let mut skipped = 0;
let mut walked_paths = std::collections::HashSet::new();
for entry in &entries {
walked_paths.insert(entry.path.clone());
if force_full {
to_index.push(entry);
continue;
}
match store.doc_id_for_path(&entry.path)? {
None => to_index.push(entry), Some(id) => {
if let Some(record) = store.get_doc(id)? {
if record.mtime == entry.mtime {
skipped += 1;
continue;
}
match file_hash(&entry.path) {
Ok(hash) if hash == record.file_hash => {
skipped += 1; continue;
}
_ => to_index.push(entry), }
} else {
to_index.push(entry);
}
}
}
}
let all_stored = store.all_docs()?;
let mut removed = 0;
for (id, path) in &all_stored {
if path.starts_with(root) && !walked_paths.contains(path) {
store.remove_doc(*id, path)?;
removed += 1;
}
}
if to_index.is_empty() {
eprintln!("Nothing to index ({} unchanged, {} removed).", skipped, removed);
return Ok(IndexStats {
added: 0,
skipped,
removed,
errors: 0,
});
}
eprintln!(
"Indexing {} files ({} unchanged, {} removed)...",
to_index.len(),
skipped,
removed
);
let progress = ProgressBar::new(to_index.len() as u64);
progress.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}")
.unwrap(),
);
let global_acc = Mutex::new(TrigramAcc::new());
let global_simhashes: Mutex<Vec<(u32, u64)>> = Mutex::new(Vec::new());
let errors = Mutex::new(0usize);
let results: Vec<_> = to_index
.par_iter()
.filter_map(|entry| {
let result = (|| -> Result<(DocRecord, crate::indexer::schema::StructuralMeta)> {
let hash = file_hash(&entry.path)?;
let parsed = parser::parse(entry)?;
let doc_id = store.next_doc_id()?;
let simhash = compute_simhash(&parsed.text);
let mut local_acc = TrigramAcc::new();
local_acc.add_document(doc_id, &parsed.text);
{
let mut g = global_acc.lock().unwrap();
g.merge(local_acc);
}
{
let mut sh = global_simhashes.lock().unwrap();
sh.push((doc_id, simhash));
}
let snippet = parsed.text.chars().take(256).collect();
let record = DocRecord {
id: doc_id,
path: entry.path.clone(),
mtime: entry.mtime,
file_hash: hash,
title: parsed.title,
snippet,
simhash,
};
Ok((record, parsed.metadata))
})();
progress.inc(1);
match result {
Ok(pair) => Some(pair),
Err(e) => {
*errors.lock().unwrap() += 1;
eprintln!(" error: {}", e);
None
}
}
})
.collect();
progress.finish_with_message("done");
for (record, meta) in &results {
store.put_doc(record, meta)?;
}
let error_count = *errors.lock().unwrap();
let added = results.len();
let segment = new_segment(config)?;
let acc = global_acc.into_inner().unwrap();
let simhashes = global_simhashes.into_inner().unwrap();
write_postings(&acc, &segment.postings_path)?;
write_simhash(&segment.simhash_path, &simhashes)?;
eprintln!(
"Done. Added {}, skipped {}, removed {}, errors {}.",
added, skipped, removed, error_count
);
eprintln!("Index segment: {}", segment.dir.display());
Ok(IndexStats {
added,
skipped,
removed,
errors: error_count,
})
}