memvid_cli/commands/
sketch.rs1use std::path::PathBuf;
8use std::time::Instant;
9
10use anyhow::{Context, Result};
11use clap::{Args, Subcommand};
12use memvid_core::{Memvid, SketchVariant};
13use serde_json::json;
14
15#[derive(Args)]
17pub struct SketchArgs {
18 #[command(subcommand)]
19 pub command: SketchCommand,
20}
21
22#[derive(Subcommand)]
24pub enum SketchCommand {
25 Build(SketchBuildArgs),
27 Info(SketchInfoArgs),
29}
30
31#[derive(Args)]
33pub struct SketchBuildArgs {
34 #[arg(value_name = "FILE")]
36 pub file: PathBuf,
37
38 #[arg(long, default_value = "small")]
40 pub variant: SketchVariantArg,
41
42 #[arg(long)]
44 pub json: bool,
45}
46
47#[derive(Args)]
49pub struct SketchInfoArgs {
50 #[arg(value_name = "FILE")]
52 pub file: PathBuf,
53
54 #[arg(long)]
56 pub json: bool,
57}
58
59#[derive(Clone, Debug)]
61pub struct SketchVariantArg(pub SketchVariant);
62
63impl Default for SketchVariantArg {
64 fn default() -> Self {
65 Self(SketchVariant::Small)
66 }
67}
68
69impl std::str::FromStr for SketchVariantArg {
70 type Err = String;
71
72 fn from_str(s: &str) -> Result<Self, Self::Err> {
73 match s.to_lowercase().as_str() {
74 "small" | "s" | "32" => Ok(Self(SketchVariant::Small)),
75 "medium" | "m" | "64" => Ok(Self(SketchVariant::Medium)),
76 "large" | "l" | "96" => Ok(Self(SketchVariant::Large)),
77 _ => Err(format!(
78 "Unknown variant '{}'. Use: small (32 bytes), medium (64 bytes), or large (96 bytes)",
79 s
80 )),
81 }
82 }
83}
84
85pub fn handle_sketch(args: SketchArgs) -> Result<()> {
87 match args.command {
88 SketchCommand::Build(build_args) => handle_sketch_build(build_args),
89 SketchCommand::Info(info_args) => handle_sketch_info(info_args),
90 }
91}
92
93fn handle_sketch_build(args: SketchBuildArgs) -> Result<()> {
95 let start = Instant::now();
96
97 let mut mem = Memvid::open(&args.file)
98 .with_context(|| format!("Failed to open memory: {}", args.file.display()))?;
99
100 let frame_count = mem.frame_count();
101 let existing_sketches = mem.sketches().len();
102
103 let new_sketches = mem.build_all_sketches(args.variant.0);
105
106 if new_sketches > 0 {
108 mem.commit()
109 .context("Failed to commit sketch track changes")?;
110 }
111
112 let elapsed = start.elapsed();
113 let stats = mem.sketch_stats();
114
115 if args.json {
116 let output = json!({
117 "file": args.file.display().to_string(),
118 "variant": format!("{:?}", args.variant.0),
119 "frames_total": frame_count,
120 "sketches_existing": existing_sketches,
121 "sketches_new": new_sketches,
122 "sketches_total": stats.entry_count,
123 "size_bytes": stats.size_bytes,
124 "elapsed_ms": elapsed.as_millis(),
125 });
126 println!("{}", serde_json::to_string_pretty(&output)?);
127 } else {
128 println!("Sketch build complete for {}", args.file.display());
129 println!();
130 println!(" Variant: {:?}", args.variant.0);
131 println!(" Total frames: {}", frame_count);
132 println!(" Existing sketches: {}", existing_sketches);
133 println!(" New sketches: {}", new_sketches);
134 println!(" Total sketches: {}", stats.entry_count);
135 println!(" Size on disk: {}", format_bytes(stats.size_bytes));
136 println!(" Time: {:.2}s", elapsed.as_secs_f64());
137
138 if new_sketches == 0 && existing_sketches == stats.entry_count as usize {
139 println!();
140 println!("All frames already have sketches.");
141 } else if new_sketches > 0 {
142 println!();
143 let rate = new_sketches as f64 / elapsed.as_secs_f64();
144 println!("Rate: {:.0} sketches/sec", rate);
145 }
146 }
147
148 Ok(())
149}
150
151fn handle_sketch_info(args: SketchInfoArgs) -> Result<()> {
153 let mem = Memvid::open_read_only(&args.file)
154 .with_context(|| format!("Failed to open memory: {}", args.file.display()))?;
155
156 let stats = mem.sketch_stats();
157 let frame_count = mem.frame_count();
158 let coverage = if frame_count > 0 {
159 stats.entry_count as f64 / frame_count as f64 * 100.0
160 } else {
161 0.0
162 };
163
164 if args.json {
165 let output = json!({
166 "file": args.file.display().to_string(),
167 "enabled": !mem.sketches().is_empty(),
168 "variant": format!("{:?}", stats.variant),
169 "entry_count": stats.entry_count,
170 "frame_count": frame_count,
171 "coverage_percent": coverage,
172 "size_bytes": stats.size_bytes,
173 "short_text_count": stats.short_text_count,
174 "entry_size_bytes": stats.variant.entry_size(),
175 });
176 println!("{}", serde_json::to_string_pretty(&output)?);
177 } else {
178 println!("Sketch Track Info: {}", args.file.display());
179 println!();
180
181 if mem.sketches().is_empty() {
182 println!(" Status: No sketches built");
183 println!();
184 println!(
185 " Run `memvid sketch build {}` to enable fast candidate generation.",
186 args.file.display()
187 );
188 } else {
189 println!(" Status: Enabled");
190 println!(
191 " Variant: {:?} ({} bytes/entry)",
192 stats.variant,
193 stats.variant.entry_size()
194 );
195 println!(" Entries: {}", stats.entry_count);
196 println!(
197 " Coverage: {:.1}% ({}/{})",
198 coverage, stats.entry_count, frame_count
199 );
200 println!(" Size: {}", format_bytes(stats.size_bytes));
201 println!(" Short text: {} entries", stats.short_text_count);
202 println!();
203 println!(
204 "Sketch-enabled queries will filter {} candidates before BM25 rerank.",
205 stats.entry_count
206 );
207 }
208 }
209
210 Ok(())
211}
212
213fn format_bytes(bytes: u64) -> String {
214 if bytes >= 1_073_741_824 {
215 format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
216 } else if bytes >= 1_048_576 {
217 format!("{:.1} MB", bytes as f64 / 1_048_576.0)
218 } else if bytes >= 1024 {
219 format!("{:.1} KB", bytes as f64 / 1024.0)
220 } else {
221 format!("{} bytes", bytes)
222 }
223}