Skip to main content

ankit_engine/
search.rs

1//! Content search helpers for finding notes.
2//!
3//! This module provides a simplified API for searching notes by content,
4//! abstracting away Anki's query syntax.
5//!
6//! # Example
7//!
8//! ```no_run
9//! # use ankit_engine::Engine;
10//! # async fn example() -> ankit_engine::Result<()> {
11//! let engine = Engine::new();
12//!
13//! // Search for text in any field
14//! let notes = engine.search().text("conjugation", Some("Japanese")).await?;
15//!
16//! // Search in a specific field
17//! let notes = engine.search().field("Front", "mangiare", None).await?;
18//!
19//! // Regex search
20//! let notes = engine.search().regex("Back", r"^to\s+", None).await?;
21//! # Ok(())
22//! # }
23//! ```
24
25use ankit::{AnkiClient, NoteInfo, QueryBuilder};
26
27use crate::Result;
28
29/// Content search engine for finding notes.
30#[derive(Debug)]
31pub struct SearchEngine<'a> {
32    client: &'a AnkiClient,
33}
34
35impl<'a> SearchEngine<'a> {
36    pub(crate) fn new(client: &'a AnkiClient) -> Self {
37        Self { client }
38    }
39
40    /// Search for text in any field.
41    ///
42    /// Returns notes containing the exact phrase in any field.
43    ///
44    /// # Arguments
45    ///
46    /// * `text` - Text to search for (exact phrase match)
47    /// * `deck` - Optional deck to limit search to
48    ///
49    /// # Example
50    ///
51    /// ```no_run
52    /// # use ankit_engine::Engine;
53    /// # async fn example() -> ankit_engine::Result<()> {
54    /// let engine = Engine::new();
55    ///
56    /// // Search all decks
57    /// let notes = engine.search().text("example sentence", None).await?;
58    ///
59    /// // Search specific deck
60    /// let notes = engine.search().text("verb", Some("Italian")).await?;
61    /// # Ok(())
62    /// # }
63    /// ```
64    pub async fn text(&self, text: &str, deck: Option<&str>) -> Result<Vec<NoteInfo>> {
65        let mut qb = QueryBuilder::new().contains(text);
66        if let Some(d) = deck {
67            qb = qb.deck(d);
68        }
69        self.execute_query(&qb.build()).await
70    }
71
72    /// Search for text in a specific field.
73    ///
74    /// # Arguments
75    ///
76    /// * `field_name` - Name of the field to search in
77    /// * `text` - Text to search for
78    /// * `deck` - Optional deck to limit search to
79    ///
80    /// # Example
81    ///
82    /// ```no_run
83    /// # use ankit_engine::Engine;
84    /// # async fn example() -> ankit_engine::Result<()> {
85    /// let engine = Engine::new();
86    ///
87    /// let notes = engine.search().field("Front", "mangiare", Some("Italian")).await?;
88    /// # Ok(())
89    /// # }
90    /// ```
91    pub async fn field(
92        &self,
93        field_name: &str,
94        text: &str,
95        deck: Option<&str>,
96    ) -> Result<Vec<NoteInfo>> {
97        let mut qb = QueryBuilder::new().field(field_name, text);
98        if let Some(d) = deck {
99            qb = qb.deck(d);
100        }
101        self.execute_query(&qb.build()).await
102    }
103
104    /// Search with regex in a specific field.
105    ///
106    /// # Arguments
107    ///
108    /// * `field_name` - Name of the field to search in
109    /// * `pattern` - Regex pattern
110    /// * `deck` - Optional deck to limit search to
111    ///
112    /// # Example
113    ///
114    /// ```no_run
115    /// # use ankit_engine::Engine;
116    /// # async fn example() -> ankit_engine::Result<()> {
117    /// let engine = Engine::new();
118    ///
119    /// // Find notes where Back field starts with "to "
120    /// let notes = engine.search().regex("Back", r"^to\s+", None).await?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    pub async fn regex(
125        &self,
126        field_name: &str,
127        pattern: &str,
128        deck: Option<&str>,
129    ) -> Result<Vec<NoteInfo>> {
130        let mut qb = QueryBuilder::new().field_regex(field_name, pattern);
131        if let Some(d) = deck {
132            qb = qb.deck(d);
133        }
134        self.execute_query(&qb.build()).await
135    }
136
137    /// Search with wildcards in a specific field.
138    ///
139    /// Use `*` for any sequence of characters, `_` for single character.
140    ///
141    /// # Arguments
142    ///
143    /// * `field_name` - Name of the field to search in
144    /// * `pattern` - Wildcard pattern (e.g., `*tion`, `h_llo`)
145    /// * `deck` - Optional deck to limit search to
146    ///
147    /// # Example
148    ///
149    /// ```no_run
150    /// # use ankit_engine::Engine;
151    /// # async fn example() -> ankit_engine::Result<()> {
152    /// let engine = Engine::new();
153    ///
154    /// // Find notes ending with "tion"
155    /// let notes = engine.search().wildcard("Front", "*tion", None).await?;
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub async fn wildcard(
160        &self,
161        field_name: &str,
162        pattern: &str,
163        deck: Option<&str>,
164    ) -> Result<Vec<NoteInfo>> {
165        let mut qb = QueryBuilder::new().field_wildcard(field_name, pattern);
166        if let Some(d) = deck {
167            qb = qb.deck(d);
168        }
169        self.execute_query(&qb.build()).await
170    }
171
172    /// Find notes where a field is empty.
173    ///
174    /// # Arguments
175    ///
176    /// * `field_name` - Name of the field to check
177    /// * `deck` - Optional deck to limit search to
178    ///
179    /// # Example
180    ///
181    /// ```no_run
182    /// # use ankit_engine::Engine;
183    /// # async fn example() -> ankit_engine::Result<()> {
184    /// let engine = Engine::new();
185    ///
186    /// // Find notes missing examples
187    /// let notes = engine.search().empty_field("Example", Some("Vocabulary")).await?;
188    /// # Ok(())
189    /// # }
190    /// ```
191    pub async fn empty_field(&self, field_name: &str, deck: Option<&str>) -> Result<Vec<NoteInfo>> {
192        let mut qb = QueryBuilder::new().field_empty(field_name);
193        if let Some(d) = deck {
194            qb = qb.deck(d);
195        }
196        self.execute_query(&qb.build()).await
197    }
198
199    /// Search with a custom query built using QueryBuilder.
200    ///
201    /// This allows combining the content search with full QueryBuilder capabilities.
202    ///
203    /// # Example
204    ///
205    /// ```no_run
206    /// use ankit::QueryBuilder;
207    /// # use ankit_engine::Engine;
208    /// # async fn example() -> ankit_engine::Result<()> {
209    /// let engine = Engine::new();
210    ///
211    /// let query = QueryBuilder::new()
212    ///     .deck("Japanese")
213    ///     .is_due()
214    ///     .not_suspended()
215    ///     .lapses_gte(3)
216    ///     .build();
217    ///
218    /// let notes = engine.search().query(&query).await?;
219    /// # Ok(())
220    /// # }
221    /// ```
222    pub async fn query(&self, query: &str) -> Result<Vec<NoteInfo>> {
223        self.execute_query(query).await
224    }
225
226    /// Execute a query and return full note info.
227    async fn execute_query(&self, query: &str) -> Result<Vec<NoteInfo>> {
228        let note_ids = self.client.notes().find(query).await?;
229        if note_ids.is_empty() {
230            return Ok(Vec::new());
231        }
232        let notes = self.client.notes().info(&note_ids).await?;
233        Ok(notes)
234    }
235}