diff --git a/benches/tag.rs b/benches/tag.rs index f539f124..d9ecbf46 100644 --- a/benches/tag.rs +++ b/benches/tag.rs @@ -53,7 +53,7 @@ fn bench_get_post_tags(c: &mut Criterion) { &[user_id, post_id], |b, ¶ms| { b.to_async(&rt).iter(|| async { - let profile = TagPost::get_by_id(params[0], Some(params[1]), None, None) + let profile = TagPost::get_by_id(params[0], params[1], None, None) .await .unwrap(); criterion::black_box(profile); diff --git a/docs/images/pubky-nexus-arch.png b/docs/images/pubky-nexus-arch.png index 656e0dd0..22c07489 100644 Binary files a/docs/images/pubky-nexus-arch.png and b/docs/images/pubky-nexus-arch.png differ diff --git a/src/models/post/view.rs b/src/models/post/view.rs index 0f292197..bffb74e4 100644 --- a/src/models/post/view.rs +++ b/src/models/post/view.rs @@ -3,15 +3,13 @@ use utoipa::ToSchema; use super::{Bookmark, PostCounts, PostDetails, PostRelationships}; use crate::models::tag::post::TagPost; -use crate::models::tag::traits::TagCollection; -use crate::models::tag::TagDetails; /// Represents a Pubky user with relational data including tags, counts, and relationship with a viewer. #[derive(Serialize, Deserialize, ToSchema, Default)] pub struct PostView { pub details: PostDetails, pub counts: PostCounts, - pub tags: Vec, + pub tags: TagPost, pub relationships: PostRelationships, pub bookmark: Option, } @@ -31,7 +29,7 @@ impl PostView { PostCounts::get_by_id(author_id, post_id), Bookmark::get_by_id(author_id, post_id, viewer_id), PostRelationships::get_by_id(author_id, post_id), - TagPost::get_by_id(author_id, Some(post_id), max_tags, max_taggers), + TagPost::get_by_id(author_id, post_id, max_tags, max_taggers), )?; let details = match details { diff --git a/src/models/tag/post.rs b/src/models/tag/post.rs index adc7c457..e9f78eae 100644 --- a/src/models/tag/post.rs +++ b/src/models/tag/post.rs @@ -1,15 +1,26 @@ -use crate::RedisOps; +use crate::db::kv::index::sorted_sets::Sorting; +use crate::models::tag::details::TagDetails; +use crate::queries; +use crate::{db::connectors::neo4j::get_neo4j_graph, RedisOps}; use axum::async_trait; use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::ops::Deref; use utoipa::ToSchema; -use super::traits::TagCollection; - const POST_TAGS_KEY_PARTS: [&str; 2] = ["Posts", "Tag"]; #[derive(Serialize, Deserialize, Debug, Clone, ToSchema, Default)] -pub struct TagPost; +pub struct TagPost(Vec); + +// Implement Deref so TagList can be used like Vec +impl Deref for TagPost { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} #[async_trait] impl RedisOps for TagPost { async fn prefix() -> String { @@ -17,8 +28,92 @@ impl RedisOps for TagPost { } } -impl TagCollection for TagPost { - fn get_tag_prefix<'a>() -> [&'a str; 2] { - POST_TAGS_KEY_PARTS +impl TagPost { + fn create_set_key_parts<'a>(user_id: &'a str, post_id: &'a str) -> Vec<&'a str> { + [&POST_TAGS_KEY_PARTS[..], &[user_id, post_id]].concat() + } + + pub async fn get_by_id( + user_id: &str, + post_id: &str, + limit_tags: Option, + limit_taggers: Option, + ) -> Result, Box> { + // TODO: Not sure if this is the place to do or in the endpoint + let limit_tags = limit_tags.unwrap_or(5); + let limit_taggers = limit_taggers.unwrap_or(5); + match Self::try_from_index(user_id, post_id, limit_tags, limit_taggers).await? { + Some(counts) => Ok(Some(counts)), + None => Self::get_from_graph(user_id, post_id).await, + } + } + + async fn try_from_index( + user_id: &str, + post_id: &str, + limit_tags: usize, + limit_taggers: usize, + ) -> Result, Box> { + let key_parts = Self::create_set_key_parts(user_id, post_id); + match Self::try_from_index_sorted_set( + &key_parts, + None, + None, + None, + Some(limit_tags), + Sorting::Descending, + ) + .await? + { + Some(tag_scores) => { + let mut tags = Vec::with_capacity(limit_tags); + for (label, _) in tag_scores.iter() { + tags.push(format!("{}:{}:{}", user_id, post_id, label)); + } + let tags_ref: Vec<&str> = tags.iter().map(|label| label.as_str()).collect(); + let taggers = Self::try_from_multiple_sets(&tags_ref, Some(limit_taggers)).await?; + let tag_details_list = TagDetails::from_index(tag_scores, taggers); + Ok(Some(TagPost(tag_details_list))) + } + None => Ok(None), + } + } + + pub async fn get_from_graph( + user_id: &str, + post_id: &str, + ) -> Result, Box> { + let mut result; + { + // We cannot use LIMIT clause because we need all data related + let query = queries::read::post_tags(user_id, post_id); + let graph = get_neo4j_graph()?; + + let graph = graph.lock().await; + result = graph.execute(query).await?; + } + + if let Some(row) = result.next().await? { + let user_exists: bool = row.get("post_exists").unwrap_or(false); + if user_exists { + let tagged_from: TagPost = row.get("post_tags").unwrap_or_default(); + Self::add_to_label_sorted_set(user_id, post_id, &tagged_from).await?; + return Ok(Some(tagged_from)); + } + } + Ok(None) + } + + async fn add_to_label_sorted_set( + user_id: &str, + post_id: &str, + tags: &[TagDetails], + ) -> Result<(), Box> { + let (tag_scores, (labels, taggers)) = TagDetails::process_tag_details(tags); + + let key_parts = Self::create_set_key_parts(user_id, post_id); + Self::put_index_sorted_set(&key_parts, tag_scores.as_slice()).await?; + + Self::put_multiple_set_indexes(&[user_id, post_id], &labels, &taggers).await } } diff --git a/src/models/tag/user.rs b/src/models/tag/user.rs index f5146d2e..fcd7e668 100644 --- a/src/models/tag/user.rs +++ b/src/models/tag/user.rs @@ -7,6 +7,7 @@ use super::traits::TagCollection; const USER_TAGS_KEY_PARTS: [&str; 2] = ["Users", "Tag"]; +// Define a newtype wrapper #[derive(Serialize, Deserialize, Debug, Clone, ToSchema, Default)] pub struct TagUser; diff --git a/src/reindex.rs b/src/reindex.rs index 341de0d6..db4eefa6 100644 --- a/src/reindex.rs +++ b/src/reindex.rs @@ -89,7 +89,7 @@ pub async fn reindex_post( PostDetails::get_from_graph(author_id, post_id), PostCounts::get_from_graph(author_id, post_id), PostRelationships::get_from_graph(author_id, post_id), - TagPost::get_from_graph(author_id, Some(post_id)) + TagPost::get_from_graph(author_id, post_id) )?; Ok(()) diff --git a/src/routes/v0/post/tags.rs b/src/routes/v0/post/tags.rs index 2bb83dc8..2b4ca5ce 100644 --- a/src/routes/v0/post/tags.rs +++ b/src/routes/v0/post/tags.rs @@ -1,5 +1,4 @@ use crate::models::tag::post::TagPost; -use crate::models::tag::traits::TagCollection; use crate::models::tag::TagDetails; use crate::routes::v0::endpoints::POST_TAGS_ROUTE; use crate::routes::v0::TagsQuery; @@ -28,19 +27,12 @@ use utoipa::OpenApi; pub async fn post_tags_handler( Path((user_id, post_id)): Path<(String, String)>, Query(query): Query, -) -> Result>> { +) -> Result> { info!( "GET {POST_TAGS_ROUTE} user_id:{}, post_id: {}, limit_tags:{:?}, limit_taggers:{:?}", user_id, post_id, query.limit_tags, query.limit_taggers ); - match TagPost::get_by_id( - &user_id, - Some(&post_id), - query.limit_tags, - query.limit_taggers, - ) - .await - { + match TagPost::get_by_id(&user_id, &post_id, query.limit_tags, query.limit_taggers).await { Ok(Some(tags)) => Ok(Json(tags)), Ok(None) => Err(Error::UserNotFound { user_id }), Err(source) => Err(Error::InternalServerError { source }), @@ -48,5 +40,5 @@ pub async fn post_tags_handler( } #[derive(OpenApi)] -#[openapi(paths(post_tags_handler), components(schemas(TagDetails)))] +#[openapi(paths(post_tags_handler), components(schemas(TagPost, TagDetails)))] pub struct PostTagsApiDoc;