You are viewing a plain text version of this content. The canonical link for it is here.
Posted to user@couchdb.apache.org by Brian Candler <B....@pobox.com> on 2009/01/30 11:18:59 UTC

Incremental map/reduce

I'm trying to understand how couchdb does incremental updates to its
map/reduce views. By this, I understand that it only has to read those
documents which have changed since the view was last updated.

I have an idea how it might work, so let me pose it as an example. Here's a
database with 6 documents and a very simple summing map/reduce function.

-------------------------------------------------------------
#!/bin/sh
set -xe

HOST1=http://localhost:5984
LOCAL1=maptest
DB1="$HOST1/$LOCAL1"

curl -X DELETE "$DB1"; echo
curl -X PUT "$DB1"; echo
curl -sX PUT -d '{"values":[1,2]}' "$DB1/doc1"
curl -sX PUT -d '{"values":[]}' "$DB1/doc2"
curl -sX PUT -d '{"values":[3,4,5]}' "$DB1/doc3"
curl -sX PUT -d '{"values":[6,7]}' "$DB1/doc4"
curl -sX PUT -d '{"values":[8]}' "$DB1/doc5"
curl -sX PUT -d '{"values":[9]}' "$DB1/doc6"
curl -sX PUT -T - "$DB1/_design/test" <<EOS
{
  "views": {
    "sum": {
      "map": "function(doc) { if (doc.values) { for (var i in doc.values) { emit(null,doc.values[i]) } } }",
      "reduce": "function(key, values) { return sum(values) }"
    }
  }
}
EOS

echo -e "\n\nReading view..."
curl "$DB1/_view/test/sum"
-------------------------------------------------------------

I am guessing there must be an upper bound to the number of keys and values
passed at once to the reduce function, so let me assume for now that limit
is 3. Then the process is:

           doc1       doc3     doc4  doc5 doc6
MAP         / \     /  |  \     / \    |   |
           1   2   3   4   5   6   7   8   9
REDUCE      \  |  /     \  |  /     \  |  /
               6          15          24
REDUCE          `--------. | ,--------'
                          45

Now, suppose I delete doc5. In order to update this sum incrementally, I
think I would have to:
(1) Regenerate the map block(s) which included doc5
    - so this would now be [7,9] instead of [7,8,9]
(2) Reduce those block(s) - giving 16 instead of 24
(3) Down the reduction tree, re-reduce those blocks which depend on that
    input

Is that an accurate description of the process?

In order to do this, couchdb would have to remember all the intermediate
reduce values, and also know that the intermediate value 24 was derived from
doc4, doc5 and doc6. Furthermore, it would have to re-map doc4 and doc6 to
generate the new value, even though they had not changed.

Alternatively, it could keep all the map results materialised, each tagged
with the the docid where it came from, so it could simply remove the 8 from
the map when doc5 is deleted. Is that what it does? (If so, I think it could
be useful to expose that in the view, so that a single view could be used
as both map and map+reduce)

Now, the other thing which I don't understand is group_level.

I can understand that emit gives (key,value) pairs, so group=true gives me
the intermediate reduction values for rows with identical keys. For example,
if I change the above map function to

      "map": "function(doc) { if (doc.values) { for (var i in doc.values) { emit(doc._id,doc.values[i]) } } }",

this gives the result I expect, and I imagine that the reduction process
is working something like this:

           doc1       doc3     doc4  doc5 doc6
MAP         / \     /  |  \     / \    |   |
           1   2   3   4   5   6   7   8   9
REDUCE      \ /     \  |  /     \ /    |   |
             3        12        13     8   9   ==> group=true
REDUCE        `------. | ,-----'        \ /
                      28                17
