1use crate::model::IndexDoc;
2use anyhow::Result;
3use rbatis::{executor::Executor, RBatis};
4use rbdc_sqlite::Driver;
5use rbs::Value;
6use serde::{Deserialize, Serialize};
7use std::{
8 collections::HashMap,
9 path::{Path, PathBuf},
10 sync::Arc,
11};
12
13const SCHEMA_SQL: &str = r#"
14 PRAGMA journal_mode=WAL;
15 PRAGMA synchronous=NORMAL;
16 PRAGMA cache_size=-64000;
17 PRAGMA temp_store=MEMORY;
18
19 CREATE TABLE IF NOT EXISTS nodes (
20 id TEXT PRIMARY KEY,
21 node_type TEXT NOT NULL,
22 parent_id TEXT,
23 path TEXT NOT NULL,
24 marks TEXT,
25 marks_json TEXT,
26 attrs TEXT,
27 attrs_json TEXT,
28 text TEXT,
29 order_i64 INTEGER,
30 created_at_i64 INTEGER,
31 updated_at_i64 INTEGER
32 );
33
34 CREATE INDEX IF NOT EXISTS idx_node_type ON nodes(node_type);
35 CREATE INDEX IF NOT EXISTS idx_parent_id ON nodes(parent_id);
36 CREATE INDEX IF NOT EXISTS idx_path ON nodes(path);
37 CREATE INDEX IF NOT EXISTS idx_created_at ON nodes(created_at_i64);
38 CREATE INDEX IF NOT EXISTS idx_updated_at ON nodes(updated_at_i64);
39 CREATE INDEX IF NOT EXISTS idx_order ON nodes(order_i64);
40
41 CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
42 id UNINDEXED,
43 text,
44 content='nodes',
45 content_rowid='rowid'
46 );
47
48 CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
49 INSERT INTO nodes_fts(rowid, id, text)
50 VALUES (new.rowid, new.id, new.text);
51 END;
52
53 CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
54 INSERT INTO nodes_fts(nodes_fts, rowid, id, text)
55 VALUES('delete', old.rowid, old.id, old.text);
56 END;
57
58 CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
59 INSERT INTO nodes_fts(nodes_fts, rowid, id, text)
60 VALUES('delete', old.rowid, old.id, old.text);
61 INSERT INTO nodes_fts(rowid, id, text)
62 VALUES (new.rowid, new.id, new.text);
63 END;
64"#;
65
66#[derive(Debug, Clone)]
68pub enum IndexMutation {
69 Add(IndexDoc),
70 Upsert(IndexDoc),
71 DeleteById(String),
72 DeleteManyById(Vec<String>),
73}
74
75#[derive(Debug, Clone, Default)]
77pub struct SearchQuery {
78 pub text: Option<String>,
80 pub node_type: Option<String>,
82 pub parent_id: Option<String>,
84 pub path_prefix: Option<String>,
86 pub marks: Vec<String>,
88 pub mark_attrs: Vec<(String, String, String)>,
90 pub attrs: Vec<(String, String)>,
92 pub limit: usize,
94 pub offset: usize,
96 pub sort_by: Option<String>,
98 pub sort_asc: bool,
100 pub include_descendants: bool,
102 pub range_field: Option<String>,
104 pub range_min: Option<i64>,
105 pub range_max: Option<i64>,
106}
107
108pub struct SqliteBackend {
110 pool: Arc<RBatis>,
111 index_dir: PathBuf,
112 _temp_dir: Option<tempfile::TempDir>,
113}
114
115impl SqliteBackend {
116 pub async fn new_in_dir(dir: &Path) -> Result<Self> {
118 std::fs::create_dir_all(dir)?;
119 let db_path = dir.join("index.db");
120 Self::new_with_path(db_path, None).await
121 }
122
123 pub async fn new_in_system_temp() -> Result<Self> {
125 let temp_dir =
126 tempfile::Builder::new().prefix("mf_index_").tempdir()?;
127 let db_path = temp_dir.path().join("index.db");
128 Self::new_with_path(db_path, Some(temp_dir)).await
129 }
130
131 pub async fn new_in_temp_root(temp_root: &Path) -> Result<Self> {
133 let temp_dir = tempfile::Builder::new()
134 .prefix("mf_index_")
135 .tempdir_in(temp_root)?;
136 let db_path = temp_dir.path().join("index.db");
137 Self::new_with_path(db_path, Some(temp_dir)).await
138 }
139
140 async fn new_with_path(
141 db_path: PathBuf,
142 temp_dir: Option<tempfile::TempDir>,
143 ) -> Result<Self> {
144 let rb = RBatis::new();
145 rb.link(Driver {}, &format!("sqlite://{}", db_path.display())).await?;
146 rb.acquire().await?.exec(SCHEMA_SQL, vec![]).await?;
147
148 Ok(Self {
149 pool: Arc::new(rb),
150 index_dir: db_path
151 .parent()
152 .map(Path::to_path_buf)
153 .unwrap_or_else(|| PathBuf::from(".")),
154 _temp_dir: temp_dir,
155 })
156 }
157
158 pub fn index_dir(&self) -> &Path {
160 &self.index_dir
161 }
162
163 pub async fn apply(
165 &self,
166 mutations: Vec<IndexMutation>,
167 ) -> Result<()> {
168 if mutations.is_empty() {
169 return Ok(());
170 }
171
172 let tx = self.pool.acquire_begin().await?;
173 for mutation in mutations {
174 match mutation {
175 IndexMutation::Add(doc) | IndexMutation::Upsert(doc) => {
176 self.upsert_doc(&tx, &doc).await?;
177 },
178 IndexMutation::DeleteById(id) => {
179 tx.exec(
180 "DELETE FROM nodes WHERE id = ?1",
181 vec![to_value(id)],
182 )
183 .await?;
184 },
185 IndexMutation::DeleteManyById(ids) => {
186 for id in ids {
187 tx.exec(
188 "DELETE FROM nodes WHERE id = ?1",
189 vec![to_value(id)],
190 )
191 .await?;
192 }
193 },
194 }
195 }
196 tx.commit().await?;
197 Ok(())
198 }
199
200 async fn upsert_doc<E>(
201 &self,
202 exec: &E,
203 doc: &IndexDoc,
204 ) -> Result<()>
205 where
206 E: Executor + ?Sized,
207 {
208 let marks_types_json = serde_json::to_string(&doc.marks)?;
209 let attrs_map: serde_json::Map<String, serde_json::Value> = doc
210 .attrs_flat
211 .iter()
212 .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
213 .collect();
214 let attrs_flat_json = serde_json::to_string(&attrs_map)?;
215 let path_str = format!("/{}", doc.path.join("/"));
216
217 exec.exec(
218 "INSERT OR REPLACE INTO nodes
219 (id, node_type, parent_id, path, marks, marks_json, attrs, attrs_json, text,
220 order_i64, created_at_i64, updated_at_i64)
221 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
222 vec![
223 to_value(doc.node_id.clone()),
224 to_value(doc.node_type.clone()),
225 to_value(doc.parent_id.clone()),
226 to_value(path_str),
227 to_value(marks_types_json),
228 to_value(doc.marks_json.clone()),
229 to_value(attrs_flat_json),
230 to_value(doc.attrs_json.clone()),
231 to_value(doc.text.clone()),
232 to_value(doc.order_i64),
233 to_value(doc.created_at_i64),
234 to_value(doc.updated_at_i64),
235 ],
236 )
237 .await?;
238 Ok(())
239 }
240
241 pub async fn rebuild_all(
243 &self,
244 docs: Vec<IndexDoc>,
245 ) -> Result<()> {
246 let tx = self.pool.acquire_begin().await?;
247 tx.exec("DELETE FROM nodes", vec![]).await?;
248 for doc in &docs {
249 self.upsert_doc(&tx, doc).await?;
250 }
251 tx.commit().await?;
252 Ok(())
253 }
254
255 pub async fn search_ids(
257 &self,
258 query: SearchQuery,
259 ) -> Result<Vec<String>> {
260 if query.include_descendants && query.parent_id.is_some() {
261 return self.search_tree(&query).await;
262 }
263 if query.text.is_some() {
264 return self.search_fulltext(&query).await;
265 }
266 self.search_structured(&query).await
267 }
268
269 pub async fn search_docs(
271 &self,
272 query: SearchQuery,
273 ) -> Result<Vec<IndexDoc>> {
274 let ids = self.search_ids(query).await?;
275 self.get_docs_by_ids(&ids).await
276 }
277
278 pub async fn get_docs_by_ids(
280 &self,
281 ids: &[String],
282 ) -> Result<Vec<IndexDoc>> {
283 if ids.is_empty() {
284 return Ok(Vec::new());
285 }
286
287 let placeholders = std::iter::repeat("?")
288 .take(ids.len())
289 .collect::<Vec<_>>()
290 .join(",");
291 let sql = format!(
292 "SELECT id, node_type, parent_id, path, marks_json, attrs_json, text,
293 order_i64, created_at_i64, updated_at_i64
294 FROM nodes WHERE id IN ({})",
295 placeholders
296 );
297 let params = ids.iter().cloned().map(to_value).collect::<Vec<Value>>();
298
299 let conn = self.pool.acquire().await?;
300 let rows: Vec<NodeRow> = conn.query_decode(&sql, params).await?;
301 let mut docs_by_id: HashMap<String, IndexDoc> =
302 HashMap::with_capacity(rows.len());
303 for row in rows {
304 let doc = IndexDoc::try_from(row)?;
305 docs_by_id.insert(doc.node_id.clone(), doc);
306 }
307
308 let mut ordered = Vec::with_capacity(ids.len());
309 for id in ids {
310 if let Some(doc) = docs_by_id.remove(id.as_str()) {
311 ordered.push(doc);
312 }
313 }
314 Ok(ordered)
315 }
316
317 async fn search_tree(
318 &self,
319 query: &SearchQuery,
320 ) -> Result<Vec<String>> {
321 let parent_id = query.parent_id.as_ref().unwrap();
322 let mut sql = String::from(
323 "WITH RECURSIVE tree(id, level) AS (
324 SELECT id, 0 as level FROM nodes WHERE id = ?1
325 UNION ALL
326 SELECT n.id, t.level + 1
327 FROM nodes n
328 JOIN tree t ON n.parent_id = t.id
329 WHERE t.level < 100
330 )
331 SELECT id FROM tree WHERE 1=1",
332 );
333
334 let mut params = vec![to_value(parent_id.clone())];
335 if let Some(node_type) = &query.node_type {
336 sql.push_str(
337 " AND id IN (SELECT id FROM nodes WHERE node_type = ?)",
338 );
339 params.push(to_value(node_type.clone()));
340 }
341
342 if let Some(sort_by) = &query.sort_by {
343 let direction = if query.sort_asc { "ASC" } else { "DESC" };
344 sql.push_str(&format!(
345 " ORDER BY (SELECT {} FROM nodes WHERE nodes.id = tree.id) {}",
346 sort_by, direction
347 ));
348 }
349
350 let limit = if query.limit == 0 { 1000 } else { query.limit };
351 sql.push_str(&format!(" LIMIT {} OFFSET {}", limit, query.offset));
352
353 let conn = self.pool.acquire().await?;
354 let rows: Vec<IdRow> = conn.query_decode(&sql, params).await?;
355 Ok(rows.into_iter().map(|r| r.id).collect())
356 }
357
358 async fn search_fulltext(
359 &self,
360 query: &SearchQuery,
361 ) -> Result<Vec<String>> {
362 let text = query.text.as_ref().unwrap();
363 let mut sql = String::from(
364 "SELECT nodes.id FROM nodes_fts
365 JOIN nodes ON nodes_fts.id = nodes.id
366 WHERE nodes_fts.text MATCH ?",
367 );
368 let mut params = vec![to_value(text.clone())];
369
370 if let Some(node_type) = &query.node_type {
371 sql.push_str(" AND nodes.node_type = ?");
372 params.push(to_value(node_type.clone()));
373 }
374 if let Some(parent_id) = &query.parent_id {
375 sql.push_str(" AND nodes.parent_id = ?");
376 params.push(to_value(parent_id.clone()));
377 }
378
379 for mark in &query.marks {
380 sql.push_str(" AND nodes.marks LIKE ?");
381 params.push(to_value(format!("%\"{}\"%%", mark)));
382 }
383
384 for (mark_type, attr_key, attr_value) in &query.mark_attrs {
385 sql.push_str(&format!(
386 " AND EXISTS (
387 SELECT 1 FROM json_each(nodes.marks_json)
388 WHERE json_extract(value, '$.type') = ?
389 AND json_extract(value, '$.attrs.{}') = ?
390 )",
391 attr_key
392 ));
393 params.push(to_value(mark_type.clone()));
394 params.push(to_value(attr_value.clone()));
395 }
396
397 if let Some(sort_by) = &query.sort_by {
398 let direction = if query.sort_asc { "ASC" } else { "DESC" };
399 sql.push_str(&format!(" ORDER BY nodes.{} {}", sort_by, direction));
400 } else {
401 sql.push_str(" ORDER BY rank");
402 }
403
404 let limit = if query.limit == 0 { 50 } else { query.limit };
405 sql.push_str(&format!(" LIMIT {} OFFSET {}", limit, query.offset));
406
407 let conn = self.pool.acquire().await?;
408 let rows: Vec<IdRow> = conn.query_decode(&sql, params).await?;
409 Ok(rows.into_iter().map(|r| r.id).collect())
410 }
411
412 async fn search_structured(
413 &self,
414 query: &SearchQuery,
415 ) -> Result<Vec<String>> {
416 let mut sql = String::from("SELECT id FROM nodes WHERE 1=1");
417 let mut params: Vec<Value> = Vec::new();
418
419 if let Some(node_type) = &query.node_type {
420 sql.push_str(" AND node_type = ?");
421 params.push(to_value(node_type.clone()));
422 }
423 if let Some(parent_id) = &query.parent_id {
424 sql.push_str(" AND parent_id = ?");
425 params.push(to_value(parent_id.clone()));
426 }
427 if let Some(path_prefix) = &query.path_prefix {
428 sql.push_str(" AND path LIKE ?");
429 params.push(to_value(format!("{}%", path_prefix)));
430 }
431
432 for mark in &query.marks {
433 sql.push_str(" AND marks LIKE ?");
434 params.push(to_value(format!("%\"{}\"%%", mark)));
435 }
436
437 for (mark_type, attr_key, attr_value) in &query.mark_attrs {
438 sql.push_str(&format!(
439 " AND EXISTS (
440 SELECT 1 FROM json_each(marks_json)
441 WHERE json_extract(value, '$.type') = ?
442 AND json_extract(value, '$.attrs.{}') = ?
443 )",
444 attr_key
445 ));
446 params.push(to_value(mark_type.clone()));
447 params.push(to_value(attr_value.clone()));
448 }
449
450 for (key, value) in &query.attrs {
451 sql.push_str(&format!(" AND json_extract(attrs, '$.{}') = ?", key));
452 params.push(to_value(value.clone()));
453 }
454
455 if let Some(field) = &query.range_field {
456 if let Some(min) = query.range_min {
457 sql.push_str(&format!(" AND {} >= ?", field));
458 params.push(to_value(min));
459 }
460 if let Some(max) = query.range_max {
461 sql.push_str(&format!(" AND {} <= ?", field));
462 params.push(to_value(max));
463 }
464 }
465
466 if let Some(sort_by) = &query.sort_by {
467 let direction = if query.sort_asc { "ASC" } else { "DESC" };
468 sql.push_str(&format!(" ORDER BY {} {}", sort_by, direction));
469 }
470
471 let limit = if query.limit == 0 { 50 } else { query.limit };
472 sql.push_str(&format!(" LIMIT {} OFFSET {}", limit, query.offset));
473
474 let conn = self.pool.acquire().await?;
475 let rows: Vec<IdRow> = conn.query_decode(&sql, params).await?;
476 Ok(rows.into_iter().map(|r| r.id).collect())
477 }
478}
479
480fn to_value<T: Serialize>(value: T) -> Value {
481 rbs::value_def(value)
482}
483
484#[derive(Debug, Deserialize)]
485struct IdRow {
486 id: String,
487}
488
489#[derive(Debug, Deserialize)]
490struct NodeRow {
491 id: String,
492 node_type: String,
493 parent_id: Option<String>,
494 path: String,
495 marks_json: serde_json::Value,
496 attrs_json: serde_json::Value,
497 text: Option<String>,
498 order_i64: Option<i64>,
499 created_at_i64: Option<i64>,
500 updated_at_i64: Option<i64>,
501}
502
503impl TryFrom<NodeRow> for IndexDoc {
504 type Error = anyhow::Error;
505
506 fn try_from(row: NodeRow) -> Result<Self> {
507 let marks_json_str = json_to_string(&row.marks_json);
508 let attrs_json_str = json_to_string(&row.attrs_json);
509
510 Ok(IndexDoc {
511 node_id: row.id,
512 node_type: row.node_type,
513 parent_id: row.parent_id,
514 path: parse_path(&row.path),
515 marks: marks_from_value(&row.marks_json),
516 marks_json: marks_json_str,
517 attrs_flat: flatten_attrs(&row.attrs_json),
518 attrs_json: attrs_json_str,
519 text: row.text,
520 order_i64: row.order_i64,
521 created_at_i64: row.created_at_i64,
522 updated_at_i64: row.updated_at_i64,
523 })
524 }
525}
526
527fn parse_path(path: &str) -> Vec<String> {
528 path.trim_start_matches('/')
529 .split('/')
530 .filter(|s| !s.is_empty())
531 .map(|s| s.to_string())
532 .collect()
533}
534
535fn marks_from_value(value: &serde_json::Value) -> Vec<String> {
536 match value {
537 serde_json::Value::Array(items) => items
538 .iter()
539 .filter_map(|mark| {
540 mark.get("type").and_then(|v| v.as_str()).map(ToOwned::to_owned)
541 })
542 .collect(),
543 serde_json::Value::String(raw) => serde_json::from_str::<
544 serde_json::Value,
545 >(raw)
546 .ok()
547 .and_then(|parsed| parsed.as_array().cloned())
548 .unwrap_or_default()
549 .into_iter()
550 .filter_map(|mark| {
551 mark.get("type").and_then(|v| v.as_str()).map(ToOwned::to_owned)
552 })
553 .collect(),
554 _ => Vec::new(),
555 }
556}
557
558fn flatten_attrs(value: &serde_json::Value) -> Vec<(String, String)> {
559 match value {
560 serde_json::Value::Object(map) => map
561 .iter()
562 .map(|(k, v)| (k.clone(), json_value_to_string(v)))
563 .collect(),
564 serde_json::Value::String(raw) => serde_json::from_str::<
565 serde_json::Map<String, serde_json::Value>,
566 >(raw)
567 .map(|map| {
568 map.into_iter()
569 .map(|(k, v)| (k, json_value_to_string(&v)))
570 .collect()
571 })
572 .unwrap_or_default(),
573 _ => Vec::new(),
574 }
575}
576
577fn json_to_string(value: &serde_json::Value) -> String {
578 match value {
579 serde_json::Value::String(s) => s.clone(),
580 other => other.to_string(),
581 }
582}
583
584fn json_value_to_string(value: &serde_json::Value) -> String {
585 match value {
586 serde_json::Value::Null => "null".to_string(),
587 serde_json::Value::Bool(b) => b.to_string(),
588 serde_json::Value::Number(n) => n.to_string(),
589 serde_json::Value::String(s) => s.clone(),
590 _ => value.to_string(),
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[tokio::test]
599 async fn test_basic_operations() {
600 let backend = SqliteBackend::new_in_system_temp().await.unwrap();
601
602 let doc = IndexDoc {
603 node_id: "test1".to_string(),
604 node_type: "paragraph".to_string(),
605 parent_id: Some("root".to_string()),
606 path: vec!["root".to_string(), "test1".to_string()],
607 marks: vec!["bold".to_string()],
608 marks_json: r#"[{"type":"bold","attrs":{}}]"#.to_string(),
609 attrs_flat: vec![("status".to_string(), "published".to_string())],
610 attrs_json: r#"{"status":"published"}"#.to_string(),
611 text: Some("测试文本".to_string()),
612 order_i64: Some(1),
613 created_at_i64: Some(1000),
614 updated_at_i64: Some(2000),
615 };
616
617 backend.apply(vec![IndexMutation::Add(doc.clone())]).await.unwrap();
618
619 let ids = backend
620 .search_ids(SearchQuery {
621 node_type: Some("paragraph".to_string()),
622 limit: 10,
623 ..Default::default()
624 })
625 .await
626 .unwrap();
627 assert_eq!(ids, vec!["test1"]);
628
629 let docs = backend.get_docs_by_ids(&ids).await.unwrap();
630 assert_eq!(docs.len(), 1);
631 assert_eq!(docs[0].node_id, "test1");
632 }
633
634 #[tokio::test]
635 async fn test_tree_query() {
636 let backend = SqliteBackend::new_in_system_temp().await.unwrap();
637
638 let docs = vec![
639 IndexDoc {
640 node_id: "root".to_string(),
641 node_type: "doc".to_string(),
642 parent_id: None,
643 path: vec!["root".to_string()],
644 marks: vec![],
645 marks_json: "[]".to_string(),
646 attrs_flat: vec![],
647 attrs_json: "{}".to_string(),
648 text: None,
649 order_i64: None,
650 created_at_i64: None,
651 updated_at_i64: None,
652 },
653 IndexDoc {
654 node_id: "child1".to_string(),
655 node_type: "paragraph".to_string(),
656 parent_id: Some("root".to_string()),
657 path: vec!["root".to_string(), "child1".to_string()],
658 marks: vec![],
659 marks_json: "[]".to_string(),
660 attrs_flat: vec![],
661 attrs_json: "{}".to_string(),
662 text: None,
663 order_i64: None,
664 created_at_i64: None,
665 updated_at_i64: None,
666 },
667 IndexDoc {
668 node_id: "child2".to_string(),
669 node_type: "paragraph".to_string(),
670 parent_id: Some("root".to_string()),
671 path: vec!["root".to_string(), "child2".to_string()],
672 marks: vec![],
673 marks_json: "[]".to_string(),
674 attrs_flat: vec![],
675 attrs_json: "{}".to_string(),
676 text: None,
677 order_i64: None,
678 created_at_i64: None,
679 updated_at_i64: None,
680 },
681 ];
682
683 backend.rebuild_all(docs).await.unwrap();
684
685 let results = backend
686 .search_ids(SearchQuery {
687 parent_id: Some("root".to_string()),
688 include_descendants: true,
689 limit: 100,
690 ..Default::default()
691 })
692 .await
693 .unwrap();
694
695 assert_eq!(results.len(), 3); }
697
698 #[tokio::test]
699 async fn test_search_docs() {
700 let backend = SqliteBackend::new_in_system_temp().await.unwrap();
701
702 let docs = vec![
703 IndexDoc {
704 node_id: "doc1".to_string(),
705 node_type: "paragraph".to_string(),
706 parent_id: Some("root".to_string()),
707 path: vec!["root".to_string(), "doc1".to_string()],
708 marks: vec!["bold".to_string()],
709 marks_json: r#"[{"type":"bold","attrs":{}}]"#.to_string(),
710 attrs_flat: vec![(
711 "status".to_string(),
712 "published".to_string(),
713 )],
714 attrs_json: r#"{"status":"published"}"#.to_string(),
715 text: Some("第一篇文章".to_string()),
716 order_i64: Some(1),
717 created_at_i64: Some(1000),
718 updated_at_i64: Some(1500),
719 },
720 IndexDoc {
721 node_id: "doc2".to_string(),
722 node_type: "paragraph".to_string(),
723 parent_id: Some("root".to_string()),
724 path: vec!["root".to_string(), "doc2".to_string()],
725 marks: vec!["italic".to_string()],
726 marks_json: r#"[{"type":"italic","attrs":{}}]"#.to_string(),
727 attrs_flat: vec![("status".to_string(), "draft".to_string())],
728 attrs_json: r#"{"status":"draft"}"#.to_string(),
729 text: Some("第二篇文章".to_string()),
730 order_i64: Some(2),
731 created_at_i64: Some(2000),
732 updated_at_i64: Some(2500),
733 },
734 ];
735
736 backend.rebuild_all(docs).await.unwrap();
737
738 let results = backend
739 .search_docs(SearchQuery {
740 node_type: Some("paragraph".to_string()),
741 limit: 10,
742 ..Default::default()
743 })
744 .await
745 .unwrap();
746
747 assert_eq!(results.len(), 2);
748 let doc1 = results.iter().find(|d| d.node_id == "doc1").unwrap();
749 assert_eq!(doc1.text, Some("第一篇文章".to_string()));
750 assert_eq!(doc1.marks, vec!["bold".to_string()]);
751
752 let doc2 = results.iter().find(|d| d.node_id == "doc2").unwrap();
753 assert_eq!(doc2.text, Some("第二篇文章".to_string()));
754 assert_eq!(doc2.marks, vec!["italic".to_string()]);
755 }
756
757 #[tokio::test]
758 async fn test_get_docs_by_ids() {
759 let backend = SqliteBackend::new_in_system_temp().await.unwrap();
760
761 let doc = IndexDoc {
762 node_id: "test123".to_string(),
763 node_type: "heading".to_string(),
764 parent_id: None,
765 path: vec!["test123".to_string()],
766 marks: vec![],
767 marks_json: "[]".to_string(),
768 attrs_flat: vec![("level".to_string(), "1".to_string())],
769 attrs_json: r#"{"level":"1"}"#.to_string(),
770 text: Some("标题文本".to_string()),
771 order_i64: None,
772 created_at_i64: None,
773 updated_at_i64: None,
774 };
775
776 backend.apply(vec![IndexMutation::Add(doc)]).await.unwrap();
777
778 let docs =
779 backend.get_docs_by_ids(&["test123".to_string()]).await.unwrap();
780
781 assert_eq!(docs.len(), 1);
782 assert_eq!(docs[0].node_id, "test123");
783 assert_eq!(docs[0].node_type, "heading");
784 assert_eq!(docs[0].text, Some("标题文本".to_string()));
785
786 let empty = backend.get_docs_by_ids(&[]).await.unwrap();
787 assert_eq!(empty.len(), 0);
788 }
789}