Búsqueda y Descubrimiento — Referencia técnica
← Volver a Búsqueda y Descubrimiento
Dónde vive esto
Backend
apps/backend/graphql/resolvers/user-search-discovery.resolver.js— resuelvesearchUsers,suggestedUsers,getTrendingUsers,getRecommendedUsers,getSearchStats,getDiscoveryInsights,refreshRecommendations,hideFromSuggestions,reportSearchResultapps/backend/graphql/resolvers/user-recommendations.resolver.js— resuelveusersYouMayKnow/similarUsers/dismissSuggestion, coincide congraphql/types/user-recommendations.type.js. Este archivo de resolver existe y su forma de retorno ({ peopleYouMayKnow, limit, offset, total, hasMore }/{ similarUsers, ... }) coincide exactamente con el schema — confirmado funcionando, no es un stub.apps/backend/graphql/types/user-presence.type.js— también declaragetRecommendedUsers,getDiscoveryInsights,getSearchStats(declaraciones de campos duplicadas entre dos archivos de tipos que apuntan a los mismos métodos del manager), másgetBulkOperationStatus/getBulkOperationHistory/getBulkOperationLimits/cancelBulkOperationapps/backend/graphql/types/user.type.js— también declaraUser.isFollowing: BooleanyUser.followContext: MutualFollowContext({ count, sample: User }), resuelto por campo engraphql/resolvers/user-fields.resolver.js—followContextcalcula la prueba social para las tarjetas de sugerencias (personas que sigue quien mira que también siguen a este usuario), null para quienes llaman de forma anónima o al ver el propio perfilapps/backend/graphql/types/user.type.js— declarabulkFollowUsers(userIds: [ID!]!)/bulkUnfollowUsers(userIds: [ID!]!)que devuelvenBulkOperationResponse(success,message,successCount,failedCount,total,errors: [BulkError!])apps/backend/graphql/types/social-actions.type.js— declarabulkBlockUsers(userIds: [ID!]!),bulkRemoveFollowers(followerIds: [ID!]!),bulkProcessFollowRequests(requestIds: [ID!]!, action: String!)que devuelvenBulkOperationResult(success,message,processedCount,failedCount,operationId)apps/backend/graphql/resolvers/user-bulk-operations.resolver.js— resuelve las mutations de arriba másgetBulkOperationLimits/getBulkOperationHistory/getBulkOperationStatusapps/backend/managers/user-managers/search-discovery.manager.js— lógica de ranking/búsqueda detrás de las recomendacionesapps/backend/managers/user-managers/bulk-operations.manager.js— lógica de negocio de las acciones en lote
Frontend
apps/frontend-nextjs/src/components/SuggestedUsers.tsx—suggestedUsers(seleccionaisFollowingyfollowContext { count sample { id username firstName profilePicture } }para la etiqueta de prueba social "Seguido por"); su enlace "Ver todos" va a/discover-peopleapps/frontend-nextjs/src/components/discovery/RecommendationsSection.tsx—usersYouMayKnow,similarUsers,getTrendingUsers,getRecommendedUsers,nearbyUsers,dismissSuggestion; se renderiza dentro depage-components/ExplorePage.tsxarriba de la cuadrícula de descubrimiento. Corregido en esta sesión: las pestañas de categoría (Recomendados / Quizás conozcas / Similares / Tendencia / Cercanos) antes estaban conectadas pero ocultas —activeTabestaba fijado a'recommended', así que sologetRecommendedUsersllegaba a renderizarse. Ahora hay una barra de pestañas real (role="tablist", arregloTABS, estado de clienteactiveTabmediantesetActiveTab) que permite al usuario alternar entre las cinco, cada una respaldada por su propia query (skip: activeTab !== '<tab>'en cadauseQuery), de modo que solo se ejecuta la query de la pestaña activa.apps/frontend-nextjs/src/page-components/DiscoverPeoplePage.tsx— versión de página completa de la misma familia de recomendaciones (getRecommendedUsers,similarUsers,usersYouMayKnow,getTrendingUsers,nearbyUsers), cada una renderizada como su propia sección siempre visible en lugar de una sola franja con pestañas; enrutada enapp/discover-people/page.tsx(/discover-people, solo para usuarios autenticados)apps/frontend-nextjs/src/page-components/settings/BulkFollowerActionsPage.tsx—bulkFollowUsers/bulkUnfollowUsers/bulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequests, en Configuración → Acciones en lote (/settings/bulk-follower-actions)apps/frontend-nextjs/src/page-components/settings/AccountPrivacyPage.tsx— Corregido en esta sesión:hideFromSuggestions, en Configuración → Privacidad → Mostrar en Explorar — ver la nota más abajosearchUserstambién se llama desde:components/chat/ConversationList.tsx(iniciar una nueva conversación),components/CreatePostModal.tsx(etiquetar personas),page-components/settings/StoryLivePage.tsx(ocultar historia de).app/search/page.tsxahora se bifurca según el estado de autenticación: los usuarios autenticados obtienenExplorePage(con una nueva prophideRecommendationsque suprimeRecommendationsSection, ya que los resultados la reemplazan), los visitantes sin sesión obtienenpage-components/PublicSearchPage.tsx, una página de resultados de solo lectura consearchUsers+searchHashtags.
Un bug preexistente del backend corregido al conectar esta página
Las mutations de user-bulk-operations.resolver.js estaban escritas para desestructurar { input } de sus argumentos de GraphQL y leer input.userIds / input.requestIds / input.action. Pero el schema real (user.type.js / social-actions.type.js) declara cada mutation en lote con argumentos planos (userIds: [ID!]!, followerIds: [ID!]!, requestIds: [ID!]!, action: String!) — no hay ningún envoltorio input en ninguna parte del schema. Llamar a cualquiera de estas mutations tal como estaban escritas originalmente habría lanzado Cannot read properties of undefined (reading 'userIds') de inmediato. Por separado, la forma de retorno del manager ({ success, message, results: { success, failed, skipped, errors } }) no coincidía ni con BulkOperationResponse ni con BulkOperationResult, cuyos campos planos y no-nulos (successCount!, failedCount!, total!, processedCount!) habrían lanzado un error de GraphQL "Cannot return null for non-nullable field". Ambos problemas se corrigieron directamente en el resolver (ver los helpers mapToBulkOperationResponse / mapToBulkOperationResult en el archivo) como parte de construir el frontend de esta página — no era una función preexistente "terminada" como asumían notas anteriores.
getBulkOperationHistory y getBulkOperationStatus llaman a userManager.getBulkOperationHistory / userManager.getBulkOperationStatus, pero ninguno de los dos métodos existe en bulk-operations.manager.js (solo existe getBulkOperationLimits) — no hay ninguna capa de persistencia de seguimiento de operaciones detrás de estos dos campos ni de cancelBulkOperation. Se dejaron tal cual (siguen lanzando un error limpio de GraphQL si alguna vez se llaman) pero el frontend deliberadamente no los llama; las acciones en lote se ejecutan de forma síncrona y reportan su resultado en línea en vez de con una barra de progreso.
hideFromSuggestions ahora está conectada al interruptor "Mostrar en Explorar"
Corregido en esta sesión: el schema (user-presence.type.js) declara desde hace tiempo la única mutation hideFromSuggestions(enabled: Boolean!): Boolean!, resuelta (user-search-discovery.resolver.js) hacia search-discovery.manager.js#hideFromSuggestions/#showInSuggestions según el valor de enabled — pero ninguna pantalla del frontend la llamaba. El interruptor Configuración → Privacidad → Mostrar en Explorar de AccountPrivacyPage.tsx antes persistía mediante la mutation genérica updatePrivacySettings, que cambia la misma bandera subyacente appear_in_suggestions pero no invalida los resultados de recomendaciones en caché de quien llama. El manejador del interruptor (handleToggleShowInExplore) ahora llama en su lugar a la mutation dedicada hideFromSuggestions (enabled: !nextValue, ya que el enabled de la mutation significa "ocúltame" mientras que el propio valor del interruptor significa "muéstrame"), así que desactivarlo también invalida la caché de inmediato en lugar de esperar a que expire de forma natural.
Checklist de implementación técnica
-
searchUsers— conectado; tres puntos de llamada distintos, la barra de búsqueda de Explorar, yPublicSearchPage.tsxpara visitantes sin sesión -
interestSuggestions(limit, interests)+updateInterests(interests)— sugerencias basadas en intereses;user.intereststext[] (atributo del modelointerestTags), coincidencia por superposición congetUsersByInterests(migración20260721120000) -
suggestedUsers— conectado;SuggestedUsers.tsx, ahora con datos de prueba socialisFollowing+followContext -
usersYouMayKnow/similarUsers— conectado;RecommendationsSection.tsxyDiscoverPeoplePage.tsx -
getRecommendedUsers/getTrendingUsers/nearbyUsers— conectado;RecommendationsSection.tsx(las cinco pestañas — Recomendados/Quizás conozcas/Similares/Tendencia/Cercanos — ahora se renderizan mediante una barra de pestañas real, corregido en esta sesión, antes solo se renderizabagetRecommendedUsersporqueactiveTabestaba fijado) yDiscoverPeoplePage.tsx(todas se renderizan) -
dismissSuggestion(userId)— conectado; el botón de descartar (X) por tarjeta deRecommendationsSection.tsx -
hideFromSuggestions— Corregido en esta sesión: el interruptor Configuración → Privacidad → Mostrar en Explorar deAccountPrivacyPage.tsxahora llama a la mutation dedicadahideFromSuggestionsen lugar del campo genéricoupdatePrivacySettings— ver la nota más arriba -
bulkFollowUsers/bulkUnfollowUsers/bulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequests— conectado;BulkFollowerActionsPage.tsx(bug del resolver del backend corregido como parte de este trabajo — ver arriba) -
getBulkOperationHistory/getBulkOperationStatus/cancelBulkOperation— ahora implementado: una tablabulk_operation+ modelo + servicio de acceso; cada acción en lote registra conteos/estado reales, el estado/historial leen las filas reales, y cancelar devuelve false para operaciones (síncronas) ya finalizadas
Búsqueda
query SearchUsers($query: String!, $limit: Int, $offset: Int, $filters: JSON) {
searchUsers(query: $query, limit: $limit, offset: $offset, filters: $filters) {
id username profilePicture isVerified
}
}
query SuggestedUsers($limit: Int, $offset: Int, $category: String) {
suggestedUsers(limit: $limit, offset: $offset, category: $category) {
id username profilePicture
}
}
Recomendaciones
query UsersYouMayKnow($limit: Int, $offset: Int) {
usersYouMayKnow(limit: $limit, offset: $offset) {
peopleYouMayKnow {
user { id username }
matchScore
reasons { type mutualConnectionsCount confidence }
}
total
hasMore
}
}
query SimilarUsers($limit: Int, $offset: Int) {
similarUsers(limit: $limit, offset: $offset) {
similarUsers { user { id username } similarityScore sharedInterestScore activitySimilarity }
total
hasMore
}
}
query GetTrendingUsers($limit: Int, $timeframe: String) {
getTrendingUsers(limit: $limit, timeframe: $timeframe) {
user { username }
trendingReason
}
}
query GetRecommendedUsers($limit: Int, $offset: Int, $algorithm: String) {
getRecommendedUsers(limit: $limit, offset: $offset, algorithm: $algorithm) { username }
}
query NearbyUsers($radiusKm: Float, $limit: Int) {
nearbyUsers(radiusKm: $radiusKm, limit: $limit) { user { id username } city distanceKm }
}
mutation DismissSuggestion($userId: ID!) { dismissSuggestion(userId: $userId) }
mutation HideFromSuggestions($enabled: Boolean!) { hideFromSuggestions(enabled: $enabled) }
dismissSuggestion(userId) (declarada en user-recommendations.type.js, resuelta por user-recommendations.resolver.js hacia search-discovery.manager.js#dismissSuggestion) elimina a ese usuario de las sugerencias de "personas que quizás conozcas" de quien llama, de ahí en adelante; RecommendationsSection.tsx también oculta la tarjeta del lado del cliente de inmediato, antes de que la mutation se resuelva.
hideFromSuggestions(enabled: true) es un interruptor sobre uno mismo — oculta a quien lo llama de las listas de sugerencias de otros usuarios (persiste appear_in_suggestions: false); no oculta a un usuario específico de las propias sugerencias de quien lo llama, y no afecta a searchUsers (la búsqueda directa no se filtra por esta bandera hoy). Corregido en esta sesión: el interruptor Configuración → Privacidad → Mostrar en Explorar de AccountPrivacyPage.tsx ahora llama a esta mutation (ver la nota más arriba) — antes ninguna pantalla del frontend lo hacía.
suggestedUsers y las tarjetas de sugerencias en otros lugares también pueden seleccionar User.isFollowing y User.followContext { count sample { id username } } — followContext devuelve datos de prueba social ("Seguido por...") sobre cuántas cuentas que sigue quien mira también siguen a este usuario, más un usuario de muestra para la etiqueta. Ambos campos se resuelven como null para quienes llaman de forma anónima o al ver el propio perfil.
Acciones en lote sobre relaciones
mutation BulkFollowUsers($userIds: [ID!]!) {
bulkFollowUsers(userIds: $userIds) { success message successCount failedCount total errors { userId reason } }
}
mutation BulkUnfollowUsers($userIds: [ID!]!) {
bulkUnfollowUsers(userIds: $userIds) { success message successCount failedCount total errors { userId reason } }
}
mutation BulkBlockUsers($userIds: [ID!]!) {
bulkBlockUsers(userIds: $userIds) { success message processedCount failedCount }
}
mutation BulkRemoveFollowers($followerIds: [ID!]!) {
bulkRemoveFollowers(followerIds: $followerIds) { success message processedCount failedCount }
}
mutation BulkProcessFollowRequests($requestIds: [ID!]!, $action: String!) {
bulkProcessFollowRequests(requestIds: $requestIds, action: $action) { success message processedCount failedCount }
}
action para bulkProcessFollowRequests es 'accept' o 'reject', aplicado a cada id en requestIds — el manager en sí soporta acciones mixtas por solicitud ({requestId, action}[]), pero el campo de GraphQL solo expone la forma de una sola acción para muchos ids, así que el resolver expande requestIds a ese arreglo del lado del servidor.