pub mod keys;
pub mod keys_governance;
pub mod writer;
use std::path::{Path, PathBuf};
use fjall::{Database, Keyspace, KeyspaceCreateOptions};
use thiserror::Error;
const INDEX_PARTITION_REVISION: u32 = 4;
pub const INDEX_SCHEMA_VER: u32 = crate::version::RELEASE_MINOR as u32 + INDEX_PARTITION_REVISION;
const META_SCHEMA_VER: &[u8] = b"schema_ver";
const META_BM25_DOC_COUNT: &[u8] = b"code_bm25_n";
const META_BM25_TOTAL_LEN: &[u8] = b"code_bm25_total_len";
const INDEX_DIR: &str = "index.fjall";
const INDEX_CACHE_FLOOR_BYTES: u64 = 32 * 1_024 * 1_024;
const INDEX_CACHE_CEILING_BYTES: u64 = 256 * 1_024 * 1_024;
const INDEX_CACHE_DISK_FRACTION: f64 = 0.20;
const INDEX_CACHE_BYTES_ENV: &str = "BASEMIND_INDEX_CACHE_BYTES";
fn dir_size_bytes(dir: &Path) -> u64 {
let mut total = 0u64;
let mut stack = vec![dir.to_path_buf()];
while let Some(current) = stack.pop() {
let Ok(entries) = std::fs::read_dir(¤t) else {
continue;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
stack.push(entry.path());
} else if file_type.is_file()
&& let Ok(metadata) = entry.metadata()
{
total += metadata.len();
}
}
}
total
}
fn index_cache_bytes(dir: &Path) -> u64 {
if let Some(bytes) = std::env::var(INDEX_CACHE_BYTES_ENV)
.ok()
.and_then(|v| v.parse::<u64>().ok())
{
return bytes;
}
let disk_bytes = dir_size_bytes(dir);
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let scaled_bytes = (disk_bytes as f64 * INDEX_CACHE_DISK_FRACTION) as u64;
scaled_bytes.clamp(INDEX_CACHE_FLOOR_BYTES, INDEX_CACHE_CEILING_BYTES)
}
#[derive(Debug, Error)]
pub enum IndexError {
#[error("fjall error: {0}")]
Fjall(#[from] fjall::Error),
#[error("io error on {path}: {source}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("msgpack encode error: {0}")]
Encode(#[from] rmp_serde::encode::Error),
#[error("msgpack decode error: {0}")]
Decode(#[from] rmp_serde::decode::Error),
}
#[derive(Clone)]
pub struct IndexDb {
pub(crate) db: Database,
#[allow(dead_code)]
pub(crate) meta: Keyspace,
pub(crate) symbols_by_path: Keyspace,
pub(crate) symbols_by_name: Keyspace,
pub(crate) calls_by_path: Keyspace,
pub(crate) calls_by_callee: Keyspace,
pub(crate) imports_by_module: Keyspace,
pub(crate) imports_by_path: Keyspace,
pub(crate) implementations_by_trait: Keyspace,
pub(crate) implementations_by_path: Keyspace,
pub(crate) refs_by_def: Keyspace,
pub(crate) refs_by_path: Keyspace,
#[allow(dead_code)]
pub(crate) code_bm25_postings: Keyspace,
pub(crate) code_bm25_by_path: Keyspace,
#[allow(dead_code)]
pub(crate) embeddings: Keyspace,
#[allow(dead_code)]
pub(crate) memory_by_key: Keyspace,
#[allow(dead_code)]
pub(crate) memory_archive: Keyspace,
#[allow(dead_code)]
pub(crate) proposals: Keyspace,
}
impl IndexDb {
pub fn open(view_dir: &Path) -> Result<Self, IndexError> {
let dir = view_dir.join(INDEX_DIR);
std::fs::create_dir_all(&dir).map_err(|source| IndexError::Io {
path: dir.clone(),
source,
})?;
let cache_bytes = index_cache_bytes(&dir);
let mut db = Database::builder(&dir).cache_size(cache_bytes).open()?;
let mut meta = db.keyspace("meta", KeyspaceCreateOptions::default)?;
let on_disk_ver = meta
.get(META_SCHEMA_VER)?
.and_then(|bytes| <[u8; 4]>::try_from(&bytes[..]).ok())
.map(u32::from_be_bytes);
if matches!(on_disk_ver, Some(ver) if ver != INDEX_SCHEMA_VER) {
drop(meta);
drop(db);
std::fs::remove_dir_all(&dir).map_err(|source| IndexError::Io {
path: dir.clone(),
source,
})?;
std::fs::create_dir_all(&dir).map_err(|source| IndexError::Io {
path: dir.clone(),
source,
})?;
db = Database::builder(&dir).cache_size(cache_bytes).open()?;
meta = db.keyspace("meta", KeyspaceCreateOptions::default)?;
}
let symbols_by_path = db.keyspace("symbols_by_path", KeyspaceCreateOptions::default)?;
let symbols_by_name = db.keyspace("symbols_by_name", KeyspaceCreateOptions::default)?;
let calls_by_path = db.keyspace("calls_by_path", KeyspaceCreateOptions::default)?;
let calls_by_callee = db.keyspace("calls_by_callee", KeyspaceCreateOptions::default)?;
let imports_by_module = db.keyspace("imports_by_module", KeyspaceCreateOptions::default)?;
let imports_by_path = db.keyspace("imports_by_path", KeyspaceCreateOptions::default)?;
let implementations_by_trait = db.keyspace("implementations_by_trait", KeyspaceCreateOptions::default)?;
let implementations_by_path = db.keyspace("implementations_by_path", KeyspaceCreateOptions::default)?;
let refs_by_def = db.keyspace("refs_by_def", KeyspaceCreateOptions::default)?;
let refs_by_path = db.keyspace("refs_by_path", KeyspaceCreateOptions::default)?;
let code_bm25_postings = db.keyspace("code_bm25_postings", KeyspaceCreateOptions::default)?;
let code_bm25_by_path = db.keyspace("code_bm25_by_path", KeyspaceCreateOptions::default)?;
let embeddings = db.keyspace("embeddings", KeyspaceCreateOptions::default)?;
let memory_by_key = db.keyspace("memory_by_key", KeyspaceCreateOptions::default)?;
let memory_archive = db.keyspace("memory_archive", KeyspaceCreateOptions::default)?;
let proposals = db.keyspace("proposals", KeyspaceCreateOptions::default)?;
meta.insert(META_SCHEMA_VER, INDEX_SCHEMA_VER.to_be_bytes())?;
Ok(Self {
db,
meta,
symbols_by_path,
symbols_by_name,
calls_by_path,
calls_by_callee,
imports_by_module,
imports_by_path,
implementations_by_trait,
implementations_by_path,
refs_by_def,
refs_by_path,
code_bm25_postings,
code_bm25_by_path,
embeddings,
memory_by_key,
memory_archive,
proposals,
})
}
pub fn writer(&self) -> writer::IndexWriter {
writer::IndexWriter::new(self.clone())
}
pub fn symbols_index_is_empty(&self) -> bool {
self.symbols_by_path.iter().next().is_none()
}
pub fn references_to(&self, def_path: &crate::path::RelPath, def_start: u32) -> Vec<(crate::path::RelPath, u32)> {
let prefix = keys::refs_by_def_prefix(def_path, def_start);
let mut out = Vec::new();
for guard in self.refs_by_def.prefix(prefix) {
if let Ok((k, _)) = guard.into_inner()
&& let Some((_def_path, _def_start, use_path, use_start)) = keys::parse_ref_by_def(&k)
{
out.push((use_path, use_start));
}
}
out
}
pub fn definition_of(
&self,
use_path: &crate::path::RelPath,
use_start: u32,
) -> Option<(crate::path::RelPath, u32)> {
let prefix = keys::refs_by_use_prefix(use_path, use_start);
for guard in self.refs_by_path.prefix(prefix) {
if let Ok((k, _)) = guard.into_inner()
&& let Some((_use_path, _use_start, def_path, def_start)) = keys::parse_ref_by_path(&k)
{
return Some((def_path, def_start));
}
}
None
}
pub fn symbols_by_name_lookup(
&self,
name: &str,
cap: usize,
) -> Vec<(String, crate::extract::SymbolKind, crate::path::RelPath, u32)> {
let prefix = keys::symbols_by_name_prefix(name);
let mut out = Vec::new();
for guard in self.symbols_by_name.prefix(prefix) {
if out.len() >= cap {
break;
}
if let Ok((k, _)) = guard.into_inner()
&& let Some((matched, kind, rel, start_byte)) = keys::parse_symbol_by_name(&k)
{
out.push((matched, kind, rel, start_byte));
}
}
out
}
pub fn bm25_stats(&self) -> Option<(u64, u64)> {
let n = self
.meta
.get(META_BM25_DOC_COUNT)
.ok()
.flatten()
.and_then(|b| <[u8; 8]>::try_from(&b[..]).ok())
.map(u64::from_be_bytes)?;
let total_len = self
.meta
.get(META_BM25_TOTAL_LEN)
.ok()
.flatten()
.and_then(|b| <[u8; 8]>::try_from(&b[..]).ok())
.map(u64::from_be_bytes)
.unwrap_or(0);
Some((n, total_len))
}
pub fn recompute_bm25_stats(&self) -> Result<(), IndexError> {
let mut n: u64 = 0;
let mut total_len: u64 = 0;
for guard in self.code_bm25_by_path.iter() {
let (_k, v) = guard.into_inner()?;
if v.len() >= 4 {
total_len += u64::from(u32::from_be_bytes([v[0], v[1], v[2], v[3]]));
}
n += 1;
}
self.meta.insert(META_BM25_DOC_COUNT, n.to_be_bytes())?;
self.meta.insert(META_BM25_TOTAL_LEN, total_len.to_be_bytes())?;
Ok(())
}
}