cellstate/entity/
search.rs1use anyhow::Result;
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{SearchRequest, SearchResponse};
9use cellstate_core::EntityType;
10
11use super::client::ApiClient;
12use super::output::OutputConfig;
13
14const BASE: &str = "/api/v1/search";
15
16pub fn build_command() -> Command {
17 Command::new("search")
18 .about("Search entities across the memory graph")
19 .arg(Arg::new("query").required(true).help("Search query text"))
20 .arg(
21 Arg::new("entity-types")
22 .long("entity-types")
23 .short('t')
24 .value_delimiter(',')
25 .help(
26 "Entity types to search (comma-separated: artifact,note,turn,trajectory,scope)",
27 ),
28 )
29 .arg(
30 Arg::new("limit")
31 .long("limit")
32 .short('l')
33 .default_value("20")
34 .help("Maximum number of results"),
35 )
36 .arg(
37 Arg::new("vector")
38 .long("vector")
39 .action(clap::ArgAction::SetTrue)
40 .help("Enable vector-based semantic search instead of keyword matching"),
41 )
42 .arg(
43 Arg::new("filter")
44 .long("filter")
45 .short('f')
46 .action(clap::ArgAction::Append)
47 .help("Filter expression as JSON (can be repeated)"),
48 )
49}
50
51pub async fn dispatch(
52 matches: &ArgMatches,
53 client: &ApiClient,
54 output: &OutputConfig,
55 _session: &crate::session::CliSession,
56) -> Result<()> {
57 let query = matches.get_one::<String>("query").unwrap().clone();
58 let limit: i64 = matches.get_one::<String>("limit").unwrap().parse()?;
59
60 let entity_types: Vec<EntityType> = match matches.get_many::<String>("entity-types") {
61 Some(vals) => vals
62 .map(|s| {
63 s.parse::<EntityType>()
64 .map_err(|e| anyhow::anyhow!("invalid entity type '{s}': {e}"))
65 })
66 .collect::<Result<Vec<_>>>()?,
67 None => vec![],
68 };
69
70 let use_vector_search = if matches.get_flag("vector") {
71 Some(true)
72 } else {
73 None
74 };
75
76 let filters = match matches.get_many::<String>("filter") {
77 Some(vals) => vals
78 .map(|s| {
79 serde_json::from_str(s)
80 .map_err(|e| anyhow::anyhow!("invalid filter JSON '{s}': {e}"))
81 })
82 .collect::<Result<Vec<_>>>()?,
83 None => vec![],
84 };
85
86 let req = SearchRequest {
87 query,
88 entity_types,
89 filters,
90 limit: Some(limit),
91 use_vector_search,
92 };
93
94 let resp: SearchResponse = client.post(BASE, &req).await?;
95 output.print_list(&resp.results, resp.total);
96
97 Ok(())
98}