MongoDB - paginación

81

Cuando se usa MongoDB, ¿existen patrones especiales para, por ejemplo, hacer una vista paginada? digamos un blog que enumera las 10 últimas publicaciones donde puede navegar hacia atrás a publicaciones más antiguas.

¿O uno lo resuelve con un índice en, por ejemplo, blogpost.publishdate y simplemente salta y limita el resultado?

Roger Johansson
fuente
1
Voy a dejar este pendiente ya que parece haber algún desacuerdo sobre cuál es la forma correcta de hacer esta escala.
Roger Johansson

Respuestas:

98

Usar skip + limit no es una buena manera de realizar la paginación cuando el rendimiento es un problema o con colecciones grandes; se volverá más y más lento a medida que aumente el número de página. El uso de omitir requiere que el servidor recorra todos los documentos (o valores de índice) desde 0 hasta el valor de compensación (omitir).

Es mucho mejor usar una consulta de rango (+ límite) donde pasa el valor de rango de la última página. Por ejemplo, si está ordenando por "fecha de publicación", simplemente pasaría el último valor de "fecha de publicación" como criterio para que la consulta obtenga la siguiente página de datos.

Scott Hernandez
fuente
4
Será genial ver algunos documentos que confirman que el salto en mongodb recorre todos los documentos.
Andrew Orsich
5
Aquí tiene: omitir documentos Si hay algún otro lugar donde la información deba actualizarse, hágamelo saber.
Scott Hernandez
2
@ScottHernandez: Tengo paginación con enlaces a varias páginas (así: Página: Primera, 2, 3, 4, 5, Última) y ordenando todos los campos. Solo uno de mis campos es único (e indexado), ¿funcionará una consulta de rango para este caso de uso? Me temo que no, solo quería confirmar si era posible. Gracias.
user183037
7
Aquí está el enlace para omitir documentos
Ulises
7
Parece que esto no funcionaría si hubiera varios documentos con el mismo valor de fecha de publicación.
d512
12
  1. La paginación basada en rango es difícil de implementar si necesita ordenar los elementos de muchas formas.
  2. Recuerde que si el valor de campo del parámetro de ordenación no es único, la paginación basada en rango no será realista.

Posible solución: intente simplificar el diseño, pensando si solo podemos ordenar por id o algún valor único.

Y si podemos, entonces se puede utilizar la paginación basada en rango.

La forma común es usar sort (), skip () y limit () para implementar la paginación que se describe arriba.

Jackalope
fuente
un buen artículo con ejemplos de código Python se puede encontrar aquí codementor.io/arpitbhayani/…
Gianfranco P.
1
Gracias - ¡gran respuesta! Me molesta que la gente sugiera la paginación mediante el uso de filtros, por ejemplo { _id: { $gt: ... } }... simplemente no funciona si se utilizan pedidos personalizados, por ejemplo .sort(...).
Nick Grealy
@NickGrealy Seguí un tutorial para hacer precisamente esto y ahora estoy en una situación en la que la paginación 'parece' que funciona, pero me faltan documentos porque estoy usando el ID de mongo pero a medida que se insertan nuevos datos en la base de datos, y luego el La colección se ordena alfabéticamente si la página de inicio contiene registros que comienzan con A, pero los ID son más altos que los registros que comienzan con AA porque se insertaron después de que la paginación no devuelva los registros AA. ¿Es adecuado saltar y limitar? Tengo alrededor de 60 millones de documentos para buscar.
berimbolo
@berimbolo - esto es digno de conversación - no obtendrás tu respuesta aquí en los comentarios. Pregunta: ¿qué comportamiento esperas? Está trabajando con un sistema en vivo, con registros que se crean y eliminan todo el tiempo. Si vuelve a solicitar una instantánea en vivo de sus datos para cada nueva carga de página, debe esperar que cambien los datos subyacentes. ¿Cuál debería ser el comportamiento? Si trabaja con una instantánea de datos de "punto en el tiempo", tendrá "páginas fijas", pero también tendrá datos "desactualizados". ¿Qué tan grande es el problema que está describiendo y con qué frecuencia la gente lo encuentra?
Nick Grealy
1
Definitivamente vale la pena conversar, mi problema es que recuperé un archivo único en orden alfabético de placas y cada 15 minutos aplico actualizaciones a las placas cambiadas (eliminadas o agregadas), el problema es que si se agrega una nueva placa y comienza con una A, por ejemplo, y debido al tamaño de la página es el último en la página, entonces si se solicita el siguiente, creo que no se devuelven registros (una suposición y un ejemplo artificial, pero ilustrativo de mi problema) porque la ID es más alta que cualquier otra en el conjunto. Estoy considerando usar la matrícula completa para manejar la mayor parte de la consulta ahora.
berimbolo
5

Esta es la solución que utilicé cuando mi colección creció demasiado para devolverla en una sola consulta. Aprovecha el orden inherente del _idcampo y le permite recorrer una colección por tamaño de lote especificado.

Aquí está como un módulo npm, paginación de mongoose , el código completo está a continuación:

function promiseWhile(condition, action) {
  return new Promise(function(resolve, reject) {
    process.nextTick(function loop() {
      if(!condition()) {
        resolve();
      } else {
        action().then(loop).catch(reject);
      }
    });
  });
}

function findPaged(query, fields, options, iterator, cb) {
  var Model  = this,
    step     = options.step,
    cursor   = null,
    length   = null;

  promiseWhile(function() {
    return ( length===null || length > 0 );
  }, function() {
    return new Promise(function(resolve, reject) {

        if(cursor) query['_id'] = { $gt: cursor };

        Model.find(query, fields, options).sort({_id: 1}).limit(step).exec(function(err, items) {
          if(err) {
            reject(err);
          } else {
            length  = items.length;
            if(length > 0) {
              cursor  = items[length - 1]._id;
              iterator(items, function(err) {
                if(err) {
                  reject(err);
                } else {
                  resolve();
                }
              });
            } else {
              resolve();
            }
          }
        });
      });
  }).then(cb).catch(cb);

}

module.exports = function(schema) {
  schema.statics.findPaged = findPaged;
};

Adjúntelo a su modelo de esta manera:

MySchema.plugin(findPaged);

Luego consulta como esta:

MyModel.findPaged(
  // mongoose query object, leave blank for all
  {source: 'email'},
  // fields to return, leave blank for all
  ['subject', 'message'],
  // number of results per page
  {step: 100},
  // iterator to call on each set of results
  function(results, cb) {
    console.log(results);
    // this is called repeatedly while until there are no more results.
    // results is an array of maximum length 100 containing the
    // results of your query

    // if all goes well
    cb();

    // if your async stuff has an error
    cb(err);
  },
  // function to call when finished looping
  function(err) {
    throw err;
    // this is called once there are no more results (err is null),
    // or if there is an error (then err is set)
  }
);
mz3
fuente
No sé por qué esta respuesta no tiene más votos a favor. esta es una forma más eficiente de paginar que omitir / limitar
nxmohamad
También obtuve este paquete, pero ¿cómo es el rendimiento en comparación con skip / limit y la respuesta proporcionada por @Scott Hernandez?
Tanckom
4
¿Cómo funcionaría esta respuesta para clasificar en cualquier otro campo?
Nick Grealy
1

La paginación basada en rango es factible, pero debe ser inteligente acerca de cómo minimiza / maximiza la consulta.

Si puede permitírselo, intente almacenar en caché los resultados de una consulta en un archivo o colección temporal. Gracias a las colecciones TTL en MongoDB, puede insertar sus resultados en dos colecciones.

  1. Búsqueda + Usuario + Consulta de parámetros (TTL lo que sea)
  2. Resultados de la consulta (TTL lo que sea + intervalo de limpieza + 1)

El uso de ambos asegura que no obtendrá resultados parciales cuando el TTL esté cerca de la hora actual. Puede utilizar un contador simple cuando almacena los resultados para hacer una consulta de rango MUY simple en ese punto.

más duro
fuente
1

A continuación se muestra un ejemplo de cómo recuperar una lista de Userdocumentos ordenados por CreatedDate(donde pageIndexestá basado en cero) utilizando el controlador oficial de C #.

public void List<User> GetUsers() 
{
  var connectionString = "<a connection string>";
  var client = new MongoClient(connectionString);
  var server = client.GetServer();
  var database = server.GetDatabase("<a database name>");

  var sortBy = SortBy<User>.Descending(u => u.CreatedDate);
  var collection = database.GetCollection<User>("Users");
  var cursor = collection.FindAll();
  cursor.SetSortOrder(sortBy);

  cursor.Skip = pageIndex * pageSize;
  cursor.Limit = pageSize;
  return cursor.ToList();
}

Todas las operaciones de clasificación y paginación se realizan en el lado del servidor. Aunque este es un ejemplo en C #, supongo que lo mismo se puede aplicar a otros puertos de idioma.

Consulte http://docs.mongodb.org/ecosystem/tutorial/use-csharp-driver/#modifying-a-cursor-before-enumerating-it .

Alex Ho
fuente
0
    // file:ad-hoc.js
    // an example of using the less binary as pager in the bash shell
    //
    // call on the shell by:
    // mongo localhost:27017/mydb ad-hoc.js | less
    //
    // note ad-hoc.js must be in your current directory
    // replace the 27017 wit the port of your mongodb instance
    // replace the mydb with the name of the db you want to query
    //
    // create the connection obj
    conn = new Mongo();

    // set the db of the connection
    // replace the mydb with the name of the db you want to query
    db = conn.getDB("mydb");

    // replace the products with the name of the collection
    // populate my the products collection
    // this is just for demo purposes - you will probably have your data already
    for (var i=0;i<1000;i++ ) {
    db.products.insert(
        [
            { _id: i, item: "lamp", qty: 50, type: "desk" },
        ],
        { ordered: true }
    )
    }


    // replace the products with the name of the collection
    cursor = db.products.find();

    // print the collection contents
    while ( cursor.hasNext() ) {
        printjson( cursor.next() );
    }
    // eof file: ad-hoc.js
Yordan Georgiev
fuente