use anyhow::{Context, Result};
use rusqlite::OptionalExtension;
use serde::Serialize;
use crate::inspect::now_unix;
use crate::search::{
ConfidenceBreakdown, compute_confidence_breakdown, format_confidence_breakdown_token,
};
use crate::store::Store;
pub fn record_query_success(store: &Store, chunk_id: &str) -> Result<i64> {
let conn = store.conn();
let rows = conn
.execute(
"UPDATE chunks SET query_success_count = query_success_count + 1 WHERE id = ?1",
rusqlite::params![chunk_id],
)
.with_context(|| format!("recording query success for chunk {chunk_id}"))?;
if rows == 0 {
anyhow::bail!("no chunk with id {chunk_id}");
}
get_query_success_count(store, chunk_id)?
.ok_or_else(|| anyhow::anyhow!("chunk {chunk_id} disappeared after query-success update"))
}
pub fn get_query_success_count(store: &Store, chunk_id: &str) -> Result<Option<i64>> {
let conn = store.conn();
let count = conn
.query_row(
"SELECT query_success_count FROM chunks WHERE id = ?1",
rusqlite::params![chunk_id],
|row| row.get::<_, i64>(0),
)
.optional()?;
Ok(count)
}
#[derive(Debug, Clone, Serialize)]
pub struct QuerySuccessReport {
pub chunk_id: String,
pub count: i64,
pub confidence: f64,
pub confidence_breakdown: ConfidenceBreakdown,
pub access_decay_at: Option<i64>,
}
fn current_confidence(
store: &Store,
chunk_id: &str,
at_unix: i64,
) -> Result<(f64, ConfidenceBreakdown, Option<i64>)> {
let conn = store.conn();
let (access_decay_at, last_accessed_at, timestamp_unix, access_count, feedback_score, query_success_count) =
conn.query_row(
"SELECT access_decay_at, last_accessed_at, timestamp_unix, access_count, feedback_score, query_success_count FROM chunks WHERE id = ?1",
rusqlite::params![chunk_id],
|row| {
Ok((
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
row.get(5)?,
))
},
)?;
let (confidence, confidence_breakdown) = compute_confidence_breakdown(
at_unix,
last_accessed_at,
timestamp_unix,
access_count,
feedback_score,
query_success_count,
);
Ok((confidence, confidence_breakdown, access_decay_at))
}
pub fn apply_query_success(store: &Store, chunk_id: &str) -> Result<QuerySuccessReport> {
let count = record_query_success(store, chunk_id)?;
let (confidence, confidence_breakdown, access_decay_at) =
current_confidence(store, chunk_id, now_unix())?;
Ok(QuerySuccessReport {
chunk_id: chunk_id.to_string(),
count,
confidence,
confidence_breakdown,
access_decay_at,
})
}
pub(crate) fn format_text(report: &QuerySuccessReport) -> String {
let mut text = format!(
"query success recorded: chunk={} count={} confidence={:.3} freshness_source={} {}",
report.chunk_id,
report.count,
report.confidence,
report.confidence_breakdown.freshness_source.as_str(),
format_confidence_breakdown_token(&report.confidence_breakdown),
);
if let Some(access_decay_at) = report.access_decay_at {
text.push_str(&format!(" access_decay_at={access_decay_at}"));
}
text
}
pub fn print_text(report: &QuerySuccessReport) {
println!("{}", format_text(report));
}
pub fn print_json(report: &QuerySuccessReport) -> Result<()> {
println!("{}", serde_json::to_string_pretty(report)?);
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::ingest::ingest_path;
use crate::search::FreshnessSource;
use crate::store::Store;
use tempfile::tempdir;
use super::*;
fn open_store_with_chunk() -> (tempfile::TempDir, Store, String) {
let root = tempdir().expect("tempdir");
let mut store = Store::initialize(&root.path().join("store")).expect("init store");
let data = root.path().join("data");
fs::create_dir_all(&data).expect("create data dir");
fs::write(data.join("a.txt"), "hello world").expect("write fixture");
ingest_path(&mut store, &data).expect("ingest fixture");
let chunk_id = store
.conn()
.query_row("SELECT id FROM chunks LIMIT 1", [], |row| row.get(0))
.expect("chunk id");
(root, store, chunk_id)
}
fn sample_report() -> QuerySuccessReport {
QuerySuccessReport {
chunk_id: "chunk-1".into(),
count: 4,
confidence: 0.812,
confidence_breakdown: ConfidenceBreakdown {
freshness: 0.5,
freshness_source: FreshnessSource::LastAccessedAt,
access_boost: 0.5,
base: 0.5,
feedback_factor: 0.0,
query_success_factor: 0.0,
},
access_decay_at: Some(1_700_000_800),
}
}
#[test]
fn apply_query_success_increments_and_returns_report() {
let (_dir, store, chunk_id) = open_store_with_chunk();
let first = apply_query_success(&store, &chunk_id).expect("first apply");
assert_eq!(first.chunk_id, chunk_id);
assert_eq!(first.count, 1);
let second = apply_query_success(&store, &chunk_id).expect("second apply");
assert_eq!(second.count, 2);
assert_eq!(
get_query_success_count(&store, &chunk_id).unwrap(),
Some(2),
"stored count should match returned count"
);
}
#[test]
fn apply_query_success_errors_for_unknown_chunk() {
let (_dir, store, _chunk_id) = open_store_with_chunk();
let err = apply_query_success(&store, "deadbeef").unwrap_err();
assert!(
err.to_string().contains("no chunk with id"),
"unexpected error: {err}"
);
}
#[test]
fn format_text_surfaces_freshness_source_token() {
let text = format_text(&sample_report());
assert!(text.contains("freshness_source=last_accessed_at"), "{text}");
assert!(text.contains("confidence=0.812"), "{text}");
assert!(text.contains("access_decay_at=1700000800"), "{text}");
}
}