pub struct TemporalHnsw<D: DistanceMetric> {
graph: HnswGraph<D>,
timestamps: Vec<i64>,
entity_ids: Vec<u64>,
entity_index: BTreeMap<u64, Vec<(i64, u32)>>,
min_timestamp: i64,
max_timestamp: i64,
metadata_store: Option<MetadataStore>,
centroid: Option<Vec<f32>>,
rewards: Vec<f32>,
}Expand description
Spatiotemporal HNSW index.
Each inserted point has an entity_id, a timestamp, and a vector.
Internal node IDs are assigned sequentially.
Fields§
§graph: HnswGraph<D>§timestamps: Vec<i64>node_id → timestamp
entity_ids: Vec<u64>node_id → entity_id
entity_index: BTreeMap<u64, Vec<(i64, u32)>>entity_id → sorted vec of (timestamp, node_id)
min_timestamp: i64Global temporal range for normalization
max_timestamp: i64§metadata_store: Option<MetadataStore>Optional per-node metadata store.
centroid: Option<Vec<f32>>Optional centroid for anisotropy correction (RFC-012 Part B).
When set, all distance computations center vectors by subtracting this mean vector, amplifying the discriminative signal that is otherwise compressed by the dominant “average text” direction.
rewards: Vec<f32>Optional per-node reward for outcome-aware search (RFC-012 P4).
NaN means “no reward assigned”. Stored parallel to timestamps/entity_ids.
Implementations§
Source§impl<D: DistanceMetric> TemporalHnsw<D>
impl<D: DistanceMetric> TemporalHnsw<D>
Sourcepub fn new(config: HnswConfig, metric: D) -> Self
pub fn new(config: HnswConfig, metric: D) -> Self
Create a new empty spatiotemporal index.
Sourcepub fn entity_last_node(&self, entity_id: u64) -> Option<u32>
pub fn entity_last_node(&self, entity_id: u64) -> Option<u32>
Get the last (most recent) node_id for an entity, or None if not found.
Sourcepub fn set_ef_construction(&mut self, ef: usize)
pub fn set_ef_construction(&mut self, ef: usize)
Set ef_construction at runtime (lower for bulk load, higher for quality).
Sourcepub fn set_ef_search(&mut self, ef: usize)
pub fn set_ef_search(&mut self, ef: usize)
Set ef_search at runtime.
Sourcepub fn config(&self) -> &HnswConfig
pub fn config(&self) -> &HnswConfig
Get the current configuration.
Sourcepub fn enable_scalar_quantization(&mut self, min_val: f32, max_val: f32)
pub fn enable_scalar_quantization(&mut self, min_val: f32, max_val: f32)
Enable scalar quantization for faster distance computation.
Sourcepub fn disable_scalar_quantization(&mut self)
pub fn disable_scalar_quantization(&mut self)
Disable scalar quantization.
Sourcepub fn is_quantized(&self) -> bool
pub fn is_quantized(&self) -> bool
Whether scalar quantization is active.
Sourcepub fn insert(&mut self, entity_id: u64, timestamp: i64, vector: &[f32]) -> u32
pub fn insert(&mut self, entity_id: u64, timestamp: i64, vector: &[f32]) -> u32
Insert a temporal point into the index.
Returns the internal node_id assigned to this point.
Sourcepub fn bulk_insert_parallel(
&mut self,
entity_ids: &[u64],
timestamps: &[i64],
vectors: &[&[f32]],
) -> usize
pub fn bulk_insert_parallel( &mut self, entity_ids: &[u64], timestamps: &[i64], vectors: &[&[f32]], ) -> usize
Bulk insert multiple points with parallel distance computation (RFC-012 P9).
Faster than sequential insert() calls for large batches. Uses rayon
to parallelize neighbor search across chunks while keeping graph
modifications sequential.
Returns the number of points inserted.
Sourcepub fn insert_with_reward(
&mut self,
entity_id: u64,
timestamp: i64,
vector: &[f32],
reward: f32,
) -> u32
pub fn insert_with_reward( &mut self, entity_id: u64, timestamp: i64, vector: &[f32], reward: f32, ) -> u32
Insert a temporal point with an outcome reward.
reward annotates this point with an outcome signal (e.g., 0.0-1.0).
Use f32::NAN for “no reward assigned”.
Sourcepub fn insert_with_metadata(
&mut self,
entity_id: u64,
timestamp: i64,
vector: &[f32],
metadata: HashMap<String, String>,
) -> u32
pub fn insert_with_metadata( &mut self, entity_id: u64, timestamp: i64, vector: &[f32], metadata: HashMap<String, String>, ) -> u32
Insert a temporal point with metadata.
Sourcepub fn node_metadata(&self, node_id: u32) -> HashMap<String, String>
pub fn node_metadata(&self, node_id: u32) -> HashMap<String, String>
Get metadata for a node. Returns empty map if metadata store not enabled.
Sourcepub fn build_filter_bitmap(&self, filter: &TemporalFilter) -> RoaringBitmap
pub fn build_filter_bitmap(&self, filter: &TemporalFilter) -> RoaringBitmap
Build a Roaring Bitmap of node IDs matching the temporal filter.
Sourcepub fn temporal_distance_normalized(&self, t1: i64, t2: i64) -> f32
pub fn temporal_distance_normalized(&self, t1: i64, t2: i64) -> f32
Compute normalized temporal distance between two timestamps.
Returns a value in [0.0, 1.0] where 0 = same timestamp, 1 = max range.
Sourcepub(crate) fn normalize_semantic_distance(&self, d: f32) -> f32
pub(crate) fn normalize_semantic_distance(&self, d: f32) -> f32
Normalize semantic distance to [0, 1] range (RFC-012 P8).
Cosine distance ∈ [0, 2], L2 distance ∈ [0, ∞). This clamps and scales to [0, 1] so it’s comparable with temporal distance [0, 1].
Sourcepub(crate) fn recency_penalty(
&self,
node_timestamp: i64,
recency_lambda: f32,
) -> f32
pub(crate) fn recency_penalty( &self, node_timestamp: i64, recency_lambda: f32, ) -> f32
Compute recency penalty for a node (RFC-012 P7).
Returns a value in [0.0, 1.0] where 0 = most recent, 1 = oldest.
Uses exponential decay: 1 - exp(-λ · age) where age is normalized.
recency_lambda controls decay speed:
- λ = 0: no recency effect
- λ = 1: moderate decay
- λ = 3: strong decay (old nodes heavily penalized)
Sourcepub fn search_with_recency(
&self,
query: &[f32],
k: usize,
filter: TemporalFilter,
alpha: f32,
query_timestamp: i64,
recency_lambda: f32,
recency_weight: f32,
) -> Vec<(u32, f32)>
pub fn search_with_recency( &self, query: &[f32], k: usize, filter: TemporalFilter, alpha: f32, query_timestamp: i64, recency_lambda: f32, recency_weight: f32, ) -> Vec<(u32, f32)>
Search with full composite scoring (RFC-012 P7 + P8).
Enhanced distance: d = α·d_sem_norm + (1-α)·d_temporal + γ·recency
alpha: semantic vs temporal weight (1.0 = pure semantic)recency_lambda: recency decay strength (0.0 = off, 1.0 = moderate, 3.0 = strong)recency_weight: weight of recency term in composite score (0.0-1.0)
Sourcepub fn reward(&self, node_id: u32) -> f32
pub fn reward(&self, node_id: u32) -> f32
Get the reward for a node. Returns NaN if no reward was assigned.
Sourcepub fn set_reward(&mut self, node_id: u32, reward: f32)
pub fn set_reward(&mut self, node_id: u32, reward: f32)
Set the reward for a node retroactively.
Useful for annotating outcomes after an episode completes.
Sourcepub fn build_reward_bitmap(&self, min_reward: f32) -> RoaringBitmap
pub fn build_reward_bitmap(&self, min_reward: f32) -> RoaringBitmap
Build a bitmap of node_ids with reward >= min_reward.
Sourcepub fn search_with_reward(
&self,
query: &[f32],
k: usize,
filter: TemporalFilter,
alpha: f32,
query_timestamp: i64,
min_reward: f32,
) -> Vec<(u32, f32)>
pub fn search_with_reward( &self, query: &[f32], k: usize, filter: TemporalFilter, alpha: f32, query_timestamp: i64, min_reward: f32, ) -> Vec<(u32, f32)>
Search with reward filtering: only return nodes with reward >= min_reward.
Combines temporal filter + reward filter as bitmap pre-filter.
Sourcepub fn compute_centroid(&self) -> Option<Vec<f32>>
pub fn compute_centroid(&self) -> Option<Vec<f32>>
Compute the centroid (mean vector) of all indexed vectors.
Single O(N×D) pass over stored vectors. Returns None if the index
is empty.
Sourcepub fn set_centroid(&mut self, centroid: Vec<f32>)
pub fn set_centroid(&mut self, centroid: Vec<f32>)
Set the centroid for anisotropy correction.
Once set, centered_vector() subtracts this from any vector,
and search operations use centered distances. The centroid is
serialized with the index snapshot.
You can provide an externally computed centroid (e.g., from a
larger corpus) or use compute_centroid() for the index contents.
Sourcepub fn clear_centroid(&mut self)
pub fn clear_centroid(&mut self)
Clear the centroid, reverting to raw (uncentered) distances.
Sourcepub fn centered_vector(&self, vec: &[f32]) -> Vec<f32>
pub fn centered_vector(&self, vec: &[f32]) -> Vec<f32>
Return a centered copy of the given vector (vec - centroid).
If no centroid is set, returns the vector unchanged (cloned).
Sourcepub fn search(
&self,
query: &[f32],
k: usize,
filter: TemporalFilter,
alpha: f32,
query_timestamp: i64,
) -> Vec<(u32, f32)>
pub fn search( &self, query: &[f32], k: usize, filter: TemporalFilter, alpha: f32, query_timestamp: i64, ) -> Vec<(u32, f32)>
Search for the k nearest neighbors with temporal filtering and composite scoring.
query: the query vectork: number of resultsfilter: temporal constraint (Snapshot, Range, Before, After, All)alpha: weight for semantic distance (1.0 = pure semantic, 0.0 = pure temporal)query_timestamp: reference timestamp for temporal distance computation
Returns (node_id, combined_score) sorted by combined score ascending.
Sourcepub fn trajectory(
&self,
entity_id: u64,
filter: TemporalFilter,
) -> Vec<(i64, u32)>
pub fn trajectory( &self, entity_id: u64, filter: TemporalFilter, ) -> Vec<(i64, u32)>
Retrieve the full trajectory for an entity within a time range.
Returns (timestamp, node_id) pairs sorted by timestamp ascending.
Sourcepub fn bitmap_memory_bytes(&self) -> usize
pub fn bitmap_memory_bytes(&self) -> usize
Approximate memory usage of the Roaring Bitmaps for a full-index filter.
Useful for verifying the < 1 byte/vector target.
Sourcepub fn graph(&self) -> &HnswGraph<D>
pub fn graph(&self) -> &HnswGraph<D>
Access the underlying HNSW graph (for recall comparisons, etc.).
Sourcepub fn regions(&self, level: usize) -> Vec<(u32, Vec<f32>, usize)>
pub fn regions(&self, level: usize) -> Vec<(u32, Vec<f32>, usize)>
Get semantic regions at a given HNSW level.
Returns (hub_node_id, hub_vector, n_assigned_nodes) for each region.
Use level 2-3 for interpretable granularity (~N/M^L regions).
Sourcepub fn region_trajectory(
&self,
entity_id: u64,
level: usize,
window_days: i64,
alpha: f32,
) -> Vec<(i64, Vec<f32>)>
pub fn region_trajectory( &self, entity_id: u64, level: usize, window_days: i64, alpha: f32, ) -> Vec<(i64, Vec<f32>)>
Compute smoothed region-distribution trajectory for an entity (RFC-004).
level: HNSW level for region granularitywindow_days: sliding window in timestamp units (same scale as ingested timestamps)alpha: EMA smoothing factor (0.3 typical, higher = more reactive)
Returns (timestamp, distribution) where distribution is a Vec
Sourcepub fn region_members(
&self,
region_hub: u32,
level: usize,
filter: TemporalFilter,
) -> Vec<(u32, u64, i64)>
pub fn region_members( &self, region_hub: u32, level: usize, filter: TemporalFilter, ) -> Vec<(u32, u64, i64)>
Get points assigned to a specific region, optionally time-filtered (RFC-004, RFC-005).
Returns (node_id, entity_id, timestamp) for all points in the region.
This is the “SELECT * FROM points WHERE region = R” equivalent.
Performance warning: This does a full scan of all nodes. For multiple regions,
use region_assignments() instead (single scan for all regions).
Sourcepub fn region_assignments(
&self,
level: usize,
filter: TemporalFilter,
) -> HashMap<u32, Vec<(u64, i64)>>
pub fn region_assignments( &self, level: usize, filter: TemporalFilter, ) -> HashMap<u32, Vec<(u64, i64)>>
Assign ALL nodes to their regions in a single pass, optionally time-filtered.
Returns a HashMap from hub_id → Vec<(entity_id, timestamp)>.
This is O(N) — one full scan instead of O(N × K) for K region_members calls.
Source§impl<D: DistanceMetric> TemporalHnsw<D>
impl<D: DistanceMetric> TemporalHnsw<D>
Trait Implementations§
Source§impl<D: DistanceMetric> TemporalIndexAccess for TemporalHnsw<D>
impl<D: DistanceMetric> TemporalIndexAccess for TemporalHnsw<D>
Source§fn search_raw(
&self,
query: &[f32],
k: usize,
filter: TemporalFilter,
alpha: f32,
query_timestamp: i64,
) -> Vec<(u32, f32)>
fn search_raw( &self, query: &[f32], k: usize, filter: TemporalFilter, alpha: f32, query_timestamp: i64, ) -> Vec<(u32, f32)>
Source§fn trajectory(&self, entity_id: u64, filter: TemporalFilter) -> Vec<(i64, u32)>
fn trajectory(&self, entity_id: u64, filter: TemporalFilter) -> Vec<(i64, u32)>
Source§fn vector(&self, node_id: u32) -> Vec<f32>
fn vector(&self, node_id: u32) -> Vec<f32>
Source§fn regions(&self, level: usize) -> Vec<(u32, Vec<f32>, usize)>
fn regions(&self, level: usize) -> Vec<(u32, Vec<f32>, usize)>
(hub_node_id, hub_vector, n_assigned) per region.Source§fn region_members(
&self,
region_hub: u32,
level: usize,
filter: TemporalFilter,
) -> Vec<(u32, u64, i64)>
fn region_members( &self, region_hub: u32, level: usize, filter: TemporalFilter, ) -> Vec<(u32, u64, i64)>
(node_id, entity_id, timestamp) per member.Source§fn region_trajectory(
&self,
entity_id: u64,
level: usize,
window_days: i64,
alpha: f32,
) -> Vec<(i64, Vec<f32>)>
fn region_trajectory( &self, entity_id: u64, level: usize, window_days: i64, alpha: f32, ) -> Vec<(i64, Vec<f32>)>
Source§fn metadata(&self, node_id: u32) -> HashMap<String, String>
fn metadata(&self, node_id: u32) -> HashMap<String, String>
Source§fn search_with_metadata(
&self,
query: &[f32],
k: usize,
filter: TemporalFilter,
alpha: f32,
query_timestamp: i64,
metadata_filter: &MetadataFilter,
) -> Vec<(u32, f32)>
fn search_with_metadata( &self, query: &[f32], k: usize, filter: TemporalFilter, alpha: f32, query_timestamp: i64, metadata_filter: &MetadataFilter, ) -> Vec<(u32, f32)>
Source§fn region_assignments(
&self,
_level: usize,
_filter: TemporalFilter,
) -> HashMap<u32, Vec<(u64, i64)>>
fn region_assignments( &self, _level: usize, _filter: TemporalFilter, ) -> HashMap<u32, Vec<(u64, i64)>>
Auto Trait Implementations§
impl<D> Freeze for TemporalHnsw<D>where
D: Freeze,
impl<D> RefUnwindSafe for TemporalHnsw<D>where
D: RefUnwindSafe,
impl<D> Send for TemporalHnsw<D>
impl<D> Sync for TemporalHnsw<D>
impl<D> Unpin for TemporalHnsw<D>where
D: Unpin,
impl<D> UnwindSafe for TemporalHnsw<D>where
D: UnwindSafe,
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more