REDUCE                  `------. ,-----'
                               45              ==> group=false

Is that right? But in that case, what does group_level=2 do? Is this
something which comes into play if emitted keys are structured as arrays?

Anyway, sorry for the long E-mail. I'm just trying to get all this clear in
my head :-)

Many thanks,

Brian.

Re: Incremental map/reduce

Posted by Jan Lehnardt <ja...@apache.org>.
We have a vote running on dev@, please join there :)

Cheers
Jan
--


On 31 Jan 2009, at 18:56, Brian Candler wrote:

> On Fri, Jan 30, 2009 at 08:56:25PM +0530, Niket Patel wrote:
>> http://couchdb.markmail.org/message/hn6k7qxc62r2whtf
>> It have script in python and ruby to get pretty output form curl
>>
>> and about \n at end of response also discussed previously  
>> aggressively
>> :-)
>
> OK, I see that in the thread you reference.
>
> Noah wrote:
>
> | This proposal could go two ways:
> |
> | * CouchDB consistently omits the trailing newline from text files
> | * CouchDB consistently adds the trailing newline to text files
> |
> | Where "file" is a reference to the HTTP response entity-body with  
> a media
> | type of application/json, which is in turn a text based format.
>
> I think there is a third option: Couchdb consistently adds a newline  
> to the
> end of any JSON-encoded text which *it* generates.
>
> Arguments about cryptographic signatures are moot: Couchdb does not  
> store
> the JSON document it receives, byte-for-byte as-is. It parses it  
> into some
> internal data structure, and then encodes that data structure back  
> into
> JSON. White-space is not preserved; and an example was posted a few  
> days ago
> showing a unicode character being converted into its \uxxxx form.
>
> The thread ended with Chris Anderson writing:
>
> | I'm not absolutely certain of the merits in this case, but as long  
> as we're
> | consistent I don't see the harm in adding newlines to JSON output.  
> There's
> | nothing in the JSON RFC that says we can't.
> |
> | I've added a ticket and a patch for when we decide which way to go:
> | https://issues.apache.org/jira/browse/COUCHDB-107
>
> and JIRA remains marked "Please discuss (again, sorry)"
>
> So, for what it's worth, I add my +1 to adding a trailing newline to
> Couchdb-generated JSON.
>
> Regards,
>
> Brian.
>


Re: Incremental map/reduce

Posted by Brian Candler <B....@pobox.com>.
On Fri, Jan 30, 2009 at 08:56:25PM +0530, Niket Patel wrote:
> http://couchdb.markmail.org/message/hn6k7qxc62r2whtf
> It have script in python and ruby to get pretty output form curl
>
> and about \n at end of response also discussed previously aggressively 
> :-)

OK, I see that in the thread you reference.

Noah wrote:

| This proposal could go two ways:
| 
| * CouchDB consistently omits the trailing newline from text files
| * CouchDB consistently adds the trailing newline to text files
| 
| Where "file" is a reference to the HTTP response entity-body with a media
| type of application/json, which is in turn a text based format.

I think there is a third option: Couchdb consistently adds a newline to the
end of any JSON-encoded text which *it* generates.

Arguments about cryptographic signatures are moot: Couchdb does not store
the JSON document it receives, byte-for-byte as-is. It parses it into some
internal data structure, and then encodes that data structure back into
JSON. White-space is not preserved; and an example was posted a few days ago
showing a unicode character being converted into its \uxxxx form.

The thread ended with Chris Anderson writing:

| I'm not absolutely certain of the merits in this case, but as long as we're
| consistent I don't see the harm in adding newlines to JSON output. There's
| nothing in the JSON RFC that says we can't.
| 
| I've added a ticket and a patch for when we decide which way to go:
| https://issues.apache.org/jira/browse/COUCHDB-107 

and JIRA remains marked "Please discuss (again, sorry)"

So, for what it's worth, I add my +1 to adding a trailing newline to
Couchdb-generated JSON.

Regards,

Brian.

Re: Incremental map/reduce

Posted by Niket Patel <ne...@me.com>.
Brian,

http://couchdb.markmail.org/message/hn6k7qxc62r2whtf
It have script in python and ruby to get pretty output form curl

and about \n at end of response also discussed previously  
aggressively :-)
just for nice curl output things should not be changed but it is  
personal preference

- NIket

On Jan 30, 2009, at 8:39 PM, Brian Candler wrote:

> On Fri, Jan 30, 2009 at 03:17:58PM +0100, Patrick Antivackis wrote:
>> normally in your view if you add a reduce=false parameter you  
>> should only
>> get the map
>
> That's exactly what I was thinking of, and I have no idea why I  
> overlooked
> this parameter on the wiki :-) Thanks for putting me straight.
>
> I have one, teeny suggestion. I notice that couchdb does insert  
> newlines in
> its JSON sometimes, e.g.
>
> $ curl 'http://127.0.0.1:5984/maptest/_view/test/sum?reduce=false'
> {"total_rows":9,"offset":0,"rows":[
> {"id":"doc1","key":"doc1","value":1},
> {"id":"doc1","key":"doc1","value":2},
> {"id":"doc3","key":"doc3","value":3},
> {"id":"doc3","key":"doc3","value":4},
> {"id":"doc3","key":"doc3","value":5},
> {"id":"doc4","key":"doc4","value":6},
> {"id":"doc4","key":"doc4","value":7},
> {"id":"doc5","key":"doc5","value":8},
> {"id":"doc6","key":"doc6","value":9}
> ]}$
>
> But not in others:
>
> $ curl 'http://127.0.0.1:54/maptest/_view/test/sum?group=true'
> {"rows":[{"key":"doc1","value":3},{"key":"doc3","value":12}, 
> {"key":"doc4","value":13},{"key":"doc5","value":8}, 
> {"key":"doc6","value":9}]}$
>
> What I'd really like is for it to send a single newline at the end  
> of the
> entire response, so that curl output terminates nicely with my shell  
> prompt
> on a new line. Would this be considered an excessive waste of  
> bandwidth? :-)
>
> Cheers,
>
> Brian.


Re: Incremental map/reduce

Posted by Brian Candler <B....@pobox.com>.
On Fri, Jan 30, 2009 at 03:17:58PM +0100, Patrick Antivackis wrote:
> normally in your view if you add a reduce=false parameter you should only
> get the map

That's exactly what I was thinking of, and I have no idea why I overlooked
this parameter on the wiki :-) Thanks for putting me straight.

I have one, teeny suggestion. I notice that couchdb does insert newlines in
its JSON sometimes, e.g.

$ curl 'http://127.0.0.1:5984/maptest/_view/test/sum?reduce=false'
{"total_rows":9,"offset":0,"rows":[
{"id":"doc1","key":"doc1","value":1},
{"id":"doc1","key":"doc1","value":2},
{"id":"doc3","key":"doc3","value":3},
{"id":"doc3","key":"doc3","value":4},
{"id":"doc3","key":"doc3","value":5},
{"id":"doc4","key":"doc4","value":6},
{"id":"doc4","key":"doc4","value":7},
{"id":"doc5","key":"doc5","value":8},
{"id":"doc6","key":"doc6","value":9}
]}$ 

But not in others:

$ curl 'http://127.0.0.1:54/maptest/_view/test/sum?group=true'
{"rows":[{"key":"doc1","value":3},{"key":"doc3","value":12},{"key":"doc4","value":13},{"key":"doc5","value":8},{"key":"doc6","value":9}]}$ 

What I'd really like is for it to send a single newline at the end of the
entire response, so that curl output terminates nicely with my shell prompt
on a new line. Would this be considered an excessive waste of bandwidth? :-)

Cheers,

Brian.

Re: Incremental map/reduce

Posted by Patrick Antivackis <pa...@gmail.com>.
Hello Brian,
normally in your view if you add a reduce=false parameter you should only
get the map

2009/1/30 Brian Candler <B....@pobox.com>

> On Fri, Jan 30, 2009 at 11:34:23AM +0100, Jan Lehnardt wrote:
> > Hi Brian,
> >
> > http://damienkatz.net/2008/02/incremental_map.html
> > http://damienkatz.net/2008/02/incremental_map_1.html
> > http://horicky.blogspot.com/2008/10/couchdb-implementation.html
>
> Thank you.
>
> According to the last of these, "The intermediate objects of the map() and
> the reduce() is stored in the view indexes."
>
> So potentially it would be possible to query just the map part of a
> map/reduce view? That could be useful. A few times now I've had to remove
> the reduce function from a view just to check whether the map was doing
> what
> I expected :-)
>
> Regards,
>
> Brian.
>

Re: Incremental map/reduce

Posted by Brian Candler <B....@pobox.com>.
On Fri, Jan 30, 2009 at 11:34:23AM +0100, Jan Lehnardt wrote:
> Hi Brian,
>
> http://damienkatz.net/2008/02/incremental_map.html
> http://damienkatz.net/2008/02/incremental_map_1.html
> http://horicky.blogspot.com/2008/10/couchdb-implementation.html

Thank you.

According to the last of these, "The intermediate objects of the map() and
the reduce() is stored in the view indexes."

So potentially it would be possible to query just the map part of a
map/reduce view? That could be useful. A few times now I've had to remove
the reduce function from a view just to check whether the map was doing what
I expected :-)

Regards,

Brian.

Re: Incremental map/reduce

Posted by Jan Lehnardt <ja...@apache.org>.
Hi Brian,

http://damienkatz.net/2008/02/incremental_map.html
http://damienkatz.net/2008/02/incremental_map_1.html
http://horicky.blogspot.com/2008/10/couchdb-implementation.html

Cheers
Jan
--
On 30 Jan 2009, at 11:18, Brian Candler wrote:

> I'm trying to understand how couchdb does incremental updates to its
> map/reduce views. By this, I understand that it only has to read those
> documents which have changed since the view was last updated.
>
> I have an idea how it might work, so let me pose it as an example.  
> Here's a
> database with 6 documents and a very simple summing map/reduce  
> function.
>
> -------------------------------------------------------------
> #!/bin/sh
> set -xe
>
> HOST1=http://localhost:5984
> LOCAL1=maptest
> DB1="$HOST1/$LOCAL1"
>
> curl -X DELETE "$DB1"; echo
> curl -X PUT "$DB1"; echo
> curl -sX PUT -d '{"values":[1,2]}' "$DB1/doc1"
> curl -sX PUT -d '{"values":[]}' "$DB1/doc2"
> curl -sX PUT -d '{"values":[3,4,5]}' "$DB1/doc3"
> curl -sX PUT -d '{"values":[6,7]}' "$DB1/doc4"
> curl -sX PUT -d '{"values":[8]}' "$DB1/doc5"
> curl -sX PUT -d '{"values":[9]}' "$DB1/doc6"
> curl -sX PUT -T - "$DB1/_design/test" <<EOS
> {
>  "views": {
>    "sum": {
>      "map": "function(doc) { if (doc.values) { for (var i in  
> doc.values) { emit(null,doc.values[i]) } } }",
>      "reduce": "function(key, values) { return sum(values) }"
>    }
>  }
> }
> EOS
>
> echo -e "\n\nReading view..."
> curl "$DB1/_view/test/sum"
> -------------------------------------------------------------
>
> I am guessing there must be an upper bound to the number of keys and  
> values
> passed at once to the reduce function, so let me assume for now that  
> limit
> is 3. Then the process is:
>
>           doc1       doc3     doc4  doc5 doc6
> MAP         / \     /  |  \     / \    |   |
>           1   2   3   4   5   6   7   8   9
> REDUCE      \  |  /     \  |  /     \  |  /
>               6          15          24
> REDUCE          `--------. | ,--------'
>                          45
>
> Now, suppose I delete doc5. In order to update this sum  
> incrementally, I
> think I would have to:
> (1) Regenerate the map block(s) which included doc5
>    - so this would now be [7,9] instead of [7,8,9]
> (2) Reduce those block(s) - giving 16 instead of 24
> (3) Down the reduction tree, re-reduce those blocks which depend on  
> that
>    input
>
> Is that an accurate description of the process?
>
> In order to do this, couchdb would have to remember all the  
> intermediate
> reduce values, and also know that the intermediate value 24 was  
> derived from
> doc4, doc5 and doc6. Furthermore, it would have to re-map doc4 and  
> doc6 to
> generate the new value, even though they had not changed.
>
> Alternatively, it could keep all the map results materialised, each  
> tagged
> with the the docid where it came from, so it could simply remove the  
> 8 from
> the map when doc5 is deleted. Is that what it does? (If so, I think  
> it could
> be useful to expose that in the view, so that a single view could be  
> used
> as both map and map+reduce)
>
> Now, the other thing which I don't understand is group_level.
>
> I can understand that emit gives (key,value) pairs, so group=true  
> gives me
> the intermediate reduction values for rows with identical keys. For  
> example,
> if I change the above map function to
>
>      "map": "function(doc) { if (doc.values) { for (var i in  
> doc.values) { emit(doc._id,doc.values[i]) } } }",
>
> this gives the result I expect, and I imagine that the reduction  
> process
> is working something like this:
>
>           doc1       doc3     doc4  doc5 doc6
> MAP         / \     /  |  \     / \    |   |
>           1   2   3   4   5   6   7   8   9
> REDUCE      \ /     \  |  /     \ /    |   |
>             3        12        13     8   9   ==> group=true
> REDUCE        `------. | ,-----'        \ /
>                      28                17
> REDUCE                  `------. ,-----'
>                               45              ==> group=false
>
> Is that right? But in that case, what does group_level=2 do? Is this
> something which comes into play if emitted keys are structured as  
> arrays?
>
> Anyway, sorry for the long E-mail. I'm just trying to get all this  
> clear in
> my head :-)
>
> Many thanks,
>
> Brian.
>


