use crate::documents::build_text_to_source;
use crate::embed::Embedder;
use crate::parsers::DocumentParsers;
use crate::store::{ScoredChunk, VectorStore, embed_and_insert};
use crate::types::{ChunkConfig, DocumentType};
use anyhow::{Result, anyhow};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub fn get_embeddings_file_path(folder: &Path) -> PathBuf {
folder.join(".ragrig_embeddings.json")
}
pub fn scan_document_files(folder: &Path) -> Vec<(DocumentType, String)> {
WalkDir::new(folder)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|entry| {
let path = entry.path().to_path_buf();
if !path.is_file() {
return None;
}
let ext = path.extension()?.to_str()?;
let doc_type = match ext {
"pdf" => DocumentType::Pdf(path.clone()),
"epub" => DocumentType::Epub(path.clone()),
"html" | "htm" => DocumentType::Html(path.clone()),
"docx" => DocumentType::Docx(path.clone()),
"md" | "rmd" | "Rmd" | "qmd" | "Qmd" => DocumentType::Markdown(path.clone()),
_ => return None,
};
let name = doc_type.file_name().to_string();
Some((doc_type, name))
})
.collect()
}
pub async fn embed_documents(
embedder: &dyn Embedder,
parsers: &DocumentParsers,
config: &ChunkConfig,
document_files: Vec<(DocumentType, String)>,
store: &dyn VectorStore,
) -> Result<()> {
log::info!("Parsing {} documents...", document_files.len());
let (all_texts, text_to_source) = build_text_to_source(&document_files, parsers, config)?;
if all_texts.is_empty() {
return Ok(());
}
log::info!(
"Generating embeddings for {} total text chunks...",
all_texts.len()
);
let embedded = embedder.embed(all_texts).await?;
embed_and_insert(store, embedded, &text_to_source).await
}
pub async fn collect_documents(
embedder: &dyn Embedder,
parsers: &DocumentParsers,
folder: &Path,
config: &ChunkConfig,
store: &dyn VectorStore,
) -> Result<()> {
log::info!("Scanning folder recursively: {:?}", folder);
let document_files = scan_document_files(folder);
log::info!(
"Found {} document files (PDF + EPUB).",
document_files.len()
);
let (all_texts, text_to_source) = build_text_to_source(&document_files, parsers, config)?;
if all_texts.is_empty() {
return Err(anyhow!("No text extracted from documents."));
}
log::info!(
"Generating embeddings for {} total text chunks...",
all_texts.len()
);
let embedded = embedder.embed(all_texts).await?;
let count = embedded.len();
embed_and_insert(store, embedded, &text_to_source).await?;
log::info!("Collection complete: {} chunks stored.", count);
Ok(())
}
pub async fn search_similar(
embedder: &dyn Embedder,
top_k: usize,
similarity_threshold: f64,
store: &dyn VectorStore,
query: &str,
) -> Result<Vec<ScoredChunk>> {
let embedded = embedder.embed(vec![query.to_string()]).await?;
let query_vec: Vec<f32> = embedded
.first()
.map(|(_, v)| v.clone())
.ok_or_else(|| anyhow!("Failed to get query embedding"))?;
store
.search(&query_vec, query, top_k, similarity_threshold)
.await
}
pub async fn remove_deleted_embeddings(
store: &dyn VectorStore,
current_files: &[(DocumentType, String)],
) -> Result<()> {
let current_file_names: HashSet<String> = current_files
.iter()
.map(|(doc_type, _)| doc_type.file_name().to_string())
.collect();
let stored_sources = store.sources();
for name in &stored_sources {
if !current_file_names.contains(name) {
log::info!("Removing chunks for deleted file: {}", name);
store.delete_by_source(name).await?;
}
}
Ok(())
}