Skip to main content

ipfrs_cli/commands/
semantic.rs

1//! Semantic search commands
2//!
3//! This module provides semantic search operations:
4//! - `semantic_search` - Vector search
5//! - `semantic_index` - Manual indexing
6//! - `semantic_similar` - Find similar content
7//! - `semantic_stats` - Index statistics
8//! - `semantic_save` - Save semantic index
9//! - `semantic_load` - Load semantic index
10
11use anyhow::Result;
12
13use crate::output::{self, print_cid, print_header, print_kv};
14use crate::progress;
15
16/// Generate a deterministic hash-based embedding vector for query text.
17///
18/// Uses `DefaultHasher` with (text, dimension_index) as inputs so the same
19/// text always produces the same normalised unit vector.  This is intentionally
20/// a stand-in until a real embedding model is wired in.
21fn text_to_embedding(text: &str, dim: usize) -> Vec<f32> {
22    use std::collections::hash_map::DefaultHasher;
23    use std::hash::{Hash, Hasher};
24
25    let mut embedding = vec![0.0f32; dim];
26    for (i, slot) in embedding.iter_mut().enumerate() {
27        let mut hasher = DefaultHasher::new();
28        text.hash(&mut hasher);
29        (i as u64).hash(&mut hasher);
30        let hash_val = hasher.finish();
31        *slot = (hash_val as f32 / u64::MAX as f32) * 2.0 - 1.0;
32    }
33    // L2-normalise
34    let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
35    if norm > 0.0 {
36        for x in &mut embedding {
37            *x /= norm;
38        }
39    }
40    embedding
41}
42
43/// Internal helper: run semantic search and return a `(cids, results_printed)`
44/// pair.  The caller decides whether to also print the results.
45///
46/// Returns the list of CID strings that were found so the hybrid query can
47/// optionally pipe them through the logic filter.
48async fn semantic_query_inner(
49    text: &str,
50    top_k: usize,
51    threshold: f32,
52    json_output: bool,
53    print_results: bool,
54) -> Result<Vec<String>> {
55    use ipfrs::{Node, NodeConfig, QueryFilter};
56    use ipfrs_semantic::RouterConfig;
57
58    let mut node = Node::new(NodeConfig::default().with_semantic(RouterConfig::default()))?;
59    node.start().await?;
60
61    // Default RouterConfig uses dimension 128
62    let embedding = text_to_embedding(text, 128);
63
64    let filter = QueryFilter {
65        min_score: if threshold > 0.0 {
66            Some(threshold)
67        } else {
68            None
69        },
70        max_score: None,
71        max_results: Some(top_k),
72        cid_prefix: None,
73    };
74
75    let results = match node.search_hybrid(&embedding, top_k, filter).await {
76        Ok(r) => r,
77        Err(_) => {
78            output::warning(
79                "Semantic index not initialized. Use 'ipfrs semantic index <cid>' to index content first.",
80            );
81            node.stop().await?;
82            if json_output && print_results {
83                println!("[]");
84            }
85            return Ok(Vec::new());
86        }
87    };
88
89    node.stop().await?;
90
91    if print_results {
92        if json_output {
93            println!("[");
94            for (idx, result) in results.iter().enumerate() {
95                let comma = if idx + 1 < results.len() { "," } else { "" };
96                println!(
97                    "  {{\"cid\": \"{}\", \"score\": {:.4}}}{}",
98                    result.cid, result.score, comma
99                );
100            }
101            println!("]");
102        } else {
103            print_header(&format!("Semantic search: \"{}\"", text));
104            if threshold > 0.0 {
105                println!(
106                    "Found {} results (threshold: {:.2})",
107                    results.len(),
108                    threshold
109                );
110            } else {
111                println!("Found {} results", results.len());
112            }
113            println!();
114            for result in &results {
115                println!("  CID: {} (score: {:.2})", result.cid, result.score);
116            }
117        }
118    }
119
120    let cids: Vec<String> = results.into_iter().map(|r| r.cid.to_string()).collect();
121    Ok(cids)
122}
123
124/// Semantic similarity search: `ipfrs semantic query "<text>" --top-k 10`
125///
126/// Prints results and returns `()`.
127pub async fn semantic_query(
128    text: &str,
129    top_k: usize,
130    threshold: f32,
131    json_output: bool,
132) -> Result<()> {
133    semantic_query_inner(text, top_k, threshold, json_output, true).await?;
134    Ok(())
135}
136
137/// Semantic similarity search that also returns the matching CIDs.
138///
139/// Used by the hybrid query pipeline so that logic post-filtering can be
140/// applied to the semantic result set without re-running the search.
141pub async fn semantic_query_with_cids(
142    text: &str,
143    top_k: usize,
144    threshold: f32,
145    json_output: bool,
146) -> Result<Vec<String>> {
147    semantic_query_inner(text, top_k, threshold, json_output, true).await
148}
149
150/// Vector search
151#[allow(dead_code)]
152pub async fn semantic_search(query: &str, top_k: usize, format: &str) -> Result<()> {
153    let pb = progress::spinner("Searching for similar content...");
154    progress::finish_spinner_success(&pb, "Search initialization complete");
155
156    // Note: Full implementation requires an embedding model to convert query text to vectors
157    output::warning("Semantic search requires an embedding model (not yet configured)");
158
159    match format {
160        "json" => {
161            println!("{{");
162            println!("  \"query\": \"{}\",", query);
163            println!("  \"top_k\": {},", top_k);
164            println!("  \"status\": \"not_implemented\",");
165            println!("  \"message\": \"Semantic search requires embedding model configuration\"");
166            println!("}}");
167        }
168        _ => {
169            print_header(&format!("Semantic Search: {}", query));
170            println!("Query: {}", query);
171            println!("Top K: {}", top_k);
172            println!();
173            println!("To enable semantic search:");
174            println!("  1. Configure an embedding model in config.toml");
175            println!("  2. Index your content with 'ipfrs semantic index <cid>'");
176            println!("  3. Run your query again");
177        }
178    }
179
180    Ok(())
181}
182
183/// Manual indexing
184#[allow(dead_code)]
185pub async fn semantic_index(cid: &str, metadata: Option<&str>) -> Result<()> {
186    let pb = progress::spinner("Preparing to index content...");
187    progress::finish_spinner_success(&pb, "Index preparation complete");
188
189    // Note: Full implementation requires embedding extraction from content
190    output::warning("Semantic indexing requires an embedding model (not yet configured)");
191
192    print_cid("CID", cid);
193    if let Some(meta) = metadata {
194        println!("  Metadata: {}", meta);
195    }
196
197    println!();
198    println!("To enable semantic indexing:");
199    println!("  1. Configure an embedding model in config.toml");
200    println!("  2. Ensure the content exists in IPFRS");
201    println!("  3. Run indexing again to extract and store embeddings");
202
203    Ok(())
204}
205
206/// Find similar content
207#[allow(dead_code)]
208pub async fn semantic_similar(cid: &str, top_k: usize, format: &str) -> Result<()> {
209    let pb = progress::spinner("Preparing similarity search...");
210    progress::finish_spinner_success(&pb, "Search preparation complete");
211
212    // Note: Full implementation requires content retrieval and embedding extraction
213    output::warning("Similarity search requires an embedding model (not yet configured)");
214
215    match format {
216        "json" => {
217            println!("{{");
218            println!("  \"cid\": \"{}\",", cid);
219            println!("  \"top_k\": {},", top_k);
220            println!("  \"status\": \"not_implemented\",");
221            println!("  \"message\": \"Similarity search requires embedding model configuration\"");
222            println!("}}");
223        }
224        _ => {
225            print_header("Similarity Search");
226            print_cid("Query CID", cid);
227            println!("  Top K: {}", top_k);
228            println!();
229            println!("To enable similarity search:");
230            println!("  1. Configure an embedding model in config.toml");
231            println!("  2. Index your content with 'ipfrs semantic index'");
232            println!("  3. Run similarity search again");
233        }
234    }
235
236    Ok(())
237}
238
239/// Index statistics
240#[allow(dead_code)]
241pub async fn semantic_stats(format: &str) -> Result<()> {
242    let pb = progress::spinner("Retrieving semantic index statistics...");
243    progress::finish_spinner_success(&pb, "Statistics retrieved");
244
245    output::warning("Semantic index not yet initialized");
246
247    match format {
248        "json" => {
249            println!("{{");
250            println!("  \"total_vectors\": 0,");
251            println!("  \"index_size_bytes\": 0,");
252            println!("  \"num_dimensions\": 0,");
253            println!("  \"status\": \"not_initialized\"");
254            println!("}}");
255        }
256        _ => {
257            print_header("Semantic Index Statistics");
258            print_kv("Total Vectors", "0");
259            print_kv("Index Size", "0 B");
260            print_kv("Status", "Not initialized");
261            println!();
262            println!("To initialize the semantic index:");
263            println!("  1. Configure an embedding model");
264            println!("  2. Index content with 'ipfrs semantic index <cid>'");
265        }
266    }
267
268    Ok(())
269}
270
271/// Save semantic index
272pub async fn semantic_save(path: &str) -> Result<()> {
273    use ipfrs::{Node, NodeConfig};
274
275    let mut node = Node::new(NodeConfig::default())?;
276    node.start().await?;
277
278    println!("Saving semantic index to {}...", path);
279    node.save_semantic_index(path).await?;
280    println!("Semantic index saved successfully");
281
282    node.stop().await?;
283    Ok(())
284}
285
286/// Load semantic index
287pub async fn semantic_load(path: &str) -> Result<()> {
288    use ipfrs::{Node, NodeConfig};
289
290    let mut node = Node::new(NodeConfig::default())?;
291    node.start().await?;
292
293    println!("Loading semantic index from {}...", path);
294    node.load_semantic_index(path).await?;
295    println!("Semantic index loaded successfully");
296
297    node.stop().await?;
298    Ok(())
299}