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(¬e_ids).await?;
233 Ok(notes)
234 }
235}