$unwind, produce complement of results in addition to target of "path" - mongodb

I have a mongo collection called scorerecords that has documents that look like:
{status: 'in progress', teams: {count: 2, team: [ {name: "team 1", score: 100}, {name: "team 2", score: 95}]}}
I would like to return a unique record for each element in the "teams.team" array (as can be done with $unwind), but have it also return the opposite team record (the "opponent"). Example:
{status: 'in progress', teams: {count: 2, team: {name: "team 1", score: 100}, opponent: {name: "team 2", score: 95}}}
{status: 'in progress', teams: {count: 2, team: {name: "team 2", score: 95}, opponent: {name: "team 1", score: 100}}}
This way, I would have 1 record per team (instead of 1 per matchup), and could calculate the number of times a team won or lost (e.g. by using a $project statement to define a "winner:true/false" field or something similar).
Is there any way to do that with some combination of $aggregate functions like $project/$unwind?

I ended up finding a solution:
db.scorerecords.aggregate([{$addFields: {"teams.team": {$map: {input: "$teams.team", as: "tm", in: {"name": "$$tm.name", "score":"$$tm.score", "idx": {$indexOfArray: ["$teams.team", "$$tm"]} }}}}},{$addFields: {"teams.team": {$map: {input: "$teams.team", as: "tm", in: {"name": "$$tm.name", "score": "$$tm.score", "opp": {$arrayElemAt: ["$teams.team", {$subtract: [1, "$$tm.idx"]}]}} }}}}, {$unwind: "$teams.team"} ])
It seems like there might have been a more convenient/efficient method, but this way seems to return the desired result.
Steps:
1) Add a field that identifies the "index" of the current item in the array with a $map call.
2) Using another $map call, add an "opp" field, assuming the opp is the (1-idx)th entry in the array (see caution below).
3) Use $unwind to flatten, creating 2x the original recordset with the complementary records included.
Caution on step #2:
(This assumes that every scorerecord will have <=2 teams. If there is only 1, the "opp" field will not be populated, which is desirable. It will have unexpected results if there are more than 2 teams in the "teams.team" array. (e.g. for every team beyond the 2nd, it will return the last element in the array as the "opp").

Related

MongoDB aggregate query for values in an array

So I have data that looks like this:
{
_id: 1,
ranking: 5,
tags: ['Good service', 'Clean room']
}
Each of these stand for a review. There can be multiple reviews with a ranking of 5. The tags field can be filled with up to 4 different tags.
4 tags are: 'Good service', 'Good food', 'Clean room', 'Need improvement'
I want to make a MongoDB aggregate query where I say 'for each ranking (1-5) give me the number of times each tag occurred for each ranking.
So an example result might look like this, _id being the ranking:
[
{ _id: 5,
totalCount: 5,
tags: {
goodService: 1,
goodFood: 3,
cleanRoom: 1,
needImprovement: 0
},
{ _id: 4,
totalCount: 7,
tags: {
goodService: 0,
goodFood: 2,
cleanRoom: 3,
needImprovement: 0
},
...
]
Having trouble with the counting the occurrences of each tag. Any help would be appreciated
You can try below aggregation.
db.colname.aggregate([
{"$unwind":"$tags"},
{"$group":{
"_id":{
"ranking":"$ranking",
"tags":"$tags"
},
"count":{"$sum":1}
}},
{"$group":{
"_id":"$_id.ranking",
"totalCount":{"$sum":"$count"},
"tags":{"$push":{"tags":"$_id.tags","count":"$count"}}
}}
])
To get the key value pair instead of array you can replace $push with $mergeObjects from 3.6 version.
"tags":{"$mergeObjects":{"$arrayToObject":[[["$_id.tags","$count"]]]}}

Mongo query: array of objects where a key's value is repeated

I am new to Mongo. Posting this question because i am not sure how to search this on google
i have a book documents like below
{
bookId: 1
title: 'some title',
publicationDate: DD-MM-YYYY,
editions: [{
editionId: 1
},{
editionId: 2
}]
}
and another one like this
{
bookId: 2
title: 'some title 2',
publicationDate: DD-MM-YYYY,
editions: [{
editionId: 1
},{
editionId: 1
}]
}
I want to write a query db.books.find({}) which would return only those books where editions.editionId has been duplicated for a book.
So in this example, for bookId: 2 there are two editions with the editionId:1.
Any suggestions?
You can use the aggregation framework; specifically, you can use the $group operator to group the records together by book and edition id, and count how many times they occur : if the count is greater than 1, then you've found a duplication.
Here is an example:
db.books.aggregate([
{$unwind: "$editions"},
{$group: {"_id": {"_id": "$_id", "editionId": "$editions.editionId"}, "count": {$sum: 1}}},
{$match: {"count" : {"$gt": 1}}}
])
Note that this does not return the entire book records, but it does return their identifiers; you can then use these in a subsequent query to fetch the entire records, or do some de-duplication for example.

MongoDB find closest match

I'm wondering if it is possible to access a document in MongoDB via closest match.
e.g. my search query always contains:
name
country
city
Following rules are in place:
1. name always has to match
2. if either country or city is present, country has a higher priority
3. if country or city does not match only consider this document, if they have the default value (e.g. for String: "")
example Query:
name = "Test"
country = "USA"
city = "Seattle"
Documents:
db.stuff.insert([
{
name:"Test",
country:"",
city:"Seattle"
},{
name:"Test3",
country:"USA",
city:"Seattle"
},{
name:"Test",
country:"USA",
city:""
},{
name:"Test",
country:"Germany",
city:"Seattle"
},{
name:"Test",
country:"USA",
city:"Washington"
}
])
It should return the 3rd document
thanks!
Considering uncertain requirements and contradicting updates, the answer is rather a guideline addressing the "Is it possible at all" part.
The example should be adjusted to meet expectation.
db.stuff.aggregate([
{$match: {name: "Test"}}, // <== the fields that should always match
{$facet: {
matchedBoth: [
{$match: {country: "USA", city: "Seattle"}}, // <== bull's-eye
{$addFields: {weight: 10}} // <== 10 stones
],
matchedCity: [
{$match: {country: "", city: "Seattle"}}, // <== the $match may need to be improved, see below
{$addFields: {weight: 5}}
],
matchedCountry: [
{$match: {country: "USA", city: ""}},
{$addFields: {weight: 0}} // <== weightless, yet still a match
]
// add more rules here, if needed
}},
// get them together. Should list all rules from above
{$project: {doc: {$concatArrays: ["$matchedBoth", "$matchedCity", "$matchedCountry"]}}},
{$unwind: "$doc"}, // <== split them apart
{$sort: {"doc.weight": -1}}, // <== and order by weight, desc
// reshape to retrieve documents in its original format
{$project: {_id: "$doc._id", name: "$doc.name", country: "$doc.country", city: "$doc.city"}}
]);
The least explained part of the question affect how we build up facets. e.g.
{$match: {country: "", city: "Seattle"}}
matches all documents where country explicitly present and is an empty string.
It very well might be
{$match: {country: {$ne: "USA"}, city: "Seattle"}}
to get all documents with matching name and city and any country/no country, or even
{$match: {$and: [{$or: [{country: null}, {country: ""}]}, {city: "Seattle"}]}}
etc.
Here is a query
db.collection.aggregate([
{$match: {name:"Test"}},
{$project: {
name:"$name",
country: "$country",
city:"$city",
countryMatch: {$cond: [{$eq:["$country", "USA"]}, true, false]},
cityMatch: {$cond:[{$eq:["$city", "Seattle"]}, true, false]}
}},
{$match: {$and: [
{$or:[{countryMatch:true},{country:""}]},
{$or:[{cityMatch:true},{city:""}]}
]}},
{$sort: {countryMatch:-1, cityMatch:-1}},
{$project: {name:"$name", country:"$country", city:"$city"}}
])
Explanation:
First match filters out docs which don't match name (because rule #1 - name should match).
Next projection selects doc fields plus some information about country and city matches. We will need it to further filter and sort documents.
Second match filters out those documents which don't match both country and city and don't have default values for these fields (rule #3).
Sorting documents moves country matches before city matches as rule #2 states. And last - projection selects required fields.
Output:
{
_id: 3,
name : "Test",
country : "USA",
city : ""
},
{
_id: 1,
name : "Test",
country : "",
city : "Seattle"
}
You can limit query results to get only closest match.

In MongoDB, is it possible to use $set or $mul but specify another field in the same document rather than a constant?

Say that I have a document:
{ _id: 1, item: "ABC", supplier: "XYZ", price: 10, available: 23 }
and then I run something like
db.products.update(
{ _id: 1, supplier: "XYZ" },
{ stock_value: {$mul: ["price", "available", 0.8] }}
)
to get a document
{ _id: 1, item: "ABC", supplier: "XYZ", price: 10, available: 23, stock_value: 184 }
I'd like to do this without loading everything into the client. And I need to be able to specify a different constant (e.g. the 0.8) for each supplier.
I'm thinking I should just use an aggregation with an $out to the same collection, to overwrite the whole then when the update is done, but I can't do a different aggregate() call for each supplier since I'm overwriting the collection - all other suppliers will be skipped. Is there some sort of "in place" aggregation? or a way to append $out ?

Using functions for creating an index in MongoDB?

I have a blog that allow users to vote for articles. The documents of an article looks like:
{
title: "title of post",
body: "text text text text text text",
votes: [
{user: "user1", points: 2},
{user: "user13", points: 1},
{user: "user30", points: 1},
{user: "user2", points: -1},
{user: "user51", points: 3},
],
sum_of_votes: 6
}
I'd like to sort by the number of votes the post received. At the moment I added a field sum_of_votes that needs to be updated every time somebody voted. Since votes already contains the raw-data for sum_of_votes I thought there might be a more elegant way. I came up with two ideas:
Using a function while creating an index, for example:
db.coll.ensureIndex({{$sum: votes.points}: 1})
Having a dynamic field. The document could look like this:
{
title: "title of post",
body: "text text text text text text",
votes: [
{user: "user1", points: 2},
{user: "user13", points: 1},
{user: "user30", points: 1},
{user: "user2", points: -1},
{user: "user51", points: 3},
],
sum_of_votes: {$sum: votes.points}
}
In those cases I only had to update the votes-array. Is something like this possible in MongoDB?
Depending on how you do the update on the votes array, either as updating the document by inserting a new vote in the votes array or updating an existing vote, an approach you could take is use the $inc operator on the sum_of_votes with the value you are using to update the votes.points with. For example, in the case where you are inserting a new vote for a user with value in variable userId and the actual vote points in variable points (which must be a number), you could try:
db.articles.update(
{"_id" : article_id},
{
"$addToSet": {
"votes": {
"user": userId,
"points" : points
}
},
"$inc": { "sum_of_votes": points }
}
)
And you will need to factor in Neil Lunn's point where you wouldn't want the same user to vote more than once in your updates.