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

Configure Feed

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

perf(search): searchActors from 12sec to 200ms

+63 -18
+23 -3
api/so/sprk/actor/searchActors.ts
··· 25 25 server.so.sprk.actor.searchActors({ 26 26 auth: ctx.authVerifier.standardOptional, 27 27 handler: async ({ auth, params, req }) => { 28 + const cleanedQuery = params.q?.trim() ?? ""; 29 + const labelers = ctx.reqLabelers(req); 30 + if (!cleanedQuery) { 31 + return { 32 + encoding: "application/json", 33 + body: { 34 + actors: [], 35 + }, 36 + headers: resHeaders({ labelers }), 37 + }; 38 + } 39 + 28 40 const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); 29 - const labelers = ctx.reqLabelers(req); 30 41 const hydrateCtx = await ctx.hydrator.createContext({ 31 42 viewer, 32 43 labelers, 33 44 includeTakedowns, 34 45 }); 35 - const results = await searchActors({ ...params, hydrateCtx }, ctx); 46 + const results = await searchActors({ 47 + ...params, 48 + q: cleanedQuery, 49 + hydrateCtx, 50 + }, ctx); 36 51 return { 37 52 encoding: "application/json", 38 53 body: results, ··· 46 61 47 62 const skeleton = async (inputs: SkeletonFnInput<Context, Params>) => { 48 63 const { ctx, params } = inputs; 49 - const term = params.q ?? ""; 64 + const term = params.q?.trim() ?? ""; 65 + if (!term) { 66 + return { 67 + dids: [], 68 + }; 69 + } 50 70 51 71 const res = await ctx.dataplane.search.actors( 52 72 term,
+6 -1
data-plane/db/models.ts
··· 94 94 height: number; 95 95 }; 96 96 } 97 + export interface StoryEmbed { 98 + $type: string; 99 + [key: string]: unknown; 100 + } 97 101 export interface Caption { 98 102 text: string; 99 103 facets?: Facet[]; ··· 227 231 followersCount: { type: Number, required: true, default: 0 }, 228 232 followsCount: { type: Number, required: true, default: 0 }, 229 233 }) 230 - .index({ displayName: "text", description: "text" }); 234 + .index({ displayName: "text", description: "text" }) 235 + .index({ indexedAt: -1, authorDid: -1 }); 231 236 232 237 // audio 233 238
+34 -14
data-plane/routes/search.ts
··· 19 19 20 20 async actors(term: string, limit = 50, cursor?: string) { 21 21 const cleanedTerm = cleanQuery(term); 22 - const regex = new RegExp(cleanedTerm, "i"); 22 + if (!cleanedTerm) { 23 + return { 24 + dids: [], 25 + cursor: undefined, 26 + }; 27 + } 23 28 24 - // First, find Actor DIDs that match the handle 25 - const matchingActors = await this.db.models.Actor.find({ 26 - handle: { $regex: regex }, 27 - }).select("did").lean(); 28 - const actorDids = matchingActors.map((actor) => actor.did); 29 + const handlePrefix = cleanedTerm.toLowerCase(); 30 + const handleRangeEnd = `${handlePrefix}\uffff`; 29 31 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 - ]; 32 + const [matchingActors, matchingProfiles] = await Promise.all([ 33 + this.db.models.Actor.find({ 34 + handle: { 35 + $gte: handlePrefix, 36 + $lt: handleRangeEnd, 37 + }, 38 + }).select("did -_id").lean(), 39 + this.db.models.Profile.find({ 40 + $text: { $search: cleanedTerm }, 41 + }).select("authorDid -_id").lean(), 42 + ]); 34 43 35 - if (actorDids.length > 0) { 36 - queryConditions.push({ authorDid: { $in: actorDids } }); 44 + const matchingActorDids = matchingActors.map((actor) => actor.did); 45 + const matchingProfileDids = matchingProfiles.map((profile) => 46 + profile.authorDid 47 + ); 48 + const dids = Array.from( 49 + new Set([...matchingActorDids, ...matchingProfileDids]), 50 + ); 51 + 52 + if (dids.length === 0) { 53 + return { 54 + dids: [], 55 + cursor: undefined, 56 + }; 37 57 } 38 58 39 59 const profilesQuery = this.db.models.Profile.find({ 40 - $or: queryConditions, 41 - }).populate("actor"); 60 + authorDid: { $in: dids }, 61 + }).select("authorDid indexedAt -_id"); 42 62 43 63 const paginatedQuery = this.indexedAtDidKeyset.paginate( 44 64 profilesQuery,