store: add weight to index, index-only key matching

- KEY_TO_UUID now stores weight (30 bytes: uuid+type+ts+deleted+weight)
- UUID_OFFSETS changed to composite key for O(log n) max-offset lookup
- Add NODES_BY_TYPE index for efficient type+date range queries
- Add for_each_key_weight() to StoreView for index-only iteration
- match_seeds uses index-only path when content not needed
- Fix transaction consistency in ops (single txn for related updates)
- rebuild() now records all uuid→offset mappings for version history
- Backwards compatible: old index formats decoded with default weight

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-15 05:03:32 -04:00
commit ba4e01b6f3
9 changed files with 774 additions and 500 deletions

View file

@ -4,6 +4,10 @@ use super::store::Store;
use crate::graph::Graph;
use crate::neuro::{consolidation_priority, ReplayItem};
// All functions take `provenance: &str` for interface uniformity (MCP tools
// pass it to everything), but read-only operations ignore it (_provenance).
// Only write operations actually record the provenance string.
// ── Memory operations ──────────────────────────────────────────
pub fn memory_render(store: &Store, _provenance: &str, key: &str, raw: Option<bool>) -> Result<String> {
@ -125,30 +129,7 @@ pub fn memory_history(store: &Store, _provenance: &str, key: &str, full: Option<
let key = store.resolve_key(key).unwrap_or_else(|_| key.to_string());
let full = full.unwrap_or(false);
let path = crate::store::nodes_path();
if !path.exists() {
anyhow::bail!("No node log found");
}
use std::io::BufReader;
let file = std::fs::File::open(&path)
.map_err(|e| anyhow::anyhow!("open {}: {}", path.display(), e))?;
let mut reader = BufReader::new(file);
let mut versions: Vec<crate::store::Node> = Vec::new();
while let Ok(msg) = capnp::serialize::read_message(&mut reader, capnp::message::ReaderOptions::new()) {
let log = msg.get_root::<crate::memory_capnp::node_log::Reader>()
.map_err(|e| anyhow::anyhow!("read log: {}", e))?;
for node_reader in log.get_nodes()
.map_err(|e| anyhow::anyhow!("get nodes: {}", e))? {
let node = crate::store::Node::from_capnp_migrate(node_reader)
.map_err(|e| anyhow::anyhow!("{}", e))?;
if node.key == key {
versions.push(node);
}
}
}
let versions = store.get_history(&key)?;
if versions.is_empty() {
anyhow::bail!("No history found for '{}'", key);
}
@ -305,19 +286,23 @@ pub fn journal_tail(store: &Store, _provenance: &str, count: Option<u64>, level:
.map(|dt| dt.and_utc().timestamp())
});
let all_keys = store.all_keys()?;
let mut entries: Vec<_> = all_keys.iter()
.filter_map(|key| store.get_node(key).ok()?)
.filter(|n| n.node_type == node_type)
.filter(|n| after_ts.map(|ts| n.created_at >= ts).unwrap_or(true))
.map(|n| JournalEntry {
key: n.key.clone(),
content: n.content,
created_at: n.created_at,
})
.collect();
entries.sort_by_key(|e| std::cmp::Reverse(e.created_at));
entries.truncate(count);
// Use NODES_BY_TYPE index: O(log n + k) instead of O(n)
let db = store.db()?;
let uuids = crate::store::nodes_by_type(db, node_type as u8, count, after_ts)?;
let mut entries = Vec::with_capacity(uuids.len());
for uuid in uuids {
if let Ok(Some(node)) = store.get_node_by_uuid(&uuid) {
if !node.deleted {
entries.push(JournalEntry {
key: node.key.clone(),
content: node.content.clone(),
created_at: node.created_at,
});
}
}
}
// Already sorted by timestamp from index, no need to sort again
Ok(entries)
}
@ -366,13 +351,17 @@ pub fn journal_new(store: &Store, provenance: &str, name: &str, title: &str, bod
pub fn journal_update(store: &Store, provenance: &str, body: &str, level: Option<i64>) -> Result<String> {
let level = level.unwrap_or(0);
let node_type = level_to_node_type(level);
let all_keys = store.all_keys()?;
let latest_key = all_keys.iter()
.filter_map(|key| store.get_node(key).ok()?)
.filter(|n| n.node_type == node_type)
.max_by_key(|n| n.created_at)
.map(|n| n.key.clone());
let Some(key) = latest_key else {
// Use NODES_BY_TYPE index to find most recent
let db = store.db()?;
let uuids = crate::store::nodes_by_type(db, node_type as u8, 1, None)?;
let key = match uuids.first() {
Some(uuid) => store.get_node_by_uuid(uuid)?
.filter(|n| !n.deleted)
.map(|n| n.key),
None => None,
};
let Some(key) = key else {
anyhow::bail!("no entry at level {} to update — use journal_new first", level);
};
let existing = store.get_node(&key)?.ok_or_else(|| anyhow::anyhow!("node not found"))?.content;