Re: Incremental map/reduce

Posted by Brian Candler <B....@pobox.com>.
On Sat, Jan 31, 2009 at 01:42:54PM -0800, Chris Anderson wrote:
> It's how it works today. The reason we see a small cost with each
> reduce query is that the intermediate reduction values are cached
> according to the btree structure, instead of according to the query
> params. So unless your range happens to match exactly the keys
> underneath a given inner node (and probably a this point even if it
> does) you'll end up running at least one javascript reduction per
> reduce query.

... and a group=true or group_level=N query basically generates a load of
individual group=false queries across different key ranges, so may take O(n)
in the number of groups returned.

I'm much happier about this now. Thank you!

Cheers,

Brian.

Re: Incremental map/reduce

Posted by Chris Anderson <jc...@apache.org>.
On Sat, Jan 31, 2009 at 1:00 PM, Brian Candler <B....@pobox.com> wrote:
> On Fri, Jan 30, 2009 at 10:32:15AM -0800, Chris Anderson wrote:
>> Once you understand how normal reduce queries (with group=false) work,
>> eg: those that return a single reduction value for whatever key-range
>> you specify, group_level queries are not more complex. Group_level
>> queries are essentially a macro, which run one normal (group=false)
>> reduce query automatically for each interval on a set of intervals as
>> defined by the level.
>
> Ah - it was new to me that map/reduce queries with group=false could run
> over arbitary key intervals:
>
> $ kurl 'http://localhost:5984/maptest/_view/test/sum'
> {"rows":[{"key":null,"value":45}]}
> $ kurl 'http://localhost:5984/maptest/_view/test/sum?startkey="doc3"'
> {"rows":[{"key":null,"value":42}]}
> $ kurl 'http://localhost:5984/maptest/_view/test/sum?startkey="doc3"&endkey="doc5"'
> {"rows":[{"key":null,"value":33}]}
>
> This means that couchdb *must* be performing the reduce part of the query
> on-demand, as opposed to keeping precomputed values stored like the map
> part.
>
> In SQL terms, this is like "count(*)" doing an index scan, rather than
> having the answer precomputed in a materialised view. And suddenly the
> various forms of reduce make much more sense.
>
> However at http://damienkatz.net/2008/02/incremental_map.html it says:
>
> "This requirement of reduce functions allows CouchDB to store off
> intermediated reductions directly into inner nodes of btree indexes, and the
> view index updates and retrievals will have logarithmic cost. It also allows
> the indexes to be spread across machines and reduced at query time with
> logarithmic cost."
>
> Is storing the reductions a planned future feature, rather than describing
> how it works today?

