How to keep appending subdocuments in MongoDB? - mongodb

I am trying to do a bulk insert in MongoDB using PyMongo.
I have millions of product/review documents to insert into MongoDB. Here is the structure of the document:
{
"_id" : ObjectId("553858a14483e94d1e563ce9"),
"product_id" : "B000GIKZ4W",
"product_category" : "Arts",
"product_brand" : "unknown",
"reviews" : [
{
"date" : ISODate("2012-01-09T00:00:00Z"),
"score" : 3,
"user_id" : "A3DLA3S8QKLBNW",
"sentiment" : 0.2517857142857143,
"text" : "The ink was pretty dried up upon arrival. It was...",
"user_gender" : "male",
"voted_total" : 0,
"voted_helpful" : 0,
"user_name" : "womans_roar \"rohrra\"",
"summary" : "Cute stamps but came with dried up ink"
}
],
"product_price" : "9.43",
"product_title" : "Melissa & Doug Deluxe Wooden Happy Handle Stamp Set"
}
There can be multiple reviews for a single product. The requirement is to insert one document per product_id and keep appending more reviews as subdocument in the reviews array. Can you please provide some pointers on how this can be achieved? Also, will be nice to do implement bulk insert for performance.

will be nice to do implement bulk insert for performance.
In pymongo can execute Ordered bulk write operations or Unordered Bulk Write Operations
The requirement is to insert one document per product_id and keep appending more reviews as subdocument in the reviews array
You can use the update_one or update_many (Pymongo 3 or newer) or update method to $push subdocument to the reviews array
collection.update_one({"_id": <doc_id>}, {"$push": {"reviews": <subdocument>}})
or
collection.update({"_id": <doc_id>}, {"$push": {"reviews": <subdocument>}})
To insert need document if no document matches the given criteria use upsert option
collection.update({"_id": <doc_id>}, {"$push": {"reviews": <subdocument>}}, upsert=True)

Related

Add object to object array if an object property is not given yet

