Comment paginer avec Mongoose dans Node.js?


Réponses:


278

Je suis très déçu par les réponses acceptées dans cette question. Cela ne sera pas mis à l'échelle. Si vous lisez les petits caractères sur cursor.skip ():

La méthode cursor.skip () est souvent coûteuse car elle nécessite que le serveur marche depuis le début de la collection ou de l'index pour obtenir la position de décalage ou de saut avant de commencer à renvoyer le résultat. Au fur et à mesure que l'offset (par exemple pageNumber ci-dessus) augmente, cursor.skip () deviendra plus lent et plus gourmand en CPU. Avec des collections plus importantes, cursor.skip () peut devenir lié aux entrées-sorties.

Pour obtenir une pagination de manière évolutive, combinez une limite () avec au moins un critère de filtre, une date createdOn convient à de nombreuses fins.

MyModel.find( { createdOn: { $lte: request.createdOnBefore } } )
.limit( 10 )
.sort( '-createdOn' )

105
Mais comment obtiendriez-vous la page deux de cette requête sans sauter? Si vous affichez 10 résultats par page et qu'il y a 100 résultats, comment définissez-vous alors la valeur de décalage ou de saut? Vous ne répondez pas à la question de la pagination, vous ne pouvez donc pas être «déçu», bien qu'il s'agisse d'une mise en garde valable. Bien que le même problème se trouve dans le décalage MySQL, limitez. Il faut traverser l'arbre jusqu'à l'offset avant de retourner les résultats. Je prendrais cela avec un grain de sel, si vos jeux de résultats sont inférieurs à 1 mil et qu'il n'y a pas de performances préservables, utilisez skip ().
Lex

13
Je suis un noob en ce qui concerne la mangouste / mongodb, mais pour répondre à la question de Lex, il semble que, comme les résultats sont classés par ' -createdOn', vous remplaceriez la valeur de request.createdOnBeforepar la plus petite valeur createdOnretournée dans le jeu de résultats précédent, puis requery.
Terry Lewis

9
@JoeFrambach La demande basée sur createdOn semble problématique. Skip a été intégré pour une raison. Les documents ne font qu'avertir de la performance atteinte par le fait de parcourir l'index btree, ce qui est le cas avec tous les SGBD. Pour les utilisateurs, question "quelque chose de comparable MySQL à LIMIT 50,100". Skip est tout à fait exact.
Lex

8
Bien qu'intéressant, un problème avec cette réponse, comme le note le commentaire de @Lex, est que vous ne pouvez sauter que "en avant" ou "en arrière" dans les résultats - vous ne pouvez pas avoir de "pages" sur lesquelles vous pouvez sauter (par exemple Page 1, Page 2 , Page 3) sans faire de multiples requêtes séquentielles pour déterminer d'où commencer la pagination, ce qui, je le soupçonne, sera plus lent dans la plupart des cas que d'utiliser simplement skip. Bien sûr, vous n'aurez peut-être pas besoin d'ajouter la possibilité de passer à des pages spécifiques.
Iain Collins

20
Cette réponse contient des points intéressants, mais elle ne répond pas à la question initiale posée.
vapeur

228

Après avoir examiné de plus près l'API Mongoose avec les informations fournies par Rodolphe, j'ai trouvé cette solution:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });

21
Et le "compte"? Vous en avez besoin pour savoir combien de pages il y a.
Aleksey Saatchi du

36
N'évolue pas.
Chris Hinkle

4
Explication de Chris Hinkle expliquant pourquoi cela n'évolue pas: stackoverflow.com/a/23640287/165330 .
imme

7
@ChrisHinkle Cela semble être le cas avec tous les SGBD. Le commentaire de Lex sous la réponse liée semble expliquer davantage.
csotiriou

2
@Avij ouais. J'ai utilisé l'identifiant pour cela. ce que vous faites là-dedans est de renvoyer le dernier identifiant des enregistrements au serveur et d'obtenir des enregistrements avec un identifiant supérieur à celui envoyé. Comme Id est indexé, ce sera beaucoup plus rapide
George Bailey

108

Pagination avec mangouste, express et jade - Voici un lien vers mon blog avec plus de détails

