tuwunel_service/rooms/search/
mod.rs1use std::sync::Arc;
2
3use futures::{Stream, StreamExt};
4use ruma::{RoomId, UserId, api::client::search::search_events::v3::Criteria};
5use tuwunel_core::{
6 PduCount, Result,
7 arrayvec::ArrayVec,
8 implement,
9 matrix::event::{Event, Matches},
10 trace,
11 utils::{
12 ArrayVecExt, IterStream, ReadyExt, set,
13 stream::{TryIgnore, WidebandExt},
14 },
15};
16use tuwunel_database::{Map, Txn, keyval::Val};
17
18use crate::rooms::{
19 short::ShortRoomId,
20 timeline::{PduId, RawPduId},
21};
22
23pub struct Service {
24 db: Data,
25 services: Arc<crate::services::OnceServices>,
26}
27
28struct Data {
29 tokenids: Arc<Map>,
30}
31
32#[derive(Clone, Debug)]
33pub struct RoomQuery<'a> {
34 pub room_id: &'a RoomId,
35 pub user_id: Option<&'a UserId>,
36 pub criteria: &'a Criteria,
37 pub limit: usize,
38 pub skip: usize,
39}
40
41type TokenId = ArrayVec<u8, TOKEN_ID_MAX_LEN>;
42
43const TOKEN_ID_MAX_LEN: usize =
44 size_of::<ShortRoomId>() + WORD_MAX_LEN + 1 + size_of::<RawPduId>();
45const WORD_MAX_LEN: usize = 50;
46
47impl crate::Service for Service {
48 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
49 Ok(Arc::new(Self {
50 db: Data { tokenids: args.db["tokenids"].clone() },
51 services: args.services.clone(),
52 }))
53 }
54
55 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
56}
57
58#[implement(Service)]
59pub fn index_pdu(&self, shortroomid: ShortRoomId, pdu_id: &RawPduId, message_body: &str) {
60 let items = tokenize(message_body).map(|word| {
61 let mut key = shortroomid.to_be_bytes().to_vec();
62 key.extend_from_slice(word.as_bytes());
63 key.push(0xFF);
64 key.extend_from_slice(pdu_id.as_ref()); (key, [])
67 });
68
69 Txn::insert(&self.db.tokenids, items).execute();
70}
71
72#[implement(Service)]
73pub fn deindex_pdu(&self, shortroomid: ShortRoomId, pdu_id: &RawPduId, message_body: &str) {
74 let batch = tokenize(message_body).map(|word| {
75 let mut key = shortroomid.to_be_bytes().to_vec();
76 key.extend_from_slice(word.as_bytes());
77 key.push(0xFF);
78 key.extend_from_slice(pdu_id.as_ref()); key
80 });
81
82 for token in batch {
83 self.db.tokenids.remove(&token);
84 }
85}
86
87#[implement(Service)]
88pub async fn search_pdus<'a>(
89 &'a self,
90 query: &'a RoomQuery<'a>,
91) -> Result<(usize, impl Stream<Item = impl Event + use<>> + Send + '_)> {
92 let pdu_ids: Vec<_> = self.search_pdu_ids(query).await?.collect().await;
93
94 let filter = &query.criteria.filter;
95 let count = pdu_ids.len();
96 let pdus = pdu_ids
97 .into_iter()
98 .stream()
99 .wide_filter_map(async |result_pdu_id: RawPduId| {
100 self.services
101 .timeline
102 .get_pdu_from_id(&result_pdu_id)
103 .await
104 .ok()
105 })
106 .ready_filter(|pdu| !pdu.is_redacted())
107 .ready_filter(move |pdu| filter.matches(pdu))
108 .wide_filter_map(async |pdu| {
109 self.services
110 .state_accessor
111 .user_can_see_event(query.user_id?, pdu.room_id(), pdu.event_id())
112 .await
113 .then_some(pdu)
114 })
115 .skip(query.skip)
116 .take(query.limit);
117
118 Ok((count, pdus))
119}
120
121#[implement(Service)]
124pub async fn search_pdu_ids(
125 &self,
126 query: &RoomQuery<'_>,
127) -> Result<impl Stream<Item = RawPduId> + Send + '_ + use<'_>> {
128 let shortroomid = self
129 .services
130 .short
131 .get_shortroomid(query.room_id)
132 .await?;
133
134 let pdu_ids = self
135 .search_pdu_ids_query_room(query, shortroomid)
136 .await;
137
138 let iters = pdu_ids.into_iter().map(IntoIterator::into_iter);
139
140 Ok(set::intersection(iters).stream())
141}
142
143#[implement(Service)]
144async fn search_pdu_ids_query_room(
145 &self,
146 query: &RoomQuery<'_>,
147 shortroomid: ShortRoomId,
148) -> Vec<Vec<RawPduId>> {
149 tokenize(&query.criteria.search_term)
150 .stream()
151 .wide_then(async |word| {
152 self.search_pdu_ids_query_words(shortroomid, &word)
153 .collect::<Vec<_>>()
154 .await
155 })
156 .collect::<Vec<_>>()
157 .await
158}
159
160#[implement(Service)]
162fn search_pdu_ids_query_words<'a>(
163 &'a self,
164 shortroomid: ShortRoomId,
165 word: &'a str,
166) -> impl Stream<Item = RawPduId> + Send + '_ {
167 self.search_pdu_ids_query_word(shortroomid, word)
168 .map(move |key| -> RawPduId {
169 let key = &key[prefix_len(word)..];
170 key.into()
171 })
172}
173
174#[implement(Service)]
176fn search_pdu_ids_query_word(
177 &self,
178 shortroomid: ShortRoomId,
179 word: &str,
180) -> impl Stream<Item = Val<'_>> + Send + '_ + use<'_> {
181 let end_id: RawPduId = PduId { shortroomid, count: PduCount::max() }.into();
183
184 let end = make_tokenid(shortroomid, word, &end_id);
186 let prefix = make_prefix(shortroomid, word);
187 self.db
188 .tokenids
189 .rev_raw_keys_from(&end)
190 .ignore_err()
191 .ready_take_while(move |key| key.starts_with(&prefix))
192}
193
194#[implement(Service)]
195pub async fn delete_all_search_tokenids_for_room(&self, room_id: &RoomId) -> Result {
196 let Ok(shortroomid) = self.services.short.get_shortroomid(room_id).await else {
197 return Ok(());
198 };
199
200 let txn = self
201 .db
202 .tokenids
203 .keys_prefix_raw(&shortroomid)
204 .ignore_err()
205 .ready_fold(self.services.db.txn(), |mut txn, key| {
206 trace!("Removing key: {key:?}");
207 txn.del_raw(&self.db.tokenids, key);
208 txn
209 })
210 .await;
211
212 txn.execute();
213
214 Ok(())
215}
216
217fn tokenize(body: &str) -> impl Iterator<Item = String> + Send + '_ {
222 body.split_terminator(|c: char| !c.is_alphanumeric())
223 .filter(|s| !s.is_empty())
224 .filter(|word| word.len() <= WORD_MAX_LEN)
225 .map(str::to_lowercase)
226}
227
228fn make_tokenid(shortroomid: ShortRoomId, word: &str, pdu_id: &RawPduId) -> TokenId {
229 let mut key = make_prefix(shortroomid, word);
230 key.extend_from_slice(pdu_id.as_ref());
231 key
232}
233
234fn make_prefix(shortroomid: ShortRoomId, word: &str) -> TokenId {
235 let mut key = TokenId::new();
236 key.extend_from_slice(&shortroomid.to_be_bytes());
237 key.extend_from_slice(word.as_bytes());
238 key.push(tuwunel_database::SEP);
239 key
240}
241
242fn prefix_len(word: &str) -> usize {
243 size_of::<ShortRoomId>()
244 .saturating_add(word.len())
245 .saturating_add(1)
246}