use crate::constants::{RETRIEVAL_RESULTS_COUNT_DEFAULT, RETRIEVAL_RESULTS_COUNT_MAX};
use crate::storage::Entity;
#[derive(Debug, Clone)]
pub struct SearchOptions {
pub limit: usize,
pub deep_search: bool,
pub time_range: Option<(u64, u64)>,
}
impl SearchOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_limit(mut self, limit: usize) -> Self {
debug_assert!(
limit > 0 && limit <= RETRIEVAL_RESULTS_COUNT_MAX,
"limit must be 1-{}: got {}",
RETRIEVAL_RESULTS_COUNT_MAX,
limit
);
self.limit = limit;
self
}
#[must_use]
pub fn with_deep_search(mut self, deep_search: bool) -> Self {
self.deep_search = deep_search;
self
}
#[must_use]
pub fn with_time_range(mut self, start_ms: u64, end_ms: u64) -> Self {
debug_assert!(
start_ms <= end_ms,
"start_ms must be <= end_ms: {} > {}",
start_ms,
end_ms
);
self.time_range = Some((start_ms, end_ms));
self
}
#[must_use]
pub fn fast_only(mut self) -> Self {
self.deep_search = false;
self
}
}
impl Default for SearchOptions {
fn default() -> Self {
Self {
limit: RETRIEVAL_RESULTS_COUNT_DEFAULT,
deep_search: true,
time_range: None,
}
}
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub entities: Vec<Entity>,
pub query: String,
pub deep_search_used: bool,
pub query_variations: Vec<String>,
}
impl SearchResult {
#[must_use]
pub fn new(
entities: Vec<Entity>,
query: impl Into<String>,
deep_search_used: bool,
query_variations: Vec<String>,
) -> Self {
Self {
entities,
query: query.into(),
deep_search_used,
query_variations,
}
}
#[must_use]
pub fn fast_only(entities: Vec<Entity>, query: impl Into<String>) -> Self {
let query = query.into();
Self {
entities,
query: query.clone(),
deep_search_used: false,
query_variations: vec![query],
}
}
#[must_use]
pub fn len(&self) -> usize {
self.entities.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
#[must_use]
pub fn first(&self) -> Option<&Entity> {
self.entities.first()
}
pub fn iter(&self) -> impl Iterator<Item = &Entity> {
self.entities.iter()
}
}
impl IntoIterator for SearchResult {
type Item = Entity;
type IntoIter = std::vec::IntoIter<Entity>;
fn into_iter(self) -> Self::IntoIter {
self.entities.into_iter()
}
}
pub const QUESTION_WORDS: &[&str] = &["who", "what", "when", "where", "why", "how"];
pub const RELATIONSHIP_TERMS: &[&str] =
&["related", "about", "regarding", "involving", "connected"];
pub const TEMPORAL_TERMS: &[&str] = &[
"yesterday",
"today",
"last",
"recent",
"before",
"after",
"week",
"month",
"year",
];
pub const ABSTRACT_TERMS: &[&str] = &["similar", "like", "connections", "associated", "linked"];
#[must_use]
pub fn needs_deep_search(query: &str) -> bool {
debug_assert!(!query.is_empty(), "query must not be empty");
let query_lower = query.to_lowercase();
for word in QUESTION_WORDS {
if query_lower.contains(word) {
return true;
}
}
for word in TEMPORAL_TERMS {
if query_lower.contains(word) {
return true;
}
}
for word in ABSTRACT_TERMS {
if query_lower.contains(word) {
return true;
}
}
for term in RELATIONSHIP_TERMS {
if query_lower.contains(term) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::{Entity, EntityType};
#[test]
fn test_search_options_default() {
let options = SearchOptions::default();
assert_eq!(options.limit, RETRIEVAL_RESULTS_COUNT_DEFAULT);
assert!(options.deep_search);
assert!(options.time_range.is_none());
}
#[test]
fn test_search_options_builder() {
let options = SearchOptions::new()
.with_limit(50)
.with_deep_search(false)
.with_time_range(1000, 2000);
assert_eq!(options.limit, 50);
assert!(!options.deep_search);
assert_eq!(options.time_range, Some((1000, 2000)));
}
#[test]
fn test_search_options_fast_only() {
let options = SearchOptions::new().fast_only();
assert!(!options.deep_search);
}
#[test]
#[should_panic(expected = "limit must be")]
fn test_search_options_invalid_limit_zero() {
let _ = SearchOptions::new().with_limit(0);
}
#[test]
#[should_panic(expected = "limit must be")]
fn test_search_options_invalid_limit_too_large() {
let _ = SearchOptions::new().with_limit(RETRIEVAL_RESULTS_COUNT_MAX + 1);
}
#[test]
#[should_panic(expected = "start_ms must be")]
fn test_search_options_invalid_time_range() {
let _ = SearchOptions::new().with_time_range(2000, 1000);
}
#[test]
fn test_search_result_new() {
let entities = vec![Entity::new(
EntityType::Note,
"test".to_string(),
"content".to_string(),
)];
let result = SearchResult::new(
entities,
"query",
true,
vec!["query".to_string(), "variation".to_string()],
);
assert_eq!(result.len(), 1);
assert_eq!(result.query, "query");
assert!(result.deep_search_used);
assert_eq!(result.query_variations.len(), 2);
}
#[test]
fn test_search_result_fast_only() {
let entities = vec![Entity::new(
EntityType::Note,
"test".to_string(),
"content".to_string(),
)];
let result = SearchResult::fast_only(entities, "query");
assert_eq!(result.len(), 1);
assert!(!result.deep_search_used);
assert_eq!(result.query_variations, vec!["query"]);
}
#[test]
fn test_search_result_empty() {
let result = SearchResult::fast_only(vec![], "query");
assert!(result.is_empty());
assert_eq!(result.len(), 0);
assert!(result.first().is_none());
}
#[test]
fn test_search_result_iter() {
let entities = vec![
Entity::new(EntityType::Note, "a".to_string(), "content a".to_string()),
Entity::new(EntityType::Note, "b".to_string(), "content b".to_string()),
];
let result = SearchResult::fast_only(entities, "query");
let names: Vec<_> = result.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn test_needs_deep_search_question_words() {
assert!(needs_deep_search("Who works at Acme?"));
assert!(needs_deep_search("What is the project about?"));
assert!(needs_deep_search("When did we meet?"));
assert!(needs_deep_search("Where is the office?"));
assert!(needs_deep_search("Why did that happen?"));
assert!(needs_deep_search("How does it work?"));
}
#[test]
fn test_needs_deep_search_temporal_terms() {
assert!(needs_deep_search("yesterday's meeting"));
assert!(needs_deep_search("What happened today?"));
assert!(needs_deep_search("last week's notes"));
assert!(needs_deep_search("recent updates"));
assert!(needs_deep_search("before the deadline"));
assert!(needs_deep_search("after the conference"));
}
#[test]
fn test_needs_deep_search_abstract_terms() {
assert!(needs_deep_search("similar projects"));
assert!(needs_deep_search("something like that"));
assert!(needs_deep_search("connections to Acme"));
assert!(needs_deep_search("associated topics"));
assert!(needs_deep_search("linked issues"));
}
#[test]
fn test_needs_deep_search_relationship_terms() {
assert!(needs_deep_search("related to Alice"));
assert!(needs_deep_search("about the meeting"));
assert!(needs_deep_search("regarding the project"));
assert!(needs_deep_search("involving customers"));
assert!(needs_deep_search("connected to sales"));
}
#[test]
fn test_needs_deep_search_simple_query() {
assert!(!needs_deep_search("Alice"));
assert!(!needs_deep_search("Acme Corp"));
assert!(!needs_deep_search("project status"));
assert!(!needs_deep_search("meeting notes"));
}
#[test]
fn test_needs_deep_search_case_insensitive() {
assert!(needs_deep_search("WHO works here?"));
assert!(needs_deep_search("YESTERDAY"));
assert!(needs_deep_search("Similar items"));
}
}