cellstate/entity/
search.rs

1//! `cellstate search` — Search entities across the memory graph.
2//!
3//! Single action: search (POST "").
4
5use 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}