#![allow(clippy::unwrap_used)] //! tidalDB quickstart: schema, items, signals, ranking. //! //! Demonstrates the full ingestion-to-ranking loop: //! 1. Define a schema with `view` and `like` signals //! 2. Open an ephemeral database //! 3. Write 20 items with metadata and 128D random embeddings //! 4. Record engagement: view 5 items, like 3 of those //! 5. Retrieve ranked results using the `trending` profile //! 6. Print ranked items with scores //! //! # Running //! //! ```bash //! cargo run -p tidaldb --example quickstart //! ``` use std::{collections::HashMap, time::Duration}; use rand::Rng; use tidaldb::{ TidalDb, schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window}, }; /// Generate a random unit-normalized embedding of the given dimensionality. fn random_unit_vector(dim: usize, rng: &mut impl Rng) -> Vec { let v: Vec = (0..dim).map(|_| rng.random::() - 0.5).collect(); let norm = v.iter().map(|x| x * x).sum::().sqrt(); if norm < f32::EPSILON { // Degenerate case: return a unit vector along the first axis. let mut unit = vec![0.0_f32; dim]; unit[0] = 1.0; return unit; } v.iter().map(|x| x / norm).collect() } fn main() -> Result<(), Box> { // Initialize tracing so spans emitted by tidalDB are visible. tracing_subscriber::fmt() .with_env_filter("tidaldb=info") .init(); // ── 1. Define the schema ──────────────────────────────────────────── let mut schema = SchemaBuilder::new(); // View signal: 7-day half-life, 1h + 24h windows, velocity enabled. let _ = schema .signal( "view", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::OneHour, Window::TwentyFourHours, Window::AllTime]) .velocity(true) .add(); // Like signal: 30-day half-life, AllTime window. let _ = schema .signal( "like", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(30 * 24 * 3600), }, ) .windows(&[Window::AllTime]) .velocity(false) .add(); // Share signal: needed by the trending profile's boost definitions. let _ = schema .signal( "share", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(3 * 24 * 3600), }, ) .windows(&[Window::TwentyFourHours, Window::AllTime]) .velocity(true) .add(); let schema = schema.build()?; // ── 2. Open an ephemeral database ─────────────────────────────────── let db = TidalDb::builder().ephemeral().with_schema(schema).open()?; db.health_check()?; println!("tidalDB opened (ephemeral, build: {})", tidaldb::BUILD_HASH); println!(); // ── 3. Write 20 items with metadata and embeddings ────────────────── let mut rng = rand::rng(); let categories = ["music", "tech", "cooking", "sports", "art"]; let dim = 128; for i in 1..=20 { let mut metadata = HashMap::new(); metadata.insert("title".to_string(), format!("Item {i}: Great Content")); metadata.insert( "category".to_string(), categories[i % categories.len()].to_string(), ); metadata.insert("format".to_string(), "video".to_string()); metadata.insert("duration".to_string(), format!("{}", 60 + i * 30)); metadata.insert( "created_at".to_string(), Timestamp::now().as_nanos().to_string(), ); db.write_item_with_metadata(EntityId::new(i as u64), &metadata)?; let embedding = random_unit_vector(dim, &mut rng); db.write_item_embedding(EntityId::new(i as u64), &embedding)?; } println!( "Wrote {} items with metadata and {dim}D embeddings.", db.item_count() ); // ── 4. Record engagement signals ──────────────────────────────────── // // Different items receive different engagement counts. The `hot` profile // ranks by cumulative view count with age decay — items with more views // score higher, producing clearly differentiated results. // // Note: the `trending` profile ranks by *velocity* (events/second over a // rolling window), which requires signals arriving over real elapsed time // to populate hour-level buckets. For a self-contained demo, `hot` is the // right choice. let now = Timestamp::now(); // (item_id, signal_type, count) — different counts drive different scores. let signals: &[(u64, &str, u32)] = &[ (7, "view", 8), (18, "view", 6), (3, "view", 5), (12, "view", 4), (1, "view", 3), (4, "view", 2), (8, "view", 1), (3, "like", 4), (7, "like", 3), (18, "like", 2), (7, "share", 2), (3, "share", 1), ]; let mut view_count = 0u32; let mut like_count = 0u32; let mut share_count = 0u32; for &(item_id, signal_type, count) in signals { for _ in 0..count { db.signal(signal_type, EntityId::new(item_id), 1.0, now)?; } match signal_type { "view" => view_count += count, "like" => like_count += count, "share" => share_count += count, _ => {} } } println!("Recorded {view_count} views, {like_count} likes, {share_count} shares."); // Verify signal state is live. let score = db.read_decay_score(EntityId::new(3), "view", 0)?; println!("Item 3 view decay score: {:.4}", score.unwrap_or(0.0)); println!(); // ── 5. Retrieve ranked results ────────────────────────────────────── // The `hot` builtin profile ranks by cumulative view count with age decay // (Reddit/HN-style). Items with more views score higher; the age factor // penalises older content. All items are treated as 24 hours old here // since metadata-based age lookup is wired in M3+. let query = tidaldb::query::retrieve::Retrieve::builder() .profile("hot") .limit(10) .build()?; let results = db.retrieve(&query)?; println!( "RETRIEVE profile=hot: {} results from {} candidates", results.items.len(), results.total_candidates ); println!("{:<6} {:<12} {:<8} Signals", "Rank", "Entity ID", "Score"); println!("{}", "-".repeat(50)); for item in &results.items { let signal_summary: String = item .signals .iter() .map(|s| format!("{}={:.3}", s.name, s.value)) .collect::>() .join(", "); println!( "{:<6} {:<12} {:<8.4} {}", item.rank, item.entity_id.as_u64(), item.score, if signal_summary.is_empty() { "-".to_string() } else { signal_summary } ); } println!(); // ── 6. Clean up ───────────────────────────────────────────────────── db.close()?; println!("tidalDB closed. Quickstart complete."); Ok(()) }