It's how it works today. The reason we see a small cost with each
reduce query is that the intermediate reduction values are cached
according to the btree structure, instead of according to the query
params. So unless your range happens to match exactly the keys
underneath a given inner node (and probably a this point even if it
does) you'll end up running at least one javascript reduction per
reduce query.

Thanks for looking after the wiki!

Chris

-- 
Chris Anderson
http://jchris.mfdz.com

Re: Incremental map/reduce

Posted by Brian Candler <B....@pobox.com>.
On Fri, Jan 30, 2009 at 10:32:15AM -0800, Chris Anderson wrote:
> Once you understand how normal reduce queries (with group=false) work,
> eg: those that return a single reduction value for whatever key-range
> you specify, group_level queries are not more complex. Group_level
> queries are essentially a macro, which run one normal (group=false)
> reduce query automatically for each interval on a set of intervals as
> defined by the level.

Ah - it was new to me that map/reduce queries with group=false could run
over arbitary key intervals:

$ kurl 'http://localhost:5984/maptest/_view/test/sum'
{"rows":[{"key":null,"value":45}]}
$ kurl 'http://localhost:5984/maptest/_view/test/sum?startkey="doc3"'
{"rows":[{"key":null,"value":42}]}
$ kurl 'http://localhost:5984/maptest/_view/test/sum?startkey="doc3"&endkey="doc5"'
{"rows":[{"key":null,"value":33}]}

