NetSuiteSuiteScriptJavaScript

NetSuite Search, el límite de 4,000 resultados y cómo paginar correctamente

Cuando empiezas a trabajar con NetSuite y haces búsquedas con N/search, probablemente al principio todo parece bastante sencillo.

Creas una búsqueda:

const transactionSearch = search.create({
  type: search.Type.TRANSACTION,
  filters: [['mainline', 'is', 'T']],
  columns: ['internalid', 'tranid', 'trandate']
})

Y recorres los resultados:

transactionSearch.run().each(function (result) {
  const internalId = result.getValue({
    name: 'internalid'
  })

  // Procesar resultado

  return true
})

Funciona perfectamente.

Hasta que la cantidad de registros empieza a crecer.

Ahí aparece uno de esos problemas que no siempre se detectan durante el desarrollo: una búsqueda puede tener miles de resultados, pero run().each() no está diseñada para recorrerlos todos.

El problema con run().each()

El ResultSet obtenido mediante search.run() tiene un límite de 4,000 resultados.

Por ejemplo, imaginemos que tenemos:

35,000 transacciones

pero hacemos:

transactionSearch.run().each(function (result) {
  // ...
  return true
})

No deberíamos asumir que estamos recorriendo las 35,000.

El modelo sería:

35,000 resultados

search.run()

ResultSet

run().each()

máximo 4,000 resultados

Esto puede ser especialmente peligroso porque durante desarrollo quizá tenemos:

500 registros

y todo funciona.

Pero en producción el cliente puede tener:

50,000 registros

y recién entonces descubrimos el problema.

Una primera alternativa: getRange()

Una opción que normalmente aparece es getRange():

const resultSet = transactionSearch.run()

const results = resultSet.getRange({
  start: 0,
  end: 1000
})

Podríamos intentar hacer:

resultSet.getRange({
  start: 0,
  end: 1000
})

resultSet.getRange({
  start: 1000,
  end: 2000
})

resultSet.getRange({
  start: 2000,
  end: 3000
})

Pero aquí hay que tener cuidado.

getRange() trabaja sobre el ResultSet obtenido mediante run(), por lo que no es una solución para eliminar el límite de 4,000 resultados.

Si realmente necesitamos procesar más de 4,000 resultados, debemos utilizar paginación.

La solución: runPaged()

Para grandes volúmenes podemos utilizar:

const pagedData = transactionSearch.runPaged({
  pageSize: 1000
})

Ahora NetSuite divide los resultados en páginas.

Por ejemplo:

25,000 resultados

Página 1  → 1,000
Página 2  → 1,000
Página 3  → 1,000
...
Página 25 → 1,000

Podemos recorrerlas de esta manera:

const pagedData = transactionSearch.runPaged({
  pageSize: 1000
})

pagedData.pageRanges.forEach(function (pageRange) {
  const page = pagedData.fetch({
    index: pageRange.index
  })

  page.data.forEach(function (result) {
    const internalId = result.getValue({
      name: 'internalid'
    })

    // Procesar resultado
  })
})

También podemos conocer previamente cuántos resultados tenemos:

log.audit({
  title: 'Total de resultados',
  details: pagedData.count
})

Esto nos permite trabajar con búsquedas que superan ampliamente los 4,000 resultados.

Un detalle importante: el ordenamiento

Aquí viene una parte que considero incluso más importante que conocer runPaged().

Cuando utilizamos paginación, el orden de los resultados importa.

No conviene hacer una búsqueda paginada sin un ordenamiento determinístico.

Por ejemplo:

search.createColumn({
  name: 'trandate',
  sort: search.Sort.ASC
})

A primera vista parece suficiente.

Pero imaginemos que tenemos:

2026-09-01 → Invoice 100
2026-09-01 → Invoice 101
2026-09-01 → Invoice 102
2026-09-01 → Invoice 103
...

Tenemos muchas transacciones con exactamente la misma fecha.

La fecha por sí sola no identifica de forma única la posición de cada registro.

Por eso, cuando una búsqueda va a ser paginada, prefiero utilizar un campo adicional que permita establecer un orden estable.

Por ejemplo:

columns: [
  search.createColumn({
    name: 'trandate',
    sort: search.Sort.ASC
  }),

  search.createColumn({
    name: 'internalid',
    sort: search.Sort.ASC
  })
]

De esta manera tenemos:

trandate ASC
internalid ASC

La fecha establece el orden principal y internalid funciona como criterio de desempate.

¿Por qué utilizar internalid?

Porque cuando trabajamos con paginación queremos que cada registro tenga una posición lo más determinística posible.

Por ejemplo:

Fecha         Internal ID

2026-09-01    100
2026-09-01    101
2026-09-01    102
2026-09-01    103
2026-09-02    104
2026-09-02    105

En lugar de depender únicamente de:

ORDER BY trandate

podemos pensar conceptualmente en:

ORDER BY trandate, internalid

Así, cuando NetSuite divide los resultados en páginas, tenemos un orden mucho más estable.

Esto es especialmente importante en procesos donde no podemos permitir registros duplicados o que algún registro quede fuera del procesamiento.

Una implementación práctica

Un ejemplo completo podría ser:

const transactionSearch = search.create({
  type: search.Type.TRANSACTION,

  filters: [['mainline', 'is', 'T']],

  columns: [
    search.createColumn({
      name: 'trandate',
      sort: search.Sort.ASC
    }),

    search.createColumn({
      name: 'internalid',
      sort: search.Sort.ASC
    }),

    'tranid',
    'entity'
  ]
})

const pagedData = transactionSearch.runPaged({
  pageSize: 1000
})

log.audit({
  title: 'Total de resultados',
  details: pagedData.count
})

pagedData.pageRanges.forEach(function (pageRange) {
  const page = pagedData.fetch({
    index: pageRange.index
  })

  page.data.forEach(function (result) {
    const internalId = result.getValue({
      name: 'internalid'
    })

    const tranId = result.getValue({
      name: 'tranid'
    })

    // Procesamiento
  })
})

La estructura queda bastante clara:

Search

runPaged()

Page

Resultados

Procesamiento

¿Y si tenemos cientos de miles de registros?

Aquí ya no pensaría únicamente en runPaged().

Supongamos que tenemos:

500,000 transacciones

La primera pregunta que me haría no sería:

¿Cómo obtengo las 500,000?

Sino:

¿Realmente necesito procesar las 500,000 en una sola ejecución?

Porque obtener muchos resultados y procesarlos son dos problemas diferentes.

Podemos tener:

Saved Search

runPaged()

1,000 resultados

Map/Reduce

procesamiento distribuido

En lugar de intentar guardar todos los resultados en un array:

const results = []

page.data.forEach(function (result) {
  results.push(result)
})

Eso puede convertirse rápidamente en un problema de memoria.

Es mejor procesar los resultados según la arquitectura que necesite nuestro proceso.

Otra alternativa: SuiteQL

Cuando la consulta comienza a ser más compleja, otra opción que vale la pena considerar es SuiteQL.

Por ejemplo:

const query = `
    SELECT
        id,
        tranid,
        trandate
    FROM
        transaction
    ORDER BY
        trandate ASC,
        id ASC
`

Y podemos utilizar:

const pagedData = query.runSuiteQLPaged({
  query: query,
  pageSize: 1000
})

Esto resulta especialmente interesante cuando necesitamos más control sobre la consulta:

JOIN
WHERE
ORDER BY
GROUP BY

En determinados escenarios, SuiteQL puede resultar más apropiado que intentar construir una Saved Search extremadamente compleja.

No uses runPaged() por defecto

Algo que también aprendí es que runPaged() no debería convertirse en una regla automática.

Si tenemos:

300 registros

probablemente:

search.run().each(...)

sea perfectamente suficiente.

Si tenemos:

20,000 registros

entonces:

search.runPaged(...)

empieza a tener mucho más sentido.

Y si tenemos cientos de miles de registros y además debemos realizar bastante procesamiento:

Search

runPaged()

Map/Reduce

puede ser una arquitectura mucho más adecuada.

La cantidad de datos y el trabajo que tenemos que hacer sobre ellos deberían determinar la solución.

Recomendaciones para búsquedas grandes

Cuando tengo una búsqueda que va a alimentar un proceso importante, intento revisar estas cosas:

1. ¿Cuántos resultados puede devolver?

2. ¿Estoy utilizando run().each()?

3. ¿Necesito realmente todos los resultados?

4. ¿Tengo un ordenamiento determinístico?

5. ¿Estoy utilizando internalid como criterio de desempate?

6. ¿Estoy acumulando todos los resultados en memoria?

7. ¿El procesamiento debería ejecutarse en Map/Reduce?

8. ¿Una Saved Search es suficiente?

9. ¿SuiteQL sería una mejor alternativa?

10. ¿Qué pasará cuando el volumen de datos sea 10 veces mayor?

Esta última pregunta es especialmente importante.

Un script que funciona perfectamente con:

1,000 registros

no necesariamente está preparado para:

100,000 registros

El error más común

Creo que uno de los errores más comunes es pensar:

search.run().each(...)

y asumir:

Estoy recorriendo todos los resultados de mi búsqueda.

No necesariamente.

Si la búsqueda puede superar los 4,000 resultados, necesitamos pensar en otra estrategia.

Y cuando utilizamos paginación, también tenemos que pensar en el orden.

No es simplemente:

Tengo muchos registros

Uso runPaged()

Sino:

Tengo muchos registros

Necesito paginar

Necesito un orden estable

Necesito procesarlos correctamente

¿Necesito Map/Reduce?

En resumen

Para mí, la idea principal es esta:

Hasta 4,000 resultados

search.run().each()

Más de 4,000

search.runPaged()

Consulta más compleja

SuiteQL / runSuiteQLPaged()

Muchísimo procesamiento

Map/Reduce

Pero hay una regla que no deberíamos olvidar cuando usamos paginación:

runPaged()
    +
ordenamiento determinístico

Si estoy ordenando por una fecha, un nombre u otro campo que puede repetirse, agregaría un criterio único como internalid para hacer el orden mucho más estable.

Al final, el objetivo no es simplemente hacer que una búsqueda funcione hoy.

El objetivo es que siga funcionando cuando el cliente pase de 1,000 registros a 10,000, 100,000 o incluso más.

Ese es uno de los cambios de mentalidad que empiezas a tener cuando dejas de pensar solamente en “hacer que el script funcione” y empiezas a pensar en cómo va a comportarse cuando el volumen real de NetSuite crezca.