Use Case
I've got a collection band_profiles and I've got a collection band_profiles_history. The history collection is supposed to store a band_profile snapshot every 24 hour and therefore I am using MongoDB's recommended format for historical tracking: Each month+year is it's own document and in an object array I will store the bandProfile snapshot along with the current day of the month.
My models:
A document in band_profiles_history looks like this:
{
"_id" : ObjectId("599e3bc406955db4cbffe0a8"),
"month" : 7,
"tag_lowercased" : "9yq88gg",
"year" : 2017,
"values" : [
{
"_id" : ObjectId("599e3bc41c073a7418fead91"),
"profile" : {
"_id" : ObjectId("5989a65d0f39d9fd70cde1fe"),
"tag" : "9YQ88GG",
"name_normalized" : "example name1",
},
"day" : 1
},
{
"_id" : ObjectId("599e3bc41c073a7418fead91"),
"profile" : {
"_id" : ObjectId("5989a65d0f39d9fd70cde1fe"),
"tag" : "9YQ88GG",
"name_normalized" : "new name",
},
"day" : 2
}
]
}
And a document in band_profiles:
{
"_id" : ObjectId("5989a6190f39d9fd70cddeb1"),
"tag" : "9V9LRGU",
"name_normalized" : "example name",
"tag_lowercased" : "9v9lrgu",
}
This is how I upsert my documents into band_profiles_history at the moment:
BandProfileHistory.update(
{ tag_lowercased: tag, year, month},
{ $push: {
values: { day, profile }
}
},
{ upsert: true }
)
My problem:
I only want to insert ONE snapshot for every day. Right now it would always push a new object into the object array values no matter if I already have an object for that day or not. How can I achieve that it would only push that object if there is no object for the current day yet?
Putting mongoose aside for a moment:
There is an operation addToSet that will add an element to an array if it doesn't already exists.
Caveat:
If the value is a document, MongoDB determines that the document is a duplicate if an existing document in the array matches the to-be-added document exactly; i.e. the existing document has the exact same fields and values and the fields are in the same order. As such, field order matters and you cannot specify that MongoDB compare only a subset of the fields in the document to determine whether the document is a duplicate of an existing array element.
Since you are trying to add an entire document you are subjected to this restriction.
So I see the following solutions for you:
Solution 1:
Read in the array, see if it contains the element you want and if not push it to the values array with push.
This has the disadvantage of NOT being an atomic operation meaning that you could end up would duplicates anyways. This could be acceptable if you ran a periodical clean up job to remove duplicates from this field on each document.
It's up to you to decide if this is acceptable.
Solution 2:
Assuming you are putting the field _id in the subdocuments of your values field, stop doing it. Assuming mongoose is doing this for you (because it does, from what I understand) stop it from doing it like it says here: Stop mongoose from creating _id for subdocument in arrays.
Next you need to ensure that the fields in the document always have the same order, because order matters when comparing documents in the addToSet operation as stated in the citation above.
Solution 3
Change the schema of your band_profiles_history to something like:
{
"_id" : ObjectId("599e3bc406955db4cbffe0a8"),
"month" : 7,
"tag_lowercased" : "9yq88gg",
"year" : 2017,
"values" : {
"1": { "_id" : ObjectId("599e3bc41c073a7418fead91"),
"profile" : {
"_id" : ObjectId("5989a65d0f39d9fd70cde1fe"),
"tag" : "9YQ88GG",
"name_normalized" : "example name1"
}
},
"2": {
"_id" : ObjectId("599e3bc41c073a7418fead91"),
"profile" : {
"_id" : ObjectId("5989a65d0f39d9fd70cde1fe"),
"tag" : "9YQ88GG",
"name_normalized" : "new name"
}
}
}
Notice that the day field became the key for the subdocuments on the values. Notice also that values is now an Object instead of an Array.
No you can run an update query that would update values.<day> only if values.<day> didn't exist.
Personally I don't like this as it is using the fact that JSON doesn't allow duplicate keys to support the schema.
First of all, sadly mongodb does not support uniqueness of a field in an array of a collection. You can see there is major bug opened for 7 years and not closed yet(that is a shame in my opinion).
What you can do from here is limited and all is on application level. I had same problem and solve it in application level. Do something like this:
First read your document with document _id and values.day.
If your reading in step 1 returns null, that means there is no record on values array for given day, so you can push the new value(I assume band_profile_history has record with _id value).
If your reading in step 1 returns a document, that means values array has a record for given day. In that case you can use setoperation with $operator.
Like others said, they will be not atomic but while you are dealing with your problem in application level, you can make whole bunch of code synchronized. There will be 2 queries to run on mongodb among of 3 queries. Like below:
db.getCollection('band_profiles_history').find({"_id": "1", "values.day": 3})
if returns null:
db.getCollection('band_profiles_history').update({"_id": "1"}, {$push: {"values": {<your new band profile history for given day>}}})
if returns not null:
db.getCollection('band_profiles_history').update({"_id": "1", "values.day": 3}, {$set: {"values.$": {<your new band profile history for given day>}}})
To check if object is empty
{ field: {$exists: false} }
or if it is an array
{ field: {$eq: []} }
Mongoose also supports field: {type: Date} so you can use it instead counting a days, and do updates only for current date.

MongoDB indexing and counting for subdocument

Please give me an advise, how i can resolve my issue.
I have a MongoDB collection "cards" with over 5 million documents.
Here is example of typycal document in my collection
{
"_id" : "300465-905543",
"products" : "00000",
"groupQuality" : {
"defQuality" : 100,
"summQuality" : 92.22
}
}
I need count up quantity of documents with some products and with some value of quality so i tried to use something like this
db.cards.count({groupQuality.defQuality : {$gt : 50, $lte : 100}})
For improving speed of this operation i created index {groupQuality.defQuality : 1}
It was good decision, count of documents returned fast, but after adding into imbedded document with name "groupQuality" one more group of quality i must create another index for this group.
Quantity of new groups of quality may be huge, so i don't want build index for every new group of quality.
I start thinking about creating index {groupQuality : 1} which will cower all groups of quality in imbedded document. Does it possible in MongoDB?
If i can create such index, how can i make a query to count up documents with some products and with some value of quality in case using index by groupQuality?
I tryed next query but it always return 0.
db.cards.count({ products : "00000", groupQuality : { defQuality : {$gt : 50, $lte : 100}, summQuality : {$gt : 0, $lte : 100}}})
Where is my mistake?
When you have nested fields, you need to provide the full path for every field you match instead of providing a nested document:
db.cards.count({
"products" : "00000",
"groupQuality.defQuality" : {$gt : 50, $lte : 100},
"groupQuality.summQuality" : {$gt : 0, $lte : 100}
})

MongoDb Schema Structure

I have a collection named 'Category' with this structure:
{
"CategoryID" : 1,
"ParentID" : 0,
"Name" : "Sample Cat"
}
And another collection which will be using this category
{
"DocumentID" : 1,
"CategoryID" : 1,
"DocumentName" : "Doc XPXSAX"
}
The problem with this design is that when is that I cannot use it to make a live search which will show me the document as
Doc XPXSAX found in Sample Cat"(along with category name without using join)
Also I cannot embed the documents inside the Category collection (as an array in one of the fields) as I am expecting the number of documents to go up to 50k.
What alternate schema design will enable me to incorporate an efficient search functionality without using hacks imitating joins ?
Thanks.
If you dislike an application-level join, why not embed the categories inside the document documents?
{
"DocumentID" : 1,
"category" : {
"ID" : 1,
"Name" : "Sample Cat",
"ParentID" : 0
},
"DocumentName" : "Doc XPXSAX"
}
Keep the category information you need for display in the document document. Information you need more rarely can live in the category document and be found with a second query or application-level join.

I have big database on mongodb and can't find and use my info

This my code:
db.test.find() {
"_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
"title" : "Sir",
"name" : {
"_id" : ObjectId("4d3ed089fb60ab534684b7ff"),
"first_name" : "Farid"
},
"addresses" : [
{
"city" : "Baku",
"country" : "Azerbaijan"
},{
"city" : "Susha",
"country" : "Azerbaijan"
},{
"city" : "Istanbul",
"country" : "Turkey"
}
]
}
I want get output only all city. Or I want get output only all country. How can i do it?
I'm not 100% about your code example, because if your 'find' by ID there's no need to search by anything else... but I wonder whether the following can help:
db.test.insert({name:'farid', addresses:[
{"city":"Baku", "country":"Azerbaijan"},
{"city":"Susha", "country":"Azerbaijan"},
{"city" : "Istanbul","country" : "Turkey"}
]});
db.test.insert({name:'elena', addresses:[
{"city" : "Ankara","country" : "Turkey"},
{"city":"Baku", "country":"Azerbaijan"}
]});
Then the following will show all countries:
db.test.aggregate(
{$unwind: "$addresses"},
{$group: {_id:"$country", countries:{$addToSet:"$addresses.country"}}}
);
result will be
{ "result" : [
{ "_id" : null,
"countries" : [ "Turkey", "Azerbaijan"]
}
],
"ok" : 1
}
Maybe there are other ways, but that's one I know.
With 'cities' you might want to take more care (because I know cities with the same name in different countries...).
Based on your question, there may be two underlying issues here:
First, it looks like you are trying to query a Collection called "test". Often times, "test" is the name of an actual database you are using. My concern, then, is that you are trying to query the database "test" to find any collections that have the key "city" or "country" on any of the internal documents. If this is the case, what you actually need to do is identify all of the collections in your database, and search them individually to see if any of these collections contain documents that include the keys you are looking for.
(For more information on how the db.collection.find() method works, check the MongoDB documentation here: http://docs.mongodb.org/manual/reference/method/db.collection.find/#db.collection.find)
Second, if this is actually what you are trying to do, all you need to for each collection is define a query that only returns the key of the document you are looking for. If you get more than 0 results from the query, you know documents have the "city" key. If they don't return results, you can ignore these collections. One caveat here is if data about "city" is in embedded documents within a collection. If this is the case, you may actually need to have some idea of which embedded documents may contain the key you are looking for.

matching fields internally in mongodb

I am having following document in mongodb
{
"_id" : ObjectId("517b88decd483543a8bdd95b"),
"studentId" : 23,
"students" : [
{
"id" : 23,
"class" : "a"
},
{
"id" : 55,
"class" : "b"
}
]
}
{
"_id" : ObjectId("517b9d05254e385a07fc4e71"),
"studentId" : 55,
"students" : [
{
"id" : 33,
"class" : "c"
}
]
}
Note: Not an actual data but schema is exactly same.
Requirement: Finding the document which matches the studentId and students.id(id inside the students array using single query.
I have tried the code like below
db.data.aggregate({$match:{"students.id":"$studentId"}},{$group:{_id:"$student"}});
Result: Empty Array, If i replace {"students.id":"$studentId"} to {"students.id":33} it is returning the second document in the above shown json.
Is it possible to get the documents for this scenario using single query?
If possible, I'd suggest that you set the condition while storing the data so that you can do a quick truth check (isInStudentsList). It would be super fast to do that type of query.
Otherwise, there is a relatively complex way of using the Aggregation framework pipeline to do what you want in a single query:
db.students.aggregate(
{$project:
{studentId: 1, studentIdComp: "$students.id"}},
{$unwind: "$studentIdComp"},
{$project : { studentId : 1,
isStudentEqual: { $eq : [ "$studentId", "$studentIdComp" ] }}},
{$match: {isStudentEqual: true}})
Given your input example the output would be:
{
"result" : [
{
"_id" : ObjectId("517b88decd483543a8bdd95b"),
"studentId" : 23,
"isStudentEqual" : true
}
],
"ok" : 1
}
A brief explanation of the steps:
Build a projection of the document with just studentId and a new field with an array containing just the id (so the first document it would contain [23, 55].
Using that structure, $unwind. That creates a new temporary document for each array element in the studentIdComp array.
Now, take those documents, and create a new document projection, which continues to have the studentId and adds a new field called isStudentEqual that compares the equality of two fields, the studentId and studentIdComp. Remember that at this point there is a single temporary document that contains those two fields.
Finally, check that the comparison value isStudentEqual is true and return those documents (which will contain the original document _id and the studentId.
If the student was in the list multiple times, you might need to group the results on studentId or _id to prevent duplicates (but I don't know that you'd need that).
Unfortunately it's impossible ;(
to solve this problem it is necessary to use a $where statement
(example: Finding embeded document in mongodb?),
but $where is restricted from being used with aggregation framework
db.data.find({students: {$elemMatch: {id: 23}} , studentId: 23});