This means that couchdb *must* be performing the reduce part of the query
on-demand, as opposed to keeping precomputed values stored like the map
part.

In SQL terms, this is like "count(*)" doing an index scan, rather than
having the answer precomputed in a materialised view. And suddenly the
various forms of reduce make much more sense.

However at http://damienkatz.net/2008/02/incremental_map.html it says:

"This requirement of reduce functions allows CouchDB to store off
intermediated reductions directly into inner nodes of btree indexes, and the
view index updates and retrievals will have logarithmic cost. It also allows
the indexes to be spread across machines and reduced at query time with
logarithmic cost."

Is storing the reductions a planned future feature, rather than describing
how it works today?

Sorry to keep asking, but I'm going to try to update the wiki in as accurate
a way as I can.

Thanks,

Brian.

Re: Incremental map/reduce

Posted by Chris Anderson <jc...@apache.org>.
On Fri, Jan 30, 2009 at 2:18 AM, Brian Candler <B....@pobox.com> wrote:
> Now, the other thing which I don't understand is group_level.

I think you're understanding of most of this well enough such that the
other links will get you the rest of the way there. I'll respond on
group_level because it is typically the most confusing (or at least it
was to me) until you realize how overwhelmingly simple it is.

Once you understand how normal reduce queries (with group=false) work,
eg: those that return a single reduction value for whatever key-range
you specify, group_level queries are not more complex. Group_level
queries are essentially a macro, which run one normal (group=false)
reduce query automatically for each interval on a set of intervals as
defined by the level.

