Permalink
Browse files

Added a map-reduce function.

  • Loading branch information...
christophermaier committed Aug 20, 2010
1 parent 6fc8345 commit df433fc11ab76c48dcfe8fa77c4bf19227161a92
Showing with 121 additions and 0 deletions.
  1. +57 −0 src/somnium/congomongo.clj
  2. +64 −0 test/congomongo_test.clj
View
@@ -323,6 +323,63 @@ When with-mongo and set-connection! interact, last one wins"
(:retval result)
(throw (Exception. (format "failure executing javascript: %s" (str result))))))))
+(defunk map-reduce
+ "Performs a map-reduce job on the server.
+
+ Mandatory arguments
+ collection -> the collection to run the job on
+ mapfn -> a JavaScript map function, as a String. Should take no arguments.
+ reducefn -> a JavaScript reduce function, as a String. Should take two arguments: a key, and a corresponding array of values
+
+ See http://www.mongodb.org/display/DOCS/MapReduce for more information, as well as the test code in congomongo_test.clj.
+
+ Optional Arguments
+ :query -> a query map against collection; if this is specified, the map-reduce job is run on the result of this query instead of on the collection as a whole.
+ :query-from -> if query is supplied, specifies what form it is in (:clojure, :json, or :mongo). Defaults to :clojure.
+ :sort -> if you want query sorted (for optimization), specify a map of sort clauses here.
+ :sort-from -> if sort is supplied, specifies what form it is in (:clojure, :json, or :mongo). Defaults to :clojure.
+ :limit -> the number of objects to return from a query collection (defaults to 0; that is, everything). This pertains to query, NOT the result of the overall map-reduce job!
+ :out -> output collection name (String or keyword). If this is specified, the output collection is made permanent.
+ :keeptemp -> should the output collection be treated as permanent? true or false (defaults to false).
+ :finalize -> a finalizaton function (JavaScript, as a String). Should take two arguments: a key and a single value (not an array of values).
+ :scope -> a scope object; variables in the object will be available in the global scope of map, reduce, and finalize functions.
+ :scope-from -> if scope is supplied, specifies what form it is in (:clojure, :json, or :mongo). Defaults to :clojure.
+ :output -> if you want the resulting documents from the map-reduce job, specify :documents; otherwise, if you want the name of the result collection as a keyword, specify :collection. Defaults to :documents.
+ :as -> if :output is set to :documents, determines the form the results take (:clojure, :json, or :mongo) (has no effect if :output is set to :collection; that is always returned as a Clojure keyword).
+"
+ {:arglists
+ '([collection mapfn reducefn :query :query-from :sort :sort-from :limit :out :keeptemp :finalize :scope :scope-from :output :as])}
+ [collection mapfn reducefn
+ :query {}
+ :query-from :clojure
+ :sort {}
+ :sort-from :clojure
+ :limit 0
+ :out nil
+ :keeptemp nil
+ :finalize ""
+ :scope nil
+ :scope-from :clojure
+ :output :documents
+ :as :clojure]
+ (let [mr-query {:mapreduce collection
+ :map mapfn
+ :reduce reducefn
+ :query (coerce query [query-from :mongo])
+ :sort (coerce sort [sort-from :mongo])
+ :limit limit
+ :out out
+ :keeptemp keeptemp
+ :finalize finalize
+ :scope (coerce scope [scope-from :mongo])}
+ mr-query (coerce mr-query [:clojure :mongo])
+ result (.mapReduce (get-coll collection) mr-query)]
+ (if (= output :documents)
+ (coerce (.results result) [:mongo as] :many :true)
+ (-> (.getOutputCollection result)
+ .getName
+ keyword))))
+
(def write-concern-map
{:none com.mongodb.DB$WriteConcern/NONE
:normal com.mongodb.DB$WriteConcern/NORMAL
View
@@ -194,6 +194,70 @@
(let [return (fetch-one :stuff :where {:name "name"})]
(is (vector? (:vector return))))))
+(deftest test-map-reduce
+ (with-test-mongo
+ (insert! :mr {:fruit "bananas" :count 1})
+ (insert! :mr {:fruit "bananas" :count 2})
+ (insert! :mr {:fruit "plantains" :count 3})
+ (insert! :mr {:fruit "plantains" :count 2})
+ (insert! :mr {:fruit "pineapples" :count 4})
+ (insert! :mr {:fruit "pineapples" :count 2})
+ (let [mapfn
+ "function(){
+ emit(this.fruit, {count: this.count});
+ }"
+ mapfn-with-scope
+ "function(){
+ emit((adj + ' ' + this.fruit), {count: this.count});
+ }"
+ reducefn
+ "function(key, values){
+ var total = 0;
+ for ( var i=0; i<values.length; i++ ){
+ total += values[i].count;
+ }
+ return { count : total };
+ }"
+ target-collection :monkey-shopping-list]
+ ;; See that the base case works
+ (is (= (map-reduce :mr mapfn reducefn)
+ (seq [{:_id "bananas" :value {:count 3}}
+ {:_id "pineapples" :value {:count 6}}
+ {:_id "plantains" :value {:count 5}}])))
+ ;; See if we can assign a name to the results collection
+ (is (= (map-reduce :mr mapfn reducefn :out target-collection)
+ (seq [{:_id "bananas" :value {:count 3}}
+ {:_id "pineapples" :value {:count 6}}
+ {:_id "plantains" :value {:count 5}}])))
+ (is (= (fetch target-collection)
+ (seq [{:_id "bananas" :value {:count 3}}
+ {:_id "pineapples" :value {:count 6}}
+ {:_id "plantains" :value {:count 5}}])))
+ ;; Make sure we get the collection name back, too
+ (is (= (map-reduce :mr mapfn reducefn :out target-collection :output :collection)
+ target-collection))
+ ;; Check limit
+ (is (= (map-reduce :mr mapfn reducefn :limit 2)
+ (seq [{:_id "bananas" :value {:count 3}}])))
+ ;; Check sort
+ (is (= (map-reduce :mr mapfn reducefn :sort {:fruit -1} :limit 2)
+ (seq [{:_id "plantains" :value {:count 5}}])))
+ ;; check query
+ (is (= (map-reduce :mr mapfn reducefn :query {:fruit "pineapples"})
+ (seq [{:_id "pineapples" :value {:count 6}}])))
+ ;; check finalize
+ (is (= (map-reduce :mr mapfn reducefn
+ :finalize "function(key, value){return 'There are ' + value.count + ' ' + key}")
+ (seq [{:_id "bananas" :value "There are 3 bananas"}
+ {:_id "pineapples" :value "There are 6 pineapples"}
+ {:_id "plantains" :value "There are 5 plantains"}])))
+ ;; check scope
+ (is (= (map-reduce :mr mapfn-with-scope reducefn
+ :scope {:adj "tasty"})
+ (seq [{:_id "tasty bananas" :value {:count 3}}
+ {:_id "tasty pineapples" :value {:count 6}}
+ {:_id "tasty plantains" :value {:count 5}}]))))))
+
(deftest test-server-eval
(with-test-mongo
(is (= (server-eval

0 comments on commit df433fc

Please sign in to comment.