How to find into mongodb to the last item of an array? - php

I want to find documents where last elements in an array equals to some value.
Array elements may be accessed by specific array position:
// i.e. comments[0].by == "Abe"
db.example.find( { "comments.0.by" : "Abe" } )
but how do i search using the last item as criteria?
i.e.
db.example.find( { "comments.last.by" : "Abe" } )
By the way, i'm using php

I know this question is old, but I found it on google after answering a similar new question. So I thought this deserved the same treatment.
You can avoid the performance hit of $where by using aggregate instead:
db.example.aggregate([
// Use an index, which $where cannot to narrow down
{$match: { "comments.by": "Abe" }},
// De-normalize the Array
{$unwind: "$comments"},
// The order of the array is maintained, so just look for the $last by _id
{$group: { _id: "$_id", comments: {$last: "$comment"} }},
// Match only where that $last comment by `by.Abe`
{$match: { "comments.by": "Abe" }},
// Retain the original _id order
{$sort: { _id: 1 }}
])
And that should run rings around $where since we were able to narrow down the documents that had a comment by "Abe" in the first place. As warned, $where is going to test every document in the collection and never use an index even if one is there to be used.
Of course, you can also maintain the original document using the technique described here as well, so everything would work just like a find().
Just food for thought for anyone finding this.
Update for Modern MongoDB releases
Modern releases have added the $redact pipeline expression as well as $arrayElemAt ( the latter as of 3.2, so that would be the minimal version here ) which in combination would allow a logical expression to inspect the last element of an array without processing an $unwind stage:
db.example.aggregate([
{ "$match": { "comments.by": "Abe" }},
{ "$redact": {
"$cond": {
"if": {
"$eq": [
{ "$arrayElemAt": [ "$comments.by", -1 ] },
"Abe"
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
])
The logic here is done in comparison where $arrayElemAt is getting the last index of the array -1, which is transformed to just an array of the values in the "by" property via $map. This allows comparison of the single value against the required parameter, "Abe".
Or even a bit more modern using $expr for MongoDB 3.6 and greater:
db.example.find({
"comments.by": "Abe",
"$expr": {
"$eq": [
{ "$arrayElemAt": [ "$comments.by", -1 ] },
"Abe"
]
}
})
This would be by far the most performant solution for matching the last element within an array, and actually expected to supersede the usage of $where in most cases and especially here.

You can't do this in one go with this schema design. You can either store the length and do two queries, or store the last comment additionally in another field:
{
'_id': 'foo';
'comments' [
{ 'value': 'comment #1', 'by': 'Ford' },
{ 'value': 'comment #2', 'by': 'Arthur' },
{ 'value': 'comment #3', 'by': 'Zaphod' }
],
'last_comment': {
'value': 'comment #3', 'by': 'Zaphod'
}
}
Sure, you'll be duplicating some data, but atleast you can set this data with $set together with the $push for the comment.
$comment = array(
'value' => 'comment #3',
'by' => 'Zaphod',
);
$collection->update(
array( '_id' => 'foo' ),
array(
'$set' => array( 'last_comment' => $comment ),
'$push' => array( 'comments' => $comment )
)
);
Finding the last one is easy now!

You could do this with a $where operator:
db.example.find({ $where:
'this.comments.length && this.comments[this.comments.length-1].by === "Abe"'
})
The usual slow performance caveats for $where apply. However, you can help with this by including "comments.by": "Abe" in your query:
db.example.find({
"comments.by": "Abe",
$where: 'this.comments.length && this.comments[this.comments.length-1].by === "Abe"'
})
This way, the $where only needs to be evaluated against documents that include comments by Abe and the new term would be able to use an index on "comments.by".

I'm just doing :
db.products.find({'statusHistory.status':'AVAILABLE'},{'statusHistory': {$slice: -1}})
This gets me products for which the last statusHistory item in the array, contains the property status='AVAILABLE' .

I am not sure why my answer above is deleted. I am reposting it. I am pretty sure without changing the schema, you should be able to do it this way.
db.example.find({ "comments:{$slice:-1}.by" : "Abe" }
// ... or
db.example.find({ "comments.by" : "Abe" }
This by default takes the last element in the array.

Related

Find value with associative array without know the key in MongoDB

I have a document with this structure:
{"user":{
"nice":{
"funny":"sure"
}
,
"notnice":{
"funny":"maybe"
}
}
}
I know the keys "user","funny" and the value "sure" and "maybe" but I don't know "nice" and "notnice".
How do I do an optimized query to search through many documents.
For example, if I want to search "sure" value knowing the middle keys I do:
$document = $users->findOne([
'$or' => [
['user.nice.funny' => 'sure'],
['user.notnice.funny' => 'sure']
]
]
);
But how do I do the same without knowing "nice" and "notnice".
This should point you in the right direction:
db.collection.aggregate({
$addFields: {
"userTransformed": {
$objectToArray: "$user" // transform "user" field into key-value pair
}
}
}, {
$match: {
"userTransformed.v.funny": "sure" // just filter on the values
}
})
Frankly, this is not going to be fast for lots of documents but there is no other way. Indexes will not be used by this query. If you want to get faster you will need to change your document structure.

PhpMongo - how to apply AND condition for a single document present in an array?

My Mongo collection has two documents
{
"_id":ObjectId("567168393d5c6cd46a00002a"),
"type":"SURVEY",
"description":"YOU HAVE AN UNANSWERED SURVEY.",
"user_to_notification_seen_status":[
{
"user_id":1,
"status":"UNSEEN",
"time_updated":1450272825
},
{
"user_id":2,
"status":"SEEN",
"time_updated":1450273798
},
{
"user_id":3,
"status":"UNSEEN",
"time_updated":1450272825
}
],
"feed_id":1,
"time_created":1450272825,
"time_updated":1450273798
}
Here is the query I used to fetch only if the user_id is 2 & status is "UNSEEN".
**$query = array('$and' => array(array('user_to_notification_seen_status.user_id'=> 2,'user_to_notification_seen_status.status' => "UNSEEN")));**
$cursor = $notification_collection->find($query);
Ideally the above query shouldn't retrieve results but it returning results. If I give an invalid id or invalid status, it is not returning any record.
You're misunderstanding how the query works. It matches your document because user_to_notification_seen_status contains elements with user_id: 2 and status: UNSEEN.
What you can do to get the desired results is use the aggregation framework; unwind the array and then match both conditions. That way you'll only get the unwinded documents with the array element satisfying both conditions.
Run this in mongo shell (or convert to PHP equivalent). Also, change YourCollection to your actual collection name:
db.YourCollection.aggregate([ { $unwind: "$user_to_notification_seen_status" }, { $match: { "user_to_notification_seen_status.status": "UNSEEN", "user_to_notification_seen_status.user_id": 2 } } ] );
This will return no records, but if you change the id to 3 for example, it will return one.
Try:
$query = array(
array('$unwind' => '$user_to_notification_seen_status'),
array(
'$match' => array('user_to_notification_seen_status.status' => 'UNSEEN', 'user_to_notification_seen_status.user_id' => 2),
),
);
$cursor = $notification_collection->aggregate($query);

get first element of an array

Lets assume, the return value of an search-fuction is something like this
// If only one record is found
$value = [
'records' => [
'record' => ['some', 'Important', 'Information']
]
]
// If multiple records are found
$value = [
'records' => [
'record' => [
0 => ['some', 'important', 'information'],
1 => ['some', 'information', 'I dont care']
]
]
]
what woul'd be the best way to get the important information (in case of multiple records, it is always the first one)?
Should I check something like
if (array_values($value['record']['records'])[0] == 0){//do something};
But I guess, there is a way more elegant solution.
Edit:
And btw, this is not realy a duplicate of the refered question which only covers the multiple records.
If you want the first element of an array, you should use reset. This function sets the pointer to the first element and returns it.
$firstValue = reset($value['record']['records']);
Edit.. after reading your question again, it seems, you dont want the first element.
You rather want this
if (isset($value['record']['records'][0]) && is_array($value['record']['records'][0])) {
// multiple return values
} else {
// single return value
}
Doing this is kind of error proun and i wouldn't suggest that one function returns different kinds of array structures.
check like this..
if(is_array($value['records']['record'][0])) {
// multiple records
} else {
// single record
}

Json_Encode outputting with additional arrays

I'm struggling with the output of json_encode. I'm attempting to speed up our large dropdown navigation menu by storing everything in a json file that gets updated once a day and calling that when required.
I'm producing the json using json_encode, but it seems to be looping everything into additional, unneccessary, arrays and I can't figure out how to prevent this.
I've even tried fiddling with str_replace but had no success in generating valid json (though clearly this isn't really a long term solution in any case). I've also tried to figure out what combination of "each" I would need to get into the nestled arrays, but haven't found the right combination.
Below is the json I'm ending up with (I've reduced the number of entries to make it easier to see, the format is the same... just within each of Film, Gaming etc there are more items).
[
[
"Film",
[
{
"title": "13 Awkward Moments That Must Have Happened After The End Of Famous Movies",
"link": "http:\/\/whatculture.com\/film\/13-awkward-moments-that-must-have-happened-after-the-end-of-famous-movies.php",
"image": [
"http:\/\/cdn3.whatculture.com\/wp-content\/uploads\/2013\/08\/HP-100x60.jpg",
100,
60,
true
]
}
]
],
[
"TV",
[
{
"title": "10 Awesome TV Twists You Never Saw Coming",
"link": "http:\/\/whatculture.com\/tv\/10-awesome-tv-twists-you-never-saw-coming.php",
"image": [
"http:\/\/cdn3.whatculture.com\/wp-content\/uploads\/2013\/08\/lost-locke-100x60.jpg",
100,
60,
true
]
}
]
],
[
"Gaming",
[
{
"title": "WWE 2K14: Every Possible Classic Match",
"link": "http:\/\/whatculture.com\/gaming\/wwe-2k14-every-possible-classic-match.php",
"image": [
"http:\/\/cdn3.whatculture.com\/wp-content\/uploads\/2013\/08\/444-100x60.jpg",
100,
60,
true
]
}
]
]
]
And this is the script I'm using to generate said code:
I've included everything for completeness. A lot of the below is just the Wordpress query to pull back my relevant data:
$cats = array("Film","TV","Gaming","Sport","Music");
function filter_where($where = '') {
$where .= " AND post_date > '" . date('Y-m-d', strtotime('-3 days')) . "'";
return $where;
}
add_filter('posts_where', 'filter_where');
foreach($cats as $cat) {
$the_query = array(
'numberposts' => 5,
'category_name' => $cat,
'meta_key' => "visitcount",
'orderby' => "meta_value_num",
'suppress_filters' => false );
$special_query_results = get_posts($the_query);
foreach( $special_query_results as $post ) {
setup_postdata($post);
$myposts[] = array('title'=> html_entity_decode(get_the_title()),'link'=>get_permalink(get_the_ID()),'image'=>wp_get_attachment_image_src( get_post_thumbnail_id(get_the_ID()), 'smallthumb' ));
}
$pop_posts[] = array($cat,$myposts);
unset($myposts);
} // foreach cats as cat1000
wp_reset_postdata();
remove_filter('posts_where', 'filter_where');
$json_pop = json_encode($pop_posts,JSON_PRETTY_PRINT);
This is what I'm using to pull it back when user hovers on the nav item:
$.getJSON('http://whatculture.com/data/wc6.json', function(popular) {
$.each(popular.Sport, function() {
$('.popularMenu').append("<li><img src="+this.image[0]+" />"+this.title+"</li>");
});
});
This is a bit of a guess (see my comment regarding a need to clarify which arrays you see as "unnecessary") but the line that stands out to me is this:
$pop_posts[] = array($cat,$myposts);
This can be translated as "create a 2-element array, whose first member is $cat (the name of the category) and whose second member is $myposts (an array of posts); add this 2-element array as the last member of the array $pop_posts". The result is that $pop_posts is an array of two-element arrays.
Perhaps what you wanted to say was "set the key $cat of the associative array $pop_posts to the value $myposts (an array of posts)", which would be this:
$pop_posts[$cat] = $myposts;
That would make the resulting structure simpler, as instead of an array of 2-element arrays, you would have a single hash (PHP associative array, JS object) whose keys were categories, and whose values were the popular posts for that category.
However, there are two disadvantages:
It wouldn't work if the category names were not unique, since a key cannot exist more than once in a hash. I don't think this applies here.
JSON hashes (and the JS object they create) are unordered key-value collections. So if you want to preserve the order of your categories, you need a different mechanism (either the array-of-arrays you already have, or an additional array storing the "correct" order to visit the keys). In your example, you reference Sport specifically, so this may not be an issue.

Map Reduce To Get Most popular tags

I have a problem that I need some help on but I feel I'm close. It involves Lithium and MongoDB Code looks like this:
http://pastium.org/view/0403d3e4f560e3f790b32053c71d0f2b
$db = PopularTags::connection();
$map = new \MongoCode("function() {
if (!this.saved_terms) {
return;
}
for (index in this.saved_terms) {
emit(this.saved_terms[index], 1);
}
}");
$reduce = new \MongoCode("function(previous, current) {
var count = 0;
for (index in current) {
count += current[index];
}
return count;
}");
$metrics = $db->connection->command(array(
'mapreduce' => 'users',
'map' => $map,
'reduce' => $reduce,
'out' => 'terms'
));
$cursor = $db->connection->selectCollection($metrics['result'])->find()->limit(1);
print_r($cursor);
/**
User Data In Mongo
{
"_id" : ObjectId("4e789f954c734cc95b000012"),
"email" : "example#bob.com",
"saved_terms" : [
null,
[
"technology",
" apple",
" iphone"
],
[
"apple",
" water",
" beryy"
]
] }
**/
I am having a user savings terms they search on and then I am try to get the most populars terms
but I keep getting errors like :Uncaught exception 'Exception' with message 'MongoDB::__construct( invalid name '. does anyone have any idea how to do this or some direction?
First off I would not store this in the user object. MongoDb objects have an upper limit of 4/16MB (depending on version). Now this limit is normally not a problem, but when logging inline in one object you might be able to reach it. However a more real problem is that every time you need to act on these objects you need to load them into RAM and it becomes consuming. I dont think you want that on your user objects.
Secondly arrays in objects are not sortable and have other limitations that might come back to bite you later.
But, if you want to have it like this (low volume of searches should not be a problem really) you can solve this most easy by using a group query.
A group query is pretty much like a group query in sql, so its a slight trick as you need to group on something most objects share. (An active field on users maybe).
So, heres a working group example that will sum words used based on your structure.
Just put this method in your model and do MyModel::searchTermUsage() to get a Document object back.
public static function searchTermUsage() {
$reduce = 'function(obj, prev) {
obj.terms.forEach(function(terms) {
terms.forEach(function(term) {
if (!(term in prev)) prev[term] = 0;
prev[term]++;
});
});
}';
return static::all(array(
'initial' => new \stdclass,
'reduce' => $reduce,
'group' => 'common-value-key' // Change this
));
}
There is no protection against non-array types in the terms field (you had a null value in your example). I removed it for simplicity, its better to probably strip this before it ends up in the database.

Categories