Skip to content

Commit 5763797

Browse files
committed
Improve recommendations endpoint
1 parent f2c4ecc commit 5763797

2 files changed

Lines changed: 95 additions & 13 deletions

File tree

src/http_server/handlers.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,52 @@ use super::router::ApiError;
22
use super::AppState;
33
use crate::relay_subscriber::GetEventsOf;
44
use crate::repo::{Recommendation, RepoTrait};
5-
use axum::{extract::State, http::HeaderMap, response::Html, response::IntoResponse, Json};
5+
use axum::{
6+
extract::{Query, State},
7+
http::HeaderMap,
8+
response::Html,
9+
response::IntoResponse,
10+
Json,
11+
};
612
use nostr_sdk::PublicKey;
13+
use serde::Deserialize;
714
use std::sync::Arc;
815

16+
#[derive(Debug, Deserialize)]
17+
pub struct RecommendationParams {
18+
min_pagerank: Option<f64>,
19+
limit: Option<usize>,
20+
}
21+
922
pub async fn cached_get_recommendations<T, U>(
1023
State(state): State<Arc<AppState<T, U>>>,
1124
axum::extract::Path(pubkey): axum::extract::Path<String>,
25+
Query(params): Query<RecommendationParams>,
1226
) -> Result<Json<Vec<Recommendation>>, ApiError>
1327
where
1428
T: RepoTrait,
1529
U: GetEventsOf,
1630
{
1731
let public_key = PublicKey::from_hex(&pubkey).map_err(ApiError::InvalidPublicKey)?;
18-
if let Some(cached_recommendation_result) = state.recommendation_cache.get(&pubkey).await {
32+
33+
// Create cache key that includes parameters
34+
let cache_key = format!(
35+
"{}:{}:{}",
36+
pubkey,
37+
params.min_pagerank.unwrap_or(0.3),
38+
params.limit.unwrap_or(50)
39+
);
40+
41+
if let Some(cached_recommendation_result) = state.recommendation_cache.get(&cache_key).await {
1942
return Ok(Json(cached_recommendation_result));
2043
}
2144

22-
let recommendations = get_recommendations(&state.repo, public_key).await?;
45+
let recommendations =
46+
get_recommendations(&state.repo, public_key, params.min_pagerank, params.limit).await?;
2347

2448
state
2549
.recommendation_cache
26-
.insert(pubkey, recommendations.clone())
50+
.insert(cache_key, recommendations.clone())
2751
.await;
2852

2953
Ok(Json(recommendations))
@@ -32,11 +56,13 @@ where
3256
async fn get_recommendations<T>(
3357
repo: &Arc<T>,
3458
public_key: PublicKey,
59+
min_pagerank: Option<f64>,
60+
limit: Option<usize>,
3561
) -> Result<Vec<Recommendation>, ApiError>
3662
where
3763
T: RepoTrait,
3864
{
39-
repo.get_recommendations(&public_key)
65+
repo.get_recommendations(&public_key, min_pagerank, limit)
4066
.await
4167
.map_err(ApiError::from)
4268
}

src/repo.rs

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ pub trait RepoTrait: Sync + Send + 'static {
103103
fn get_recommendations(
104104
&self,
105105
_pubkey: &PublicKey,
106+
_min_pagerank: Option<f64>,
107+
_limit: Option<usize>,
106108
) -> impl std::future::Future<Output = Result<Vec<Recommendation>, RepoError>> + std::marker::Send
107109
{
108110
async { panic!("Not implemented") }
@@ -458,12 +460,29 @@ impl RepoTrait for Repo {
458460
async fn get_recommendations(
459461
&self,
460462
pubkey: &PublicKey,
463+
min_pagerank: Option<f64>,
464+
limit: Option<usize>,
461465
) -> Result<Vec<Recommendation>, RepoError> {
466+
// Validate and set defaults
467+
let min_pagerank = min_pagerank.unwrap_or(0.3);
468+
if min_pagerank < 0.0 {
469+
return Err(RepoError::InvalidParameter(
470+
"min_pagerank must be non-negative".into(),
471+
));
472+
}
473+
474+
let limit = limit.unwrap_or(50);
475+
if limit == 0 {
476+
return Err(RepoError::InvalidParameter(
477+
"limit must be greater than 0".into(),
478+
));
479+
}
480+
462481
let statement = r#"
463482
// Step 1: Get valid target nodes
464483
MATCH (source:User {pubkey: $pubkey_val})
465484
MATCH (target:User)
466-
WHERE target.pagerank >= 0.3
485+
WHERE target.pagerank >= $min_pagerank
467486
AND NOT EXISTS {
468487
MATCH (source)-[:FOLLOWS]->(target)
469488
}
@@ -473,19 +492,28 @@ impl RepoTrait for Repo {
473492
CALL gds.nodeSimilarity.filtered.stream('filteredGraph', {
474493
sourceNodeFilter: [id(source)],
475494
targetNodeFilter: targetNodeIds,
476-
topK: 10,
495+
topK: $limit,
477496
similarityCutoff: 0.1
478497
})
479498
YIELD node1, node2, similarity
480499
WITH gds.util.asNode(node2) AS targetUser, similarity
500+
501+
// Step 3: Count how many of source's follows also follow the target
502+
OPTIONAL MATCH (source)-[:FOLLOWS]->(mutual:User)-[:FOLLOWS]->(targetUser)
503+
WITH targetUser, similarity, count(DISTINCT mutual) AS mutualFollowCount
504+
481505
RETURN targetUser.pubkey AS target_pubkey,
482506
targetUser.friendly_id AS friendly_id,
483507
similarity,
484-
targetUser.pagerank AS pagerank
508+
targetUser.pagerank AS pagerank,
509+
mutualFollowCount
485510
ORDER BY similarity DESC, pagerank DESC;
486511
"#;
487512

488-
let query = query(statement).param("pubkey_val", pubkey.to_hex());
513+
let query = query(statement)
514+
.param("pubkey_val", pubkey.to_hex())
515+
.param("min_pagerank", min_pagerank)
516+
.param("limit", limit as i64);
489517

490518
let mut records = self
491519
.graph
@@ -507,13 +535,28 @@ impl RepoTrait for Repo {
507535
let pagerank: f64 = row.get::<f64>("pagerank").map_err(|e| {
508536
RepoError::deserialization_with_context(e, "deserializing 'pagerank' field")
509537
})?;
538+
let mutual_follow_count: i64 = row.get::<i64>("mutualFollowCount").unwrap_or(0);
539+
540+
// Determine recommendation reason
541+
let reason_type = if similarity > 0.5 {
542+
RecommendationReason::HighSimilarity
543+
} else if similarity > 0.3 {
544+
RecommendationReason::ModerateSimularity
545+
} else if mutual_follow_count > 5 {
546+
RecommendationReason::FollowedByMany
547+
} else if pagerank > 1.0 {
548+
RecommendationReason::PopularAccount
549+
} else {
550+
RecommendationReason::ModerateSimularity
551+
};
510552

511553
recommendations.push(Recommendation {
512554
pubkey: PublicKey::from_hex(&pubkey)
513555
.map_err(RepoError::GetRecommendationsPubkey)?,
514556
friendly_id,
515557
similarity,
516558
pagerank,
559+
reason_type,
517560
});
518561
}
519562

@@ -710,6 +753,9 @@ pub enum RepoError {
710753

711754
#[error("Failed to remove pubkey: {0}")]
712755
RemovePubkey(neo4rs::Error),
756+
757+
#[error("Invalid parameter: {0}")]
758+
InvalidParameter(String),
713759
}
714760

715761
impl RepoError {
@@ -724,10 +770,20 @@ impl RepoError {
724770
}
725771
}
726772

773+
#[derive(Debug, Serialize, Clone)]
774+
#[serde(rename_all = "snake_case")]
775+
pub enum RecommendationReason {
776+
HighSimilarity, // Similarity score > 0.5
777+
ModerateSimularity, // Similarity score 0.3-0.5
778+
PopularAccount, // High pagerank but lower similarity
779+
FollowedByMany, // Many of your follows also follow them
780+
}
781+
727782
#[derive(Debug, Serialize, Clone)]
728783
pub struct Recommendation {
729-
pubkey: PublicKey,
730-
friendly_id: String,
731-
similarity: f64,
732-
pagerank: f64,
784+
pub pubkey: PublicKey,
785+
pub friendly_id: String,
786+
pub similarity: f64,
787+
pub pagerank: f64,
788+
pub reason_type: RecommendationReason,
733789
}

0 commit comments

Comments
 (0)