use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::config::SearchConfig;
use crate::constants::{DEFAULT_LIMIT, RELATED_DEFAULT_DEPTH};
use crate::contracts::PositiveCount;
use crate::error::TalonResult;
use crate::search::constants::CANDIDATE_FLOOR_U16;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SearchMode {
#[default]
Hybrid,
Semantic,
Fulltext,
Title,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Direction {
Outgoing,
Backlinks,
#[default]
Both,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FrontmatterFilter {
Text(String),
Texts(Vec<String>),
Fields(BTreeMap<String, FrontmatterValue>),
}
pub use crate::text::frontmatter::{FrontmatterValue, FrontmatterValueType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WhereOperator {
Equals,
NotEquals,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
Contains,
Exists,
StartsWith,
GlobMatch,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WhereClause {
pub key: String,
pub op: WhereOperator,
pub value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchInput {
pub query: Option<String>,
#[serde(default)]
pub queries: Vec<String>,
#[serde(
default,
deserialize_with = "crate::search::intent::deserialize_optional",
skip_serializing_if = "Option::is_none"
)]
pub intent: Option<String>,
#[serde(default)]
pub mode: SearchMode,
#[serde(default)]
pub fast: bool,
#[serde(default)]
pub limit: PositiveCount,
#[serde(default)]
pub candidate_limit: PositiveCount,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub tag: Vec<String>,
#[serde(default)]
pub frontmatter: Option<FrontmatterFilter>,
#[serde(default)]
pub related: bool,
#[serde(default = "default_depth")]
pub depth: u8,
#[serde(default)]
pub direction: Direction,
#[serde(default)]
pub scope: Vec<String>,
#[serde(default)]
pub scope_only: Vec<String>,
#[serde(default)]
pub scope_all: bool,
#[serde(default)]
pub where_: Vec<WhereClause>,
#[serde(default)]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub anchors: Option<bool>,
}
impl Default for SearchInput {
fn default() -> Self {
Self {
query: None,
queries: Vec::new(),
intent: None,
mode: SearchMode::Hybrid,
fast: false,
limit: PositiveCount::from_const(DEFAULT_LIMIT),
candidate_limit: PositiveCount::from_const(CANDIDATE_FLOOR_U16),
path: None,
tag: Vec::new(),
frontmatter: None,
related: false,
depth: RELATED_DEFAULT_DEPTH,
direction: Direction::Both,
scope: Vec::new(),
scope_only: Vec::new(),
scope_all: false,
where_: Vec::new(),
since: None,
anchors: None,
}
}
}
impl SearchInput {
pub fn from_cli_query(
query: String,
intent: Option<String>,
mode: SearchMode,
fast: bool,
limit: Option<u16>,
candidate_limit: Option<u16>,
config: Option<&SearchConfig>,
) -> TalonResult<Self> {
let mut input = Self {
query: Some(query),
intent: crate::search::intent::normalize_optional(intent),
mode,
fast,
..Self::from_search_config(config)?
};
if let Some(limit) = limit {
input.limit = PositiveCount::new(limit, "limit")?;
}
if let Some(candidate_limit) = candidate_limit {
input.candidate_limit = PositiveCount::new(candidate_limit, "candidate_limit")?;
}
Ok(input)
}
pub fn from_search_config(config: Option<&SearchConfig>) -> TalonResult<Self> {
let Some(config) = config else {
return Ok(Self::default());
};
Ok(Self {
limit: PositiveCount::new(config.limit, "limit")?,
candidate_limit: PositiveCount::new(config.candidate_limit, "candidate_limit")?,
..Self::default()
})
}
}
const fn default_depth() -> u8 {
RELATED_DEFAULT_DEPTH
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::SearchConfig;
#[test]
fn from_cli_query_uses_config_defaults_when_flags_are_absent() {
let config = SearchConfig {
candidate_limit: 60,
limit: 12,
..SearchConfig::default()
};
let input = SearchInput::from_cli_query(
"hello".to_string(),
None,
SearchMode::Hybrid,
false,
None,
None,
Some(&config),
)
.unwrap_or_else(|err| panic!("search input should build: {err}"));
assert_eq!(input.limit.get(), 12);
assert_eq!(input.candidate_limit.get(), 60);
}
#[test]
fn from_cli_query_flags_override_config_defaults() {
let config = SearchConfig {
candidate_limit: 60,
limit: 12,
..SearchConfig::default()
};
let input = SearchInput::from_cli_query(
"hello".to_string(),
None,
SearchMode::Hybrid,
false,
Some(20),
Some(80),
Some(&config),
)
.unwrap_or_else(|err| panic!("search input should build: {err}"));
assert_eq!(input.limit.get(), 20);
assert_eq!(input.candidate_limit.get(), 80);
}
}