matrix_sdk/client/
search.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{collections::hash_map::HashMap, path::PathBuf, sync::Arc};
16
17use matrix_sdk_search::{error::IndexError, index::RoomIndex};
18use ruma::{events::AnySyncMessageLikeEvent, OwnedEventId, OwnedRoomId, RoomId};
19use tokio::sync::{Mutex, MutexGuard};
20use tracing::{debug, error};
21
22/// Type of location to store [`RoomIndex`]
23#[derive(Clone, Debug)]
24pub enum SearchIndexStoreKind {
25    /// Store in file system folder
26    Directory(PathBuf),
27    /// Store in memory
28    InMemory,
29}
30
31/// Object that handles inteeraction with [`RoomIndex`]'s for search
32#[derive(Clone, Debug)]
33pub(crate) struct SearchIndex {
34    /// HashMap that links each joined room to its RoomIndex
35    room_indexes: Arc<Mutex<HashMap<OwnedRoomId, RoomIndex>>>,
36
37    /// Base directory that stores the directories for each RoomIndex
38    search_index_store_kind: SearchIndexStoreKind,
39}
40
41impl SearchIndex {
42    /// Create a new [`SearchIndexHandler`]
43    pub fn new(
44        room_indexes: Arc<Mutex<HashMap<OwnedRoomId, RoomIndex>>>,
45        search_index_store_kind: SearchIndexStoreKind,
46    ) -> Self {
47        Self { room_indexes, search_index_store_kind }
48    }
49
50    pub async fn lock(&self) -> SearchIndexGuard<'_> {
51        SearchIndexGuard {
52            index_map: self.room_indexes.lock().await,
53            search_index_store_kind: &self.search_index_store_kind,
54        }
55    }
56}
57
58pub(crate) struct SearchIndexGuard<'a> {
59    /// Guard around the [`RoomIndex`] map
60    index_map: MutexGuard<'a, HashMap<OwnedRoomId, RoomIndex>>,
61
62    /// Base directory that stores the directories for each RoomIndex
63    search_index_store_kind: &'a SearchIndexStoreKind,
64}
65
66impl SearchIndexGuard<'_> {
67    fn create_index(&self, room_id: &RoomId) -> Result<RoomIndex, IndexError> {
68        let index = match self.search_index_store_kind {
69            SearchIndexStoreKind::Directory(path) => RoomIndex::open_or_create(path, room_id)?,
70            SearchIndexStoreKind::InMemory => RoomIndex::new_in_memory(room_id)?,
71        };
72        Ok(index)
73    }
74
75    /// Handle an [`AnySyncMessageLikeEvent`] in the [`RoomIndex`] of a given
76    /// [`RoomId`]
77    ///
78    /// This which will add/remove/edit an event in the index based on the
79    /// event type.
80    pub(crate) fn handle_event(
81        &mut self,
82        event: AnySyncMessageLikeEvent,
83        room_id: &RoomId,
84    ) -> Result<(), IndexError> {
85        if !self.index_map.contains_key(room_id) {
86            let index = self.create_index(room_id)?;
87            self.index_map.insert(room_id.to_owned(), index);
88        }
89
90        let index = self.index_map.get_mut(room_id).expect("index should exist");
91        let result = index.handle_event(event);
92
93        match result {
94            Ok(_) => {}
95            Err(IndexError::CannotIndexRedactedMessage)
96            | Err(IndexError::EmptyMessage)
97            | Err(IndexError::MessageTypeNotSupported) => {
98                debug!("failed to parse event for indexing: {result:?}")
99            }
100            Err(IndexError::TantivyError(err)) => {
101                error!("failed to handle event in index: {err:?}")
102            }
103            Err(_) => error!("unexpected error during indexing: {result:?}"),
104        }
105        Ok(())
106    }
107
108    /// Search a [`Room`]'s index for the query and return at most
109    /// max_number_of_results results.
110    pub(crate) fn search(
111        &self,
112        query: &str,
113        max_number_of_results: usize,
114        room_id: &RoomId,
115    ) -> Option<Vec<OwnedEventId>> {
116        if let Some(index) = self.index_map.get(room_id) {
117            index
118                .search(query, max_number_of_results)
119                .inspect_err(|err| {
120                    error!("error occurred while searching index: {err:?}");
121                })
122                .ok()
123        } else {
124            debug!("Tried to search in a room with no index");
125            None
126        }
127    }
128
129    /// Commit a [`Room`]'s [`RoomIndex`] and reload searchers
130    pub(crate) fn commit_and_reload(&mut self, room_id: &RoomId) {
131        if let Some(index) = self.index_map.get_mut(room_id) {
132            let _ = index.commit_and_reload().inspect_err(|err| {
133                error!("error occurred while committing: {err:?}");
134            });
135        }
136    }
137}