Sorting CouchDB result by value - node.js

I'm brand new to CouchDB (and NoSQL in general), and am creating a simple Node.js + express + nano app to get a feel for it. It's a simple collection of books with two fields, 'title' and 'author'.
Example document:
{
"_id": "1223e03eade70ae11c9a3a20790001a9",
"_rev": "2-2e54b7aa874059a9180ac357c2c78e99",
"title": "The Art of War",
"author": "Sun Tzu"
}
Reduce function:
function(doc) {
if (doc.title && doc.author) {
emit(doc.title, doc.author);
}
}
Since CouchDB sorts by key and supports a 'descending=true' query param, it was easy to implement a filter in the UI to toggle sort order on the title, which is the key in my results set. Here's the UI:
List of books with link to sort title by ascending or descending
But I'm at a complete loss on how to do this for the author field.
I've seen this question, which helped a poster sort by a numeric reduce value, and I've read a blog post that uses a list to also sort by a reduce value, but I've not seen any way to do this on a string value without a reduce.

If you want to sort by a particular property, you need to ensure that that property is the key (or, in the case of an array key, the first element in the array).
I would recommend using the sort key as the key, emitting a null value and using include_docs to fetch the full document to allow you to display multiple properties in the UI (this also keeps the deserialized value consistent so you don't need to change how you handle the return value based on sort order).
Your map functions would be as simple as the following.
For sorting by author:
function(doc) {
if (doc.title && doc.author) {
emit(doc.author, null);
}
}
For sorting by title:
function(doc) {
if (doc.title && doc.author) {
emit(doc.title, null);
}
}
Now you just need to change which view you call based on the selected sort order and ensure you use the include_docs=true parameter on your query.
You could also use a single view for this by emitting both at once...
emit(["by_author", doc.author], null);
emit(["by_title", doc.title], null);
... and then using the composite key for your query.

Related

How to add empty startkey array index in couchDB view?

Currently I'm using hyperledger fabric with couchDB container. I have a CouchDB view :
function (doc) {
if(doc.groups.length > 0) {
doc.groups.forEach(function (tag) {
emit([doc.id, tag.fname, tag.lname, tag.no], tag);
});
}
}
I want to filter data using the start & end keys.
Can we use an empty index in startkey array for filter on view?
Here is example for the same:
http://0.0.0.0:7984/mychannel_team/_design/team_view/_view/team_view_filter
?skip=0&limit=21&reduce=false&startkey=[1,"isha",{},"45"]&endkey=[1,"isha",{},"45"]
Yes, by querying the view with startkey=[""]. One could use null too (3.2.2.5. Collation Specification).
Do note the parameters
startkey=[1,"isha",{},"45"]&endkey=[1,"isha",{},"45"]
is poorly constructed in the sense that the use of {} seems to imply a wildcard, which is does not.
These parameters are equivalent to the prior
startkey=[1,"isha",{}]&endkey=[1,"isha",{}]
Because any field value following {} is ignored. Think of {} as a match-all, not a field/placeholder wildcard.
As another matter: regarding the OP's map function, the first field of the complex key will always be null. It is probable the intention is to emit doc._id rather than doc.id.

CouchDb sort posts by a user in descending timestamp order

I have posts that have fields like username (author of the post) and timestamp (uploaded time). I am struggling to create a view, which when queried on, grabs posts by a particular user in descending order of the timestamp.
map: function(doc) {
if (doc.type == 'post'){
emit(doc.username, [doc._id, doc.timestamp]);
};
},
I can query documents authored by a particular user but how do I apply descending = true only on the timestamp field?
With CouchDB views, only the key can determine the sort order for the index. The value does not factor into sorting/grouping at all.
Thus, if you want to have a view that outputs posts by a user in order of creation (ascending or descending), you'll emit an array as a sort of "composite key". I would highly recommend reading through the Guide to Views in the CouchDB documentation.
For your example, I would make a map function like this:
function (doc) {
if (doc.type === 'post') {
emit([ doc.username, doc.timestamp ]);
}
}
Then, you can get the posts for a specific user with a query like:
?start_key=["username"]&end_key=["username","\ufff0"]
Which will find the posts for the given "username" ordered by the timestamp ascending. To reverse the ordering, use the following query instead:
?start_key=["username","\ufff0"]&end_key=["username"]&descending=true
Note that the values for start_key and end_key have swapped, and descending=true has been added.
As mentioned before, read through their documentation as it's an excellent way to wrap your head around the best way to use CouchDB views.

Reduce output must shrink more rapidly -- Reducing to a list of documents

I have a few documents in my couch db with json as below. The cId will change for each. And I have created a view with map/reduce function to filter out few documents and return a list of json documents.
Document structure -
{
"_id": "ccf8a36e55913b7cf5b015d6c50009f7",
"_rev": "8-586130996ad60ccef54775c51599e73f",
"cId": 1,
"Status": true
}
Here is the sample map:
function(doc) {
if(doc.Key && doc.Value && doc.Status == true)
emit(null, doc);
}
Here is the sample reduce:
function(key, values, rereduce){
var kv = [];
values.forEach(function(value){
if(value.cId != <some_val>){
kv.push({"k": value.cId, "v" : value});
}
});
return kv;
}
If there are two documents and reduce output has list containing 1 document, this works fine. But if I add one more document (with cId = 2), it throws the errors - "reduce output must shrink more rapidly". Why is this caused? And how can I achieve what I intend to do?
The cause of the error is, that the reduce function does not actually reduce anything (it rather is collecting objects). The documentation mentions this:
The way the B-tree storage works means that if you don’t actually
reduce your data in the reduce function, you end up having CouchDB
copy huge amounts of data around that grow linearly, if not faster
with the number of rows in your view.
CouchDB will be able to compute the final result, but only for views
with a few rows. Anything larger will experience a ridiculously slow
view build time. To help with that, CouchDB since version 0.10.0 will
throw an error if your reduce function does not reduce its input
values.
It is unclear to me, what you intend to achieve.
Do you want to retrieve a list of docs based on certain criteria? In this case, a view without reduce should suffice.
Edit: If the desired result depends on a value stored in a certain document, then CouchDB has a feature called list. It is a design function, that provides access to all docs of a given view, if you pass include_docs=true.
A list URL follow this pattern:
/db/_design/foo/_list/list-name/view-name
Like views, lists are defined in a design document:
{
"_id" : "_design/foo",
"lists" : {
"bar" : "function(head, req) {
var row;
while (row = getRow()) {
if (row.doc._id === 'baz') // Do stuff based on a certain doc
}
}"
},
... // views and other design functions
}

