//! In-memory index for concept matching by tail path segments. //! //! Maps `{tail_seg1}/{tail_seg2}::{predicate}` → `Vec`. //! This enables matching claims across different URI schemes by their //! trailing path components. use std::collections::HashMap; use stemedb_core::types::Assertion; use crate::types::PredicateAliasSet; /// In-memory index for concept matching by tail path segments. /// /// Maps `{tail_seg1}/{tail_seg2}::{predicate}` → `Vec`. /// This enables matching claims across different URI schemes by their /// trailing path components. /// /// # Example /// /// Both of these subjects produce the same key `"tls/cert_verification::enabled"`: /// - `rfc://5246/tls/cert_verification` /// - `code://rust/myapp/client/tls/cert_verification` pub struct ConceptIndex { pub entries: HashMap>, } impl ConceptIndex { /// Build a ConceptIndex from a slice of assertions. pub fn build(assertions: &[Assertion]) -> Self { // Pre-allocate based on expected unique keys let mut entries: HashMap> = HashMap::with_capacity(assertions.len()); for assertion in assertions { if let Some(key) = Self::make_key(&assertion.subject, &assertion.predicate) { entries.entry(key).or_default().push(assertion.clone()); } } Self { entries } } /// Look up assertions matching the tail segments of a subject and predicate. pub fn lookup(&self, subject: &str, predicate: &str) -> Option<&Vec> { let key = Self::make_key(subject, predicate)?; self.entries.get(&key) } /// Create a lookup key from subject and predicate. /// /// Algorithm: /// 1. Split subject on `"://"`, take path part /// 2. Split path on `"/"` in reverse, get last 2 non-empty segments /// 3. If < 2 segments, return None /// 4. Return `"{seg[-2]}/{seg[-1]}::{predicate}"` pub fn make_key(subject: &str, predicate: &str) -> Option { Self::make_key_with_predicate(subject, predicate) } /// Internal key creation with explicit predicate. fn make_key_with_predicate(subject: &str, predicate: &str) -> Option { // Split on "://" to separate scheme from path let path = subject.find("://").map(|i| &subject[i + 3..]).unwrap_or(subject); // Get last two non-empty segments using rsplit (avoids Vec allocation) let mut segments = path.rsplit('/').filter(|s| !s.is_empty()); let tail2 = segments.next()?; let tail1 = segments.next()?; Some(format!("{}/{}::{}", tail1, tail2, predicate)) } /// Normalize a predicate using the given alias sets. /// /// Returns the canonical form if found, otherwise the original predicate. pub fn normalize_predicate<'a>( predicate: &'a str, aliases: &'a [PredicateAliasSet], ) -> &'a str { for alias_set in aliases { if let Some(canonical) = alias_set.normalize(predicate) { return canonical; } } predicate } /// Build a ConceptIndex with predicate alias normalization. /// /// Predicates are normalized to their canonical form before indexing, /// enabling semantic matching across equivalent predicates. pub fn build_with_aliases( assertions: &[Assertion], predicate_aliases: &[PredicateAliasSet], ) -> Self { let mut entries: HashMap> = HashMap::with_capacity(assertions.len()); for assertion in assertions { let normalized_predicate = Self::normalize_predicate(&assertion.predicate, predicate_aliases); if let Some(key) = Self::make_key_with_predicate(&assertion.subject, normalized_predicate) { entries.entry(key).or_default().push(assertion.clone()); } } Self { entries } } /// Look up assertions with predicate alias normalization. /// /// The given predicate is normalized using the alias sets before lookup. pub fn lookup_with_aliases( &self, subject: &str, predicate: &str, predicate_aliases: &[PredicateAliasSet], ) -> Option<&Vec> { let normalized = Self::normalize_predicate(predicate, predicate_aliases); let key = Self::make_key_with_predicate(subject, normalized)?; self.entries.get(&key) } }