javascript - nodejs - Consulta de rango para la paginación de MongoDB
node js mongodb example (5)
Quiero implementar la paginación encima de un MongoDB. Para mi consulta de rango, pensé en usar ObjectIDs:
db.tweets.find({ _id: { $lt: maxID } }, { limit: 50 })
Sin embargo, de acuerdo con los documentos , la estructura del ObjectID significa que "los valores de ObjectId no representan un orden de inserción estricto":
La relación entre el orden de los valores de ObjectId y el tiempo de generación no es estricta en un solo segundo. Si múltiples sistemas o múltiples procesos o subprocesos en un solo sistema generan valores, en un solo segundo; Los valores de ObjectId no representan un orden de inserción estricto. El sesgo del reloj entre los clientes también puede dar como resultado un orden no estricto incluso para los valores, ya que los controladores del cliente generan valores de ObjectId, no el proceso de mongod.
Entonces pensé en consultar con una marca de tiempo:
db.tweets.find({ created: { $lt: maxDate } }, { limit: 50 })
Sin embargo, no hay garantía de que la fecha sea única; es muy probable que se puedan crear dos documentos en el mismo segundo. Esto significa que los documentos podrían perderse cuando se busca.
¿Hay algún tipo de consulta a distancia que me proporcione más estabilidad?
¿No sería un tweet la marca de tiempo "real" (es decir, el tiempo tuiteado y los criterios por los que desea que sea ordenado) diferente de una marca de tiempo de "inserción" de tweet (es decir, tiempo agregado a la colección local). Esto depende de su aplicación, por supuesto, pero es probable que las inserciones de tweets se puedan agrupar o de lo contrario terminen siendo insertadas en el orden "incorrecto". Entonces, a menos que trabaje en Twitter (y tenga acceso a las colecciones insertadas en el orden correcto), no podrá confiar solo en $natural
o ObjectID
para ordenar la lógica.
Los documentos de Mongo sugieren skip
y limit
búsqueda :
db.tweets.find({created: {$lt: maxID}).
sort({created: -1, username: 1}).
skip(50).limit(50); //second page
Sin embargo, hay un problema de rendimiento al usar omisión:
El método
cursor.skip()
menudo es costoso porque requiere que el servidor camine desde el comienzo de la colección o índice para obtener la posición de desplazamiento u omisión antes de comenzar a devolver el resultado. A medida que aumenta el desplazamiento,cursor.skip()
se volverá más lento ycursor.skip()
más CPU.
Esto sucede porque el skip
no encaja en el modelo de MapReduce y no es una operación que se escalaría bien, debe esperar a que esté disponible una colección ordenada antes de que pueda ser "cortada". Ahora el limit(n)
suena como un método igualmente pobre ya que aplica una restricción similar "desde el otro extremo"; sin embargo, con la ordenación aplicada, el motor puede optimizar algo el proceso manteniendo solo en memoria n
elementos por fragmento a medida que atraviesa la colección.
Una alternativa es usar paginación basada en rango. Después de recuperar la primera página de tweets, usted sabe cuál es el valor created
para el último tweet, por lo que todo lo que tiene que hacer es sustituir el original con este nuevo valor:
db.tweets.find({created: {$lt: lastTweetOnCurrentPageCreated}).
sort({created: -1, username: 1}).
limit(50); //next page
Realizar una condición de find
como esta se puede paralelizar fácilmente. ¿Pero cómo lidiar con páginas distintas a la siguiente? ¡No sabe la fecha de inicio de las páginas número 5, 10, 20 o incluso la página anterior ! @SergioTulentsev sugiere un encadenamiento creativo de métodos, pero recomendaría calcular previamente los primeros y últimos rangos del campo agregado en una colección de pages
separada; estos pueden ser recalculados en la actualización. Además, si no está satisfecho con DateTime
(tenga en cuenta las observaciones de rendimiento) o si le preocupan los valores duplicados, debe considerar los índices compuestos de timestamp + account tie (ya que un usuario no puede twittear dos veces al mismo tiempo), o incluso un agregado artificial de los dos:
db.pages.
find({pagenum: 3})
> {pagenum:3; begin:"01-01-2014@BillGates"; end:"03-01-2014@big_ben_clock"}
db.tweets.
find({_sortdate: {$lt: "03-01-2014@big_ben_clock", $gt: "01-01-2014@BillGates"}).
sort({_sortdate: -1}).
limit(50) //third page
El uso de un campo agregado para la clasificación funcionará "en el doblez" (aunque tal vez haya más formas kosher de manejar la condición). Esto podría configurarse como un índice único con valores corregidos en el momento de insertar, con un único documento de tweet que se parece a
{
_id: ...,
created: ..., //to be used in markup
user: ..., //also to be used in markup
_sortdate: "01-01-2014@BillGates" //sorting only, use date AND time
}
Construí una paginación usando mongodb _id de esta manera.
// import ObjectId from mongodb
let sortOrder = -1;
let query = []
if (prev) {
sortOrder = 1
query.push({title: ''findTitle'', _id:{$gt: ObjectId(''_idValue'')}})
}
if (next) {
sortOrder = -1
query.push({title: ''findTitle'', _id:{$lt: ObjectId(''_idValue'')}})
}
db.collection.find(query).limit(10).sort({_id: sortOrder})
El siguiente enfoque funcionará incluso si hay múltiples documentos insertados / actualizados en el mismo milisegundo, incluso si provienen de varios clientes (lo que genera ObjectId). Para simiplicidad, en las siguientes consultas estoy proyectando _id, lastModifiedDate.
Primera página, obtenga el resultado Ordenado por modifiedTime (Descending), ObjectId (Ascending) para la primera página.
db.product.find({},{"_id":1,"lastModifiedDate":1}).sort({"lastModifiedDate":-1, "_id":1}).limit(2)
Anote el ObjectId y lastModifiedDate del último registro recuperado en esta página. (loid, lmd)
- Para la página sencod, incluya la condición de consulta para buscar si (lastModifiedDate = lmd AND oid> loid) O (lastModifiedDate <loid)
db.productfind({$or:[{"lastModifiedDate":{$lt:lmd}},{"_id":1,"lastModifiedDate":1},{$and:[{"lastModifiedDate":lmd},{"_id":{$gt:loid}}]}]},{"_id":1,"lastModifiedDate":1}).sort({"lastModifiedDate":-1, "_id":1}).limit(2)
repita lo mismo para las páginas siguientes.
Está perfectamente bien usar ObjectId () aunque su sintaxis para paginación es incorrecta. Usted quiere:
db.tweets.find().limit(50).sort({"_id":-1});
Esto _id
que quiere tweets ordenados por valor _id
en orden descendente y desea los 50 más recientes. Su problema es el hecho de que la paginación es complicada cuando el conjunto de resultados actual está cambiando, por lo que en lugar de usar el salto para la página siguiente, desea anote el _id
más _id
en el conjunto de resultados (el 50º valor más reciente de _id
y luego obtenga la página siguiente con:
db.tweets.find( {_id : { "$lt" : <50th _id> } } ).limit(50).sort({"_id":-1});
Esto le proporcionará los próximos tweets "más recientes", sin nuevos tweets entrantes que arruinen su paginación en el tiempo.
No hay necesidad de preocuparse por si _id
value corresponde estrictamente al orden de inserción: será 99.999% lo suficientemente cerca, y nadie realmente se preocupa por el nivel secundario, cuyo tweet fue el primero, incluso podría notar que Twitter muestra tweets con frecuencia fuera de servicio, simplemente no es tan crítico.
Si es crítico, entonces tendría que usar la misma técnica pero con "fecha de tweet" donde esa fecha tendría que ser una marca de tiempo, en lugar de solo una fecha.
ObjectIds debería ser lo suficientemente bueno para la paginación si limita sus consultas al segundo anterior (o no le importa la posibilidad de rareza en segundo lugar). Si eso no es lo suficientemente bueno para sus necesidades, entonces deberá implementar un sistema de generación de ID que funcione como un autoincremento.
Actualizar:
Para consultar el segundo anterior de ObjectIds, deberá construir un ObjectID manualmente.
Consulte la especificación de ObjectId http://docs.mongodb.org/manual/reference/object-id/
Intenta usar esta expresión para hacerlo desde un mongos.
{ _id :
{
$lt : ObjectId(Math.floor((new Date).getTime()/1000 - 1).toString(16)+"ffffffffffffffff")
}
}
Las ''f'' al final son para maximizar los posibles bits aleatorios que no están asociados con una marca de tiempo, ya que está haciendo menos de la consulta.
Recomiendo durante la creación real de ObjectId en su servidor de aplicaciones en lugar de hacerlo en los mongos, ya que este tipo de cálculo puede ralentizarlo si tiene muchos usuarios.