Search multiple field in MongoDB with MeteorJS - meteor

I am beginner in programming and interested to learn MeteorJS. I want to search category_name and subcategory_name by keyword or alphabet.:)
This is my code.
collections: subcategory
{
_id:"ZbwCsJEMi2DesyJA7",
category_name: "ICT",
subcategory_name: "Laptop"
}
subcategory.js
Template.Subcategory.events({
"keyup .searchbox": function(event){
var query = event.target.value;
Session.set('query', query);
}
});
Template.Subcategory.helpers({
subcategory: function(){
var filter = {sort: {}};
var query = Session.get('query');
filter.sort[Session.get('sortby')] = 1;
return Subcategory.find({ subcategory_name: new RegExp(query, 'i')} , filter );
}
});

Seems like you are looking for $or operator:
var queryRegexp = new RegExp(query, 'i');
return Subcategory.find({
$or: [
{ category_name: queryRegexp },
{ subcategory_name: queryRegexp }
]
} , filter);
Official Mongo's documentation for $or: https://docs.mongodb.org/manual/reference/operator/query/or/

You'll have to create a text index in Mongo. You can do this in Meteor still I think with something like this:
MyCollection._ensureIndex({
"$**": "text"
});
The above uses a wildcard but you can be more specific. See https://docs.mongodb.org/v3.0/core/index-text/ for more info.
For larger collections I tend to pass a text search query through a subscriptions so it can be performed on the server.
Assuming you want to show a list of things and allow users to filter them with the search box, you could do something like this:
Meteor.publish('MyCollection', function (searchTerm) {
return searchTerm ? MyCollection.find() : MyCollection.find({ $text: {$search: searchTerm} });
});
On the client, assuming you're using template-level subscriptions, you could set up your subscription like this:
Template.Subcategory.onCreated(function(){
var self = this;
// requires the reactive-var package
self.searchTerm = new ReactiveVar(false);
self.autorun(function(){
self.subscribe( "MyCollection", self.searchTerm.get() );
});
});
It'd then just be a case of setting your search term:
Template.Subcategory.events({
'keyup .searchbox': function(e,t){
var inputValue = e.currentTarget.value,
//you could set an arbitrary minimum search term length like so
searchTerm = inputValue.length > 1 ? inputValue : false;
t.searchTerm.set(searchTerm);
}
});
There's a couple of caveats on relying on the subscriptions so heavily like this. For example, if your collections are scoped globally on the client you run the risk of multiple subscriptions to the same collection giving you results you might not want to render within your list. Nevertheless, I quite like this approach. Food for thought.

Related

Meteor-angular autocomplete from huge data

I have angular-meteor app that needs Material md-autocomplete from a collection with 53,296 documents with angularUtils.directives.dirPagination but this amount of data make my browser hang.
I'm publishing the collection with:
Meteor.publish('city', function (options, searchString) {
var where = {
'city_name': {
'$regex': '.*' + (searchString || '') + '.*' ,
'$options': 'i'
}
};
return City.find(where, options);
});
I subscribe with:
subscriptions: function () {
Meteor.subscribe('city');
this.register('city', Meteor.subscribe('city'));
}
and have pagination on controller :
$scope.currentPage = 1;
$scope.pageSize = 100;
$scope.sort = {city_name_sort : 1};
$scope.orderProperty = '1';
$scope.helpers({
city: function(){
return City.find({});
}
});
but it takes a long time to load and its make chrome stop working.
You already have most of the server-side searching done because your search is running inside a subscription. You should make sure that the city_name field is indexed in mongo! You should only return that field to minimize data transfer. You can also simplify your regex.
Meteor.publish('city', function (searchString) {
const re = new RegExp(searchString,'i');
const where = { city_name: { $regex: re }};
return City.find(where, {sort: {city_name: 1}, fields: {city_name: 1}});
});
What I've found helps with server-side auto-complete is:
Don't start searching until the user has typed 3 or 4 characters. This drastically narrows down the search results.
Throttle the search to only run every 500ms so that you're not sending every character to the server because then it has to keep re-executing the search. If the person is typing fast the search might only run every 2 or 3 characters.
Run the same .find() on the client that you're running on the server (instead of just querying for {}). That's just good practice since the client-side collection is the union of all subscriptions on that collection, there might be documents there that you don't want to list.
Lastly I don't know why you're subscribing twice here:
subscriptions: function () {
Meteor.subscribe('city');
this.register('city', Meteor.subscribe('city'));
}
only one of those Meteor.subscribe('city') calls is necessary.

