From e8c6d04168c6e9f44a3b9bc996b77e1fc42aad1f Mon Sep 17 00:00:00 2001 From: Timothy Quilling Date: Sat, 6 Dec 2025 14:29:15 -0500 Subject: [PATCH] fix: add indexes, use id cache --- .../down.sql | 9 ++ .../up.sql | 37 +++++++++ parakeet-db/src/schema.rs | 12 +++ parakeet/src/db.rs | 6 +- parakeet/src/db/graph.rs | 82 +++++++++++++++++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 migrations/2025-12-06-192255_add_performance_indexes/down.sql create mode 100644 migrations/2025-12-06-192255_add_performance_indexes/up.sql diff --git a/migrations/2025-12-06-192255_add_performance_indexes/down.sql b/migrations/2025-12-06-192255_add_performance_indexes/down.sql new file mode 100644 index 00000000..ed2a5a07 --- /dev/null +++ b/migrations/2025-12-06-192255_add_performance_indexes/down.sql @@ -0,0 +1,9 @@ +-- Rollback performance indexes + +DROP INDEX IF EXISTS idx_actors_handle_trgm; +DROP INDEX IF EXISTS idx_follows_subject_rkey; +DROP INDEX IF EXISTS idx_follows_actor_rkey; +DROP INDEX IF EXISTS idx_follows_actor_subject; +DROP INDEX IF EXISTS idx_posts_actor_rkey_complete; +DROP INDEX IF EXISTS idx_blocks_subject_rkey; +DROP INDEX IF EXISTS idx_blocks_actor_subject; diff --git a/migrations/2025-12-06-192255_add_performance_indexes/up.sql b/migrations/2025-12-06-192255_add_performance_indexes/up.sql new file mode 100644 index 00000000..2c8de759 --- /dev/null +++ b/migrations/2025-12-06-192255_add_performance_indexes/up.sql @@ -0,0 +1,37 @@ +-- Add performance-critical indexes identified in XRPC endpoint analysis +-- Note: Not using CONCURRENTLY due to TimescaleDB hypertables limitation + +-- 1. Trigram index for fuzzy actor search (used by searchActors) +-- Enables fast similarity matching with handle % 'query' operator +CREATE INDEX IF NOT EXISTS idx_actors_handle_trgm +ON actors USING gin (handle gin_trgm_ops) +WHERE handle IS NOT NULL; + +-- 2. Composite index for getFollowers pagination +-- Optimizes: SELECT ... FROM follows WHERE subject_actor_id = ? ORDER BY rkey DESC +CREATE INDEX IF NOT EXISTS idx_follows_subject_rkey +ON follows (subject_actor_id, rkey DESC); + +-- 3. Composite index for getFollows pagination +-- Optimizes: SELECT ... FROM follows WHERE actor_id = ? ORDER BY rkey DESC +CREATE INDEX IF NOT EXISTS idx_follows_actor_rkey +ON follows (actor_id, rkey DESC); + +-- 4. Composite index for batch relationship checks (getRelationships) +-- Optimizes: WHERE actor_id = ? AND subject_actor_id = ANY(?) +CREATE INDEX IF NOT EXISTS idx_follows_actor_subject +ON follows (actor_id, subject_actor_id); + +-- 5. Composite index for author feed queries (getAuthorFeed, getTimeline) +-- Optimizes: SELECT ... FROM posts WHERE actor_id = ? AND status = 'complete' ORDER BY rkey DESC +CREATE INDEX IF NOT EXISTS idx_posts_actor_rkey_complete +ON posts (actor_id, rkey DESC) +WHERE status = 'complete'; + +-- 6. Similar composite for blocks table +CREATE INDEX IF NOT EXISTS idx_blocks_subject_rkey +ON blocks (subject_actor_id, rkey DESC); + +-- 7. Similar composite for blocks batch checks +CREATE INDEX IF NOT EXISTS idx_blocks_actor_subject +ON blocks (actor_id, subject_actor_id); diff --git a/parakeet-db/src/schema.rs b/parakeet-db/src/schema.rs index 251824a9..d71ef905 100644 --- a/parakeet-db/src/schema.rs +++ b/parakeet-db/src/schema.rs @@ -473,6 +473,17 @@ diesel::table! { } } +diesel::table! { + post_likes (actor_id, rkey) { + actor_id -> Int4, + rkey -> Int8, + post_actor_id -> Int4, + post_rkey -> Int8, + via_repost_actor_id -> Nullable, + via_repost_rkey -> Nullable, + } +} + diesel::table! { postgate_detached (post_actor_id, post_rkey, detached_post_actor_id, detached_post_rkey) { post_actor_id -> Int4, @@ -701,6 +712,7 @@ diesel::allow_tables_to_appear_in_same_query!( mutes, notifications, post_aggregate_stats, + post_likes, postgate_detached, postgates, posts, diff --git a/parakeet/src/db.rs b/parakeet/src/db.rs index 3faaede0..4228c3f5 100644 --- a/parakeet/src/db.rs +++ b/parakeet/src/db.rs @@ -37,9 +37,9 @@ pub use bookmarks::get_user_bookmarks; pub use feeds::{get_author_feed, get_list_feed, get_list_feed_by_ids, get_quotes, get_quotes_by_ids, get_reposted_by, get_reposted_by_ids, get_timeline_posts, get_timeline_posts_by_ids, get_timeline_reposts, get_timeline_reposts_by_ids, AuthorFeedFilter, AuthorFeedItem}; pub use feedgens::{get_actor_feedgens, get_all_feedgen_uris, get_feedgen_service_did}; pub use graph::{ - get_actor_followers, get_actor_follows, get_actor_lists, get_followed_by_batch, - get_following_batch, get_list_id_by_uri, get_list_items, get_mutual_followers, - get_user_blocks, get_user_list_blocks, get_user_list_mutes, get_user_mutes, + get_actor_followers, get_actor_followers_by_id, get_actor_follows, get_actor_follows_by_id, + get_actor_lists, get_followed_by_batch, get_following_batch, get_list_id_by_uri, get_list_items, + get_mutual_followers, get_user_blocks, get_user_list_blocks, get_user_list_mutes, get_user_mutes, }; pub use likes::{get_actor_likes, get_like_state, get_like_states, get_post_likes}; pub use notification_records::{get_follow_record, get_like_record, get_post_record, get_repost_record}; diff --git a/parakeet/src/db/graph.rs b/parakeet/src/db/graph.rs index 0d2899b5..22d22d72 100644 --- a/parakeet/src/db/graph.rs +++ b/parakeet/src/db/graph.rs @@ -150,6 +150,47 @@ pub async fn get_actor_followers( }) } +/// Get followers of an actor with cursor pagination (OPTIMIZED VERSION) +/// +/// Returns list of (rkey, follower_actor_id) tuples ordered by rkey descending. +/// The caller should resolve follower_actor_ids → DIDs via IdCache. +/// +/// This version eliminates the 2x actors table JOINs that the non-optimized version has. +pub async fn get_actor_followers_by_id( + conn: &mut AsyncPgConnection, + subject_actor_id: i32, + cursor_rkey: Option, + limit: u8, +) -> QueryResult> { + #[derive(QueryableByName)] + struct FollowerRow { + #[diesel(sql_type = BigInt)] + rkey: i64, + #[diesel(sql_type = Integer)] + follower_actor_id: i32, + } + + // OPTIMIZED: No actors JOINs! Uses actor_ids directly + diesel::sql_query( + "SELECT DISTINCT ON (f.actor_id) f.rkey, f.actor_id as follower_actor_id + FROM follows f + WHERE f.subject_actor_id = $1 + AND ($2::bigint IS NULL OR f.rkey < $2) + ORDER BY f.actor_id, f.rkey DESC + LIMIT $3" + ) + .bind::(subject_actor_id) + .bind::, _>(cursor_rkey) + .bind::(i64::from(limit)) + .load::(conn) + .await + .map(|rows| { + rows.into_iter() + .map(|r| (r.rkey, r.follower_actor_id)) + .collect() + }) +} + /// Get accounts followed by an actor with cursor pagination /// /// Returns list of (rkey, subject_did) tuples ordered by rkey descending @@ -190,6 +231,47 @@ pub async fn get_actor_follows( }) } +/// Get accounts followed by an actor with cursor pagination (OPTIMIZED VERSION) +/// +/// Returns list of (rkey, subject_actor_id) tuples ordered by rkey descending. +/// The caller should resolve subject_actor_ids → DIDs via IdCache. +/// +/// This version eliminates the 2x actors table JOINs that the non-optimized version has. +pub async fn get_actor_follows_by_id( + conn: &mut AsyncPgConnection, + actor_id: i32, + cursor_rkey: Option, + limit: u8, +) -> QueryResult> { + #[derive(QueryableByName)] + struct FollowRow { + #[diesel(sql_type = BigInt)] + rkey: i64, + #[diesel(sql_type = Integer)] + subject_actor_id: i32, + } + + // OPTIMIZED: No actors JOINs! Uses actor_ids directly + diesel::sql_query( + "SELECT DISTINCT ON (f.subject_actor_id) f.rkey, f.subject_actor_id + FROM follows f + WHERE f.actor_id = $1 + AND ($2::bigint IS NULL OR f.rkey < $2) + ORDER BY f.subject_actor_id, f.rkey DESC + LIMIT $3" + ) + .bind::(actor_id) + .bind::, _>(cursor_rkey) + .bind::(i64::from(limit)) + .load::(conn) + .await + .map(|rows| { + rows.into_iter() + .map(|r| (r.rkey, r.subject_actor_id)) + .collect() + }) +} + /// Get follow relationships for batch queries /// /// Returns (target_did, follower_did, rkey) tuples for actor following others -- 2.51.2