So with group_level=1, and keys like

["a",1,1]
["a",3,4]
["a",3,8]
["b",2,6]
["b",2,6]
["c",1,5]
["c",4,2]

CouchDB will internally run 3 reduce queries for you. One that reduces
all rows where the first element of the key = "a", one for "b", and
one for "c".

If you were to query with group_level=2, you'd get a reduce query run
for each unique set of keys (according to their first two elements),
eg ["a",1], ["a",3], ["b",2"], ["c",1], ["c",4]

group=true is the conceptual equivalent of group_level=exact , so
CouchDB runs a reduce per unique key in the map row set.

I find that thinking of group_level and group=true as macros, which
are just running a series of group=false queries internally, clarifies
understanding and expectations about these features. For instance, in
a group=true query, since we see that Couch will run a reduce query
per unique key (in the overall key range, as specified by start and
end keys), we can expect the cost to be O(n) where n is the number of
unique keys in the range. I used to be surprised when group=true
queries were "slow" but now that I understand the mechanism it's hard
to see how they couldn't be.

In the future we may cache the final reduction values for group_level
queries in another index, which could speed up these queries when they
are run a second time, as well as potentially allowing for
sort-by-value queries to be done more efficiently. Then you'll be able
to ask what the most popular tags in a corpus are. Currently queries
like that need to be done by running group=true, then sorting on the
client.

Hope that helps!

-- 
Chris Anderson
http://jchris.mfdz.com