I'd like to create a reactive filter for textbox input - best practice?

I have a list of company names I'm populating from a collection
the helper function I have is:
Template.companyList.helpers({
companies: function () {
return Companies.find({owner: Meteor.userId()}, {sort: {a: 1}, name:1, createdAt:1});
}
});
It's looped through using a
{{#each companies}}
which outputs
<LI> Company Name </LI>
Above this I have a text box, and would like to filter the list of companies by what I type in the textbox - I'd prefer to have a "containing" filter as opposed to "starting with" filter, but i'll take either one - is there an established way of doing this in Meteor? If not, is there a plugin that someone wrote that does this?
Also, whatever answer you give, please consider the fact that I've been using Meteor for, oh, 5 days now, and i'm still learning it, so, a Newbie style answer would be great.
Thanks for Reading!
edit
This is the updated answer I came up with - combining David's answer with my previous companies helper:
Template.companyList.helpers({
companies: function () {
var query = Session.get('query');
selector = {owner: Meteor.userId()};
options = {sort: {a: 1}, companyName:1, createdAt:1};
if (query && query.length) {
var re = new RegExp(query, 'i');
selector.companyName = re;
}
return Companies.find(selector, options);
}
});
Here is the outline for a simple search-as-you-type interface:
Template.myTemplate.helpers({
companies: function() {
// build a regular expression based on the current search
var search = Session.get('search');
var re = new RegExp(search, 'i');
selector = {owner: Meteor.userId()};
// add a search filter only if we are searching
if (search && search.length)
selector.name = re;
options = {sort: {createdAt: -1}};
return Companies.find(selector, options);
}
});
Template.myTemplate.events({
'keyup #search': function() {
// save the current search query in a session variable as the user types
return Session.set('search', $('#search').val());
}
});
This assumes:
You are trying to search Companies by name.
You have an input with an id of search.
Please modify as needed for your use case. Let me know if you have any questions.

Change publicated collection fields value

hope my first question here is not a stupid one.
Lets say we want to build a chat application with meteor, with logedin and anonymous users. The chat should be filled like that:
var post = {
userId: user._id, // is empty if anonymous user
submitted: new Date().getTime(),
text: chat_message
});
var postId = Posts.insert(post);
The publication could looks like this to make sure that the userId is not transfered
Meteor.publish('getTheChat', function() {
return Post.find({}, {fields: {userId: false});
});
But is there a way to add a field in the returned collection dynamically?
The userId should not be published but a status like "Your_Post","another_logedin_user" or "an_anonymous_user". By having that, I could include some css, so the chat looks a little bit more like WhatsApp or iMessage.
The logic inside the publish method could be something like
if (userId == this.userId) {
status = "Your_Post";
} else if (userId != null) {
status = "another_logedin_user";
} else {
status = "an_anonymous_user";
}
You see, the publication should include different values when called from different users. Is there a way in Meteor.publish?
Thanks for any insight or suggestions
Thank you both for your ideas! But as I had to find out (just for my inward peace) how it is possible inside the publish method server sided, I came, with the help of David's link, to this solution -maybe it will help someone later:
Meteor.publish('getTheChat', function(postId) {
var currentUserId = this.userId;
var ownerUserId = Posts.findOne({'_id':postId}).userId;
var findOptions = {}; // in my final coding these differ for 'chat room owners' and 'normal users'
var transform = function(post) {
if (currentUserId && post.userId == currentUserId) {
post.userId = "posted by you";
} else if (post.userId == null) {
post.userId = "anonym posted";
} else if (post.userId == ownerUserId) {
post.userId = "posted by owner";
} else {
post.userID = "posted by another loged in";
return post;
};
var self = this;
var handle = Posts.find(findOptions).observe({
added: function (document) {
self.added('posts', document._id, transform(document));
},
changed: function (newDocument, oldDocument) {
self.changed('posts', document._id, transform(newDocument));
},
removed: function (oldDocument) {
self.removed('posts', oldDocument._id);
}
});
self.ready();
self.onStop(function(){
handle.stop();
});
By having this I am finally able to overwrite values dynamically.
It looks like you need to add a transform on your Posts collection. You can do this in your publish function (as seen here), but server-side transforms are computationally inefficient and tricky to write. Though they are necessary in cases where only the server could perform the action - e.g. signed URLs. In your case, I'd recommend a standard collection transform which is a filter applied after the documents are fetched.
Unfortunately, this kind of transform would require the userId on the client. I've never seen a case where simply publishing a id could cause a security issue. If you believe this is the case with your app, I'm very interested to know why. If you can overcome this restriction, keep reading...
You can read about transforms in the documentation on collections, and you can see an example on evented mind. Personally I like to use the collection-helpers package for this.
If you try collection-helpers, your transform could look like:
Posts.helpers({
status: function() {
if (this.userId === Meteor.userId()) {
return 'Your_Post';
} else if (this.userId != null) {
return 'another_logedin_user';
} else {
return 'an_anonymous_user';
}
}
});
And then you could use it in your template like:
{{#each posts}}
<p>{{text}} - <span class='status'>{{status}}</span></p>
{{/each}}
Of course, you can also use template helpers to achieve the same result but transforms are more easily reusable across your application.
Sadly, this has been a huge issue for me too, and I am sorry to say, it is not technically possible to just add a field on the publisher's query and use it conveniently in your view. BUT, I have a solution that may work for you. It will also give you an idea of how complex it can become as soon as you want to keep some reactive data private in Meteor.
Server side:
First, create two different publishers: one for the current user's posts, one for all the others:
Meteor.publish('getTheOthersChat', function() {
return Post.find({$ne:{userId: this.userId}}, {fields: {userId: false});
});
Meteor.publish('getTheOwnChat', function() {
return Post.find({userId: this.userId});
});
Client/router side:
Then, subscribe to both of these: what this will do is include the post's userId only when it is the own user's id. If not, it'll be undefined.
Then, we still need to identify the case "anonymously posted" vs "posted by user". For this, you can add another field during the post creation, for example is_anonymous, which you then set to true or false depending on the case if the user is logged in or not. The check would then be:
if (userId) {
status = "Your_Post";
} else if (is_anonymous === false) {
status = "another_logedin_user";
} else {
status = "an_anonymous_user";
}
This solution should work. I know, it is sad to have to come to this kind of means. It makes Meteor look clunky and impractical for tasks that should be dead easy. Such a shame for such a cool framework!

Why is Meteor removing an object when observing a collection?

Background
I have "Lists" and "Products" collections, Products belong to a List
A List has a description, from which products are generated
On startup, a new List is created that's unique for that visitor
The List id is stored in the Session
What I Want
I want Products to be generated when the description of a List changes.
The first step is that when the list for the current visitor is changed, I want a new product to be inserted.
I get the feeling I'm going about this totally wrong...
The Problem
The product is inserted, appears in the browser for a split second, then vanishes. It's been removed by Meteor.
Code
Products = new Meteor.Collection("products");
Lists = new Meteor.Collection("lists");
if (Meteor.isClient) {
Meteor.startup(function () {
var my_list_id = Lists.insert({description: "Default list"});
Session.set("my_list", my_list_id);
var observed = Lists.find({_id: my_list_id}).observe({
changed: function (newDocument, oldDocument) {
Products.insert({list: newDocument._id, name: newDocument.description});
}
});
});
toggleElement = function (elementName) {
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Lists.update(Session.get("my_list"), {description: newListDescription});
setEditedElement("");
} else {
setEditedElement(elementName);
}
};
// Including the rest in case I've misunderstood something.
// I don't see how any of this could cause the issue.
setEditedElement = function (elementName) {
return Session.set("edited_element", elementName);
};
editedElementIs = function (elementName) {
return Session.get("edited_element") == elementName;
};
Handlebars.registerHelper('editedElementIs', editedElementIs);
Handlebars.registerHelper('products', function() {
return Products.find({list: Session.get("my_list")});
});
Template.list_form.listDescription = function () {
return Lists.findOne({_id: Session.get("my_list")}).description;
};
Template.adminbar.events({
'click a#editlist' : function () {
toggleElement("list");
},
'click a#editsidebar' : function () {
toggleElement("sidebar");
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
What I've Tried
Obviously, I can just do this:
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Products.insert({list: Session.get("my_list"), name: newListDescription});
Lists.update(Session.get("my_list"), {description: newListDescription});
...
But that's writing clumsy update code that I'd like to house in an observer.
It looked like the product was being removed. So I've observed when a product is removed thus:
Products.find({list:my_list_id}).observe({
removed: function (oldDocument) {
throw error("wow");
console.log("Removed Product" + oldDocument);
}
})
and this observer is called immediately after the Product is inserted.
I get the stack trace:
at Object.Products.find.observe.removed (http://localhost:3000/ListyMeteor.js?2d867b7481df6389658be864b54d864151e87da5:22:15)
at Object.cursor.observeChanges.removed (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:909:52)
at http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:275:15
at _.extend.runTask (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:30:11)
at _.extend.flush (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:58:10)
at _.extend.drain (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:66:12)
at LocalCollection.remove (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:500:22)
at Object.self._connection.registerStore.update (http://localhost:3000/packages/mongo-livedata/collection.js?682caa185350aa26968d4ffc274579a33922f0e6:109:32)
at Object.store.(anonymous function) [as update] (http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:404:48)
at http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:984:19
It looks like Meteor is flushing the Products collection on the client side.
I'm clearly misunderstanding how Meteor works.
Any ideas on why this is happening?
Update 1
It looks like this is happening because insert is being called within an observer:
Why does meteor undo changes to collections nested in an observer method?
I'll post back here once I confirm.
Is autosubscribe turned on or off?
If you turn autosubscribe off, it could happen that your client updates the server copy and then on a subsequent update from the server - does not get all the items because its not subscribed to that collection.
Easiest way to check is to query the mongo db -
meteor mongo
Query the mongo db if your product has been added to the document.
If it has, then it is an autosubscribe issue -
You will have to create publish (on server) and subscribe (on client) methods as given here http://docs.meteor.com/#meteor_publish

Meteor.js: having multiple subscriptions on one collection enforce storage results in one local collection(Workarounds?)

Is there a way to store subscriptions of the same server collection in a different minimongo collection?
If not is there any best practice to work around?
I do have a summary table having 50k datasets with a lot of details in the documents.
// Server
var collection = new Meteor.Collection("collection");
Meteor.publish("detail", function (id) {
return collection.find({_id: id});
});
// A pager that does not include the data (fields:{data:0})
Meteor.publish("master", function (filter, sort, skip, limit) {
return collection.find({name: new RegExp("^" + filter + "|\\s" + filter, "i")}, {limit: limit, skip: skip, sort: options, fields: {data: 0}});
});
// Client
var collection = new Meteor.Collection("collection");
Deps.autorun(function () {
Meteor.subscribe("master",
Session.get("search"),
Session.get("sort"),
Session.get("skip"),
Session.get("limit")
);
Meteor.subscribe("detail", Session.get("selection"));
});
Problem above: both subscriptions are feed into the same collection.
This does not work well if the results of the finds are stored in the same local collection.
Having a local collection with the name of the subscription/publish would be great.
// Client
var detail = new Meteor.Collection("detail"),
master = new Meteor.Collection("master");
Any Ideas?
If you want your client side collection to have a different name from the server side collection you can not just return a collection cursor. This can be done in the publish function though like this:
Meteor.publish("details", function (id) { //details here matches the subscribe request
var self = this;
self.added( "details", id, collection.findOne({_id: id}); //details here tells the client which collection holds the data
self.ready();
});
This will not be reactive but can be made that way by using observe as in the counts by room example at http://docs.meteor.com which is explained in detail here How does the messages-count example in Meteor docs work?.
While this answers your question of how to get a specific name for a collection without having that collection on the server. I think you probably get what you want more easily with a publish function more like this:
Meteor.publish("master", function (filter, sort, skip, limit, id) {
return [
collection.find({name: new RegExp("^" + filter + "|\\s" + filter, "i")}, {limit: limit, skip: skip, sort: options, fields: {data: 0}})
, collection.find( id , {fields: {data: 1}} )
];
});
Then subscribe on client:
Deps.autorun(function () {
Meteor.subscribe("master",
Session.get("search"),
Session.get("sort"),
Session.get("skip"),
Session.get("limit"),
Session.get("selection")
);
});
Then even though all your data is in one collection you can have a reactive cursor to your selected id with the data included. Query from the client like this:
collection.find( Session.get("selection") );

Resources