Saltar a contenido

Forrajeo y curación

Expandir y curar el corpus: el Forager rankea candidatos por information scent (estructura bibliométrica determinista, sin IA), con preview de crecimiento antes de comprometer; el Preprocessor normaliza, y los filtros PRISMA acotan el corpus de forma trazable.

Forager

Orquesta el chaining sobre un Source, rankeando candidatos por scent.

El scent mide el acoplamiento bibliométrico (ADR 0020/0022, R4): backward = cuántos corpus-papers listan al candidato en sus referencias; forward = cuántos corpus-papers cita el candidato directamente (citación directa al corpus, Wohlin; robusto ante references_id ralas en el corpus).

El forward chaining opera sobre todas las semillas (is_seed=True, sin filtrar por curation_status) y usa batcheo OR (fetch_citing_batch, ≤50 IDs/lote) para eliminar el patrón N+1.

Uso::

forager = Forager(OpenAlexSource(email="yo@example.com"), depth=1)
preview = forager.preview(corpus)
ranked = forager.chain(corpus)
corpus_expandido = corpus.merge(ranked.corpus)

Attributes:

Name Type Description
source

Source inyectado (OpenAlexSource u otro compatible).

depth

Profundidad de chaining; solo 1 está implementado.

max_candidates

Tope de candidatos en el ranking; None = sin límite.

max_citing_per_paper

Presupuesto de citantes por semilla en el forward chaining; None = sin tope.

Methods:

Name Description
preview

Estima cuántos papers nuevos agregaría un chaining.

chain

Computa candidatos rankeados por information scent.

preview

preview(
    corpus: Corpus, *, direction: Direction = "both"
) -> GrowthPreview

Estima cuántos papers nuevos agregaría un chaining.

Opera solo localmente, sin red. No realiza ninguna llamada a la API en ninguna dirección.

  • Backward: estimación exacta desde references_id local.
  • Forward (dos casos):
  • Si el corpus tiene cited_by_id poblado (pasó por b2g enrich), se cuentan los IDs únicos en cited_by_id que aún no están en el corpus — estimación local exacta sin red.
  • Si cited_by_id está vacío (corpus recién sembrado), el conteo forward no es estimable sin red; forward_requires_fetch=True y by_direction["forward"] vale 0.

NO muta el corpus de entrada.

Parameters:

Name Type Description Default
corpus Corpus

Corpus actual (semillas + curados).

required
direction Direction

'backward', 'forward' o 'both'.

'both'

Returns:

Type Description
GrowthPreview

GrowthPreview con la estimación de crecimiento local.

GrowthPreview

Cuando forward_requires_fetch es True, estimated_new

GrowthPreview

refleja solo el crecimiento estimable localmente (backward).

chain

chain(
    corpus: Corpus,
    *,
    direction: Direction = "both",
    since: date | None = None,
) -> RankedCandidates

Computa candidatos rankeados por information scent.

