$and, $or and $nor take an array of filter documents and mean "all clauses match", "at least one matches" and "none matches"; $not is different, wrapping one operator expression for one field. AND is implicit between the keys of a filter, so $and is written only when the implicit form cannot express the query — which happens more often than you would think, because two conditions on the same field cannot both survive in a JavaScript object: the later key silently wins.
Each $or clause is planned separately, so the server may use one index for the first clause and another for the second, then merge and de-duplicate; if even one clause has no usable index, the whole query becomes a collection scan. $nor inverts the list and, like $ne, keeps documents missing the fields entirely.
db.books.find({ $or: [{ stock: 0 }, { price: { $gt: 18 } }] }).toArray().map(b => b.title)
db.books.find({ tags: 'classic', tags: 'fantasy' }).toArray().map(b => b.title)
db.books.find({ $and: [{ tags: 'classic' }, { tags: 'fantasy' }] }).toArray().map(b => b.title)[ 'Neuromancer', 'Project Hail Mary', 'Ancillary Justice' ] [ 'The Hobbit', 'Piranesi' ] [ 'The Hobbit' ]
The second query only ever sent tags: 'fantasy', so Piranesi, tagged fantasy alone, slipped through. Explicit $and keeps both conditions, each in its own document; it is also how filters built by separate pieces of code merge safely.
$not inverts one operator expression for one field, and it inverts everything that is not a match, including documents where the field is missing or holds an incomparable type. A search for { price: { $not: { $gte: 13 } } } returns Dune, Neuromancer and also Snow Crash, whose string price is not comparable to the number 13 at all. $not cannot take a plain value ({ price: { $not: 13 } } is an error) and cannot contain $regex as a key; pass a regex literal, as in { title: { $not: /^The/ } }.