use crate::context::CommandContext;
use crate::db::{self, search, Database};
use crate::display::short_id;
use crate::embeddings::EmbeddingClient;
use crate::error::Result;
use crate::local_config::LocalConfig;
use crate::resolve::parse_entity_reference;
pub fn execute(
ctx: &CommandContext,
query: &str,
entity_type: Option<&str>,
text_only: bool,
json: bool,
) -> Result<()> {
let jj_client = ctx.jj();
let repo_root = jj_client.repo_root();
let db_path = repo_root.join(".jj").join("jjj.db");
let db = Database::open(&db_path)?;
db::load_from_markdown(&db, &ctx.store)?;
let local_config = LocalConfig::load(repo_root);
let embedding_client = if text_only {
None
} else {
EmbeddingClient::from_config(&local_config, local_config.embeddings_explicitly_enabled())
};
if let Some((ref_type, ref_id)) = parse_entity_reference(query) {
return execute_similarity_search(&db, ref_type, ref_id, entity_type, json);
}
execute_hybrid_search(&db, query, entity_type, embedding_client.as_ref(), json)
}
fn execute_similarity_search(
db: &Database,
entity_type: &str,
entity_id_prefix: &str,
filter_type: Option<&str>,
json: bool,
) -> Result<()> {
let conn = db.conn();
let full_id = resolve_entity_id(conn, entity_type, entity_id_prefix)?;
let results = search::find_similar(conn, entity_type, &full_id, filter_type, 20)?;
if json {
let json_results: Vec<_> = results
.iter()
.map(|r| {
serde_json::json!({
"type": r.entity_type,
"id": r.entity_id,
"title": r.title,
"similarity": r.similarity,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_results)?);
} else if results.is_empty() {
let type_char = entity_type.chars().next().unwrap_or('?');
println!(
"No similar entities found for {}/{}",
type_char, entity_id_prefix
);
println!("\nNote: Embeddings may not be computed. Run 'jjj db rebuild' with an embedding service running.");
} else {
let type_char = entity_type.chars().next().unwrap_or('?');
println!(
"Entities similar to {}/{}:\n",
type_char,
short_id(&full_id)
);
for result in results {
let result_type_char = result.entity_type.chars().next().unwrap_or('?');
println!(
" {}/{} [{:.2}] \"{}\"",
result_type_char,
short_id(&result.entity_id),
result.similarity,
result.title
);
}
}
Ok(())
}
fn execute_hybrid_search(
db: &Database,
query: &str,
entity_type: Option<&str>,
embedding_client: Option<&EmbeddingClient>,
json: bool,
) -> Result<()> {
let conn = db.conn();
let fts_results = search::search(conn, query, entity_type)?;
let final_results = if let Some(client) = embedding_client {
if let Ok(query_embedding) = client.embed(query) {
let semantic_results =
search::similarity_search(conn, &query_embedding, entity_type, None, 50)?;
if !semantic_results.is_empty() {
search::merge_with_rrf(fts_results, semantic_results, 60)
} else {
fts_results
}
} else {
fts_results
}
} else {
fts_results
};
if json {
let json_results: Vec<_> = final_results
.iter()
.map(|r| {
serde_json::json!({
"type": r.entity_type,
"id": r.entity_id,
"title": r.title,
"snippet": r.snippet,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_results)?);
} else if final_results.is_empty() {
println!("No results found for \"{}\"", query);
} else {
let hybrid_note = if embedding_client.is_some() {
" (hybrid)"
} else {
""
};
println!(
"Found {} result(s) for \"{}\"{}:\n",
final_results.len(),
query,
hybrid_note
);
for result in final_results {
println!(
"[{}] {} - {}",
result.entity_type, result.entity_id, result.title
);
if !result.snippet.is_empty() {
println!(" {}", result.snippet.replace('\n', " "));
}
println!();
}
}
Ok(())
}
fn resolve_entity_id(
conn: &rusqlite::Connection,
entity_type: &str,
prefix: &str,
) -> Result<String> {
let table = match entity_type {
"problem" => "problems",
"solution" => "solutions",
"critique" => "critiques",
"milestone" => "milestones",
_ => return Err(crate::error::JjjError::EntityNotFound(prefix.to_string())),
};
let sql = format!("SELECT id FROM {} WHERE id LIKE ?1 || '%'", table);
let pattern = prefix;
let mut stmt = conn.prepare(&sql)?;
let mut rows = stmt.query([pattern])?;
let mut matches = Vec::new();
while let Some(row) = rows.next()? {
matches.push(row.get::<_, String>(0)?);
}
match matches.len() {
0 => Err(crate::error::JjjError::EntityNotFound(prefix.to_string())),
1 => Ok(matches.remove(0)),
_ => Err(crate::error::JjjError::AmbiguousId {
prefix: prefix.to_string(),
matches,
}),
}
}