[READ ONLY MIRROR] Spark Social AppView Server github.com/sprksocial/server
atproto deno hono lexicon
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

search refined

+53 -26
+8 -8
data-plane/db/pagination.ts
··· 174 174 175 175 export class CreatedAtDidKeyset extends TimeCidKeyset<{ 176 176 createdAt: string; 177 - did: string; // dids are treated identically to cids in TimeCidKeyset 177 + authorDid: string; // dids are treated identically to cids in TimeCidKeyset 178 178 }> { 179 179 constructor() { 180 180 super(); 181 181 this.primary = "createdAt"; 182 - this.secondary = "did"; 182 + this.secondary = "authorDid"; 183 183 } 184 184 185 - override labelResult(result: { createdAt: string; did: string }) { 185 + override labelResult(result: { createdAt: string; authorDid: string }) { 186 186 // Use current time as fallback if createdAt is missing 187 187 const createdAt = result.createdAt || new Date().toISOString(); 188 - return { primary: createdAt, secondary: result.did }; 188 + return { primary: createdAt, secondary: result.authorDid }; 189 189 } 190 190 } 191 191 192 192 export class IndexedAtDidKeyset extends TimeCidKeyset<{ 193 193 indexedAt: string; 194 - did: string; // dids are treated identically to cids in TimeCidKeyset 194 + authorDid: string; // dids are treated identically to cids in TimeCidKeyset 195 195 }> { 196 196 constructor() { 197 197 super(); 198 198 this.primary = "indexedAt"; 199 - this.secondary = "did"; 199 + this.secondary = "authorDid"; 200 200 } 201 201 202 - override labelResult(result: { indexedAt: string; did: string }) { 202 + override labelResult(result: { indexedAt: string; authorDid: string }) { 203 203 // Use current time as fallback if indexedAt is missing 204 204 const indexedAt = result.indexedAt || new Date().toISOString(); 205 - return { primary: indexedAt, secondary: result.did }; 205 + return { primary: indexedAt, secondary: result.authorDid }; 206 206 } 207 207 } 208 208
+45 -18
data-plane/routes/search.ts
··· 17 17 this.timeCidKeyset = new TimeCidKeyset(); 18 18 } 19 19 20 - // @TODO actor search endpoints still fall back to search service 21 20 async actors(term: string, limit = 50, cursor?: string) { 22 21 const cleanedTerm = cleanQuery(term); 23 22 const regex = new RegExp(cleanedTerm, "i"); 24 23 25 - const actorsQuery = this.db.models.Actor.find({ 24 + // First, find Actor DIDs that match the handle 25 + const matchingActors = await this.db.models.Actor.find({ 26 26 handle: { $regex: regex }, 27 - }); 27 + }).select("did").lean(); 28 + const actorDids = matchingActors.map((actor) => actor.did); 28 29 29 - const paginatedQuery = this.indexedAtDidKeyset.paginate(actorsQuery, { 30 - limit, 31 - cursor, 32 - direction: "desc", 33 - }); 30 + // Build a single Profile query that searches displayName or handle (via authorDid) 31 + const queryConditions: Array<Record<string, unknown>> = [ 32 + { displayName: { $regex: regex } }, 33 + ]; 34 + 35 + if (actorDids.length > 0) { 36 + queryConditions.push({ authorDid: { $in: actorDids } }); 37 + } 38 + 39 + const profilesQuery = this.db.models.Profile.find({ 40 + $or: queryConditions, 41 + }).populate("actor"); 42 + 43 + const paginatedQuery = this.indexedAtDidKeyset.paginate( 44 + profilesQuery, 45 + { 46 + limit: limit + 1, // Fetch one extra to check if more results exist 47 + cursor, 48 + direction: "desc", 49 + }, 50 + ); 51 + 52 + const profiles = await paginatedQuery.exec(); 34 53 35 - const actors = await paginatedQuery.exec(); 54 + // Check if there are more results 55 + const hasMore = profiles.length > limit; 56 + const results = hasMore ? profiles.slice(0, limit) : profiles; 36 57 37 - // Generate cursor from the last item if we have a full page 58 + // Generate cursor from the last item if we have more results 38 59 let nextCursor: string | undefined; 39 - if (actors.length === limit && actors.length > 0) { 40 - const lastActor = actors[actors.length - 1]; 60 + if (hasMore && results.length > 0) { 61 + const lastProfile = results[results.length - 1]; 41 62 nextCursor = this.indexedAtDidKeyset.pack({ 42 - primary: lastActor.indexedAt, 43 - secondary: lastActor.did, 63 + primary: lastProfile.indexedAt, 64 + secondary: lastProfile.authorDid, 44 65 }); 45 66 } 46 67 47 68 return { 48 - dids: actors.map((actor) => actor.did), 69 + dids: results.map((profile: { authorDid: string; indexedAt: string }) => 70 + profile.authorDid 71 + ), 49 72 cursor: nextCursor, 50 73 }; 51 74 } 52 75 53 - // @TODO post search endpoint still falls back to search service 54 76 async posts(term: string, limit = 50, cursor?: string) { 55 77 const { q, author } = parsePostSearchQuery(term); 56 78 ··· 66 88 const query: Record<string, unknown> = {}; 67 89 68 90 if (q) { 69 - // Search in caption.text using regex 70 - query["caption.text"] = { $regex: q, $options: "i" }; 91 + // Search in multiple fields for better relevance 92 + query.$or = [ 93 + { "caption.text": { $regex: q, $options: "i" } }, 94 + { "media.images.alt": { $regex: q, $options: "i" } }, 95 + { "media.video.alt": { $regex: q, $options: "i" } }, 96 + { tags: { $regex: q, $options: "i" } }, 97 + ]; 71 98 } 72 99 73 100 if (authorDid) {