From 20087f825b48e8dd274271d3a1e31bb80961af46 Mon Sep 17 00:00:00 2001 From: Roscoe Rubin-Rottenberg Date: Tue, 3 Mar 2026 17:10:18 -0500 Subject: [PATCH] perf(search): searchActors from 12sec to 200ms --- api/so/sprk/actor/searchActors.ts | 26 ++++++++++++++-- data-plane/db/models.ts | 7 ++++- data-plane/routes/search.ts | 50 +++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/api/so/sprk/actor/searchActors.ts b/api/so/sprk/actor/searchActors.ts index 032237a..1b461f6 100644 --- a/api/so/sprk/actor/searchActors.ts +++ b/api/so/sprk/actor/searchActors.ts @@ -25,14 +25,29 @@ export default function (server: Server, ctx: AppContext) { server.so.sprk.actor.searchActors({ auth: ctx.authVerifier.standardOptional, handler: async ({ auth, params, req }) => { - const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); + const cleanedQuery = params.q?.trim() ?? ""; const labelers = ctx.reqLabelers(req); + if (!cleanedQuery) { + return { + encoding: "application/json", + body: { + actors: [], + }, + headers: resHeaders({ labelers }), + }; + } + + const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth); const hydrateCtx = await ctx.hydrator.createContext({ viewer, labelers, includeTakedowns, }); - const results = await searchActors({ ...params, hydrateCtx }, ctx); + const results = await searchActors({ + ...params, + q: cleanedQuery, + hydrateCtx, + }, ctx); return { encoding: "application/json", body: results, @@ -46,7 +61,12 @@ export default function (server: Server, ctx: AppContext) { const skeleton = async (inputs: SkeletonFnInput) => { const { ctx, params } = inputs; - const term = params.q ?? ""; + const term = params.q?.trim() ?? ""; + if (!term) { + return { + dids: [], + }; + } const res = await ctx.dataplane.search.actors( term, diff --git a/data-plane/db/models.ts b/data-plane/db/models.ts index 57d2253..a437b7d 100644 --- a/data-plane/db/models.ts +++ b/data-plane/db/models.ts @@ -94,6 +94,10 @@ export interface StoryMedia { height: number; }; } +export interface StoryEmbed { + $type: string; + [key: string]: unknown; +} export interface Caption { text: string; facets?: Facet[]; @@ -227,7 +231,8 @@ export const profileSchema = new Schema({ followersCount: { type: Number, required: true, default: 0 }, followsCount: { type: Number, required: true, default: 0 }, }) - .index({ displayName: "text", description: "text" }); + .index({ displayName: "text", description: "text" }) + .index({ indexedAt: -1, authorDid: -1 }); // audio diff --git a/data-plane/routes/search.ts b/data-plane/routes/search.ts index 5dbbaff..7b3b76c 100644 --- a/data-plane/routes/search.ts +++ b/data-plane/routes/search.ts @@ -19,26 +19,46 @@ export class Search { async actors(term: string, limit = 50, cursor?: string) { const cleanedTerm = cleanQuery(term); - const regex = new RegExp(cleanedTerm, "i"); - - // First, find Actor DIDs that match the handle - const matchingActors = await this.db.models.Actor.find({ - handle: { $regex: regex }, - }).select("did").lean(); - const actorDids = matchingActors.map((actor) => actor.did); + if (!cleanedTerm) { + return { + dids: [], + cursor: undefined, + }; + } - // Build a single Profile query that searches displayName or handle (via authorDid) - const queryConditions: Array> = [ - { displayName: { $regex: regex } }, - ]; + const handlePrefix = cleanedTerm.toLowerCase(); + const handleRangeEnd = `${handlePrefix}\uffff`; + + const [matchingActors, matchingProfiles] = await Promise.all([ + this.db.models.Actor.find({ + handle: { + $gte: handlePrefix, + $lt: handleRangeEnd, + }, + }).select("did -_id").lean(), + this.db.models.Profile.find({ + $text: { $search: cleanedTerm }, + }).select("authorDid -_id").lean(), + ]); + + const matchingActorDids = matchingActors.map((actor) => actor.did); + const matchingProfileDids = matchingProfiles.map((profile) => + profile.authorDid + ); + const dids = Array.from( + new Set([...matchingActorDids, ...matchingProfileDids]), + ); - if (actorDids.length > 0) { - queryConditions.push({ authorDid: { $in: actorDids } }); + if (dids.length === 0) { + return { + dids: [], + cursor: undefined, + }; } const profilesQuery = this.db.models.Profile.find({ - $or: queryConditions, - }).populate("actor"); + authorDid: { $in: dids }, + }).select("authorDid indexedAt -_id"); const paginatedQuery = this.indexedAtDidKeyset.paginate( profilesQuery, -- 2.51.2