var perPage = 10
  , page = Math.max(0, req.param('page'))

Event.find()
    .select('name')
    .limit(perPage)
    .skip(perPage * page)
    .sort({
        name: 'asc'
    })
    .exec(function(err, events) {
        Event.count().exec(function(err, count) {
            res.render('events', {
                events: events,
                page: page,
                pages: count / perPage
            })
        })
    })

26
Merci d'avoir posté votre réponse! Veuillez lire attentivement la FAQ sur l' autopromotion. Notez également qu'il est nécessaire que vous publiez un avertissement chaque fois que vous créez un lien vers votre propre site / produit.
Andrew Barber

Math.max(0, undefined)reviendra undefined, Cela a fonctionné pour moi:let limit = Math.abs(req.query.limit) || 10; let page = (Math.abs(req.query.page) || 1) - 1; Schema.find().limit(limit).skip(limit * page)
Monfa.red

55

Vous pouvez enchaîner comme ça:

var query = Model.find().sort('mykey', 1).skip(2).limit(5)

Exécutez la requête à l'aide de exec

query.exec(callback);

Merci pour votre réponse, comment est ajouté le rappel avec le résultat?
Thomas

2
execFind (fonction (... par exemple: var page = req.param('p'); var per_page = 10; if (page == null) { page = 0; } Location.count({}, function(err, count) { Location.find({}).skip(page*per_page).limit(per_page).execFind(function(err, locations) { res.render('index', { locations: locations }); }); });
todd

9
note: cela ne fonctionnera pas dans mangouste , mais cela fonctionnera dans mongodb-native-driver.
Jesse

39

Dans ce cas, vous pouvez ajouter la requête pageet / ou limità votre URL en tant que chaîne de requête.

Par exemple:
?page=0&limit=25 // this would be added onto your URL: http:localhost:5000?page=0&limit=25

Puisque ce serait un Stringnous devons le convertir en un Numberpour nos calculs. Faisons-le en utilisant la parseIntméthode et fournissons également quelques valeurs par défaut.

const pageOptions = {
    page: parseInt(req.query.page, 10) || 0,
    limit: parseInt(req.query.limit, 10) || 10
}

sexyModel.find()
    .skip(pageOptions.page * pageOptions.limit)
    .limit(pageOptions.limit)
    .exec(function (err, doc) {
        if(err) { res.status(500).json(err); return; };
        res.status(200).json(doc);
    });

La pagination BTW commence par0


5
veuillez ajouter le `{page: parseInt (req.query.page) || 0, ...} au paramètre.
imalik8088

@ imalik8088 Merci, cependant les paramètres de chaîne AFAIK sont gérés automatiquement par mongoose.
CENT1PEDE

1
Je m'attendais à ce comportement, mais dans mon cas, il ne pouvait pas se cacher et me montrer une erreur
imalik8088

@ imalik8088 C'est bizarre. Peut-être que si vous pouviez afficher une erreur de reproduction, je pourrais modifier ma réponse. Merci.
CENT1PEDE

2
Est-ce que la mangouste trouverait tous les enregistrements avant d'appliquer les conditions?
FluffyBeing du

37

Vous pouvez utiliser un petit paquet appelé Mongoose Paginate qui le rend plus facile.

$ npm install mongoose-paginate

Après dans vos routes ou votre contrôleur, ajoutez simplement:

/**
 * querying for `all` {} items in `MyModel`
 * paginating by second page, 10 items per page (10 results, page 2)
 **/

MyModel.paginate({}, 2, 10, function(error, pageCount, paginatedResults) {
  if (error) {
    console.error(error);
  } else {
    console.log('Pages:', pageCount);
    console.log(paginatedResults);
  }
}

2
Est-ce optimisé?
Argento

16

Voici un exemple d'exemple que vous pouvez essayer,

var _pageNumber = 2,
  _pageSize = 50;

Student.count({},function(err,count){
  Student.find({}, null, {
    sort: {
      Name: 1
    }
  }).skip(_pageNumber > 0 ? ((_pageNumber - 1) * _pageSize) : 0).limit(_pageSize).exec(function(err, docs) {
    if (err)
      res.json(err);
    else
      res.json({
        "TotalCount": count,
        "_Array": docs
      });
  });
 });

11

Essayez d'utiliser la fonction mangouste pour la pagination. La limite est le nombre d'enregistrements par page et le numéro de la page.

var limit = parseInt(body.limit);
var skip = (parseInt(body.page)-1) * parseInt(limit);

 db.Rankings.find({})
            .sort('-id')
            .limit(limit)
            .skip(skip)
            .exec(function(err,wins){
 });

10

Voilà ce que je l'ai fait sur le code

var paginate = 20;
var page = pageNumber;
MySchema.find({}).sort('mykey', 1).skip((pageNumber-1)*paginate).limit(paginate)
    .exec(function(err, result) {
        // Write some stuff here
    });

C'est comme ça que je l'ai fait.


1
Comment obtenir le nombre total de pages
Rhushikesh

Salut @Rhushikesh, Vous pouvez utiliser une fonction count () pour obtenir le nombre. Mais il semble que ce soit une autre requête de la base de données. Détails ici mongoosejs.com/docs/api.html#model_Model.count
Indra Santosa

@Rhushikesh obtient le décompte et le divise par la limite
troisième

count()est obsolète. utilisationcountDocuments()
Ruslan

7

Requete;
search = productName,

Params;
page = 1

// Pagination
router.get("/search/:page", (req, res, next) => {
  const resultsPerPage = 5;
  const page = req.params.page >= 1 ? req.params.page : 1;
  const query = req.query.search;

  Product.find({ name: query })
    .select("name")
    .sort({ name: "asc" })
    .limit(resultsPerPage)
    .skip(resultsPerPage * page)
    .then((results) => {
      return res.status(200).send(results);
    })
    .catch((err) => {
      return res.status(500).send(err);
    });
});

Merci pour cette réponse; Je l'ai essayé d'abord après avoir lu le fil, car c'était l'un des plus récents. Cependant, quand je l'ai implémenté, j'ai découvert un bogue - tel qu'il est écrit maintenant, il ne retournera jamais la première page de résultats, car il aura TOUJOURS une valeur de saut. Essayez d'ajouter "page = page-1" avant l'appel Product.find ().
Interog

6

Voici une version que j'attache à tous mes modèles. Cela dépend du trait de soulignement pour plus de commodité et de l'async pour les performances. L'option permet la sélection et le tri des champs à l'aide de la syntaxe mangouste.

var _ = require('underscore');
var async = require('async');

function findPaginated(filter, opts, cb) {
  var defaults = {skip : 0, limit : 10};
  opts = _.extend({}, defaults, opts);

  filter = _.extend({}, filter);

  var cntQry = this.find(filter);
  var qry = this.find(filter);

  if (opts.sort) {
    qry = qry.sort(opts.sort);
  }
  if (opts.fields) {
    qry = qry.select(opts.fields);
  }

  qry = qry.limit(opts.limit).skip(opts.skip);

  async.parallel(
    [
      function (cb) {
        cntQry.count(cb);
      },
      function (cb) {
        qry.exec(cb);
      }
    ],
    function (err, results) {
      if (err) return cb(err);
      var count = 0, ret = [];

      _.each(results, function (r) {
        if (typeof(r) == 'number') {
          count = r;
        } else if (typeof(r) != 'number') {
          ret = r;
        }
      });

      cb(null, {totalCount : count, results : ret});
    }
  );

  return qry;
}

Attachez-le à votre schéma de modèle.

MySchema.statics.findPaginated = findPaginated;

6

Solution de pagination simple et puissante

async getNextDocs(no_of_docs_required: number, last_doc_id?: string) {
    let docs

    if (!last_doc_id) {
        // get first 5 docs
        docs = await MySchema.find().sort({ _id: -1 }).limit(no_of_docs_required)
    }
    else {
        // get next 5 docs according to that last document id
        docs = await MySchema.find({_id: {$lt: last_doc_id}})
                                    .sort({ _id: -1 }).limit(no_of_docs_required)
    }
    return docs
}

last_doc_id: le dernier identifiant de document que vous obtenez

no_of_docs_required: le nombre de documents que vous souhaitez récupérer soit 5, 10, 50 etc.

  1. Si vous ne fournissez last_doc_idpas la méthode, vous obtiendrez, par exemple, les 5 derniers documents
  2. Si vous l'avez fourni, last_doc_idvous obtiendrez les 5 documents suivants.

5

La réponse ci-dessus est valable.

Juste un add-on pour quiconque est en asynchrone attend plutôt que de promettre !!

const findAllFoo = async (req, resp, next) => {
    const pageSize = 10;
    const currentPage = 1;

    try {
        const foos = await FooModel.find() // find all documents
            .skip(pageSize * (currentPage - 1)) // we will not retrieve all records, but will skip first 'n' records
            .limit(pageSize); // will limit/restrict the number of records to display

        const numberOfFoos = await FooModel.countDocuments(); // count the number of records for that model

        resp.setHeader('max-records', numberOfFoos);
        resp.status(200).json(foos);

    } catch (err) {
        resp.status(500).json({
            message: err
        });
    }
};

4

vous pouvez également utiliser la ligne de code suivante

per_page = parseInt(req.query.per_page) || 10
page_no = parseInt(req.query.page_no) || 1
var pagination = {
  limit: per_page ,
  skip:per_page * (page_no - 1)
}
users = await User.find({<CONDITION>}).limit(pagination.limit).skip(pagination.skip).exec()

ce code fonctionnera dans la dernière version de mongo


3

Une approche solide pour implémenter cela serait de passer les valeurs du frontend en utilisant une chaîne de requête . Disons que nous voulons obtenir la page n ° 2 et limiter également la sortie à 25 résultats .
La chaîne de requête ressemblerait à ceci:?page=2&limit=25 // this would be added onto your URL: http:localhost:5000?page=2&limit=25

Voyons le code:

// We would receive the values with req.query.<<valueName>>  => e.g. req.query.page
// Since it would be a String we need to convert it to a Number in order to do our
// necessary calculations. Let's do it using the parseInt() method and let's also provide some default values:

  const page = parseInt(req.query.page, 10) || 1; // getting the 'page' value
  const limit = parseInt(req.query.limit, 10) || 25; // getting the 'limit' value
  const startIndex = (page - 1) * limit; // this is how we would calculate the start index aka the SKIP value
  const endIndex = page * limit; // this is how we would calculate the end index

// We also need the 'total' and we can get it easily using the Mongoose built-in **countDocuments** method
  const total = await <<modelName>>.countDocuments();

// skip() will return a certain number of results after a certain number of documents.
// limit() is used to specify the maximum number of results to be returned.

// Let's assume that both are set (if that's not the case, the default value will be used for)

  query = query.skip(startIndex).limit(limit);

  // Executing the query
  const results = await query;

  // Pagination result 
 // Let's now prepare an object for the frontend
  const pagination = {};

// If the endIndex is smaller than the total number of documents, we have a next page
  if (endIndex < total) {
    pagination.next = {
      page: page + 1,
      limit
    };
  }

// If the startIndex is greater than 0, we have a previous page
  if (startIndex > 0) {
    pagination.prev = {
      page: page - 1,
      limit
    };
  }

 // Implementing some final touches and making a successful response (Express.js)

const advancedResults = {
    success: true,
    count: results.length,
    pagination,
    data: results
 }
// That's it. All we have to do now is send the `results` to the frontend.
 res.status(200).json(advancedResults);

Je suggérerais d'implémenter cette logique dans un middleware afin que vous puissiez l'utiliser pour divers itinéraires / contrôleurs.


2

Le moyen le plus simple et le plus rapide est de paginer avec l'exemple objectId;

Condition de charge initiale

condition = {limit:12, type:""};

Prendre le premier et le dernier ObjectId des données de réponse

Page condition suivante

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c662d", lastId:"57762a4c875adce3c38c6615"};

Page condition suivante

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c6645", lastId:"57762a4c875adce3c38c6675"};

Chez la mangouste

var condition = {};
    var sort = { _id: 1 };
    if (req.body.type == "next") {
        condition._id = { $gt: req.body.lastId };
    } else if (req.body.type == "prev") {
        sort = { _id: -1 };
        condition._id = { $lt: req.body.firstId };
    }

var query = Model.find(condition, {}, { sort: sort }).limit(req.body.limit);

query.exec(function(err, properties) {
        return res.json({ "result": result);
});

2

La meilleure approche (OMI) consiste à utiliser sauter et limiter MAIS dans une collection ou des documents limités.

Pour effectuer la requête dans des documents limités, nous pouvons utiliser un index spécifique tel qu'un index sur un champ de type DATE. Voir ci-dessous

let page = ctx.request.body.page || 1
let size = ctx.request.body.size || 10
let DATE_FROM = ctx.request.body.date_from
let DATE_TO = ctx.request.body.date_to

var start = (parseInt(page) - 1) * parseInt(size)

let result = await Model.find({ created_at: { $lte: DATE_FROM, $gte: DATE_TO } })
    .sort({ _id: -1 })
    .select('<fields>')
    .skip( start )
    .limit( size )        
    .exec(callback)

2

Plugin le plus simple pour la pagination.

https://www.npmjs.com/package/mongoose-paginate-v2

Ajoutez un plugin à un schéma, puis utilisez la méthode de pagination du modèle:

var mongoose         = require('mongoose');
var mongoosePaginate = require('mongoose-paginate-v2');

var mySchema = new mongoose.Schema({ 
    /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

var myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage

ce plugin est cassé avec mongoose v5.5.5
Isaac Pak

1

Ceci est un exemple de fonction pour obtenir le résultat d'un modèle de compétences avec des options de pagination et de limite

 export function get_skills(req, res){
     console.log('get_skills');
     var page = req.body.page; // 1 or 2
     var size = req.body.size; // 5 or 10 per page
     var query = {};
     if(page < 0 || page === 0)
     {
        result = {'status': 401,'message':'invalid page number,should start with 1'};
        return res.json(result);
     }
     query.skip = size * (page - 1)
     query.limit = size
     Skills.count({},function(err1,tot_count){ //to get the total count of skills
      if(err1)
      {
         res.json({
            status: 401,
            message:'something went wrong!',
            err: err,
         })
      }
      else 
      {
         Skills.find({},{},query).sort({'name':1}).exec(function(err,skill_doc){
             if(!err)
             {
                 res.json({
                     status: 200,
                     message:'Skills list',
                     data: data,
                     tot_count: tot_count,
                 })
             }
             else
             {
                 res.json({
                      status: 401,
                      message: 'something went wrong',
                      err: err
                 })
             }
        }) //Skills.find end
    }
 });//Skills.count end

}


0

Vous pouvez écrire une requête comme celle-ci.

mySchema.find().skip((page-1)*per_page).limit(per_page).exec(function(err, articles) {
        if (err) {
            return res.status(400).send({
                message: err
            });
        } else {
            res.json(articles);
        }
    });

page: numéro de page provenant du client comme paramètres de requête.
per_page: aucun résultat affiché par page

Si vous utilisez la pile MEAN, le billet de blog suivant fournit une grande partie des informations pour créer la pagination en amont à l'aide de bootstrap angular-UI et des méthodes de saut et de limitation de mangouste dans le backend.

voir: https://techpituwa.wordpress.com/2015/06/06/mean-js-pagination-with-angular-ui-bootstrap/


0

Vous pouvez soit utiliser skip () et limit (), mais c'est très inefficace. Une meilleure solution serait un tri sur un champ indexé plus une limite (). Chez Wunderflats, nous avons publié une petite bibliothèque ici: https://github.com/wunderflats/goosepage Il utilise la première façon.


0

Si vous utilisez la mangouste comme source pour une api reposante, jetez un œil à ' restify-mongoose ' et ses requêtes. Il a exactement cette fonctionnalité intégrée.

Toute requête sur une collection fournit des en-têtes utiles ici

test-01:~$ curl -s -D - localhost:3330/data?sort=-created -o /dev/null
HTTP/1.1 200 OK
link: </data?sort=-created&p=0>; rel="first", </data?sort=-created&p=1>; rel="next", </data?sort=-created&p=134715>; rel="last"
.....
Response-Time: 37

Donc, fondamentalement, vous obtenez un serveur générique avec un temps de chargement relativement linéaire pour les requêtes vers les collections. C'est génial et quelque chose à regarder si vous voulez entrer dans une propre implémentation.


0
app.get("/:page",(req,res)=>{
        post.find({}).then((data)=>{
            let per_page = 5;
            let num_page = Number(req.params.page);
            let max_pages = Math.ceil(data.length/per_page);
            if(num_page == 0 || num_page > max_pages){
                res.render('404');
            }else{
                let starting = per_page*(num_page-1)
                let ending = per_page+starting
                res.render('posts', {posts:data.slice(starting,ending), pages: max_pages, current_page: num_page});
            }
        });
});

0
**//localhost:3000/asanas/?pageNo=1&size=3**

//requiring asanas model
const asanas = require("../models/asanas");


const fetchAllAsanasDao = () => {
    return new Promise((resolve, reject) => {

    var pageNo = parseInt(req.query.pageNo);
    var size = parseInt(req.query.size);
    var query = {};
        if (pageNo < 0 || pageNo === 0) {
            response = {
                "error": true,
                "message": "invalid page number, should start with 1"
            };
            return res.json(response);
        }
        query.skip = size * (pageNo - 1);
        query.limit = size;

  asanas
            .find(pageNo , size , query)
        .then((asanasResult) => {
                resolve(asanasResult);
            })
            .catch((error) => {
                reject(error);
            });

    });
}

0

Utilisez ce plugin simple.

https://github.com/WebGangster/mongoose-paginate-v2

Installation

npm install mongoose-paginate-v2
Ajouter un plugin à un schéma, puis utiliser la méthode de pagination du modèle:

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

const mySchema = new mongoose.Schema({ 
  /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

const myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage


Ce plugin a déjà été "suggéré" dans une autre réponse. Il serait également utile de savoir que vous contribuez à ce package.
lukas_o

@lukas_o Oui. Je suis le créateur du plugin.
Aravind NC

0

selon

Chris Hinkle

répondre:

//assume every page has 50 result
const results = (req.query.page * 1) * 50;
MyModel.find( { fieldNumber: { $lte: results} })
.limit( 50 )
.sort( '+fieldNumber' )

//one thing left is create a fieldNumber on the schema thas holds ducument number

0

Utilisation de ts-mongoose-pagination

    const trainers = await Trainer.paginate(
        { user: req.userId },
        {
            perPage: 3,
            page: 1,
            select: '-password, -createdAt -updatedAt -__v',
            sort: { createdAt: -1 },
        }
    )

    return res.status(200).json(trainers)

0
let page,limit,skip,lastPage, query;
 page = req.params.page *1 || 1;  //This is the page,fetch from the server
 limit = req.params.limit * 1 || 1; //  This is the limit ,it also fetch from the server
 skip = (page - 1) * limit;   // Number of skip document
 lastPage = page * limit;   //last index 
 counts = await userModel.countDocuments() //Number of document in the collection

query = query.skip(skip).limit(limit) //current page

const paginate = {}

//For previous page
if(skip > 0) {
   paginate.prev = {
       page: page - 1,
       limit: limit
} 
//For next page
 if(lastPage < counts) {
  paginate.next = {
     page: page + 1,
     limit: limit
}
results = await query //Here is the final results of the query.

-1

A également pu obtenir des résultats avec async / wait.

Exemple de code ci-dessous utilisant un gestionnaire asynchrone avec hapi v17 et mongoose v5

{
            method: 'GET',
            path: '/api/v1/paintings',
            config: {
                description: 'Get all the paintings',
                tags: ['api', 'v1', 'all paintings']
            },
            handler: async (request, reply) => {
                /*
                 * Grab the querystring parameters
                 * page and limit to handle our pagination
                */
                var pageOptions = {
                    page: parseInt(request.query.page) - 1 || 0, 
                    limit: parseInt(request.query.limit) || 10
                }
                /*
                 * Apply our sort and limit
                */
               try {
                    return await Painting.find()
                        .sort({dateCreated: 1, dateModified: -1})
                        .skip(pageOptions.page * pageOptions.limit)
                        .limit(pageOptions.limit)
                        .exec();
               } catch(err) {
                   return err;
               }

            }
        }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.