candidate-retrieval.js
Overview
services/api/src/modules/match/candidate-retrieval.js implements the deterministic retrieval stage for Match candidates. It is the first Match entry point that touches Mongo, and it only performs coarse filtering plus bounded batching for a single Requirement.
The file keeps compatibility decisions separated. evaluateCandidatePair() from candidate-generation.js remains the only domain authority for final pair evaluation. This module only prepares the candidate set and resolves owner account status data.
[!IMPORTANT] This module is read-only. It does not create or update
Listing,User, or operational records. It only reads Mongo and returns structured Match results.
How it works
generateCandidatesForRequirement() drives the whole flow. It validates the Requirement, resolves owner status, builds a coarse Mongo filter, reads Offers in _id order, and evaluates each row with evaluateCandidatePair().
flowchart TB
req1[Requirement]
gen1[generateCandidatesForRequirement]
own1[resolveSingleOwnerAccountStatus]
elig1[getListingMatchEligibility]
filter1[buildCoarseOfferFilter]
list1[Listing Mongo]
batch1[query batches by _id]
own2[resolveOwnerAccountStatusesBatch]
user1[User Mongo]
eval1[evaluateCandidatePair]
out1[Candidates and meta]
req1 --> gen1
gen1 --> own1
own1 --> user1
gen1 --> elig1
gen1 --> filter1
filter1 --> list1
gen1 --> batch1
batch1 --> list1
batch1 --> own2
own2 --> user1
batch1 --> eval1
eval1 --> out1
The flow is intentionally conservative. The module never uses .skip(). It pages forward with a cursor based on _id, and it fetches one extra row per batch to detect whether more coarse candidates exist.
Key components
These are the main exported functions in this file. They define the retrieval boundary and the coarse filtering rules for Match.
{
"title": "buildCoarseOfferFilter",
"content": "Construye el filtro grueso de Mongo para Offers candidatos de un Requirement. Solo aplica gates de elegibilidad y compatibilidad inequ\u00edvoca.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-retrieval.js",
"badges": [
"public"
]
}
{
"title": "generateCandidatesForRequirement",
"content": "Genera candidatos de Offer para un Requirement. Valida el Requirement, consulta Mongo por lotes, eval\u00faa cada par y devuelve candidates junto con meta.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-retrieval.js",
"badges": [
"public"
]
}
{
"title": "resolveOwnerAccountStatusesBatch",
"content": "Resuelve accountStatus para varios ownerId en una sola consulta a User y devuelve un Map por _id stringificado.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-retrieval.js",
"badges": [
"read-only"
]
}
{
"title": "resolveSingleOwnerAccountStatus",
"content": "Resuelve accountStatus de un solo ownerId. Devuelve un sentinela interno cuando el due\u00f1o no existe o no puede resolverse.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-retrieval.js",
"badges": [
"read-only"
]
}
Data & fields
This module defines constants, a private sentinel, and the output contract for candidate retrieval. The values are fixed in the file and control scan limits, batching, and return metadata.
Input and output contract
[
{
"name": "requirement",
"type": "object",
"required": true,
"description": "Listing con intent=\"requirement\". Puede ser un documento Mongoose o un objeto plano."
},
{
"name": "options",
"type": "object",
"description": "Ajustes de retrieval.",
"children": [
{
"name": "targetCandidateCount",
"type": "number",
"description": "Objetivo de candidatos retenidos. Se limita entre 1 y 200. Default: 50."
},
{
"name": "queryBatchSize",
"type": "number",
"description": "Tamano del lote interno de Mongo. Se limita entre targetCandidateCount y 500. Default: 100."
},
{
"name": "includeBlocked",
"type": "boolean",
"description": "Si es true, tambien retiene evaluaciones con status blocked. Default: false."
},
{
"name": "countsTowardTarget",
"type": "function",
"description": "Predicado que decide si un candidato retenido cuenta para el objetivo. Default: todo retenido cuenta."
},
{
"name": "maxEvaluatedOffers",
"type": "number",
"description": "Techo total de Offers evaluados en una llamada. Default: 1000."
}
]
}
]
[
{
"name": "requirementId",
"type": "string | null",
"required": true,
"description": "Stringified _id del Requirement, o null cuando falta el Requirement."
},
{
"name": "requirementEligibility",
"type": "object",
"required": true,
"description": "Resultado de elegibilidad del Requirement.",
"children": [
{
"name": "eligible",
"type": "boolean",
"required": true,
"description": "Indica si el Requirement puede generar candidatos."
},
{
"name": "reasons",
"type": "string[]",
"required": true,
"description": "Motivos de elegibilidad o invalidez."
},
{
"name": "isPublished",
"type": "boolean",
"required": true,
"description": "Estado de publicación derivado."
},
{
"name": "isPubliclyVisible",
"type": "boolean",
"required": true,
"description": "Visibilidad publica derivada."
},
{
"name": "lifecycleStatus",
"type": "string | null",
"required": true,
"description": "Estado de lifecycle derivado."
},
{
"name": "intent",
"type": "string | null",
"required": true,
"description": "Intent derivado."
},
{
"name": "ownerAvailable",
"type": "boolean",
"required": true,
"description": "Indica si el owner pudo resolverse."
}
]
},
{
"name": "candidates",
"type": "array",
"required": true,
"description": "Evaluaciones devueltas por evaluateCandidatePair()."
},
{
"name": "meta",
"type": "object",
"required": true,
"description": "Contadores y banderas del retrieval.",
"children": [
{
"name": "queriedOfferCount",
"type": "number",
"required": true,
"description": "Numero de Offers leidos desde Mongo para evaluacion."
},
{
"name": "evaluatedCount",
"type": "number",
"required": true,
"description": "Numero de Offers efectivamente evaluados."
},
{
"name": "returnedCount",
"type": "number",
"required": true,
"description": "Numero de evaluaciones retenidas en candidates."
},
{
"name": "countedTowardTarget",
"type": "number",
"description": "Cuantos retenidos avanzaron el objetivo segun countsTowardTarget."
},
{
"name": "targetCandidateCount",
"type": "number",
"required": true,
"description": "Objetivo solicitado por el caller."
},
{
"name": "queryBatchSize",
"type": "number",
"required": true,
"description": "Tamano del lote interno usado."
},
{
"name": "maxEvaluatedOffers",
"type": "number",
"required": true,
"description": "Limite total de evaluaciones aplicado."
},
{
"name": "batchesRead",
"type": "number",
"required": true,
"description": "Numero de batches de Mongo consumidos."
},
{
"name": "exhausted",
"type": "boolean",
"required": true,
"description": "True cuando no quedaron candidatos gruesos y el safety cap no corto."
},
{
"name": "hasMoreCoarseCandidates",
"type": "boolean",
"required": true,
"description": "True cuando quedaban mas candidatos gruesos o el safety cap corto."
},
{
"name": "safetyCapReached",
"type": "boolean",
"required": true,
"description": "True cuando se alcanzo maxEvaluatedOffers."
},
{
"name": "requirementValidationReasons",
"type": "string[]",
"required": true,
"description": "Motivos de validacion del Requirement."
}
]
}
]
Fixed limits and projections
DEFAULT_TARGET_CANDIDATE_COUNT[badge:read-only]50— Objetivo por defecto para candidatos retenidos.MIN_TARGET_CANDIDATE_COUNT[badge:read-only]1— Límite inferior para el objetivo.MAX_TARGET_CANDIDATE_COUNT[badge:read-only]200— Límite superior para el objetivo.DEFAULT_QUERY_BATCH_SIZE[badge:read-only]100— Tamaño de lote por defecto para lectura interna.MAX_QUERY_BATCH_SIZE[badge:read-only]500— Límite superior para el lote interno.DEFAULT_MAX_EVALUATED_OFFERS[badge:read-only]1000— Techo de seguridad para ofertas evaluadas en una llamada.OFFER_SELECT_PROJECTION[badge:read-only]"_id createdBy intent publication.status lifecycle.status meta price location.declaration location.latitude location.longitude"— Proyección de campos queevaluateCandidatePair()realmente consume del lado Offer.UNRESOLVED_OWNER_ACCOUNT_STATUS[badge:read-only]Symbol("match-retrieval:unresolved-owner-account-status")— Sentinela privado para distinguir dueño no resuelto de un owner realmente activo.
[!NOTE]
OFFER_SELECT_PROJECTIONincluyemetaypricecompletos, pero limitalocation. This file only selectsdeclaration,latitude, andlongitudefor geography.
Behaviors
The file is mostly made of small helpers plus one orchestrating function. Each behavior below is grounded in the implementation.
clamp(value, min, max, fallback)— Convierte a número, valida finitud, y devuelve un valor acotado. Si la conversión falla, devuelvefallback.resolveListingId(listing)— DevuelveString(listing._id)cuando existe. Devuelvenullcuando falta_id.buildCoarseOfferFilter(requirement)— Construye el filtro grueso paraListing.find(). Excluye el mismo dueño, excluyeintent: "requirement", exigepublication.status: "published", excluye ciertoslifecycle.status, fijameta.entityType, y restringemeta.operationTypecongetCompatibleOfferOperationTypes().resolveSingleOwnerAccountStatus(ownerId)— ResuelveaccountStatusde un owner conUser.findById(). Devuelve el sentinela interno si el owner falta o no existe.resolveOwnerAccountStatusesBatch(ownerIds)— Resuelve varios owners en una sola consultaUser.find({ _id: { $in: ownerIds } }). Devuelve unMapindexado por_idstringificado.buildEmptyMeta({ targetCandidateCount, queryBatchSize, maxEvaluatedOffers })— Construye metadatos vacíos con contadores en cero y banderas seguras.generateCandidatesForRequirement(requirement, options)— Orquesta validación, lectura por lotes, resolución de owners y evaluación de pares. Devuelve un objeto estructurado conrequirementId,requirementEligibility,candidates, ymeta.
Coarse filter semantics
buildCoarseOfferFilter() only applies unequivocal gates. It does not reimplement domain compatibility. It intentionally leaves propertyType, price, physical fields, amenities, and location search logic out of Mongo filtering.
[!WARNING] The coarse filter is not the final Match decision. Final compatibility still comes from
evaluateCandidatePair().
Candidate retrieval semantics
generateCandidatesForRequirement() validates the Requirement before querying Mongo. It resolves the Requirement owner status first, then calls getListingMatchEligibility() with that status. If the Requirement is invalid, it returns a structured empty result and never queries Listing.
It then scans Listing with _id pagination. Each batch requests queryBatchSize + 1 rows. That extra row is a sentinel for hasMoreCoarseCandidates. The scan advances by the last evaluated _id, even when the candidate is blocked.
Retention and progress counting
Two internal counters serve different purposes.
includeBlockedcontrols whether blocked evaluations are retained incandidates.countsTowardTargetcontrols whether a retained evaluation advancestargetCandidateCount.
This separation appears in the current implementation and is preserved in the returned metadata.
[!CAUTION] A retained candidate does not always count toward the target. This matters when
countsTowardTargetis supplied by the caller.
Workflows
The retrieval flow follows a fixed order. The steps below reflect the actual runtime path in the file.
1. Validate Requirement | Resolve requirementId, owner status, and eligibility before any Listing query.
2. Build coarse filter | Create a Mongo filter for obvious Offer gates only.
3. Read one batch | Query Listing in _id order with a sentinel row.
4. Resolve owners in batch | Load owner accountStatus values for all offers in the batch.
5. Evaluate each pair | Call evaluateCandidatePair() for every fetched Offer.
6. Retain candidates | Keep non-blocked evaluations, and blocked ones only when includeBlocked is true.
7. Stop or continue | Stop at targetCandidateCount, at maxEvaluatedOffers, or when coarse candidates are exhausted.
The scan remains bounded even when the coarse filter is very permissive. It also remains bounded when compatibility rejects most Offers.
Gotchas & edge cases
These details are explicit in the source and affect how callers interpret the result.
[!WARNING]
UNRESOLVED_OWNER_ACCOUNT_STATUSis deliberate. It prevents missing owners from being treated asactiveby fallback logic ingetListingMatchEligibility().
[!CAUTION]
maxEvaluatedOffersis a hard safety cap. The function can stop before reachingtargetCandidateCount.
[!NOTE] The file never uses
.skip(). It advances by_idcursor, which keeps pagination deterministic.
[!WARNING]
includeBlockeddoes not influence stop conditions by itself. OnlycountsTowardTargetcontrols target progress.
[!NOTE] When the Requirement is invalid, the function returns a structured response. It does not throw for ordinary business invalidity.
[!IMPORTANT]
candidate-generation.jsremains the domain authority. This file never duplicates its final compatibility rules inside Mongo filters.
Related
This module is tightly connected to a small set of verified files. Those files define the data models and the domain checks that this file depends on.
services/api/src/models/Listing.js— Mongo source for the candidate Offers.services/api/src/models/User.js— Mongo source for owneraccountStatus.services/api/src/modules/listings/listing-match-eligibility.js— ProvidesgetListingMatchEligibility()andgetCompatibleOfferOperationTypes().services/api/src/modules/match/candidate-generation.js— ProvidesevaluateCandidatePair(), the final compatibility authority.
[!NOTE] The file’s dependency direction is one way.
ListingandUserare read here, and evaluation is delegated outward.