1pub mod builder;
17
18use std::{collections::HashSet, fmt};
19
20use ruma::{
21 EventId, OwnedEventId, OwnedRoomId, RoomId, events::room::message::OriginalSyncRoomMessageEvent,
22};
23use tantivy::{
24 Index, IndexReader, TantivyDocument, collector::TopDocs, directory::error::OpenDirectoryError,
25 query::QueryParser, schema::Value,
26};
27use tracing::{debug, error, warn};
28
29use crate::{
30 OpStamp, TANTIVY_INDEX_MEMORY_BUDGET,
31 error::IndexError,
32 schema::{MatrixSearchIndexSchema, RoomMessageSchema},
33 writer::SearchIndexWriter,
34};
35
36#[derive(Debug, Clone)]
38pub enum RoomIndexOperation {
39 Add(OriginalSyncRoomMessageEvent),
41 Remove(OwnedEventId),
44 Edit(OwnedEventId, OriginalSyncRoomMessageEvent),
48 Noop,
50}
51
52pub struct RoomIndex {
55 index: Index,
56 schema: RoomMessageSchema,
57 query_parser: QueryParser,
58 room_id: OwnedRoomId,
59 uncommitted_adds: HashSet<OwnedEventId>,
60 uncommitted_removes: HashSet<OwnedEventId>,
61 reader: Option<IndexReader>,
62}
63
64impl fmt::Debug for RoomIndex {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("RoomIndex")
67 .field("schema", &self.schema)
68 .field("room_id", &self.room_id)
69 .finish()
70 }
71}
72
73impl RoomIndex {
74 pub(crate) fn new_with(index: Index, schema: RoomMessageSchema, room_id: &RoomId) -> RoomIndex {
75 let query_parser = QueryParser::for_index(&index, schema.default_search_fields());
76 Self {
77 index,
78 schema,
79 query_parser,
80 room_id: room_id.to_owned(),
81 uncommitted_adds: HashSet::new(),
82 uncommitted_removes: HashSet::new(),
83 reader: None,
84 }
85 }
86
87 fn get_writer(&self) -> Result<SearchIndexWriter, IndexError> {
89 let writer = self.index.writer(TANTIVY_INDEX_MEMORY_BUDGET)?;
90 Ok(SearchIndexWriter::new(writer, self.schema.clone()))
91 }
92
93 fn get_reader(&mut self) -> Result<&IndexReader, IndexError> {
95 Ok(match self.reader {
96 Some(ref reader) => reader,
97 None => {
98 let reader = self.index.reader_builder().try_into()?;
99 self.reader.insert(reader)
100 }
101 })
102 }
103
104 fn commit(&mut self, writer: &mut SearchIndexWriter) -> Result<OpStamp, IndexError> {
109 let last_commit_opstamp = writer.commit()?; self.uncommitted_adds.clear();
111 self.uncommitted_removes.clear();
112 Ok(last_commit_opstamp)
113 }
114
115 fn commit_and_reload(&mut self, writer: &mut SearchIndexWriter) -> Result<OpStamp, IndexError> {
126 debug!(
127 "RoomIndex: committing and reloading: uncommitted: {:?}, {:?}",
128 self.uncommitted_adds, self.uncommitted_removes
129 );
130 let last_commit_opstamp = self.commit(writer)?;
131 self.get_reader()?.reload()?;
132 Ok(last_commit_opstamp)
133 }
134
135 pub fn search(
143 &mut self,
144 query: &str,
145 max_number_of_results: usize,
146 pagination_offset: Option<usize>,
147 ) -> Result<Vec<(f32, OwnedEventId)>, IndexError> {
148 let query = self.query_parser.parse_query(query)?;
149 let searcher = self.get_reader()?.searcher();
150
151 let offset = pagination_offset.unwrap_or(0);
152
153 let results = searcher.search(
154 &query,
155 &TopDocs::with_limit(max_number_of_results).and_offset(offset).order_by_score(),
156 )?;
157 let mut ret: Vec<(f32, OwnedEventId)> = Vec::new();
158 let pk = self.schema.primary_key();
159
160 for (score, doc_address) in results {
161 let retrieved_doc: TantivyDocument = searcher.doc(doc_address)?;
162 match retrieved_doc.get_first(pk).and_then(|maybe_value| maybe_value.as_str()) {
163 Some(value) => match OwnedEventId::try_from(value) {
164 Ok(event_id) => ret.push((score, event_id)),
165 Err(err) => error!("error while parsing event_id from search result: {err:?}"),
166 },
167 _ => error!("unexpected value type while searching documents"),
168 }
169 }
170
171 Ok(ret)
172 }
173
174 fn get_events_to_be_removed(
175 &mut self,
176 event_id: &EventId,
177 ) -> Result<Vec<OwnedEventId>, IndexError> {
178 Ok(self
179 .search(
180 format!(
181 "{}:\"{event_id}\"",
182 self.schema.get_field_name(self.schema.deletion_key())
183 )
184 .as_str(),
185 10000,
186 None,
187 )?
188 .into_iter()
189 .map(|(_, id)| id)
190 .collect())
191 }
192
193 fn add(
194 &mut self,
195 writer: &mut SearchIndexWriter,
196 event: OriginalSyncRoomMessageEvent,
197 ) -> Result<(), IndexError> {
198 if !self.contains(&event.event_id) {
199 writer.add(self.schema.make_doc(event.clone())?)?;
200 }
201 self.uncommitted_removes.remove(&event.event_id);
202 self.uncommitted_adds.insert(event.event_id);
203 Ok(())
204 }
205
206 fn remove(
207 &mut self,
208 writer: &mut SearchIndexWriter,
209 event_id: OwnedEventId,
210 ) -> Result<(), IndexError> {
211 let events = self.get_events_to_be_removed(&event_id)?;
212
213 writer.remove(&event_id);
214
215 for event in events.into_iter() {
216 self.uncommitted_adds.remove(&event);
217 self.uncommitted_removes.insert(event);
218 }
219
220 Ok(())
221 }
222
223 fn execute_impl(
224 &mut self,
225 writer: &mut SearchIndexWriter,
226 operation: &RoomIndexOperation,
227 ) -> Result<(), IndexError> {
228 debug!("INDEX: executing {operation:?}");
229 match operation.clone() {
230 RoomIndexOperation::Add(event) => {
231 self.add(writer, event)?;
232 }
233 RoomIndexOperation::Remove(event_id) => {
234 self.remove(writer, event_id)?;
235 }
236 RoomIndexOperation::Edit(remove_event_id, event) => {
237 self.remove(writer, remove_event_id)?;
238 self.add(writer, event)?;
239 }
240 RoomIndexOperation::Noop => {}
241 }
242 Ok(())
243 }
244
245 fn execute_with_retry(
247 &mut self,
248 writer: &mut SearchIndexWriter,
249 operation: &RoomIndexOperation,
250 retries: usize,
251 ) -> Result<(), IndexError> {
252 let mut num_tries = 0;
253
254 while let Err(err) = self.execute_impl(writer, operation) {
255 if num_tries == retries {
256 return Err(err);
257 }
258 match err {
259 IndexError::TantivyError(_)
261 | IndexError::IndexSchemaError(_)
262 | IndexError::IndexWriteError(_)
263 | IndexError::IO(_) => {
264 num_tries += 1;
265 }
266 IndexError::OpenDirectoryError(ref e) => match e {
267 OpenDirectoryError::IoError { io_error: _, directory_path: _ } => {
269 num_tries += 1;
270 }
271 OpenDirectoryError::DoesNotExist(_)
273 | OpenDirectoryError::FailedToCreateTempDir(_)
274 | OpenDirectoryError::NotADirectory(_) => return Err(err),
275 },
276 IndexError::QueryParserError(_) => return Err(err),
278 IndexError::CannotIndexRedactedMessage
280 | IndexError::EmptyMessage
281 | IndexError::MessageTypeNotSupported => break,
282 }
283 debug!("Failed to execute operation in room index (try {num_tries}): {err}");
284 }
285 Ok(())
286 }
287
288 pub fn execute(&mut self, operation: RoomIndexOperation) -> Result<(), IndexError> {
297 let mut writer = self.get_writer()?;
298 self.execute_with_retry(&mut writer, &operation, 5)?;
299 self.commit_and_reload(&mut writer)?;
300 Ok(())
301 }
302
303 pub fn bulk_execute(&mut self, operations: Vec<RoomIndexOperation>) -> Result<(), IndexError> {
310 let mut writer = self.get_writer()?;
311 let mut operations = operations.into_iter();
312 let mut next_operation = operations.next();
313
314 while let Some(ref operation) = next_operation {
315 self.execute_with_retry(&mut writer, operation, 5)?;
316 next_operation = operations.next();
317 }
318
319 self.commit_and_reload(&mut writer)?;
320
321 Ok(())
322 }
323
324 fn contains(&mut self, event_id: &EventId) -> bool {
325 let search_result = self.search(
326 format!("{}:\"{event_id}\"", self.schema.get_field_name(self.schema.primary_key()))
327 .as_str(),
328 1,
329 None,
330 );
331 match search_result {
332 Ok(results) => {
333 !self.uncommitted_removes.contains(event_id)
334 && (!results.is_empty() || self.uncommitted_adds.contains(event_id))
335 }
336 Err(err) => {
337 warn!("Failed to check if event has been indexed, assuming it has: {err}");
338 true
339 }
340 }
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use std::{collections::HashSet, error::Error};
347
348 use matrix_sdk_test::event_factory::EventFactory;
349 use ruma::{
350 EventId, event_id,
351 events::{
352 AnySyncMessageLikeEvent,
353 room::message::{OriginalSyncRoomMessageEvent, RoomMessageEventContentWithoutRelation},
354 },
355 room_id, user_id,
356 };
357
358 use crate::{
359 error::IndexError,
360 index::{RoomIndex, RoomIndexOperation, builder::RoomIndexBuilder},
361 };
362
363 fn index_message(
369 index: &mut RoomIndex,
370 event: AnySyncMessageLikeEvent,
371 ) -> Result<(), IndexError> {
372 if let AnySyncMessageLikeEvent::RoomMessage(ev) = event
373 && let Some(ev) = ev.as_original()
374 && ev.content.relates_to.is_none()
375 {
376 return index.execute(RoomIndexOperation::Add(ev.clone()));
377 }
378 panic!("Event was not a relationless OriginalSyncRoomMessageEvent.")
379 }
380
381 fn index_remove(index: &mut RoomIndex, event_id: &EventId) -> Result<(), IndexError> {
383 index.execute(RoomIndexOperation::Remove(event_id.to_owned()))
384 }
385
386 fn index_edit(
390 index: &mut RoomIndex,
391 event_id: &EventId,
392 new: OriginalSyncRoomMessageEvent,
393 ) -> Result<(), IndexError> {
394 index.execute(RoomIndexOperation::Edit(event_id.to_owned(), new))
395 }
396
397 #[test]
398 fn test_add_event() {
399 let room_id = room_id!("!room_id:localhost");
400 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
401
402 let event = EventFactory::new()
403 .text_msg("event message")
404 .event_id(event_id!("$event_id:localhost"))
405 .room(room_id)
406 .sender(user_id!("@user_id:localhost"))
407 .into_any_sync_message_like_event();
408
409 index_message(&mut index, event).expect("failed to add event: {res:?}");
410 }
411
412 #[test]
413 fn test_search_populated_index() -> Result<(), Box<dyn Error>> {
414 let room_id = room_id!("!room_id:localhost");
415 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
416
417 let event_id_1 = event_id!("$event_id_1:localhost");
418 let event_id_2 = event_id!("$event_id_2:localhost");
419 let event_id_3 = event_id!("$event_id_3:localhost");
420 let user_id = user_id!("@user_id:localhost");
421 let f = EventFactory::new().room(room_id).sender(user_id);
422
423 index_message(
424 &mut index,
425 f.text_msg("This is a sentence")
426 .event_id(event_id_1)
427 .into_any_sync_message_like_event(),
428 )?;
429
430 index_message(
431 &mut index,
432 f.text_msg("All new words").event_id(event_id_2).into_any_sync_message_like_event(),
433 )?;
434
435 index_message(
436 &mut index,
437 f.text_msg("A similar sentence")
438 .event_id(event_id_3)
439 .into_any_sync_message_like_event(),
440 )?;
441
442 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
443 let result: HashSet<_> = result.iter().map(|(_, id)| id).collect();
444
445 let true_value = [event_id_1.to_owned(), event_id_3.to_owned()];
446 let true_value: HashSet<_> = true_value.iter().collect();
447
448 assert_eq!(result, true_value, "search result not correct: {result:?}");
449
450 Ok(())
451 }
452
453 #[test]
454 fn test_search_empty_index() -> Result<(), Box<dyn Error>> {
455 let room_id = room_id!("!room_id:localhost");
456 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
457
458 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
459
460 assert!(result.is_empty(), "search result not empty: {result:?}");
461
462 Ok(())
463 }
464
465 #[test]
466 fn test_index_contains_false() {
467 let room_id = room_id!("!room_id:localhost");
468 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
469
470 let event_id = event_id!("$event_id:localhost");
471
472 assert!(!index.contains(event_id), "Index should not contain event");
473 }
474
475 #[test]
476 fn test_index_contains_true() -> Result<(), Box<dyn Error>> {
477 let room_id = room_id!("!room_id:localhost");
478 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
479
480 let event_id = event_id!("$event_id:localhost");
481 let event = EventFactory::new()
482 .text_msg("This is a sentence")
483 .event_id(event_id)
484 .room(room_id)
485 .sender(user_id!("@user_id:localhost"))
486 .into_any_sync_message_like_event();
487
488 index_message(&mut index, event)?;
489
490 assert!(index.contains(event_id), "Index should contain event");
491
492 Ok(())
493 }
494
495 #[test]
496 fn test_index_add_idempotency() -> Result<(), Box<dyn Error>> {
497 let room_id = room_id!("!room_id:localhost");
498 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
499
500 let event_id = event_id!("$event_id:localhost");
501 let event = EventFactory::new()
502 .text_msg("This is a sentence")
503 .event_id(event_id)
504 .room(room_id)
505 .sender(user_id!("@user_id:localhost"))
506 .into_any_sync_message_like_event();
507
508 index_message(&mut index, event.clone())?;
509
510 assert!(index.contains(event_id), "Index should contain event");
511
512 index_message(&mut index, event)?;
514
515 assert!(index.contains(event_id), "Index should still contain event");
516
517 let result = index.search("sentence", 10, None).expect("search failed with: {result:?}");
518
519 assert_eq!(result.len(), 1, "Index should have ignored second indexing");
520
521 Ok(())
522 }
523
524 #[test]
525 fn test_remove_event() -> Result<(), Box<dyn Error>> {
526 let room_id = room_id!("!room_id:localhost");
527 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
528
529 let event_id = event_id!("$event_id:localhost");
530 let user_id = user_id!("@user_id:localhost");
531 let f = EventFactory::new().room(room_id).sender(user_id);
532
533 let event =
534 f.text_msg("This is a sentence").event_id(event_id).into_any_sync_message_like_event();
535
536 index_message(&mut index, event)?;
537
538 assert!(index.contains(event_id), "Index should contain event");
539
540 index_remove(&mut index, event_id)?;
541
542 assert!(!index.contains(event_id), "Index should not contain event");
543
544 Ok(())
545 }
546
547 #[test]
548 fn test_edit_removes_old_and_adds_new_event() -> Result<(), Box<dyn Error>> {
549 let room_id = room_id!("!room_id:localhost");
550 let mut index = RoomIndexBuilder::new_in_memory(room_id).build();
551
552 let old_event_id = event_id!("$old_event_id:localhost");
553 let user_id = user_id!("@user_id:localhost");
554 let f = EventFactory::new().room(room_id).sender(user_id);
555
556 let old_event = f
557 .text_msg("This is a sentence")
558 .event_id(old_event_id)
559 .into_any_sync_message_like_event();
560
561 index_message(&mut index, old_event)?;
562
563 assert!(index.contains(old_event_id), "Index should contain event");
564
565 let new_event_id = event_id!("$new_event_id:localhost");
566 let edit = f
567 .text_msg("This is a brand new sentence!")
568 .edit(
569 old_event_id,
570 RoomMessageEventContentWithoutRelation::text_plain("This is a brand new sentence!"),
571 )
572 .event_id(new_event_id)
573 .into_original_sync_room_message_event();
574
575 index_edit(&mut index, old_event_id, edit)?;
576
577 assert!(!index.contains(old_event_id), "Index should not contain old event");
578 assert!(index.contains(new_event_id), "Index should contain edited event");
579
580 Ok(())
581 }
582}