1pub mod extensions;
25
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28use std::sync::{Arc, RwLock};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EsDocument {
32 pub id: Option<String>,
33 pub index: String,
34 pub source: serde_json::Value,
35 pub timestamp: i64,
36}
37
38impl EsDocument {
39 pub fn new(index: impl Into<String>, source: serde_json::Value) -> Self {
40 Self {
41 id: None,
42 index: index.into(),
43 source,
44 timestamp: current_timestamp(),
45 }
46 }
47
48 pub fn with_id(mut self, id: impl Into<String>) -> Self {
49 self.id = Some(id.into());
50 self
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct EsSearchRequest {
56 pub index: String,
57 pub query: EsQuery,
58 pub from: usize,
59 pub size: usize,
60 pub sort: Vec<EsSort>,
61}
62
63impl EsSearchRequest {
64 pub fn new(index: impl Into<String>, query: EsQuery) -> Self {
65 Self {
66 index: index.into(),
67 query,
68 from: 0,
69 size: 10,
70 sort: Vec::new(),
71 }
72 }
73
74 pub fn with_pagination(mut self, from: usize, size: usize) -> Self {
75 self.from = from;
76 self.size = size;
77 self
78 }
79
80 pub fn with_sort(mut self, field: impl Into<String>, order: EsSortOrder) -> Self {
81 self.sort.push(EsSort {
82 field: field.into(),
83 order,
84 });
85 self
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct EsSort {
91 pub field: String,
92 pub order: EsSortOrder,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub enum EsSortOrder {
97 Asc,
98 Desc,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102#[serde(untagged)]
103pub enum EsQuery {
104 MatchAll,
105 Term(HashMap<String, serde_json::Value>),
106 Terms(HashMap<String, Vec<serde_json::Value>>),
107 Range(HashMap<String, EsRangeQuery>),
108 Bool(EsBoolQuery),
109}
110
111impl EsQuery {
112 pub fn match_all() -> Self {
113 EsQuery::MatchAll
114 }
115
116 pub fn term(field: impl Into<String>, value: serde_json::Value) -> Self {
117 let mut terms = HashMap::new();
118 terms.insert(field.into(), value);
119 EsQuery::Term(terms)
120 }
121
122 pub fn terms(field: impl Into<String>, values: Vec<serde_json::Value>) -> Self {
123 let mut terms = HashMap::new();
124 terms.insert(field.into(), values);
125 EsQuery::Terms(terms)
126 }
127
128 pub fn range(field: impl Into<String>, range: EsRangeQuery) -> Self {
129 let mut ranges = HashMap::new();
130 ranges.insert(field.into(), range);
131 EsQuery::Range(ranges)
132 }
133
134 pub fn must(queries: Vec<EsQuery>) -> Self {
135 EsQuery::Bool(EsBoolQuery {
136 must: Some(queries),
137 should: None,
138 filter: None,
139 must_not: None,
140 minimum_should_match: None,
141 })
142 }
143
144 pub fn should(queries: Vec<EsQuery>) -> Self {
145 EsQuery::Bool(EsBoolQuery {
146 must: None,
147 should: Some(queries),
148 filter: None,
149 must_not: None,
150 minimum_should_match: None,
151 })
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156pub struct EsBoolQuery {
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub must: Option<Vec<EsQuery>>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub should: Option<Vec<EsQuery>>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub filter: Option<Vec<EsQuery>>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub must_not: Option<Vec<EsQuery>>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub minimum_should_match: Option<usize>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct EsRangeQuery {
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub gt: Option<serde_json::Value>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub gte: Option<serde_json::Value>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 pub lt: Option<serde_json::Value>,
177 #[serde(skip_serializing_if = "Option::is_none")]
178 pub lte: Option<serde_json::Value>,
179}
180
181impl EsRangeQuery {
182 pub fn new() -> Self {
183 Self {
184 gt: None,
185 gte: None,
186 lt: None,
187 lte: None,
188 }
189 }
190
191 pub fn gt(mut self, value: serde_json::Value) -> Self {
192 self.gt = Some(value);
193 self
194 }
195
196 pub fn gte(mut self, value: serde_json::Value) -> Self {
197 self.gte = Some(value);
198 self
199 }
200
201 pub fn lt(mut self, value: serde_json::Value) -> Self {
202 self.lt = Some(value);
203 self
204 }
205
206 pub fn lte(mut self, value: serde_json::Value) -> Self {
207 self.lte = Some(value);
208 self
209 }
210}
211
212impl Default for EsRangeQuery {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct EsSearchResult {
220 pub total: usize,
221 pub hits: Vec<EsHit>,
222 pub took: i64,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct EsHit {
227 pub id: String,
228 pub score: f64,
229 pub source: serde_json::Value,
230}
231
232pub trait EsSync: Send + Sync {
233 fn sync_to_es(&self, documents: Vec<EsDocument>) -> Result<EsSyncResult, EsError>;
234 fn delete_from_es(&self, index: &str, ids: Vec<String>) -> Result<EsSyncResult, EsError>;
235 fn search(&self, request: EsSearchRequest) -> Result<EsSearchResult, EsError>;
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct EsSyncResult {
240 pub indexed: usize,
241 pub failed: usize,
242 pub errors: Vec<String>,
243}
244
245impl EsSyncResult {
246 pub fn success(indexed: usize) -> Self {
247 Self {
248 indexed,
249 failed: 0,
250 errors: Vec::new(),
251 }
252 }
253
254 pub fn with_errors(indexed: usize, errors: Vec<String>) -> Self {
255 let failed = errors.len();
256 Self {
257 indexed,
258 failed,
259 errors,
260 }
261 }
262}
263
264pub struct EsSyncManager {
265 index_mappings: HashMap<String, HashMap<String, EsFieldType>>,
266 backend: Box<dyn EsSync>,
268 mock_backend: Option<InMemoryEsSync>,
270}
271
272impl EsSyncManager {
273 pub fn new() -> Self {
277 let mock = InMemoryEsSync::new();
278 Self {
279 index_mappings: HashMap::new(),
280 backend: Box::new(mock.clone()),
281 mock_backend: Some(mock),
282 }
283 }
284
285 pub fn with_backend(backend: Box<dyn EsSync>) -> Self {
298 Self {
299 index_mappings: HashMap::new(),
300 backend,
301 mock_backend: None,
302 }
303 }
304
305 pub fn create_index(
306 &mut self,
307 index: impl Into<String>,
308 mapping: HashMap<String, EsFieldType>,
309 ) {
310 self.index_mappings.insert(index.into(), mapping);
311 }
312
313 pub fn get_mapping(&self, index: &str) -> Option<&HashMap<String, EsFieldType>> {
314 self.index_mappings.get(index)
315 }
316
317 pub fn sync_to_es(&self, documents: Vec<EsDocument>) -> Result<EsSyncResult, EsError> {
318 self.backend.sync_to_es(documents)
319 }
320
321 pub fn delete_from_es(&self, index: &str, ids: Vec<String>) -> Result<EsSyncResult, EsError> {
322 self.backend.delete_from_es(index, ids)
323 }
324
325 pub fn search(&self, request: EsSearchRequest) -> Result<EsSearchResult, EsError> {
326 self.backend.search(request)
327 }
328
329 pub fn count(&self, index: &str) -> Result<usize, EsError> {
333 match &self.mock_backend {
334 Some(mock) => mock.count(index),
335 None => Err(EsError::BackendNotSupported {
336 operation: "count".to_string(),
337 reason:
338 "current backend is not InMemoryEsSync; implement count on your EsSync impl"
339 .to_string(),
340 }),
341 }
342 }
343}
344
345impl Default for EsSyncManager {
346 fn default() -> Self {
347 Self::new()
348 }
349}
350
351#[derive(Clone)]
365pub struct InMemoryEsSync {
366 documents: Arc<RwLock<HashMap<String, HashMap<String, EsDocument>>>>,
367}
368
369pub type MockEsSync = InMemoryEsSync;
373
374impl InMemoryEsSync {
375 pub fn new() -> Self {
376 Self {
377 documents: Arc::new(RwLock::new(HashMap::new())),
378 }
379 }
380
381 pub fn count(&self, index: &str) -> Result<usize, EsError> {
382 let docs = self
383 .documents
384 .read()
385 .map_err(|e| EsError::SyncError(format!("lock error: {}", e)))?;
386 Ok(docs.get(index).map(|m| m.len()).unwrap_or(0))
387 }
388
389 fn generate_id(doc: &EsDocument, counter: usize) -> String {
390 doc.id
391 .clone()
392 .unwrap_or_else(|| format!("auto-{}-{}", doc.timestamp, counter))
393 }
394
395 fn matches_query(doc: &EsDocument, query: &EsQuery) -> bool {
396 match query {
397 EsQuery::MatchAll => true,
398 EsQuery::Term(terms) => {
399 if let Some(obj) = doc.source.as_object() {
400 terms.iter().all(|(k, v)| obj.get(k) == Some(v))
401 } else {
402 false
403 }
404 }
405 EsQuery::Terms(terms) => {
406 let Some(obj) = doc.source.as_object() else {
407 return false;
408 };
409 terms.iter().all(|(k, values)| {
410 if let Some(v) = obj.get(k) {
411 values.contains(v)
412 } else {
413 false
414 }
415 })
416 }
417 EsQuery::Range(ranges) => {
418 let Some(obj) = doc.source.as_object() else {
419 return false;
420 };
421 ranges.iter().all(|(field, range)| match obj.get(field) {
422 Some(value) => range_match(value, range),
423 None => false,
424 })
425 }
426 EsQuery::Bool(b) => bool_match(doc, b),
427 }
428 }
429}
430
431impl Default for InMemoryEsSync {
432 fn default() -> Self {
433 Self::new()
434 }
435}
436
437#[allow(clippy::neg_cmp_op_on_partial_ord)]
438fn range_match(value: &serde_json::Value, range: &EsRangeQuery) -> bool {
439 match (value.as_f64(), value.as_str()) {
440 (Some(num), _) => {
441 if let Some(gt) = range.gt.as_ref().and_then(|v| v.as_f64()) {
442 if !(num > gt) {
443 return false;
444 }
445 }
446 if let Some(gte) = range.gte.as_ref().and_then(|v| v.as_f64()) {
447 if !(num >= gte) {
448 return false;
449 }
450 }
451 if let Some(lt) = range.lt.as_ref().and_then(|v| v.as_f64()) {
452 if !(num < lt) {
453 return false;
454 }
455 }
456 if let Some(lte) = range.lte.as_ref().and_then(|v| v.as_f64()) {
457 if !(num <= lte) {
458 return false;
459 }
460 }
461 true
462 }
463 (_, Some(s)) => {
464 if let Some(gt) = range.gt.as_ref().and_then(|v| v.as_str()) {
465 if s <= gt {
466 return false;
467 }
468 }
469 if let Some(gte) = range.gte.as_ref().and_then(|v| v.as_str()) {
470 if s < gte {
471 return false;
472 }
473 }
474 if let Some(lt) = range.lt.as_ref().and_then(|v| v.as_str()) {
475 if s >= lt {
476 return false;
477 }
478 }
479 if let Some(lte) = range.lte.as_ref().and_then(|v| v.as_str()) {
480 if s > lte {
481 return false;
482 }
483 }
484 true
485 }
486 _ => false,
487 }
488}
489
490fn bool_match(doc: &EsDocument, b: &EsBoolQuery) -> bool {
491 if let Some(must) = &b.must {
492 if !must.iter().all(|q| InMemoryEsSync::matches_query(doc, q)) {
493 return false;
494 }
495 }
496 if let Some(must_not) = &b.must_not {
497 if must_not
498 .iter()
499 .any(|q| InMemoryEsSync::matches_query(doc, q))
500 {
501 return false;
502 }
503 }
504 if let Some(filter) = &b.filter {
505 if !filter.iter().all(|q| InMemoryEsSync::matches_query(doc, q)) {
506 return false;
507 }
508 }
509 if let Some(should) = &b.should {
510 let matched = should
511 .iter()
512 .filter(|q| InMemoryEsSync::matches_query(doc, q))
513 .count();
514 let min_match = b.minimum_should_match.unwrap_or(1);
515 if matched < min_match {
516 return false;
517 }
518 }
519 true
520}
521
522impl EsSync for InMemoryEsSync {
523 fn sync_to_es(&self, documents: Vec<EsDocument>) -> Result<EsSyncResult, EsError> {
524 let mut store = self
525 .documents
526 .write()
527 .map_err(|e| EsError::SyncError(format!("lock error: {}", e)))?;
528 let mut indexed = 0usize;
529 let mut errors: Vec<String> = Vec::new();
530 for (i, doc) in documents.into_iter().enumerate() {
531 if doc.index.is_empty() {
532 errors.push(format!("document {} has empty index", i));
533 continue;
534 }
535 let id = Self::generate_id(&doc, i);
536 let map = store.entry(doc.index.clone()).or_default();
537 map.insert(id, doc);
538 indexed += 1;
539 }
540 if errors.is_empty() {
541 Ok(EsSyncResult::success(indexed))
542 } else {
543 Ok(EsSyncResult::with_errors(indexed, errors))
544 }
545 }
546
547 fn delete_from_es(&self, index: &str, ids: Vec<String>) -> Result<EsSyncResult, EsError> {
548 let mut store = self
549 .documents
550 .write()
551 .map_err(|e| EsError::SyncError(format!("lock error: {}", e)))?;
552 let map = store
553 .get_mut(index)
554 .ok_or_else(|| EsError::IndexNotFound(index.to_string()))?;
555 let mut deleted = 0usize;
556 let mut errors: Vec<String> = Vec::new();
557 for id in ids {
558 if map.remove(&id).is_some() {
559 deleted += 1;
560 } else {
561 errors.push(format!("document not found: {}", id));
562 }
563 }
564 if errors.is_empty() {
565 Ok(EsSyncResult::success(deleted))
566 } else {
567 Ok(EsSyncResult::with_errors(deleted, errors))
568 }
569 }
570
571 fn search(&self, request: EsSearchRequest) -> Result<EsSearchResult, EsError> {
572 let store = self
573 .documents
574 .read()
575 .map_err(|e| EsError::SyncError(format!("lock error: {}", e)))?;
576 let Some(map) = store.get(&request.index) else {
577 return Err(EsError::IndexNotFound(request.index.clone()));
578 };
579
580 let start = std::time::Instant::now();
581 let mut hits: Vec<(String, EsDocument)> = map
582 .iter()
583 .filter(|(_, doc)| Self::matches_query(doc, &request.query))
584 .map(|(id, doc)| (id.clone(), doc.clone()))
585 .collect();
586
587 for sort in request.sort.iter().rev() {
589 let field = &sort.field;
590 let asc = matches!(sort.order, EsSortOrder::Asc);
591 hits.sort_by(|a, b| {
592 let av =
593 a.1.source
594 .get(field)
595 .cloned()
596 .unwrap_or(serde_json::Value::Null);
597 let bv =
598 b.1.source
599 .get(field)
600 .cloned()
601 .unwrap_or(serde_json::Value::Null);
602 let ord = compare_values(&av, &bv);
603 if asc {
604 ord
605 } else {
606 ord.reverse()
607 }
608 });
609 }
610
611 let total = hits.len();
612 let paged = hits
613 .into_iter()
614 .skip(request.from)
615 .take(request.size)
616 .map(|(id, doc)| EsHit {
617 id,
618 score: 1.0,
619 source: doc.source,
620 })
621 .collect::<Vec<_>>();
622
623 Ok(EsSearchResult {
624 total,
625 hits: paged,
626 took: start.elapsed().as_millis() as i64,
627 })
628 }
629}
630
631fn compare_values(a: &serde_json::Value, b: &serde_json::Value) -> std::cmp::Ordering {
632 use std::cmp::Ordering;
633 match (a.as_f64(), b.as_f64()) {
634 (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
635 _ => a.to_string().cmp(&b.to_string()),
636 }
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub enum EsFieldType {
641 Text,
642 Keyword,
643 Integer,
644 Long,
645 Float,
646 Double,
647 Boolean,
648 Date,
649 Object,
650 Nested,
651 Ip,
652 GeoPoint,
653}
654
655#[derive(Debug)]
656pub enum EsError {
657 ConnectionFailed(String),
658 IndexNotFound(String),
659 DocumentNotFound(String),
660 MappingError(String),
661 QueryError(String),
662 SyncError(String),
663 BackendNotSupported {
665 operation: String,
666 reason: String,
667 },
668}
669
670impl std::fmt::Display for EsError {
671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672 match self {
673 EsError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
674 EsError::IndexNotFound(idx) => write!(f, "Index not found: {}", idx),
675 EsError::DocumentNotFound(id) => write!(f, "Document not found: {}", id),
676 EsError::MappingError(msg) => write!(f, "Mapping error: {}", msg),
677 EsError::QueryError(msg) => write!(f, "Query error: {}", msg),
678 EsError::SyncError(msg) => write!(f, "Sync error: {}", msg),
679 EsError::BackendNotSupported { operation, reason } => {
680 write!(f, "Backend not supported: {} ({})", operation, reason)
681 }
682 }
683 }
684}
685
686impl std::error::Error for EsError {}
687
688impl serde::Serialize for EsError {
689 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
690 where
691 S: serde::Serializer,
692 {
693 serializer.serialize_str(&self.to_string())
694 }
695}
696
697fn current_timestamp() -> i64 {
698 use std::time::{SystemTime, UNIX_EPOCH};
699 SystemTime::now()
700 .duration_since(UNIX_EPOCH)
701 .unwrap_or_default()
702 .as_millis() as i64
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
710 fn test_es_document_new() {
711 let doc = EsDocument::new("test-index", serde_json::json!({"name": "test"}));
712 assert_eq!(doc.index, "test-index");
713 assert_eq!(doc.source["name"], "test");
714 assert!(doc.id.is_none());
715 }
716
717 #[test]
718 fn test_es_document_with_id() {
719 let doc = EsDocument::new("test-index", serde_json::json!({})).with_id("doc1");
720 assert_eq!(doc.id, Some("doc1".to_string()));
721 }
722
723 #[test]
724 fn test_es_search_request_new() {
725 let query = EsQuery::match_all();
726 let request = EsSearchRequest::new("test-index", query);
727 assert_eq!(request.index, "test-index");
728 assert_eq!(request.from, 0);
729 assert_eq!(request.size, 10);
730 }
731
732 #[test]
733 fn test_es_search_request_pagination() {
734 let query = EsQuery::match_all();
735 let request = EsSearchRequest::new("test", query).with_pagination(20, 50);
736 assert_eq!(request.from, 20);
737 assert_eq!(request.size, 50);
738 }
739
740 #[test]
741 fn test_es_search_request_sort() {
742 let query = EsQuery::match_all();
743 let request = EsSearchRequest::new("test", query).with_sort("date", EsSortOrder::Desc);
744 assert_eq!(request.sort.len(), 1);
745 assert_eq!(request.sort[0].field, "date");
746 assert_eq!(request.sort[0].order, EsSortOrder::Desc);
747 }
748
749 #[test]
750 fn test_es_query_match_all() {
751 let query = EsQuery::match_all();
752 assert_eq!(query, EsQuery::MatchAll);
753 }
754
755 #[test]
756 fn test_es_query_term() {
757 let query = EsQuery::term("status", serde_json::json!("active"));
758 assert_eq!(
759 query,
760 EsQuery::Term(std::collections::HashMap::from([(
761 "status".to_string(),
762 serde_json::json!("active")
763 )]))
764 );
765 }
766
767 #[test]
768 fn test_es_query_terms() {
769 let query = EsQuery::terms("tags", vec![serde_json::json!("a"), serde_json::json!("b")]);
770 assert!(matches!(query, EsQuery::Terms(_)));
771 }
772
773 #[test]
774 fn test_es_query_range() {
775 let range = EsRangeQuery::new().gte(serde_json::json!(100));
776 let query = EsQuery::range("price", range);
777 assert!(matches!(query, EsQuery::Range(_)));
778 }
779
780 #[test]
781 fn test_es_query_bool_must() {
782 let queries = vec![
783 EsQuery::term("status", serde_json::json!("active")),
784 EsQuery::term("type", serde_json::json!("post")),
785 ];
786 let query = EsQuery::must(queries);
787 assert!(matches!(query, EsQuery::Bool(_)));
788 }
789
790 #[test]
791 fn test_es_range_query() {
792 let range = EsRangeQuery::new()
793 .gte(serde_json::json!(0))
794 .lt(serde_json::json!(100));
795
796 assert!(range.gte.is_some());
797 assert!(range.lt.is_some());
798 }
799
800 #[test]
801 fn test_es_sync_result_success() {
802 let result = EsSyncResult::success(10);
803 assert_eq!(result.indexed, 10);
804 assert_eq!(result.failed, 0);
805 assert!(result.errors.is_empty());
806 }
807
808 #[test]
809 fn test_es_sync_result_with_errors() {
810 let errors = vec!["error1".to_string(), "error2".to_string()];
811 let result = EsSyncResult::with_errors(8, errors.clone());
812 assert_eq!(result.indexed, 8);
813 assert_eq!(result.failed, 2);
814 assert_eq!(result.errors, errors);
815 }
816
817 #[test]
818 fn test_es_sync_manager_new() {
819 let manager = EsSyncManager::new();
820 assert!(manager.index_mappings.is_empty());
821 }
822
823 #[test]
824 fn test_es_sync_manager_create_index() {
825 let mut manager = EsSyncManager::new();
826 let mut mapping = HashMap::new();
827 mapping.insert("title".to_string(), EsFieldType::Text);
828 mapping.insert("count".to_string(), EsFieldType::Integer);
829
830 manager.create_index("test-index", mapping);
831
832 let retrieved = manager.get_mapping("test-index");
833 assert!(retrieved.is_some());
834 assert_eq!(retrieved.unwrap().len(), 2);
835 }
836
837 #[test]
838 fn test_es_sync_manager_get_mapping_not_found() {
839 let manager = EsSyncManager::new();
840 let mapping = manager.get_mapping("nonexistent");
841 assert!(mapping.is_none());
842 }
843
844 #[test]
845 fn test_inmemory_sync_and_count() {
846 let sync = InMemoryEsSync::new();
847 let docs = vec![
848 EsDocument::new("users", serde_json::json!({"name": "alice"})).with_id("u1"),
849 EsDocument::new("users", serde_json::json!({"name": "bob"})).with_id("u2"),
850 ];
851 let result = sync.sync_to_es(docs).unwrap();
852 assert_eq!(result.indexed, 2);
853 assert_eq!(sync.count("users").unwrap(), 2);
854 }
855
856 #[test]
857 fn test_inmemory_sync_replaces_by_id() {
858 let sync = InMemoryEsSync::new();
859 sync.sync_to_es(vec![
860 EsDocument::new("idx", serde_json::json!({"v": 1})).with_id("d1")
861 ])
862 .unwrap();
863 sync.sync_to_es(vec![
864 EsDocument::new("idx", serde_json::json!({"v": 2})).with_id("d1")
865 ])
866 .unwrap();
867 assert_eq!(sync.count("idx").unwrap(), 1);
868
869 let req = EsSearchRequest::new("idx", EsQuery::match_all());
870 let res = sync.search(req).unwrap();
871 assert_eq!(res.hits[0].source["v"], 2);
872 }
873
874 #[test]
875 fn test_inmemory_sync_rejects_empty_index() {
876 let sync = InMemoryEsSync::new();
877 let result = sync
878 .sync_to_es(vec![EsDocument::new("", serde_json::json!({}))])
879 .unwrap();
880 assert_eq!(result.indexed, 0);
881 assert_eq!(result.failed, 1);
882 }
883
884 #[test]
885 fn test_inmemory_delete_real() {
886 let sync = InMemoryEsSync::new();
887 sync.sync_to_es(vec![
888 EsDocument::new("i", serde_json::json!({"k": "a"})).with_id("1"),
889 EsDocument::new("i", serde_json::json!({"k": "b"})).with_id("2"),
890 ])
891 .unwrap();
892
893 let result = sync.delete_from_es("i", vec!["1".to_string()]).unwrap();
894 assert_eq!(result.indexed, 1);
895 assert_eq!(sync.count("i").unwrap(), 1);
896
897 let err = sync.delete_from_es("missing", vec!["x".to_string()]);
898 assert!(matches!(err, Err(EsError::IndexNotFound(_))));
899 }
900
901 #[test]
902 fn test_inmemory_delete_missing_records_error() {
903 let sync = InMemoryEsSync::new();
904 sync.sync_to_es(vec![
905 EsDocument::new("i", serde_json::json!({"k": "a"})).with_id("1")
906 ])
907 .unwrap();
908 let r = sync
909 .delete_from_es("i", vec!["1".to_string(), "999".to_string()])
910 .unwrap();
911 assert_eq!(r.indexed, 1);
912 assert_eq!(r.failed, 1);
913 }
914
915 #[test]
916 fn test_inmemory_search_match_all() {
917 let sync = InMemoryEsSync::new();
918 sync.sync_to_es(vec![
919 EsDocument::new("i", serde_json::json!({"n": 1})).with_id("1"),
920 EsDocument::new("i", serde_json::json!({"n": 2})).with_id("2"),
921 ])
922 .unwrap();
923 let req = EsSearchRequest::new("i", EsQuery::match_all());
924 let res = sync.search(req).unwrap();
925 assert_eq!(res.total, 2);
926 assert_eq!(res.hits.len(), 2);
927 }
928
929 #[test]
930 fn test_inmemory_search_term_filter() {
931 let sync = InMemoryEsSync::new();
932 sync.sync_to_es(vec![
933 EsDocument::new("i", serde_json::json!({"status": "active"})).with_id("1"),
934 EsDocument::new("i", serde_json::json!({"status": "inactive"})).with_id("2"),
935 ])
936 .unwrap();
937 let req = EsSearchRequest::new("i", EsQuery::term("status", serde_json::json!("active")));
938 let res = sync.search(req).unwrap();
939 assert_eq!(res.total, 1);
940 assert_eq!(res.hits[0].source["status"], "active");
941 }
942
943 #[test]
944 fn test_inmemory_search_terms_filter() {
945 let sync = InMemoryEsSync::new();
946 sync.sync_to_es(vec![
947 EsDocument::new("i", serde_json::json!({"tag": "a"})).with_id("1"),
948 EsDocument::new("i", serde_json::json!({"tag": "b"})).with_id("2"),
949 EsDocument::new("i", serde_json::json!({"tag": "c"})).with_id("3"),
950 ])
951 .unwrap();
952 let req = EsSearchRequest::new(
953 "i",
954 EsQuery::terms("tag", vec![serde_json::json!("a"), serde_json::json!("c")]),
955 );
956 let res = sync.search(req).unwrap();
957 assert_eq!(res.total, 2);
958 }
959
960 #[test]
961 fn test_inmemory_search_range_filter() {
962 let sync = InMemoryEsSync::new();
963 sync.sync_to_es(vec![
964 EsDocument::new("i", serde_json::json!({"age": 10})).with_id("1"),
965 EsDocument::new("i", serde_json::json!({"age": 20})).with_id("2"),
966 EsDocument::new("i", serde_json::json!({"age": 30})).with_id("3"),
967 ])
968 .unwrap();
969 let req = EsSearchRequest::new(
970 "i",
971 EsQuery::range("age", EsRangeQuery::new().gte(serde_json::json!(20))),
972 );
973 let res = sync.search(req).unwrap();
974 assert_eq!(res.total, 2);
975 }
976
977 #[test]
978 fn test_inmemory_search_bool_query() {
979 let sync = InMemoryEsSync::new();
980 sync.sync_to_es(vec![
981 EsDocument::new("i", serde_json::json!({"status": "active", "n": 5})).with_id("1"),
982 EsDocument::new("i", serde_json::json!({"status": "active", "n": 50})).with_id("2"),
983 EsDocument::new("i", serde_json::json!({"status": "inactive", "n": 5})).with_id("3"),
984 ])
985 .unwrap();
986 let bool = EsQuery::must(vec![
988 EsQuery::term("status", serde_json::json!("active")),
989 EsQuery::range("n", EsRangeQuery::new().gte(serde_json::json!(10))),
990 ]);
991 let req = EsSearchRequest::new("i", bool);
992 let res = sync.search(req).unwrap();
993 assert_eq!(res.total, 1);
994 assert_eq!(res.hits[0].source["n"], 50);
995 }
996
997 #[test]
998 fn test_inmemory_search_pagination_and_sort() {
999 let sync = InMemoryEsSync::new();
1000 let docs: Vec<EsDocument> = (1..=5)
1001 .map(|i| EsDocument::new("i", serde_json::json!({"v": i})).with_id(format!("d{}", i)))
1002 .collect();
1003 sync.sync_to_es(docs).unwrap();
1004
1005 let req = EsSearchRequest::new("i", EsQuery::match_all())
1006 .with_pagination(0, 2)
1007 .with_sort("v", EsSortOrder::Desc);
1008 let res = sync.search(req).unwrap();
1009 assert_eq!(res.total, 5);
1010 assert_eq!(res.hits.len(), 2);
1011 assert_eq!(res.hits[0].source["v"], 5);
1013 assert_eq!(res.hits[1].source["v"], 4);
1014
1015 let req2 = EsSearchRequest::new("i", EsQuery::match_all())
1017 .with_pagination(1, 2)
1018 .with_sort("v", EsSortOrder::Desc);
1019 let res2 = sync.search(req2).unwrap();
1020 assert_eq!(res2.hits.len(), 2);
1021 assert_eq!(res2.hits[0].source["v"], 4);
1022 assert_eq!(res2.hits[1].source["v"], 3);
1023
1024 let req3 = EsSearchRequest::new("i", EsQuery::match_all())
1026 .with_pagination(0, 2)
1027 .with_sort("v", EsSortOrder::Asc);
1028 let res3 = sync.search(req3).unwrap();
1029 assert_eq!(res3.hits[0].source["v"], 1);
1030 assert_eq!(res3.hits[1].source["v"], 2);
1031 }
1032
1033 #[test]
1034 fn test_inmemory_search_missing_index_errors() {
1035 let sync = InMemoryEsSync::new();
1036 let req = EsSearchRequest::new("missing", EsQuery::match_all());
1037 let err = sync.search(req);
1038 assert!(matches!(err, Err(EsError::IndexNotFound(_))));
1039 }
1040
1041 #[test]
1042 fn test_sync_manager_delegates_to_backend() {
1043 let mut manager = EsSyncManager::new();
1044 manager.create_index("idx", HashMap::new());
1045
1046 let r = manager
1047 .sync_to_es(vec![
1048 EsDocument::new("idx", serde_json::json!({"k": "v"})).with_id("d1")
1049 ])
1050 .unwrap();
1051 assert_eq!(r.indexed, 1);
1052 assert_eq!(manager.count("idx").unwrap(), 1);
1053
1054 let req = EsSearchRequest::new("idx", EsQuery::match_all());
1055 let res = manager.search(req).unwrap();
1056 assert_eq!(res.total, 1);
1057
1058 let r = manager
1059 .delete_from_es("idx", vec!["d1".to_string()])
1060 .unwrap();
1061 assert_eq!(r.indexed, 1);
1062 assert_eq!(manager.count("idx").unwrap(), 0);
1063 }
1064
1065 #[test]
1066 fn test_es_sync_trait_object_dyn() {
1067 let sync: Box<dyn EsSync> = Box::new(InMemoryEsSync::new());
1069 sync.sync_to_es(vec![
1070 EsDocument::new("i", serde_json::json!({"k": 1})).with_id("1")
1071 ])
1072 .unwrap();
1073 let req = EsSearchRequest::new("i", EsQuery::match_all());
1074 let res = sync.search(req).unwrap();
1075 assert_eq!(res.total, 1);
1076 }
1077
1078 #[test]
1083 fn test_with_backend_count_returns_not_supported() {
1084 struct StubBackend;
1086 impl EsSync for StubBackend {
1087 fn sync_to_es(&self, _documents: Vec<EsDocument>) -> Result<EsSyncResult, EsError> {
1088 Ok(EsSyncResult::success(0))
1089 }
1090 fn delete_from_es(
1091 &self,
1092 _index: &str,
1093 _ids: Vec<String>,
1094 ) -> Result<EsSyncResult, EsError> {
1095 Ok(EsSyncResult::success(0))
1096 }
1097 fn search(&self, _request: EsSearchRequest) -> Result<EsSearchResult, EsError> {
1098 Ok(EsSearchResult {
1099 total: 0,
1100 hits: Vec::new(),
1101 took: 0,
1102 })
1103 }
1104 }
1105
1106 let manager = EsSyncManager::with_backend(Box::new(StubBackend));
1107 let result = manager.count("any_index");
1108 assert!(
1109 matches!(result, Err(EsError::BackendNotSupported { .. })),
1110 "with_backend 注入自定义后端后 count 应返回 BackendNotSupported,实际: {:?}",
1111 result
1112 );
1113
1114 let sync_result = manager
1116 .sync_to_es(vec![EsDocument::new("i", serde_json::json!({}))])
1117 .unwrap();
1118 assert_eq!(sync_result.indexed, 0);
1119 }
1120
1121 #[test]
1123 fn test_default_new_count_works_with_mock() {
1124 let manager = EsSyncManager::new();
1125 let count = manager.count("empty_idx").unwrap();
1127 assert_eq!(count, 0);
1128 }
1129
1130 #[test]
1132 fn test_mock_es_sync_alias_equivalence() {
1133 let mock: MockEsSync = MockEsSync::new();
1134 let _: InMemoryEsSync = mock; let mock2 = InMemoryEsSync::new();
1136 let _clone: InMemoryEsSync = mock2.clone(); }
1138}