| {% include anchor.html edit="true" title="Map/reduce queries" hash="query_database" %} |
| |
| {% highlight js %} |
| db.query(fun, [options], [callback]) |
| {% endhighlight %} |
| |
| Invoke a map/reduce function, which allows you to perform more complex queries on PouchDB than what you get with [allDocs()](#batch_fetch), [changes()](#changes), or [find()](#query_index). The [CouchDB documentation for map/reduce](http://docs.couchdb.org/en/latest/couchapp/views/intro.html) applies to PouchDB. |
| |
| Since views perform a full scan of all documents, this method may be slow, unless you first save your view in a design document. Read the [query guide](/guides/queries.html) for a good tutorial. |
| |
| {% include alert/start.html variant="warning"%} |
| {% markdown %} |
| |
| **Warning: advanced API.** The map/reduce API is designed for cases that are too complex for Mango queries, which are described in |
| [createIndex()](#create_index), [find()](#query_index), [listIndexes()](#list_indexes), and [deleteIndex()](#delete_index). |
| |
| Under the hood, Mango indexes are the same as map/reduce indexes. The Mango API is just a simplified user-facing API on top of map/reduce. |
| |
| {% endmarkdown %} |
| {% include alert/end.html%} |
| |
| ### Options |
| |
| All options default to `false` unless otherwise specified. |
| |
| * `fun`: Map/reduce function, which can be one of the following: |
| * A full CouchDB-style map/reduce view: `{map : ..., reduce: ...}`. |
| * A map function by itself (no reduce). |
| * The name of a view in an existing design document (e.g. `'mydesigndoc/myview'`, or `'myview'` as a shorthand for `'myview/myview'`). |
| * `options.reduce`: Reduce function, or the string name of a built-in function: `'_sum'`, `'_count'`, or `'_stats'`. Defaults to `false` (no reduce). |
| * Tip: if you're not using a built-in, [you're probably doing it wrong](http://youtu.be/BKQ9kXKoHS8?t=865s). |
| * PouchDB will always call your reduce function with rereduce == false. As for CouchDB, refer to the [CouchDB documentation](http://docs.couchdb.org/en/1.6.1/couchapp/views/intro.html). |
| * `options.include_docs`: Include the document in each row in the `doc` field. |
| - `options.conflicts`: Include conflicts in the `_conflicts` field of a doc. |
| - `options.attachments`: Include attachment data. |
| - `options.binary`: Return attachment data as Blobs/Buffers, instead of as base64-encoded strings. |
| * `options.startkey` & `options.endkey`: Get rows with keys in a certain range (inclusive/inclusive). |
| * `options.inclusive_end`: Include rows having a key equal to the given `options.endkey`. Default: `true`. |
| * `options.limit`: Maximum number of rows to return. |
| * `options.skip`: Number of rows to skip before returning (warning: poor performance on IndexedDB/LevelDB!). |
| * `options.descending`: Reverse the order of the output rows. |
| * `options.key`: Only return rows matching this key. |
| * `options.keys`: Array of keys to fetch in a single shot. |
| - Neither `startkey` nor `endkey` can be specified with this option. |
| - The rows are returned in the same order as the supplied `keys` array. |
| - The row for a deleted document will have the revision ID of the deletion, and an extra key `"deleted":true` in the `value` property. |
| - The row for a nonexistent document will just contain an `"error"` property with the value `"not_found"`. |
| * `options.group`: True if you want the reduce function to group results by keys, rather than returning a single result. Defaults to `false`. |
| * `options.group_level`: Number of elements in a key to group by, assuming the keys are arrays. Defaults to the full length of the array. |
| * `options.stale`: One of `'ok'` or `'update_after'`. Only applies to saved views. Can be one of: |
| * unspecified (default): Returns the latest results, waiting for the view to build if necessary. |
| * `'ok'`: Returns results immediately, even if they're out-of-date. |
| * `'update_after'`: Returns results immediately, but kicks off a build afterwards. |
| |
| For details, see the [CouchDB query options documentation](http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options). |
| |
| #### Example Usage: |
| |
| {% include code/start.html id="query1" type="callback" %} |
| {% highlight js %} |
| // create a design doc |
| var ddoc = { |
| _id: '_design/index', |
| views: { |
| index: { |
| map: function mapFun(doc) { |
| if (doc.title) { |
| emit(doc.title); |
| } |
| }.toString() |
| } |
| } |
| } |
| |
| // save the design doc |
| db.put(ddoc, function (err) { |
| if (err && err.name !== 'conflict') { |
| return console.log(err); |
| } |
| // ignore if doc already exists |
| // find docs where title === 'Lisa Says' |
| db.query('index', { |
| key: 'Lisa Says', |
| include_docs: true |
| }, function (err, result) { |
| if (err) { return console.log(err); } |
| // handle result |
| }); |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query1" type="async" %} |
| {% highlight js %} |
| // create a design doc |
| var ddoc = { |
| _id: '_design/index', |
| views: { |
| index: { |
| map: function mapFun(doc) { |
| if (doc.title) { |
| emit(doc.title); |
| } |
| }.toString() |
| } |
| } |
| } |
| |
| // save the design doc |
| try { |
| try { |
| await db.put(ddoc); |
| } catch (err) { |
| if (err.name !== 'conflict') { |
| throw err; |
| } |
| // ignore if doc already exists |
| } |
| // find docs where title === 'Lisa Says' |
| var result = await db.query('index', { |
| key: 'Lisa Says', |
| include_docs: true |
| }); |
| } catch (err) { |
| console.log(err); |
| } |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query1" type="promise" %} |
| {% highlight js %} |
| // create a design doc |
| var ddoc = { |
| _id: '_design/index', |
| views: { |
| index: { |
| map: function mapFun(doc) { |
| if (doc.title) { |
| emit(doc.title); |
| } |
| }.toString() |
| } |
| } |
| } |
| |
| // save the design doc |
| db.put(ddoc).catch(function (err) { |
| if (err.name !== 'conflict') { |
| throw err; |
| } |
| // ignore if doc already exists |
| }).then(function () { |
| // find docs where title === 'Lisa Says' |
| return db.query('index', { |
| key: 'Lisa Says', |
| include_docs: true |
| }); |
| }).then(function (result) { |
| // handle result |
| }).catch(function (err) { |
| console.log(err); |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| #### Example Response: |
| {% highlight js %} |
| { |
| "offset" : 0, |
| "rows": [{ |
| "id": "doc3", |
| "key": "Lisa Says", |
| "value": null, |
| "doc": { |
| "_id": "doc3", |
| "_rev": "1-z", |
| "title": "Lisa Says" |
| } |
| }], |
| "total_rows" : 4 |
| } |
| {% endhighlight %} |
| |
| In the result,`total_rows` is the total number of possible results in the view. The response is very similar to that of `allDocs()`. |
| |
| **Note:** you can also pass in the map function instead of saving a design doc first, but this is slow because it has to do a full database scan. The following examples will use this pattern for simplicity's sake, but you should normally avoid it. |
| |
| #### Complex keys |
| |
| You can also use [complex keys](https://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Complex_Keys) for fancy ordering: |
| |
| {% include code/start.html id="query2" type="callback" %} |
| {% highlight js %} |
| function map(doc) { |
| // sort by last name, first name, and age |
| emit([doc.lastName, doc.firstName, doc.age]); |
| } |
| db.query(map, function (err, response) { |
| if (err) { return console.log(err); } |
| // handle result |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query2" type="async" %} |
| {% highlight js %} |
| function map(doc) { |
| // sort by last name, first name, and age |
| emit([doc.lastName, doc.firstName, doc.age]); |
| } |
| try { |
| var result = await db.query(map); |
| } catch (err) { |
| console.log(err); |
| } |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query2" type="promise" %} |
| {% highlight js %} |
| function map(doc) { |
| // sort by last name, first name, and age |
| emit([doc.lastName, doc.firstName, doc.age]); |
| } |
| db.query(map).then(function (result) { |
| // handle result |
| }).catch(function (err) { |
| console.log(err); |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| #### Example Response: |
| {% highlight js %} |
| { |
| "offset": 0, |
| "rows": [{ |
| "id" : "bowie", |
| "key" : ["Bowie", "David", 67] |
| }, { |
| "id" : "dylan", |
| "key" : ["Dylan", "Bob", 72] |
| }, { |
| "id" : "younger_dylan", |
| "key" : ["Dylan", "Jakob", 44] |
| }, { |
| "id" : "hank_the_third", |
| "key" : ["Williams", "Hank", 41] |
| }, { |
| "id" : "hank", |
| "key" : ["Williams", "Hank", 91] |
| }], |
| "total_rows": 5 |
| } |
| {% endhighlight %} |
| |
| **Tips:** |
| |
| * The sort order is `[nulls, booleans, numbers, strings, arrays, objects]`, so `{startkey: ['Williams'], endkey: ['Williams', {}]}` would return all people with the last name `'Williams'` because objects are higher than strings. Something like `'zzzzz'` or `'\uffff'` would also work. |
| * `group_level` can be very helpful when working with complex keys. In the example above, you can use `{group_level: 1}` to group by last name, or `{group_level: 2}` to group by last and first name. (Be sure to set `{reduce: true, group: true}` as well.) |
| |
| #### Linked documents |
| |
| PouchDB fully supports [linked documents](https://wiki.apache.org/couchdb/Introduction_to_CouchDB_views#Linked_documents). Use them to join two types of documents together, by simply adding an `_id` to the emitted value: |
| |
| {% include code/start.html id="query3" type="callback" %} |
| {% highlight js %} |
| function map(doc) { |
| // join artist data to albums |
| if (doc.type === 'album') { |
| emit(doc.name, {_id : doc.artistId, albumYear : doc.year}); |
| } |
| } |
| db.query(map, {include_docs : true}, function (err, response) { |
| if (err) { return console.log(err); } |
| // handle result |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query3" type="async" %} |
| {% highlight js %} |
| function map(doc) { |
| // join artist data to albums |
| if (doc.type === 'album') { |
| emit(doc.name, {_id : doc.artistId, albumYear : doc.year}); |
| } |
| } |
| try { |
| var result = await db.query(map, {include_docs : true}); |
| } catch (err) { |
| console.log(err); |
| } |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query3" type="promise" %} |
| {% highlight js %} |
| function map(doc) { |
| // join artist data to albums |
| if (doc.type === 'album') { |
| emit(doc.name, {_id : doc.artistId, albumYear : doc.year}); |
| } |
| } |
| db.query(map, {include_docs : true}).then(function (result) { |
| // handle result |
| }).catch(function (err) { |
| console.log(err); |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| #### Example response: |
| |
| {% highlight js %} |
| { |
| "offset": 0, |
| "rows": [ |
| { |
| "doc": { |
| "_id": "bowie", |
| "_rev": "1-fdb234b78904a5c8293f2acf4be70d44", |
| "age": 67, |
| "firstName": "David", |
| "lastName": "Bowie" |
| }, |
| "id": "album_hunkeydory", |
| "key": "Hunky Dory", |
| "value": { |
| "_id": "bowie", |
| "albumYear": 1971 |
| } |
| }, |
| { |
| "doc": { |
| "_id": "bowie", |
| "_rev": "1-fdb234b78904a5c8293f2acf4be70d44", |
| "age": 67, |
| "firstName": "David", |
| "lastName": "Bowie" |
| }, |
| "id": "album_low", |
| "key": "Low", |
| "value": { |
| "_id": "bowie", |
| "albumYear": 1977 |
| } |
| }, |
| { |
| "doc": { |
| "_id": "bowie", |
| "_rev": "1-fdb234b78904a5c8293f2acf4be70d44", |
| "age": 67, |
| "firstName": "David", |
| "lastName": "Bowie" |
| }, |
| "id": "album_spaceoddity", |
| "key": "Space Oddity", |
| "value": { |
| "_id": "bowie", |
| "albumYear": 1969 |
| } |
| } |
| ], |
| "total_rows": 3 |
| } |
| {% endhighlight %} |
| |
| #### Closures |
| |
| If you pass a function to `db.query` and give it the `emit` function as the second argument, then you can use a closure. (Since PouchDB has to use `eval()` to bind `emit`.) |
| |
| {% include code/start.html id="query4" type="callback" %} |
| {% highlight js %} |
| // BAD! will throw error |
| var myId = 'foo'; |
| db.query(function(doc) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }, function(err, results) { |
| // you'll get an error here |
| }); |
| |
| // will be fine |
| var myId = 'foo'; |
| db.query(function(doc, emit) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }, function(err, results) { |
| if (err) { return console.log(err); } |
| // handle result |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query4" type="async" %} |
| {% highlight js %} |
| // BAD! will throw error |
| var myId = 'foo'; |
| try { |
| var result = await db.query(function(doc) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }); |
| } catch (err) { |
| // you'll get an error here |
| } |
| |
| // will be fine |
| var myId = 'foo'; |
| try { |
| var result = await db.query(function(doc, emit) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }); |
| } catch (err) { |
| console.log(err); |
| } |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| {% include code/start.html id="query4" type="promise" %} |
| {% highlight js %} |
| // BAD! will throw error |
| var myId = 'foo'; |
| db.query(function(doc) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }).catch(function (err) { |
| // you'll get an error here |
| } |
| |
| // will be fine |
| var myId = 'foo'; |
| db.query(function(doc, emit) { |
| if (doc._id === myId) { |
| emit(doc); |
| } |
| }).then(function (result) { |
| // handle result |
| }).catch(function (err) { |
| console.log(err); |
| }); |
| {% endhighlight %} |
| {% include code/end.html %} |
| |
| Note that closures are only supported by local databases with temporary views. So if you are using closures, then you must use the slower method that requires a full database scan. |