How can I query multiple key criteria?

Using couchdb, with the following json:
{"total_rows":3,"offset":0,"rows":[ {"id":"bc26e5eae7f8c8c3486818e7e7971df0","key":{"user":"lili#abc.com","pal":["igol ≠ eagle"],"fecha":"10/5/2014"},"value":null},{"id":"cf0dc2e2874776958c59f2f544b5a750","key":{"user":"lili#abc.com","pal":["kat ≠cat"],"fecha":"10/6/2014"},"value":null},{"id":"df4ec96088ed52096db064f2ebd2310b","key":{"user":"dum#ghi.com","pal":["dok ≠ duck"],"fecha":"10/7/2014"},"value":null}]}
I would like to query for specific user AND specific date:
for example:
?user="lili#def.com"&fecha:"10/6/2014"
I also tried:
?user%3Dlili%40def.com%26fecha%3A10%2F6%2F2014
Needless to say, it isn't currently working as I expected (all results are shown, not only the register needed).
my view func is:
function(doc) {
if (doc.USER){
emit({user:doc.USER, pal:doc.palabras, fecha:doc.fecha});
}
}
Regards.
Remember that CouchDB views are simply key/value lookups that are built at index-time, not query time. At the minute you are emitting a key with no value. If you want to look something up by two values, you'll need to emit a composite key (an array):
function(doc) {
if (doc.USER) {
emit([doc.USER, doc.fecha], doc);
}
}
Then you can look up matching documents by passing the array as the key:
?key=%5B%22lili%40def.com%22%2C%20%2210%2F6%2F2014%22%5D
There are optimisations you can make to this (e.g. emitting a null value and using include_docs to reduce the size of the view) but this should set you off on the right track.
I do the same thing as Ant P but I tend to use strings.
function ( doc ) {
if ( doc.USER ) {
emit( 'user-' + doc.USER + '-' + doc.fecha, doc );
}
}
I would also highly recommend emitting null instead of doc as a value.
Remember, you can always emit more than once depending on what kind of queries you need.
For example, if you're looking for all posts by a specific user between two dates, you could do the following view.
function ( doc ) {
if ( doc.type == "post" ) {
emit( 'user-' + doc.nombre, null );
emit( 'fecha-' + doc.fecha, null );
}
}
Then you would query the view twice _view/posts?key="user-miUsario", and _view/posts?start_key="fecha-1413040000000"&end_key="fecha-1413049452904". Then, once you have all of the ids from both views, you take the intersection and use _all_docs to get your original documents.
You end up making three requests but it saves disk space in the view, the payloads are smaller because you return null, and your code is simpler because you can query the same view multiple ways.

CouchDB reduce function useful in this scenario?

I want to store votes in CouchDB. To get round the problem of incrementing a field in one document and having millions of revisions, each vote will be a seperate document:
{
_id: "xyz"
type: "thumbs_up"
vote_id: "test"
}
So the actual document itself is the vote. The result I'd like is basically an array of: vote_id, sumOfThumbsUp, sumOfThumbsDown
Now I think my map function would need to look like:
if(type=="thumbs_up" | type =="thumbs_down"){
emit(vote_id, type)
}
Now here's the bit I can't figure out what to do, should I build a reduce function to somehow sum the vote types, keeping in mind there's two types of votes.
Or should I just take what's been emited from the map function and put it straight into an array to work on, ignoring the reduce function completely?
This is a perfect case for map-reduce! Having each document represent a vote is the right way to go in my opinion, and will work with CouchDB's strengths.
I would recommend a document structure like this:
Documents
UPVOTE
{
"type": "vote",
"vote_id": "test",
"vote": 1
}
DOWNVOTE
{
"type": "vote",
"vote_id": "test",
"vote": -1
}
I would use a document type of "vote", so you can have other document types in your database (like the vote category information, user information, etc)
I kept "vote_id" the same
I made the value field called "vote", and just used 1/-1 instead of "thumbs_up" or "thumbs_down" (really doesn't matter, you can do whatever you want and it will work just fine)
View
Map
function (doc) {
if (doc.type === "vote") {
emit(doc.vote_id, doc.vote);
}
}
Reduce
_sum
You end up with a result like this for your map function:
And if you reduce it:
As you add more vote documents with more vote_id variety, you can query for a specific vote_id by using: /:db/_design/:ddoc/_view/:view?reduce=true&group=true&key=":vote_id"

Resources