Skip to main content

nexus_common/models/user/
search.rs

1use super::UserDetails;
2use crate::db::RedisOps;
3use crate::{models::traits::Collection, types::DynError};
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7pub const USER_NAME_KEY_PARTS: [&str; 2] = ["Users", "Name"];
8
9#[derive(Serialize, Deserialize, ToSchema, Default)]
10pub struct UserSearch(pub Vec<String>);
11
12impl RedisOps for UserSearch {}
13
14impl UserSearch {
15    pub async fn get_by_name(
16        name: &str,
17        skip: Option<usize>,
18        limit: Option<usize>,
19    ) -> Result<Option<Self>, DynError> {
20        // Perform the lexicographical range search
21        let elements = Self::get_from_index(name, skip, limit).await?;
22
23        // If elements exist, process them to extract user_ids
24        if let Some(elements) = elements {
25            let user_ids: Vec<String> = elements
26                .into_iter()
27                .filter_map(|element| {
28                    // Split by `:` and take the last part (user_id)
29                    element.split(':').next_back().map(|p| p.to_string())
30                })
31                .collect();
32
33            return Ok(Some(UserSearch(user_ids)));
34        }
35
36        Ok(None)
37    }
38
39    pub async fn get_from_index(
40        name: &str,
41        skip: Option<usize>,
42        limit: Option<usize>,
43    ) -> Result<Option<Vec<String>>, DynError> {
44        // Convert the username to lowercase to ensure case-insensitive search
45        let name = name.to_lowercase();
46
47        let min = format!("[{name}"); // Inclusive range starting with "name"
48        let max = format!("({name}~"); // Exclusive range ending just after "name"
49
50        // Perform the lexicographical range search
51        Self::try_from_index_sorted_set_lex(&USER_NAME_KEY_PARTS, &min, &max, skip, limit).await
52    }
53
54    /// Adds multiple `user_id`s to the Redis sorted set using the username as index.
55    ///
56    /// This method takes a list of `UserDetails` and adds them all to the sorted set at once.
57    pub async fn put_to_index(details_list: &[&UserDetails]) -> Result<(), DynError> {
58        // ensure existing records are deleted
59        Self::delete_existing_records(
60            details_list
61                .iter()
62                .map(|details| details.id.as_str())
63                .collect::<Vec<&str>>()
64                .as_slice(),
65        )
66        .await?;
67
68        // Collect all the `username:user_id` pairs and their corresponding scores
69        let mut items: Vec<(f64, String)> = Vec::with_capacity(details_list.len());
70
71        for details in details_list {
72            // Convert the username to lowercase before storing
73            if details.name == "[DELETED]" {
74                break;
75            }
76            let username = details.name.to_lowercase();
77            let user_id = &details.id;
78            let score = 0.0;
79
80            // The value in the sorted set will be `username:user_id`
81            let member = format!("{username}:{user_id}");
82
83            items.push((score, member));
84        }
85
86        // Perform a single Redis ZADD operation with all the items
87        Self::put_index_sorted_set(
88            &USER_NAME_KEY_PARTS,
89            &items
90                .iter()
91                .map(|(score, member)| (*score, member.as_str()))
92                .collect::<Vec<_>>(),
93            None,
94            None,
95        )
96        .await
97    }
98
99    async fn delete_existing_records(user_ids: &[&str]) -> Result<(), DynError> {
100        if user_ids.is_empty() {
101            return Ok(());
102        }
103        let mut records_to_delete: Vec<String> = Vec::with_capacity(user_ids.len());
104        let keys = user_ids
105            .iter()
106            .map(|&id| vec![id])
107            .collect::<Vec<Vec<&str>>>();
108        let users = UserDetails::get_from_index(keys.iter().map(|item| item.as_slice()).collect())
109            .await?
110            .into_iter()
111            .flatten()
112            .collect::<Vec<UserDetails>>();
113        for user_id in user_ids {
114            let existing_username = users
115                .iter()
116                .find(|user| user.id.to_string() == *user_id)
117                .map(|user| user.name.to_lowercase());
118            if let Some(existing_record) = existing_username {
119                let search_key = format!("{existing_record}:{user_id}");
120                records_to_delete.push(search_key);
121            }
122        }
123
124        Self::remove_from_index_sorted_set(
125            None,
126            &USER_NAME_KEY_PARTS,
127            records_to_delete
128                .iter()
129                .map(|item| item.as_str())
130                .collect::<Vec<&str>>()
131                .as_slice(),
132        )
133        .await?;
134        Ok(())
135    }
136}
137
138// #[cfg(test)]
139// mod tests {
140//     use crate::{
141//         models::{
142//             traits::Collection,
143//             user::{UserDetails, UserSearch},
144//         },
145//         types::DynError,
146//         RedisOps,
147//         _service::NexusApi,
148//     };
149//     use chrono::Utc;
150//     use pubky_app_specs::PubkyId;
151
152//     #[tokio_shared_rt::test(shared)]
153//     async fn test_put_to_index_no_duplicates() -> Result<(), DynError> {
154//         NexusApi::builder().init_stack().await;
155//         // Test that the `put_to_index` method does not add duplicate records to the index
156//         // when called with the same `UserDetails` multiple times.
157
158//         // Create a `UserDetails` object
159//         let user_id = "operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo";
160//         let user_name = "Test User Duplicate";
161//         let user_details = UserDetails {
162//             id: PubkyId::try_from(user_id).expect("valid pubky id"),
163//             name: user_name.to_string(),
164//             bio: None,
165//             status: None,
166//             links: None,
167//             image: None,
168//             indexed_at: Utc::now().timestamp_millis(),
169//         };
170
171//         user_details.put_to_graph().await?;
172//         user_details
173//             .put_index_json(vec![user_id].as_slice(), None, None)
174//             .await?;
175
176//         // Call `put_to_index` with the same `UserDetails` object
177//         UserSearch::put_to_index(&[&user_details]).await?;
178
179//         // Check that the index contains only one record for the user
180//         let search_result = UserSearch::get_by_name(&user_name, None, None).await?;
181//         assert_eq!(search_result.unwrap().0, vec![user_id.to_string()]);
182
183//         let new_user_name = "Some Other User Name";
184//         let new_user_details = UserDetails {
185//             id: PubkyId::try_from(user_id).expect("valid pubky id"),
186//             name: new_user_name.to_string(),
187//             bio: None,
188//             status: None,
189//             links: None,
190//             image: None,
191//             indexed_at: Utc::now().timestamp_millis(),
192//         };
193
194//         // Call `put_to_index` with new user details
195//         UserSearch::put_to_index(&[&new_user_details]).await?;
196
197//         // Check the previous record is deleted
198//         // Check that the index contains only one record for the user
199//         let search_result = UserSearch::get_by_name(&user_name, None, None).await?;
200//         assert_eq!(search_result.is_none(), true);
201
202//         Ok(())
203//     }
204//}