Skip to main content

matrix_sdk/
message_search.rs

1// Copyright 2026 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
15//! Messages search facilities and high-level helpers to perform searches across
16//! one or multiple rooms, with pagination support.
17//!
18//! # Examples
19//!
20//! ## Searching within a single room
21//!
22//! Use [`Room::search_messages`] to obtain a [`RoomSearchIterator`] and call
23//! [`RoomSearchIterator::next`] to paginate through event IDs, or
24//! [`RoomSearchIterator::next_events`] to load the full [`TimelineEvent`]s.
25//!
26//! ```no_run
27//! # use matrix_sdk::Room;
28//! # async fn example(room: Room) -> anyhow::Result<()> {
29//! let mut iter = room.search_messages("hello world".to_owned(), 10);
30//!
31//! while let Some(event_ids) = iter.next().await? {
32//!     for event_id in event_ids {
33//!         println!("Found event: {event_id}");
34//!     }
35//! }
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! ## Searching across all joined rooms
41//!
42//! Use [`Client::search_messages`] to create a [`GlobalSearchBuilder`].
43//! Optionally restrict the working set to DM rooms (or non-DM rooms) before
44//! calling [`GlobalSearchBuilder::build`] to get a [`GlobalSearchIterator`].
45//! Use [`GlobalSearchIterator::next_events`] to load full [`TimelineEvent`]s
46//! instead of plain event IDs.
47//!
48//! ```no_run
49//! # use matrix_sdk::Client;
50//! # async fn example(client: Client) -> anyhow::Result<()> {
51//! // Search only in DM rooms.
52//! let mut iter = client
53//!     .search_messages("hello world".to_owned(), 10)
54//!     .only_dm_rooms()
55//!     .await?
56//!     .build();
57//!
58//! while let Some(results) = iter.next_events().await? {
59//!     for (room_id, event) in results {
60//!         println!(
61//!             "Found event in room {room_id} with timestamp: {:?}",
62//!             event.timestamp
63//!         );
64//!     }
65//! }
66//! # Ok(())
67//! # }
68//! ```
69
70use std::collections::HashSet;
71
72use matrix_sdk_base::{RoomStateFilter, deserialized_responses::TimelineEvent};
73use matrix_sdk_search::error::IndexError;
74#[cfg(doc)]
75use matrix_sdk_search::index::RoomIndex;
76use ruma::{OwnedEventId, OwnedRoomId};
77
78use crate::{Client, Room};
79
80impl Room {
81    /// Search this room's [`RoomIndex`] for query and return at most
82    /// max_number_of_results results.
83    pub async fn search(
84        &self,
85        query: &str,
86        max_number_of_results: usize,
87        pagination_offset: Option<usize>,
88    ) -> Result<Vec<(f32, OwnedEventId)>, IndexError> {
89        let mut search_index_guard = self.client.search_index().lock().await;
90        search_index_guard.search(query, max_number_of_results, pagination_offset, self.room_id())
91    }
92}
93
94/// An error that can occur while searching messages, using the high-level
95/// search helpers provided by this module provided by this module.
96#[derive(thiserror::Error, Debug)]
97pub enum SearchError {
98    /// An error occurred while searching through the index for matching events.
99    #[error(transparent)]
100    IndexError(#[from] IndexError),
101    /// An error occurred while loading the event content for a search result.
102    #[error(transparent)]
103    EventLoadError(#[from] crate::Error),
104}
105
106impl Room {
107    /// Search for messages in this room matching the given query, returning an
108    /// iterator over the results.
109    pub fn search_messages(
110        &self,
111        query: String,
112        num_results_per_batch: usize,
113    ) -> RoomSearchIterator {
114        RoomSearchIterator {
115            room: self.clone(),
116            query,
117            offset: None,
118            is_done: false,
119            num_results_per_batch,
120        }
121    }
122}
123
124/// An async iterator for a search query in a single room.
125#[derive(Debug)]
126pub struct RoomSearchIterator {
127    /// The room in which the search is performed.
128    room: Room,
129
130    /// The search query, directly forwarded to the search API.
131    query: String,
132
133    /// The current start offset in the search results, or `None` if we haven't
134    /// called the iterator yet.
135    offset: Option<usize>,
136
137    /// Whether we have exhausted the search results.
138    is_done: bool,
139
140    /// Number of results to return (at most) per batch when calling
141    /// [`Self::next()`].
142    num_results_per_batch: usize,
143}
144
145impl RoomSearchIterator {
146    /// Return the next batch of event IDs matching the search query, or `None`
147    /// if there are no more results.
148    pub async fn next(&mut self) -> Result<Option<Vec<OwnedEventId>>, IndexError> {
149        if self.is_done {
150            return Ok(None);
151        }
152
153        // TODO: use the client/server API search endpoint for public rooms, as those
154        // may require lots of time for indexing all events.
155        let result = self.room.search(&self.query, self.num_results_per_batch, self.offset).await?;
156
157        if result.is_empty() {
158            self.is_done = true;
159            Ok(None)
160        } else {
161            self.offset = Some(self.offset.unwrap_or(0) + result.len());
162            Ok(Some(result.into_iter().map(|(_, id)| id).collect()))
163        }
164    }
165
166    /// Returns [`TimelineEvent`]s instead of event IDs, by loading the events
167    /// from the store or from network.
168    pub async fn next_events(&mut self) -> Result<Option<Vec<TimelineEvent>>, SearchError> {
169        let Some(event_ids) = self.next().await? else {
170            return Ok(None);
171        };
172        let mut results = Vec::new();
173        for event_id in event_ids {
174            results.push(self.room.load_or_fetch_event(&event_id, None).await?);
175        }
176        Ok(Some(results))
177    }
178}
179
180#[derive(Debug)]
181struct GlobalSearchRoomState {
182    /// The room for which we're storing state.
183    room: Room,
184
185    /// The current start offset in the search results for this room, or `None`
186    /// if we haven't called the iterator for this room yet.
187    offset: Option<usize>,
188}
189
190impl GlobalSearchRoomState {
191    fn new(room: Room) -> Self {
192        Self { room, offset: None }
193    }
194}
195
196/// A builder for a [`GlobalSearchIterator`] that allows to configure the
197/// initial working set of rooms to search in.
198#[derive(Debug)]
199pub struct GlobalSearchBuilder {
200    client: Client,
201
202    /// The search query, directly forwarded to the search API.
203    query: String,
204
205    /// Number of results to return (at most) per batch when calling
206    /// [`GlobalSearchIterator::next()`].
207    num_results_per_batch: usize,
208
209    /// The working set of rooms to search in.
210    room_set: Vec<Room>,
211}
212
213impl GlobalSearchBuilder {
214    /// Create a new global search on all the joined rooms.
215    fn new(client: Client, query: String, num_results_per_batch: usize) -> Self {
216        let room_set = client.rooms_filtered(RoomStateFilter::JOINED);
217        Self { client, query, room_set, num_results_per_batch }
218    }
219
220    /// Keep only the DM rooms from the initial working set.
221    pub async fn only_dm_rooms(mut self) -> Result<Self, crate::Error> {
222        let mut to_remove = HashSet::new();
223        for room in &self.room_set {
224            if !room.compute_is_dm().await? {
225                to_remove.insert(room.room_id().to_owned());
226            }
227        }
228        self.room_set.retain(|room| !to_remove.contains(room.room_id()));
229        Ok(self)
230    }
231
232    /// Keep only non-DM rooms (groups) from the initial working set.
233    pub async fn no_dms(mut self) -> Result<Self, crate::Error> {
234        let mut to_remove = HashSet::new();
235        for room in &self.room_set {
236            if room.compute_is_dm().await? {
237                to_remove.insert(room.room_id().to_owned());
238            }
239        }
240        self.room_set.retain(|room| !to_remove.contains(room.room_id()));
241        Ok(self)
242    }
243
244    /// Build the [`GlobalSearchIterator`] from this builder.
245    pub fn build(self) -> GlobalSearchIterator {
246        GlobalSearchIterator {
247            client: self.client,
248            query: self.query,
249            room_state: Vec::from_iter(self.room_set.into_iter().map(GlobalSearchRoomState::new)),
250            current_batch: Vec::new(),
251            num_results_per_batch: self.num_results_per_batch,
252        }
253    }
254}
255
256impl Client {
257    /// Search across all rooms for events with the given query, returning a
258    /// builder for an iterator over the results.
259    pub fn search_messages(
260        &self,
261        query: String,
262        num_results_per_batch: usize,
263    ) -> GlobalSearchBuilder {
264        GlobalSearchBuilder::new(self.clone(), query, num_results_per_batch)
265    }
266}
267
268/// An async iterator for a search query across multiple rooms.
269#[derive(Debug)]
270pub struct GlobalSearchIterator {
271    client: Client,
272
273    /// The search query, directly forwarded to the search API.
274    query: String,
275
276    /// The state for each room in the working list, that may still have
277    /// results.
278    ///
279    /// This list is bound to shrink as we exhaust search results for each room,
280    /// until it's empty and the overall iteration is done.
281    room_state: Vec<GlobalSearchRoomState>,
282
283    /// A buffer for the current batch of results across all rooms, sorted by
284    /// score descending so results are returned in relevance order.
285    current_batch: Vec<(f32, OwnedRoomId, OwnedEventId)>,
286
287    /// Number of results to return (at most) per batch when calling
288    /// [`Self::next()`].
289    num_results_per_batch: usize,
290}
291
292impl GlobalSearchIterator {
293    /// Return the next batch of event IDs matching the search query across all
294    /// rooms, or `None` if there are no more results.
295    pub async fn next(&mut self) -> Result<Option<Vec<(OwnedRoomId, OwnedEventId)>>, SearchError> {
296        if self.room_state.is_empty() {
297            return Ok(None);
298        }
299
300        // If there was enough results from a previous room iteration, return them
301        // immediately (they're already sorted from the previous fill).
302        if self.current_batch.len() >= self.num_results_per_batch {
303            return Ok(Some(
304                self.current_batch
305                    .drain(0..self.num_results_per_batch)
306                    .map(|(_, room_id, event_id)| (room_id, event_id))
307                    .collect(),
308            ));
309        }
310
311        let mut to_remove = HashSet::new();
312
313        // Search across all non-done rooms for `num_results`, and accumulate them in
314        // `Self::current_batch`.
315        for room_state in &mut self.room_state {
316            let room_results = room_state
317                .room
318                .search(&self.query, self.num_results_per_batch, room_state.offset)
319                .await?;
320
321            if room_results.is_empty() {
322                // We've exhausted results for this room, mark it for removal.
323                to_remove.insert(room_state.room.room_id().to_owned());
324            } else {
325                // Move the start offset for the room forward.
326                room_state.offset = Some(room_state.offset.unwrap_or(0) + room_results.len());
327
328                // Append the search results to the current batch.
329                self.current_batch.extend(room_results.into_iter().map(|(score, event_id)| {
330                    (score, room_state.room.room_id().to_owned(), event_id)
331                }));
332
333                if self.current_batch.len() >= self.num_results_per_batch {
334                    // We have enough events to return now.
335                    break;
336                }
337            }
338        }
339
340        // Delete rooms for which we've exhausted search results from the working list.
341        for room_id in to_remove {
342            self.room_state.retain(|room_state| room_state.room.room_id() != room_id);
343        }
344
345        if !self.current_batch.is_empty() {
346            // Sort by score descending so cross-room results are returned in relevance
347            // order.
348            self.current_batch.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
349            let high = self.num_results_per_batch.min(self.current_batch.len());
350            Ok(Some(
351                self.current_batch
352                    .drain(0..high)
353                    .map(|(_, room_id, event_id)| (room_id, event_id))
354                    .collect(),
355            ))
356        } else {
357            debug_assert!(self.room_state.is_empty());
358            Ok(None)
359        }
360    }
361
362    /// Returns [`TimelineEvent`]s instead of event IDs, by loading the events
363    /// from the store or from network.
364    pub async fn next_events(
365        &mut self,
366    ) -> Result<Option<Vec<(OwnedRoomId, TimelineEvent)>>, SearchError> {
367        let Some(event_ids) = self.next().await? else {
368            return Ok(None);
369        };
370        let mut results = Vec::with_capacity(event_ids.len());
371        for (room_id, event_id) in event_ids {
372            let Some(room) = self.client.get_room(&room_id) else {
373                continue;
374            };
375            results.push((room_id, room.load_or_fetch_event(&event_id, None).await?));
376        }
377        Ok(Some(results))
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use std::time::Duration;
384
385    use matrix_sdk_test::{BOB, JoinedRoomBuilder, async_test, event_factory::EventFactory};
386    use ruma::{event_id, room_id, user_id};
387
388    use crate::{sleep::sleep, test_utils::mocks::MatrixMockServer};
389
390    #[async_test]
391    async fn test_room_message_search() {
392        let server = MatrixMockServer::new().await;
393        let client = server.client_builder().build().await;
394
395        let event_cache = client.event_cache();
396        event_cache.subscribe().unwrap();
397
398        let room_id = room_id!("!room_id:localhost");
399        let room = server.sync_joined_room(&client, room_id).await;
400
401        let f = EventFactory::new().room(room_id).sender(user_id!("@user_id:localhost"));
402
403        let event_id = event_id!("$event_id:localhost");
404
405        server
406            .sync_room(
407                &client,
408                JoinedRoomBuilder::new(room_id)
409                    .add_timeline_event(f.text_msg("hello world").event_id(event_id)),
410            )
411            .await;
412
413        // Let the search indexer process the new event.
414        sleep(Duration::from_millis(200)).await;
415
416        // Search for a missing keyword.
417        {
418            let mut room_search = room.search_messages("search query".to_owned(), 5);
419
420            // Searching for an event that's non-existing should succeed.
421            let maybe_results = room_search.next().await.unwrap();
422            assert!(maybe_results.is_none());
423
424            // Calling the iterator after it's exhausted should still return `None` and not
425            // error or return more results.
426            let maybe_results = room_search.next().await.unwrap();
427            assert!(maybe_results.is_none());
428        }
429
430        // Search for an existing keyword, by event id.
431        {
432            let mut room_search = room.search_messages("world".to_owned(), 5);
433
434            // Searching for a keyword that matches an existing event should return the
435            // event ID.
436            let maybe_results = room_search.next().await.unwrap();
437            let results = maybe_results.unwrap();
438            assert_eq!(results.len(), 1);
439            assert_eq!(&results[0], event_id,);
440
441            // And no more results after that.
442            let maybe_results = room_search.next().await.unwrap();
443            assert!(maybe_results.is_none());
444        }
445
446        // Search for an existing keyword, by events.
447        {
448            let mut room_search = room.search_messages("world".to_owned(), 5);
449
450            // Searching for a keyword that matches an existing event should return the
451            // event ID.
452            let maybe_results = room_search.next_events().await.unwrap();
453            let results = maybe_results.unwrap();
454            assert_eq!(results.len(), 1);
455            assert_eq!(results[0].event_id().unwrap(), event_id,);
456
457            // And no more results after that.
458            let maybe_results = room_search.next_events().await.unwrap();
459            assert!(maybe_results.is_none());
460        }
461    }
462
463    #[async_test]
464    async fn test_global_message_search() {
465        let server = MatrixMockServer::new().await;
466        let client = server.client_builder().build().await;
467
468        let event_cache = client.event_cache();
469        event_cache.subscribe().unwrap();
470
471        let room_id1 = room_id!("!r1:localhost");
472        let room_id2 = room_id!("!r2:localhost");
473
474        let f = EventFactory::new().sender(user_id!("@user_id:localhost"));
475
476        let result_event_id1 = event_id!("$result1:localhost");
477        let result_event_id2 = event_id!("$result2:localhost");
478
479        server
480            .mock_sync()
481            .ok_and_run(&client, |sync_builder| {
482                sync_builder
483                    .add_joined_room(
484                        JoinedRoomBuilder::new(room_id1)
485                            .add_timeline_event(
486                                f.text_msg("hello world").room(room_id1).event_id(result_event_id1),
487                            )
488                            .add_timeline_event(f.text_msg("hello back").room(room_id1)),
489                    )
490                    .add_joined_room(JoinedRoomBuilder::new(room_id2).add_timeline_event(
491                        f.text_msg("it's a mad world").room(room_id2).event_id(result_event_id2),
492                    ));
493            })
494            .await;
495
496        // Let the search indexer process the new event.
497        sleep(Duration::from_millis(200)).await;
498
499        // Search for a missing keyword.
500        {
501            let mut search = client.search_messages("search query".to_owned(), 5).build();
502
503            // Searching for an event that's non-existing should succeed.
504            let maybe_results = search.next().await.unwrap();
505            assert!(maybe_results.is_none());
506
507            // Calling the iterator after it's exhausted should still return `None` and not
508            // error or return more results.
509            let maybe_results = search.next().await.unwrap();
510            assert!(maybe_results.is_none());
511        }
512
513        // Search for an existing keyword, by event id.
514        {
515            let mut search = client.search_messages("world".to_owned(), 5).build();
516
517            // Searching for a keyword that matches an existing event should return the
518            // event ID.
519            let maybe_results = search.next().await.unwrap();
520            let results = maybe_results.unwrap();
521            assert_eq!(results.len(), 2);
522            // Search results order is not guaranteed, so we check that both expected
523            // results are present in the returned batch.
524            assert!(results.contains(&(room_id1.to_owned(), result_event_id1.to_owned())));
525            assert!(results.contains(&(room_id2.to_owned(), result_event_id2.to_owned())));
526
527            // And no more results after that.
528            let maybe_results = search.next().await.unwrap();
529            assert!(maybe_results.is_none());
530        }
531
532        // Search for an existing keyword, by event.
533        {
534            let mut search = client.search_messages("world".to_owned(), 5).build();
535
536            // Searching for a keyword that matches an existing event should return the
537            // event ID.
538            let maybe_results = search.next_events().await.unwrap();
539            let results = maybe_results.unwrap();
540            assert_eq!(results.len(), 2);
541            // Search results order is not guaranteed, so we check that both expected
542            // results are present in the returned batch.
543            assert!(results.iter().any(|(room_id, event)| {
544                room_id == room_id1 && event.event_id() == Some(result_event_id1)
545            }));
546            assert!(results.iter().any(|(room_id, event)| {
547                room_id == room_id2 && event.event_id() == Some(result_event_id2)
548            }));
549
550            // And no more results after that.
551            let maybe_results = search.next_events().await.unwrap();
552            assert!(maybe_results.is_none());
553        }
554    }
555
556    #[async_test]
557    async fn test_global_message_search_score_ordering() {
558        let server = MatrixMockServer::new().await;
559        let client = server.client_builder().build().await;
560
561        let event_cache = client.event_cache();
562        event_cache.subscribe().unwrap();
563
564        let room_id1 = room_id!("!r1:localhost");
565        let room_id2 = room_id!("!r2:localhost");
566
567        let f = EventFactory::new().sender(user_id!("@user_id:localhost"));
568
569        // Both rooms get two documents of identical length (padded with filler so
570        // document-length normalization and the per-corpus IDF of "world" match across
571        // rooms). The score then depends only on how many times "world" appears.
572        //
573        // Term frequencies are 4, 3, 2, 1, split so the rooms alternate by rank:
574        // room1 holds the 4x and 2x events, room2 the 3x and 1x events. A correct
575        // cross-room sort therefore interleaves the rooms: r1, r2, r1, r2.
576        let r1_rank1 = event_id!("$r1_rank1:localhost"); // room1, "world" x4
577        let r2_rank2 = event_id!("$r2_rank2:localhost"); // room2, "world" x3
578        let r1_rank3 = event_id!("$r1_rank3:localhost"); // room1, "world" x2
579        let r2_rank4 = event_id!("$r2_rank4:localhost"); // room2, "world" x1
580
581        server
582            .mock_sync()
583            .ok_and_run(&client, |sync_builder| {
584                sync_builder
585                    .add_joined_room(
586                        JoinedRoomBuilder::new(room_id1)
587                            .add_timeline_event(
588                                f.text_msg("world world world world filler filler filler filler filler filler")
589                                    .room(room_id1)
590                                    .event_id(r1_rank1),
591                            )
592                            .add_timeline_event(
593                                f.text_msg("world world filler filler filler filler filler filler filler filler")
594                                    .room(room_id1)
595                                    .event_id(r1_rank3),
596                            ),
597                    )
598                    .add_joined_room(
599                        JoinedRoomBuilder::new(room_id2)
600                            .add_timeline_event(
601                                f.text_msg("world world world filler filler filler filler filler filler filler")
602                                    .room(room_id2)
603                                    .event_id(r2_rank2),
604                            )
605                            .add_timeline_event(
606                                f.text_msg("world filler filler filler filler filler filler filler filler filler")
607                                    .room(room_id2)
608                                    .event_id(r2_rank4),
609                            ),
610                    );
611            })
612            .await;
613
614        sleep(Duration::from_millis(200)).await;
615
616        let mut search = client.search_messages("world".to_owned(), 10).build();
617
618        let results = search.next().await.unwrap().unwrap();
619        assert_eq!(results.len(), 4);
620
621        // Results are interleaved across the two rooms strictly by score.
622        assert_eq!(results[0], (room_id1.to_owned(), r1_rank1.to_owned()));
623        assert_eq!(results[1], (room_id2.to_owned(), r2_rank2.to_owned()));
624        assert_eq!(results[2], (room_id1.to_owned(), r1_rank3.to_owned()));
625        assert_eq!(results[3], (room_id2.to_owned(), r2_rank4.to_owned()));
626    }
627
628    #[async_test]
629    async fn test_global_message_search_dm_or_groups() {
630        let server = MatrixMockServer::new().await;
631        let client = server.client_builder().build().await;
632
633        let event_cache = client.event_cache();
634        event_cache.subscribe().unwrap();
635
636        // This time, room_id1 is a DM room,
637        let room_id1 = room_id!("!r1:localhost");
638        // While room_id2 isn't.
639        let room_id2 = room_id!("!r2:localhost");
640
641        let f = EventFactory::new().sender(user_id!("@user_id:localhost"));
642
643        let result_event_id1 = event_id!("$result1:localhost");
644        let result_event_id2 = event_id!("$result2:localhost");
645
646        server
647            .mock_sync()
648            .ok_and_run(&client, |sync_builder| {
649                sync_builder
650                    .add_joined_room(
651                        JoinedRoomBuilder::new(room_id1)
652                            .add_timeline_event(
653                                f.text_msg("hello world").room(room_id1).event_id(result_event_id1),
654                            )
655                            .add_timeline_event(f.text_msg("hello back").room(room_id1)),
656                    )
657                    .add_joined_room(JoinedRoomBuilder::new(room_id2).add_timeline_event(
658                        f.text_msg("it's a mad world").room(room_id2).event_id(result_event_id2),
659                    ))
660                    // Note: adding a DM room for room_id1 here.
661                    .add_global_account_data(
662                        f.direct().add_user((*BOB).to_owned().into(), room_id1),
663                    );
664            })
665            .await;
666
667        // Let the search indexer process the new event.
668        sleep(Duration::from_millis(200)).await;
669
670        // Search for an existing keyword, by event id, only in DMs.
671        {
672            let mut search = client
673                .search_messages("world".to_owned(), 5)
674                .only_dm_rooms()
675                .await
676                .unwrap()
677                .build();
678
679            let maybe_results = search.next().await.unwrap();
680            let results = maybe_results.unwrap();
681            assert_eq!(results.len(), 1);
682            assert_eq!(&results[0], &(room_id1.to_owned(), result_event_id1.to_owned()));
683
684            // And no more results after that.
685            let maybe_results = search.next().await.unwrap();
686            assert!(maybe_results.is_none());
687        }
688
689        // Search for an existing keyword, by event, only in groups.
690        {
691            let mut search =
692                client.search_messages("world".to_owned(), 5).no_dms().await.unwrap().build();
693
694            let maybe_results = search.next_events().await.unwrap();
695            let results = maybe_results.unwrap();
696            assert_eq!(results.len(), 1);
697            assert_eq!(results[0].0, room_id2);
698            assert_eq!(results[0].1.event_id().unwrap(), result_event_id2);
699
700            // And no more results after that.
701            let maybe_results = search.next().await.unwrap();
702            assert!(maybe_results.is_none());
703        }
704    }
705}