Opción B (#54): los candidatos backward NO se materializan como filas del corpus — sus IDs salen en RankedCandidates.observed_refs para que el comando CLI los persista en referenced_but_not_fetched. El ranking backward SIGUE presente en RankedCandidates.ranking.

Los candidatos forward SÍ se materializan como filas en RankedCandidates.corpus (con metadata traída por fetch_citing_batch).

ADR 0048 (#270): en direction forward/both, corpus también incluye filas de semillas ya existentes con cited_by_id actualizado (unión con los citantes traídos en esta pasada). No son candidatos nuevos: el merge posterior (Corpus.merge) las fusiona por id con la semilla ya persistida, sin duplicar filas.

Devuelve un RankedCandidates con: - corpus: candidatos forward + actualizaciones de cited_by_id de semillas (no mergeado con el corpus semilla). - ranking: candidatos backward + forward rankeados por scent. - observed_refs: IDs backward observados (no en corpus, tabla auxiliar).

NO muta el corpus de entrada.

Parameters:

Name Type Description Default
corpus Corpus

Corpus actual (no muta).

required
direction Direction

'backward', 'forward' o 'both'.

'both'

Returns:

Type Description
RankedCandidates

RankedCandidates con candidatos, ranking y observed_refs.

RankedCandidates

Bases: BaseModel

Resultado del Forager.chain(): candidatos rankeados por scent.

El corpus contiene los candidatos nuevos materializados (forward: filas reales; backward: vacío — los IDs backward van a observed_refs) MÁS, en direction forward/both, filas de semillas ya existentes con cited_by_id actualizado (ADR 0048, #270). El corpus NO se mergeó con el corpus semilla — eso lo hace el llamador (Corpus.merge, unión de sets en columnas lista — D3, idempotente). El ranking es una lista estable ordenada por scent descendente, con desempate por id ascendente.

Attributes:

Name Type Description
corpus Corpus

Corpus de candidatos materializados con curation_status='candidate' (solo forward; backward = vacío) más las actualizaciones de cited_by_id de semillas (ADR 0048; estas últimas NO son candidatos: ya existen en el corpus semilla, el merge las fusiona por id).

ranking list[tuple[str, float]]

Lista (id, information_scent) ordenada por scent desc. Incluye tanto candidatos forward (materializados) como backward (solo IDs observados, sin fila en corpus). NO incluye las actualizaciones de cited_by_id de semillas (no son candidatos).

observed_refs list[str]

IDs de OpenAlex observados en backward chaining pero NO materializados como filas del corpus (opción B — #54). Son los candidatos backward que se persisten en la tabla auxiliar referenced_but_not_fetched por el comando CLI, no en el corpus. Vacío en chaining puramente forward.

GrowthPreview

Bases: BaseModel

Estimación de cuántos papers agregaría un chaining antes de traerlos.

Permite al investigador controlar el crecimiento del corpus sin hacer fetch (ADR 0008: control de crecimiento).

preview() opera solo localmente, sin red. El crecimiento backward se estima exactamente desde references_id.

Para el crecimiento forward hay dos casos:

  • Si el corpus fue enriquecido (b2g enrich), los papers tienen cited_by_id poblado: se calcula el número de IDs únicos aún no presentes en el corpus — estimación local exacta. En este caso forward_requires_fetch es False y forward_from_cited_by es True.
  • Si cited_by_id está vacío (corpus recién sembrado, sin enrich), el crecimiento forward no es estimable sin red. En este caso forward_requires_fetch es True, forward_from_cited_by es False y by_direction["forward"] vale 0.

Attributes:

Name Type Description
estimated_new int

Estimación total de candidatos nuevos (solo lo que puede calcularse localmente; forward vale 0 cuando forward_requires_fetch es True).

by_direction dict[str, int]

Desglose por dirección (backward/forward). La entrada "forward" vale 0 cuando el crecimiento forward no es estimable sin red.

direction Direction

Dirección pedida (backward/forward/both).

forward_requires_fetch bool

True cuando el crecimiento forward no puede estimarse localmente (corpus sin cited_by_id poblado) y es necesario llamar a chain() para obtener el conteo real. False cuando la dirección es solo backward, o cuando cited_by_id está disponible (estimación local exacta).

forward_from_cited_by bool

True cuando el crecimiento forward se estimó localmente desde cited_by_id (corpus enriquecido con b2g enrich). False en los demás casos.

capped_by_max bool

True cuando el resultado está acotado por max_candidates; False si no se aplicó límite o si el número de candidatos es menor al límite.

Preprocessor

Determinístico e idempotente. Normaliza y aplica thesaurus al Corpus.

Las funciones puras (normalize_row, apply_thesaurus_to_rows) viven en sus módulos; este orquestador reconstruye el Corpus Arrow y actualiza el Manifest.

Ver docs/API.md §6, ADR 0011.

Methods:

Name Description
normalize

Canonicaliza authors_id y language (normalización mínima).

apply_thesaurus

Normaliza keywords con el thesaurus multilingüe curado.

normalize

normalize(
    corpus: Corpus, applied_at: datetime | None = None
) -> Corpus

Canonicaliza authors_id y language (normalización mínima).

Operaciones (decisión b=A): - authors_id: lowercase + quitar acentos + colapso de espacios. - language: trunca al subtag ISO 639-1 primario.

Idempotente: aplicar dos veces == aplicar una. Registra un PreprocRef(name='normalize') en el Manifest con el timestamp applied_at (R2: reloj en la frontera).

Parameters:

Name Type Description Default
corpus Corpus

Corpus a normalizar (no muta).

required
applied_at datetime | None

Timestamp de la operación. Si None, usa datetime.now(UTC) (para uso como librería independiente). La frontera CLI inyecta un único timestamp por invocación.

None

Returns:

Type Description
Corpus

Nuevo Corpus normalizado.

apply_thesaurus

apply_thesaurus(
    corpus: Corpus,
    thesaurus: dict[str, object] | Path,
    applied_at: datetime | None = None,
) -> Corpus

Normaliza keywords con el thesaurus multilingüe curado.

Lee keywords_raw y sobrescribe keywords_id con los conceptos canónicos del thesaurus. Determinista e idempotente (ADR 0011). Registra un PreprocRef(name='apply_thesaurus') en el Manifest con el timestamp applied_at (R2: reloj en la frontera).

Parameters:

Name Type Description Default
corpus Corpus

Corpus a procesar (no muta).

required
thesaurus dict[str, object] | Path

Dict con formato {concepts: {canonical: {aliases_*: [...]}}} o Path a un JSON con esa estructura.

required
applied_at datetime | None

Timestamp de la operación. Si None, usa datetime.now(UTC) (para uso como librería independiente). La frontera CLI inyecta un único timestamp por invocación.

None

Returns:

Type Description
Corpus

Nuevo Corpus con keywords_id poblado.

apply_filters

apply_filters(
    corpus: Corpus,
    criteria: list[FilterCriterion],
    *,
    decided_at: datetime | None = None,
) -> tuple[Corpus, list[FilterStep]]

Encadena varios criterios de filtro y sella Manifest.filters.

Los filtros se aplican en orden; cada uno ve los conteos PRISMA del resultado del anterior. Al final, Manifest.filters se actualiza con todos los pasos.

R2 (ADR 0017 enmendado): decided_at se inyecta desde la frontera (CLI) para que el núcleo no llame al reloj. Si es None, el backend usa datetime.now(UTC) como conveniencia para uso como librería.

Parameters:

Name Type Description Default
corpus Corpus

Corpus inicial (no muta).

required
criteria list[FilterCriterion]

Lista de criterios a aplicar en orden.

required
decided_at datetime | None

Instante de la decisión compartido por todos los pasos. Si es None, el backend usa datetime.now(UTC) como fallback.

None

Returns:

Type Description
tuple[Corpus, list[FilterStep]]

Tupla (corpus_final, [FilterStep, ...]) con todos los pasos.

FilterCriterion

Bases: BaseModel

Criterio de filtro PRISMA.

Attributes:

Name Type Description
field Literal['year', 'type', 'language', 'min_citations']

Campo del Corpus a filtrar.

op Literal['gte', 'lte', 'in', 'not_in', 'eq']

Operador de comparación.

value int | str | list[str]

Valor de referencia (int para year/min_citations; str o list[str] para type/language).