1pub mod builder;
17
18use std::{
19 collections::{HashMap, HashSet},
20 fmt,
21};
22
23use once_cell::sync::OnceCell;
24use ruma::{
25 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UInt,
26};
27use tantivy::{
28 Index, IndexReader, ReloadPolicy, TantivyDocument, collector::TopDocs,
29 directory::error::OpenDirectoryError, query::QueryParser, schema::Value,
30};
31use tracing::{debug, error, warn};
32
33use crate::{
34 OpStamp, TANTIVY_INDEX_MEMORY_BUDGET,
35 error::IndexError,
36 schema::{MatrixSearchIndexSchema, RoomMessageSchema},
37 writer::SearchIndexWriter,
38};
39
40#[derive(Clone)]
45pub struct IndexableEvent {
46 pub(crate) event_id: OwnedEventId,
48 pub(crate) original_event_id: OwnedEventId,
51 pub(crate) sender: OwnedUserId,
53 pub(crate) timestamp: Option<MilliSecondsSinceUnixEpoch>,
59 pub(crate) body: String,
61}
62
63impl fmt::Debug for IndexableEvent {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("IndexableEvent")
67 .field("event_id", &self.event_id)
68 .field("original_event_id", &self.original_event_id)
69 .field("sender", &self.sender)
70 .field("timestamp", &self.timestamp)
71 .finish_non_exhaustive()
72 }
73}
74
75const MAX_MILLISECONDS: u64 = (i64::MAX / 1_000_000).cast_unsigned();
78
79impl IndexableEvent {
80 pub fn new(
82 event_id: OwnedEventId,
83 original_event_id: OwnedEventId,
84 sender: OwnedUserId,
85 mut timestamp: Option<MilliSecondsSinceUnixEpoch>,
86 body: String,
87 ) -> Self {
88 if let Some(timestamp) = &mut timestamp {
96 *timestamp = MilliSecondsSinceUnixEpoch(
97 timestamp.get().min(UInt::new_saturating(MAX_MILLISECONDS)),
98 );
99 }
100
101 Self { event_id, original_event_id, sender, timestamp, body }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub enum RoomIndexOperation {
108 Add(IndexableEvent),
110 Remove(OwnedEventId),
113 Edit(OwnedEventId, IndexableEvent),
117 Noop,
119}
120
121pub struct RoomIndex {
124 index: Index,
125 schema: RoomMessageSchema,
126 query_parser: QueryParser,
127 room_id: OwnedRoomId,
128 uncommitted_adds: HashMap<OwnedEventId, OwnedEventId>,
133 uncommitted_removes: HashSet<OwnedEventId>,
134 reader: OnceCell<IndexReader>,
139}
140
141impl fmt::Debug for RoomIndex {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.debug_struct("RoomIndex")
144 .field("schema", &self.schema)
145 .field("room_id", &self.room_id)
146 .finish()
147 }
148}
149
150impl RoomIndex {
151 pub(crate) fn new_with(index: Index, schema: RoomMessageSchema, room_id: &RoomId) -> RoomIndex {
152 let query_parser = QueryParser::for_index(&index, schema.default_search_fields());
153 Self {
154 index,
155 schema,
156 query_parser,
157 room_id: room_id.to_owned(),
158 uncommitted_adds: HashMap::new(),
159 uncommitted_removes: HashSet::new(),
160 reader: OnceCell::new(),
161 }
162 }
163
164 fn writer(&self) -> Result<SearchIndexWriter, IndexError> {
166 let writer = self.index.writer(TANTIVY_INDEX_MEMORY_BUDGET)?;
167 Ok(SearchIndexWriter::new(writer, self.schema.clone()))
168 }
169
170 fn reader(&self) -> Result<&IndexReader, IndexError> {
178 self.reader.get_or_try_init(|| {
179 Ok(self.index.reader_builder().reload_policy(ReloadPolicy::Manual).try_into()?)
180 })
181 }
182
183 fn commit(&mut self, writer: &mut SearchIndexWriter) -> Result<OpStamp, IndexError> {
188 let last_commit_opstamp = writer.commit()?; self.uncommitted_adds.clear();
190 self.uncommitted_removes.clear();
191 Ok(last_commit_opstamp)
192 }
193
194 fn commit_and_reload(&mut self, writer: &mut SearchIndexWriter) -> Result<OpStamp, IndexError> {
205 debug!(
206 "RoomIndex: committing and reloading: uncommitted: {:?}, {:?}",
207 self.uncommitted_adds, self.uncommitted_removes
208 );
209 let last_commit_opstamp = self.commit(writer)?;
210 self.reader()?.reload()?;
211 Ok(last_commit_opstamp)
212 }
213
214 pub fn search(
222 &self,
223 query: &str,
224 max_number_of_results: usize,
225 pagination_offset: Option<usize>,
226 ) -> Result<Vec<(f32, OwnedEventId)>, IndexError> {
227 let query = self.query_parser.parse_query(query)?;
228 let searcher = self.reader()?.searcher();
229
230 let offset = pagination_offset.unwrap_or(0);
231
232 let results = searcher.search(
233 &query,
234 &TopDocs::with_limit(max_number_of_results).and_offset(offset).order_by_score(),
235 )?;
236 let mut ret: Vec<(f32, OwnedEventId)> = Vec::new();
237 let pk = self.schema.primary_key();
238
239 for (score, doc_address) in results {
240 let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
241 match retrieved_doc.get_first(pk).and_then(|maybe_value| maybe_value.as_str()) {
242 Some(value) => match OwnedEventId::try_from(value) {
243 Ok(event_id) => ret.push((score, event_id)),
244 Err(err) => error!("error while parsing event_id from search result: {err:?}"),
245 },
246 _ => error!("unexpected value type while searching documents"),
247 }
248 }
249
250 Ok(ret)
251 }
252
253 fn events_to_be_removed(&self, event_id: &EventId) -> Result<Vec<OwnedEventId>, IndexError> {
254 Ok(self
255 .search(
256 format!(
257 "{}:\"{event_id}\"",
258 self.schema.get_field_name(self.schema.deletion_key())
259 )
260 .as_str(),
261 10000,
262 None,
263 )?
264 .into_iter()
265 .map(|(_, id)| id)
266 .collect())
267 }
268
269 fn add(
270 &mut self,
271 writer: &mut SearchIndexWriter,
272 event: IndexableEvent,
273 ) -> Result<(), IndexError> {
274 if !self.contains(&event.event_id) {
275 writer.add(self.schema.make_doc(event.clone())?)?;
276 }
277 self.uncommitted_removes.remove(&event.event_id);
278 self.uncommitted_adds.insert(event.event_id, event.original_event_id);
279 Ok(())
280 }
281
282 fn remove(
283 &mut self,
284 writer: &mut SearchIndexWriter,
285 event_id: OwnedEventId,
286 ) -> Result<(), IndexError> {
287 let events = self.events_to_be_removed(&event_id)?;
288
289 writer.remove(&event_id);
290
291 for event in events.into_iter() {
293 self.uncommitted_adds.remove(&event);
294 self.uncommitted_removes.insert(event);
295 }
296
297 let uncommitted: Vec<_> = self
302 .uncommitted_adds
303 .iter()
304 .filter(|(_, deletion_key)| **deletion_key == event_id)
305 .map(|(primary_key, _)| primary_key.clone())
306 .collect();
307 for event in uncommitted {
308 self.uncommitted_adds.remove(&event);
309 self.uncommitted_removes.insert(event);
310 }
311
312 Ok(())
313 }
314
315 fn execute_impl(
316 &mut self,
317 writer: &mut SearchIndexWriter,
318 operation: &RoomIndexOperation,
319 ) -> Result<(), IndexError> {
320 debug!("INDEX: executing {operation:?}");
321 match operation.clone() {
322 RoomIndexOperation::Add(event) => {
323 self.add(writer, event)?;
324 }
325 RoomIndexOperation::Remove(event_id) => {
326 self.remove(writer, event_id)?;
327 }
328 RoomIndexOperation::Edit(remove_event_id, event) => {
329 self.remove(writer, remove_event_id)?;
330 self.add(writer, event)?;
331 }
332 RoomIndexOperation::Noop => {}
333 }
334 Ok(())
335 }
336
337 fn execute_with_retry(
339 &mut self,
340 writer: &mut SearchIndexWriter,
341 operation: &RoomIndexOperation,
342 retries: usize,
343 ) -> Result<(), IndexError> {
344 let mut num_tries = 0;
345
346 while let Err(err) = self.execute_impl(writer, operation) {
347 if num_tries == retries {
348 return Err(err);
349 }
350 match err {
351 IndexError::TantivyError(_)
353 | IndexError::IndexSchemaError(_)
354 | IndexError::IndexWriteError(_)
355 | IndexError::IO(_) => {
356 num_tries += 1;
357 }
358 IndexError::OpenDirectoryError(ref e) => match e {
359 OpenDirectoryError::IoError { io_error: _, directory_path: _ } => {
361 num_tries += 1;
362 }
363 OpenDirectoryError::DoesNotExist(_)
365 | OpenDirectoryError::FailedToCreateTempDir(_)
366 | OpenDirectoryError::NotADirectory(_) => return Err(err),
367 },
368 IndexError::QueryParserError(_) => return Err(err),
370 IndexError::CannotIndexRedactedMessage
372 | IndexError::EmptyMessage
373 | IndexError::MessageTypeNotSupported => break,
374 }
375 debug!("Failed to execute operation in room index (try {num_tries}): {err}");
376 }
377 Ok(())
378 }
379
380 pub fn execute(&mut self, operation: RoomIndexOperation) -> Result<(), IndexError> {
389 let mut writer = self.writer()?;
390 self.execute_with_retry(&mut writer, &operation, 5)?;
391 self.commit_and_reload(&mut writer)?;
392 Ok(())
393 }
394
395 pub fn bulk_execute(&mut self, operations: Vec<RoomIndexOperation>) -> Result<(), IndexError> {
405 let mut writer = self.writer()?;
406 let mut operations = operations.into_iter();
407 let mut next_operation = operations.next();
408
409 while let Some(ref operation) = next_operation {
410 self.execute_with_retry(&mut writer, operation, 5)?;
411 next_operation = operations.next();
412 }
413
414 self.commit_and_reload(&mut writer)?;
415 writer.wait_merging_threads()?;
416
417 Ok(())
418 }
419
420 fn contains(&self, event_id: &EventId) -> bool {
421 let search_result = self.search(
422 format!("{}:\"{event_id}\"", self.schema.get_field_name(self.schema.primary_key()))
423 .as_str(),
424 1,
425 None,
426 );
427 match search_result {
428 Ok(results) => {
429 !self.uncommitted_removes.contains(event_id)
430 && (!results.is_empty() || self.uncommitted_adds.contains_key(event_id))
431 }
432 Err(err) => {
433 warn!("Failed to check if event has been indexed, assuming it has: {err}");
434 true
435 }
436 }
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use std::{collections::HashSet, error::Error};
443
444 use matrix_sdk_test::event_factory::EventFactory;
445 use ruma::{
446 EventId, event_id,
447 events::{
448 AnySyncMessageLikeEvent,
449 room::message::{
450 MessageType, OriginalSyncRoomMessageEvent, Relation,
451 RoomMessageEventContentWithoutRelation,
452 },
453 },
454 room_id, user_id,
455 };
456
457 use super::{
458 IndexableEvent, MAX_MILLISECONDS, MilliSecondsSinceUnixEpoch, RoomIndex,
459 RoomIndexOperation, UInt, builder::RoomIndexBuilder,
460 };
461 use crate::error::IndexError;
462
463 fn to_indexable(event: &OriginalSyncRoomMessageEvent) -> IndexableEvent {
466 let MessageType::Text(content) = &event.content.msgtype else {
467 panic!("test helper only supports text messages")
468 };
469 let original_event_id = match &event.content.relates_to {
470 Some(Relation::Replacement(replacement)) => replacement.event_id.clone(),
471 _ => event.event_id.clone(),
472 };
473
474 IndexableEvent::new(
475 event.event_id.clone(),
476 original_event_id,
477 event.sender.clone(),
478 Some(event.origin_server_ts),
479 content.body.clone(),
480 )
481 }
482
483 fn index_message(
490 index: &mut RoomIndex,
491 event: AnySyncMessageLikeEvent,
492 ) -> Result<(), IndexError> {
493 if let AnySyncMessageLikeEvent::RoomMessage(ev) = event
494 && let Some(ev) = ev.as_original()
495 && ev.content.relates_to.is_none()
496 {
497 return index.execute(RoomIndexOperation::Add(to_indexable(ev)));
498 }
499 panic!("Event was not a relationless OriginalSyncRoomMessageEvent.")
500 }
501
502 fn index_remove(index: &mut RoomIndex, event_id: &EventId) -> Result<(), IndexError> {
504 index.execute(RoomIndexOperation::Remove(event_id.to_owned()))
505 }
506
507 fn index_edit(
511 index: &mut RoomIndex,
512 event_id: &EventId,
513 new: OriginalSyncRoomMessageEvent,
514 ) -> Result<(), IndexError> {
515 index.execute(RoomIndexOperation::Edit(event_id.to_owned(), to_indexable(&new)))
516 }
517
518 #[test]
519 fn test_add_event() {
520 let room_id = room_id!("!room_id:localhost");
521 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
522
523 let event = EventFactory::new()
524 .text_msg("event message")
525 .event_id(event_id!("$event_id:localhost"))
526 .room(room_id)
527 .sender(user_id!("@user_id:localhost"))
528 .into_any_sync_message_like_event();
529
530 index_message(&mut index, event).expect("failed to add event");
531 }
532
533 #[test]
534 fn test_add_event_with_no_timestamp() {
535 let mut index = RoomIndexBuilder::new_in_memory(room_id!("!r")).build();
536
537 index
538 .execute(RoomIndexOperation::Add(IndexableEvent::new(
539 event_id!("$ev").to_owned(),
540 event_id!("$ev").to_owned(),
541 user_id!("@mnt_io:matrix.org").to_owned(),
542 None,
543 "body".to_owned(),
544 )))
545 .expect("failed to add event");
546 }
547
548 #[test]
549 fn test_add_event_with_raw_malformed_timestamp_must_not_panic() {
550 let mut index = RoomIndexBuilder::new_in_memory(room_id!("!r")).build();
551
552 index
553 .execute(RoomIndexOperation::Add(IndexableEvent::new(
554 event_id!("$ev").to_owned(),
555 event_id!("$ev").to_owned(),
556 user_id!("@mnt_io:matrix.org").to_owned(),
557 Some(MilliSecondsSinceUnixEpoch(UInt::new(151393755000000).unwrap())),
558 "body".to_owned(),
559 )))
560 .expect("failed to add event");
561 }
562
563 #[test]
564 fn test_add_event_with_malformed_timestamp_must_not_panic() {
565 let room_id = room_id!("!r");
566 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
567
568 let body = "body".to_owned();
569 let event = EventFactory::new()
570 .server_ts(151393755000000)
571 .text_msg(body.clone())
572 .event_id(event_id!("$ev"))
573 .room(room_id)
574 .sender(user_id!("@mnt_io:matrix.org"))
575 .into_event();
576
577 index
578 .execute(RoomIndexOperation::Add(IndexableEvent::new(
579 event.event_id().unwrap().to_owned(),
580 event.event_id().unwrap().to_owned(),
581 event.sender().unwrap(),
582 event.timestamp(),
583 body,
584 )))
585 .expect("failed to add event");
586 }
587
588 #[test]
589 fn test_indexable_event_timestamp_is_capped() {
590 let event_id = event_id!("$ev").to_owned();
591 let sender = user_id!("@mnt_io:matrix.org").to_owned();
592 let body = "body".to_owned();
593
594 let event = IndexableEvent::new(
596 event_id.clone(),
597 event_id.clone(),
598 sender.clone(),
599 Some(MilliSecondsSinceUnixEpoch(UInt::new(MAX_MILLISECONDS).unwrap())),
600 body.clone(),
601 );
602 assert_eq!(event.timestamp.unwrap().get(), UInt::new(MAX_MILLISECONDS).unwrap());
603
604 let event = IndexableEvent::new(
606 event_id.clone(),
607 event_id,
608 sender,
609 Some(MilliSecondsSinceUnixEpoch(UInt::new(MAX_MILLISECONDS + 42).unwrap())),
610 body,
611 );
612 assert_eq!(event.timestamp.unwrap().get(), UInt::new(MAX_MILLISECONDS).unwrap());
613 }
614
615 #[test]
616 fn test_search_populated_index() -> Result<(), Box<dyn Error>> {
617 let room_id = room_id!("!room_id:localhost");
618 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
619
620 let event_id_1 = event_id!("$event_id_1:localhost");
621 let event_id_2 = event_id!("$event_id_2:localhost");
622 let event_id_3 = event_id!("$event_id_3:localhost");
623 let user_id = user_id!("@user_id:localhost");
624 let f = EventFactory::new().room(room_id).sender(user_id);
625
626 index_message(
627 &mut index,
628 f.text_msg("This is a sentence")
629 .event_id(event_id_1)
630 .into_any_sync_message_like_event(),
631 )?;
632
633 index_message(
634 &mut index,
635 f.text_msg("All new words").event_id(event_id_2).into_any_sync_message_like_event(),
636 )?;
637
638 index_message(
639 &mut index,
640 f.text_msg("A similar sentence")
641 .event_id(event_id_3)
642 .into_any_sync_message_like_event(),
643 )?;
644
645 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
646 let result: HashSet<_> = result.iter().map(|(_, id)| id).collect();
647
648 let true_value = [event_id_1.to_owned(), event_id_3.to_owned()];
649 let true_value: HashSet<_> = true_value.iter().collect();
650
651 assert_eq!(result, true_value, "search result not correct: {result:?}");
652
653 Ok(())
654 }
655
656 #[test]
657 fn test_search_empty_index() -> Result<(), Box<dyn Error>> {
658 let room_id = room_id!("!room_id:localhost");
659 let index = RoomIndexBuilder::new_in_memory(room_id).build();
660
661 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
662
663 assert!(result.is_empty(), "search result not empty: {result:?}");
664
665 Ok(())
666 }
667
668 #[test]
669 fn test_index_contains_false() {
670 let room_id = room_id!("!room_id:localhost");
671 let index = RoomIndexBuilder::new_in_memory(room_id).build();
672
673 let event_id = event_id!("$event_id:localhost");
674
675 assert!(!index.contains(event_id), "Index should not contain event");
676 }
677
678 #[test]
679 fn test_index_contains_true() -> Result<(), Box<dyn Error>> {
680 let room_id = room_id!("!room_id:localhost");
681 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
682
683 let event_id = event_id!("$event_id:localhost");
684 let event = EventFactory::new()
685 .text_msg("This is a sentence")
686 .event_id(event_id)
687 .room(room_id)
688 .sender(user_id!("@user_id:localhost"))
689 .into_any_sync_message_like_event();
690
691 index_message(&mut index, event)?;
692
693 assert!(index.contains(event_id), "Index should contain event");
694
695 Ok(())
696 }
697
698 #[test]
699 fn test_index_add_idempotency() -> Result<(), Box<dyn Error>> {
700 let room_id = room_id!("!room_id:localhost");
701 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
702
703 let event_id = event_id!("$event_id:localhost");
704 let event = EventFactory::new()
705 .text_msg("This is a sentence")
706 .event_id(event_id)
707 .room(room_id)
708 .sender(user_id!("@user_id:localhost"))
709 .into_any_sync_message_like_event();
710
711 index_message(&mut index, event.clone())?;
712
713 assert!(index.contains(event_id), "Index should contain event");
714
715 index_message(&mut index, event)?;
717
718 assert!(index.contains(event_id), "Index should still contain event");
719
720 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
721
722 assert_eq!(result.len(), 1, "Index should have ignored second indexing");
723
724 Ok(())
725 }
726
727 #[test]
728 fn test_remove_event() -> Result<(), Box<dyn Error>> {
729 let room_id = room_id!("!room_id:localhost");
730 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
731
732 let event_id = event_id!("$event_id:localhost");
733 let user_id = user_id!("@user_id:localhost");
734 let f = EventFactory::new().room(room_id).sender(user_id);
735
736 let event =
737 f.text_msg("This is a sentence").event_id(event_id).into_any_sync_message_like_event();
738
739 index_message(&mut index, event)?;
740
741 assert!(index.contains(event_id), "Index should contain event");
742
743 index_remove(&mut index, event_id)?;
744
745 assert!(!index.contains(event_id), "Index should not contain event");
746
747 Ok(())
748 }
749
750 #[test]
751 fn test_edit_removes_old_and_adds_new_event() -> Result<(), Box<dyn Error>> {
752 let room_id = room_id!("!room_id:localhost");
753 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
754
755 let old_event_id = event_id!("$old_event_id:localhost");
756 let user_id = user_id!("@user_id:localhost");
757 let f = EventFactory::new().room(room_id).sender(user_id);
758
759 let old_event = f
760 .text_msg("This is a sentence")
761 .event_id(old_event_id)
762 .into_any_sync_message_like_event();
763
764 index_message(&mut index, old_event)?;
765
766 assert!(index.contains(old_event_id), "Index should contain event");
767
768 let new_event_id = event_id!("$new_event_id:localhost");
769 let edit = f
770 .text_msg("This is a brand new sentence!")
771 .edit(
772 old_event_id,
773 RoomMessageEventContentWithoutRelation::text_plain("This is a brand new sentence!"),
774 )
775 .event_id(new_event_id)
776 .into_original_sync_room_message_event();
777
778 index_edit(&mut index, old_event_id, edit)?;
779
780 assert!(!index.contains(old_event_id), "Index should not contain old event");
781 assert!(index.contains(new_event_id), "Index should contain edited event");
782
783 Ok(())
784 }
785
786 #[test]
787 fn test_bulk_add_then_edit_same_event_keeps_it_indexed() -> Result<(), Box<dyn Error>> {
788 let room_id = room_id!("!room_id:localhost");
789 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
790
791 let original_id = event_id!("$original:localhost");
792 let edit_id = event_id!("$edit:localhost");
793 let user_id = user_id!("@user_id:localhost");
794 let f = EventFactory::new().room(room_id).sender(user_id);
795
796 let edit = f
797 .text_msg("* brand new sentence")
798 .edit(
799 original_id,
800 RoomMessageEventContentWithoutRelation::text_plain("brand new sentence"),
801 )
802 .event_id(edit_id)
803 .into_original_sync_room_message_event();
804 let edit = to_indexable(&edit);
805
806 index.bulk_execute(vec![
810 RoomIndexOperation::Add(edit.clone()),
811 RoomIndexOperation::Edit(original_id.to_owned(), edit),
812 ])?;
813
814 assert!(index.contains(edit_id), "Edited document should be indexed");
815
816 let result = index.search("sentence", 10, None)?;
817 assert_eq!(result.len(), 1, "Search should find the edited document, got {result:?}");
818 assert_eq!(result[0].1, edit_id, "unexpected event id: {result:?}");
819
820 Ok(())
821 }
822}