Stefan
Stefan

Reputation: 1739

Meteor Publish Distinct Values of Field in Collection

I'm stuck on a pretty simple scenario in Meteor:

I can't figure out a way to do that without publishing the whole collection which takes way too long. How can I publish just the distinct categories and use them to fill a dropdown?

Bonus question and somewhat related: How do I publish a count of all items in the collection without publishing the whole collection?

Upvotes: 2

Views: 1262

Answers (5)

Salketer
Salketer

Reputation: 15711

I have not tested it on Meteor, and according to the replies, I'm getting skeptical that it will work but using a mongoDB distinct would do the trick.

http://docs.mongodb.org/manual/reference/method/db.collection.distinct/

Upvotes: -1

Rachel
Rachel

Reputation: 2894

Re: the bonus question, publishing counts: take a look at the meteorite package publish-counts. I think that does what you want.

Upvotes: 0

user728291
user728291

Reputation: 4138

You could use the internal this._documents.collectionName to only send new categories down to the client. Tracking which categories to remove becomes a bit ugly so you probably will still end up maintaining a separate 'categories' collection eventually.

Example:

Meteor.publish( 'categories', function(){
  var self = this;
  largeCollection.find({},{fields: {category: 1}).observeChanges({
    added: function( id, doc ){
      if( ! self._documents.categories[ doc.category ] ) 
        self.added( 'categories', doc.category, {category: doc.category});         
    },
    removed: function(){
      _.keys( self._documents.categories ).forEach( category ){
        if ( largeCollection.find({category: category},{limit: 1}).count() === 0 )
          self.removed( 'categories', category );
      }
    }
  });

  self.ready();    
};

Upvotes: 0

Jeremy S.
Jeremy S.

Reputation: 4659

A good starting point to make this easier would be to normalize your categories into a separate database collection.

However assuming that is not possible or practical, the best (though imperfect) solution will be to publish two separate versions of your collection, one which returns only the categories field of the entire collection and another which returns all fields of the collection for the selected category only. That would look like the following:

// SERVER
Meteor.startup(function(){

  Meteor.publish('allThings', function() {

    // return only id and categories field for all your things 
    return Things.find({}, {fields: {categories: 1}});

  });

  Meteor.publish('thingsByCategory', function(category) {

    // return all fields for things having the selected category
    // you can then subscribe via something like a client-side Session variable
    // e.g., Meteor.subscribe("thingsByCategory", Session.get("category"));

    return Things.find({category: category});
  });
});

Note that you will still need to assemble your array of categories client side from the Things cursor (for example, by using underscore's _.pluck and _.uniq methods to grab the categories and remove any dups). But the data set will be much smaller as you are only working with single-field documents now.

(Note that ideally, you would want to use Mongo's distinct() method in your publish function to publish only the distinct categories, but that is not possible directly as it returns an array which cannot be published).

Upvotes: 1

matthewfieger
matthewfieger

Reputation: 11

These patterns might be helpful to you. Here is a publication that publishes counts:

/*****************************************************************************/
/* Counts Publish Function
/*****************************************************************************/

// server: publish the current size of a collection
Meteor.publish("countsByProject", function (arguments) {
  var self = this;

    if (this.userId) {
        var roles = Meteor.users.findOne({_id : this.userId}).roles;
        if ( _.contains(roles, arguments.projectId) ) {

              //check(arguments.video_id, Integer);



              // observeChanges only returns after the initial `added` callbacks
              // have run. Until then, we don't want to send a lot of
              // `self.changed()` messages - hence tracking the
              // `initializing` state.

              Videos.find({'projectId': arguments.projectId}).forEach(function (video) {
                  var count = 0;
                  var initializing = true;
                  var video_id = video.video_id;
                  var handle = Observations.find({video_id: video_id}).observeChanges({
                    added: function (id) {
                      //console.log(video._id);
                      count++;
                      if (!initializing)
                        self.changed("counts", video_id, {'video_id': video_id, 'observations': count});
                    },
                    removed: function (id) {
                      count--;
                      self.changed("counts", video_id, {'video_id': video_id, 'observations': count});
                    }
                    // don't care about changed
                  });



                  // Instead, we'll send one `self.added()` message right after
                  // observeChanges has returned, and mark the subscription as
                  // ready.
                  initializing = false;
                  self.added("counts", video_id, {'video_id': video_id, 'observations': count});
                  self.ready();

                  // Stop observing the cursor when client unsubs.
                  // Stopping a subscription automatically takes
                  // care of sending the client any removed messages.
                  self.onStop(function () {
                    handle.stop();
                  });


              }); // Videos forEach


        } //if _.contains
    } // if userId

  return this.ready();  

});

And here is one that creates a new collection from a specific field:

/*****************************************************************************/
/* Tags Publish Functions
/*****************************************************************************/

// server: publish the current size of a collection
Meteor.publish("tags", function (arguments) {
  var self = this;

    if (this.userId) {
        var roles = Meteor.users.findOne({_id : this.userId}).roles;
        if ( _.contains(roles, arguments.projectId) ) {

                var observations, tags, initializing, projectId;
                initializing = true;
                projectId = arguments.projectId;
                observations = Observations.find({'projectId' : projectId}, {fields: {tags: 1}}).fetch();
                tags = _.pluck(observations, 'tags');
                tags = _.flatten(tags);
                tags = _.uniq(tags);

                var handle = Observations.find({'projectId': projectId}, {fields : {'tags' : 1}}).observeChanges({
                    added: function (id, fields) {
                      if (!initializing) {
                        tags = _.union(tags, fields.tags);
                        self.changed("tags", projectId, {'projectId': projectId, 'tags': tags});
                      }
                    },
                    removed: function (id) {
                      self.changed("tags", projectId, {'projectId': projectId, 'tags': tags});
                    }
                });

                initializing = false;
                self.added("tags", projectId,  {'projectId': projectId, 'tags': tags});
                self.ready();

                self.onStop(function () {
                    handle.stop();
                });

        } //if _.contains
    } // if userId

  return self.ready();  

});

Upvotes: -1

Related Questions