Модуль 2 · Язык фильтров

Справочник по MongoDB · модуль 2 · язык фильтров

2.3

2.3

Списки значений:
$in и $nin

В главе 2.1 было показано, что двумя парами с одним ключом, {"city": "Ярославль", "city": "Москва"}, нельзя отобрать документы из двух городов сразу. Здесь разобран оператор $in, который проверяет поле по списку допустимых значений, и обратный ему $nin. Отдельно — как оба оператора работают с полем-массивом, с отсутствующим полем и с пустым списком.

Категория
Язык запросов
Уровень
Начальный
Связанные темы
Операторы сравнения, логика $or, массивы $all
Зачем это знать
Отбирать документы по набору значений, выбранных пользователем

Как запускать примеры этой главы

БазыГлава только читает данные — сбрасывать базы не нужно.
Архивmongodb-glava-2.3-python.zip, 60 КБmongodb-glava-2.3-ruby.zip, 60 КБmongodb-glava-2.3-go.zip, 66 КБmongodb-glava-2.3-cpp.zip, 64 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
Файлlessons/ch2_3.py — пример вставляется в конец файлаlessons/ch2_3.rb — пример вставляется в конец файлаlessons/ch2_3.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch2_3.cpp — пример внутрь скобок в конце main, функции — выше main
Запускcd lessons, затем python ch2_3.pycd lessons, затем python3 ch2_3.py · в Docker: docker compose run --rm python lessons/ch2_3.pycd lessons, затем ruby ch2_3.rb · в Docker: docker compose run --rm ruby lessons/ch2_3.rbcd lessons, затем go run ch2_3.go · в Docker: docker compose run --rm go lessons/ch2_3.gocd lessons, затем g++ -std=c++17 ch2_3.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch2_3.cpp
ПорядокКаждый пример вставляется вместо предыдущего и запускается один раз, по порядку, — так получены все выводы главы. Подробно — глава 1.0, §10.

§1$in: одно из значений

Оператор $in принимает массив значений и отбирает документы, у которых значение поля совпадает хотя бы с одним элементом этого массива. Фильтр {"city": {"$in": ["Москва", "Казань"]}} читается так: город — Москва или Казань.

Совпадение внутри $in проверяется так же, как простое равенство из главы 2.1: по типу и значению, с учётом регистра. Список — обычный массив своего языка: список Python, массив Ruby, bson.A в Go, make_array в C++.

Python · PyMongoвакансии в Москве или Казани
for doc in vacancies.find({"city": {"$in": ["Москва", "Казань"]}}, {"_id": 0, "title": 1, "city": 1}):
    print(doc)
C++ · mongocxxвакансии в Москве или Казани
mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("title", 1), kvp("city", 1)));

for (const auto& doc : vacancies.find(make_document(kvp("city", make_document(kvp("$in", make_array("Москва", "Казань"))))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverвакансии в Москве или Казани
cursor, _ := vacancies.Find(ctx, bson.D{{Key: "city", Value: bson.D{{Key: "$in", Value: bson.A{"Москва", "Казань"}}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "title", Value: 1}, {Key: "city", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoвакансии в Москве или Казани
vacancies.find({ "city" => { "$in" => ["Москва", "Казань"] } }, projection: { "_id" => 0, "title" => 1, "city" => 1 })
         .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'title': 'Тестировщик', 'city': 'Казань'}
{'title': 'Администратор баз данных', 'city': 'Москва'}
{'title': 'Аналитик данных', 'city': 'Москва'}
{'title': 'Data-инженер', 'city': 'Москва'}
Вывод в терминалрезультат запуска
{ "title" : "Тестировщик", "city" : "Казань" }
{ "title" : "Администратор баз данных", "city" : "Москва" }
{ "title" : "Аналитик данных", "city" : "Москва" }
{ "title" : "Data-инженер", "city" : "Москва" }
Вывод в терминалрезультат запуска
{"title":"Тестировщик","city":"Казань"}
{"title":"Администратор баз данных","city":"Москва"}
{"title":"Аналитик данных","city":"Москва"}
{"title":"Data-инженер","city":"Москва"}
Вывод в терминалрезультат запуска
{"title"=>"Тестировщик", "city"=>"Казань"}
{"title"=>"Администратор баз данных", "city"=>"Москва"}
{"title"=>"Аналитик данных", "city"=>"Москва"}
{"title"=>"Data-инженер", "city"=>"Москва"}

Четыре вакансии: одна в Казани и три в Москве. То же условие можно записать через логическое «или» — оператор $or из главы 2.4. Когда все варианты относятся к одному полю, запись через $in короче: список значений стоит в одном месте.

Compass, коллекция hh.vacancies: фильтр city in Москва, Казань, счётчик 1 – 4 of 4
Compass · hh.vacancies, фильтр { city: { $in: ["Москва", "Казань"] } } — те же четыре вакансии, что напечатала программа

Вкладка Documents подписана числом 8, счётчик над списком — 1 – 4 of 4. В документах видно поле city: у первого — Казань, у остальных — Москва. Вакансии идут в порядке коллекции, как и в выводе программы.

§2Порядок результата

Порядок значений в списке на порядок документов в результате не влияет. Сервер проверяет документы коллекции один за другим и отдаёт подходящие в том порядке, в каком их находит. Это учитывают, когда программа достаёт документы по списку ключей — например, по списку избранных вакансий пользователя.

Python · PyMongoвакансии по списку ключей
for doc in vacancies.find({"_id": {"$in": ["v-008", "v-001"]}}, {"title": 1}):
    print(doc)
C++ · mongocxxвакансии по списку ключей
mongocxx::options::find options;
options.projection(make_document(kvp("title", 1)));

for (const auto& doc : vacancies.find(make_document(kvp("_id", make_document(kvp("$in", make_array("v-008", "v-001"))))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverвакансии по списку ключей
cursor, _ := vacancies.Find(ctx, bson.D{{Key: "_id", Value: bson.D{{Key: "$in", Value: bson.A{"v-008", "v-001"}}}}},
    options.Find().SetProjection(bson.D{{Key: "title", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoвакансии по списку ключей
vacancies.find({ "_id" => { "$in" => ["v-008", "v-001"] } }, projection: { "title" => 1 })
         .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'_id': 'v-001', 'title': 'Junior Python-разработчик'}
{'_id': 'v-008', 'title': 'Data-инженер'}
Вывод в терминалрезультат запуска
{ "_id" : "v-001", "title" : "Junior Python-разработчик" }
{ "_id" : "v-008", "title" : "Data-инженер" }
Вывод в терминалрезультат запуска
{"_id":"v-001","title":"Junior Python-разработчик"}
{"_id":"v-008","title":"Data-инженер"}
Вывод в терминалрезультат запуска
{"_id"=>"v-001", "title"=>"Junior Python-разработчик"}
{"_id"=>"v-008", "title"=>"Data-инженер"}

В списке первой стоит v-008, а в результате первой пришла v-001: в коллекции она лежит раньше. Проекция здесь содержит только title, и поле _id сервер добавил сам — так работает включающая проекция (глава 1.5). Если порядок списка важен, документы переставляют в программе после получения или задают сортировку по полю.

Compass, коллекция hh.vacancies: фильтр _id in v-008, v-001, счётчик 1 – 2 of 2, первой идёт v-001
Compass · hh.vacancies, фильтр { _id: { $in: ["v-008", "v-001"] } } — порядок коллекции

В фильтре первой стоит v-008, в списке Compass первой идёт v-001 — тот же порядок, что в выводе программы. Проекция { title: 1 } оставила название, а _id добавил сервер.

§3$in и поле-массив

Если поле — массив, условие проверяется на каждом элементе, как в главе 2.1 §6. Для $in это значит: документ подходит, если хотя бы один элемент массива совпал хотя бы с одним значением списка. Пример ищет резюме, где среди навыков есть MongoDB или ClickHouse.

Python · PyMongoхотя бы один навык из списка
for doc in resumes.find({"skills": {"$in": ["MongoDB", "ClickHouse"]}}, {"_id": 0, "fio": 1, "skills": 1}):
    print(doc)
C++ · mongocxxхотя бы один навык из списка
mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("fio", 1), kvp("skills", 1)));

for (const auto& doc : resumes.find(make_document(kvp("skills", make_document(kvp("$in", make_array("MongoDB", "ClickHouse"))))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverхотя бы один навык из списка
cursor, _ := resumes.Find(ctx, bson.D{{Key: "skills", Value: bson.D{{Key: "$in", Value: bson.A{"MongoDB", "ClickHouse"}}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "fio", Value: 1}, {Key: "skills", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoхотя бы один навык из списка
resumes.find({ "skills" => { "$in" => ["MongoDB", "ClickHouse"] } }, projection: { "_id" => 0, "fio" => 1, "skills" => 1 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'fio': 'Алина Дроздова', 'skills': ['SQL', 'Python', 'ClickHouse', 'Power BI', 'Excel']}
{'fio': 'Тимур Валиев', 'skills': ['PostgreSQL', 'MongoDB', 'Bash', 'Zabbix']}
{'fio': 'Марк Ефремов', 'skills': ['Python', 'FastAPI', 'MongoDB', 'Git']}
{'fio': 'Ольга Пирогова', 'skills': ['SQL', 'Excel', 'Python', 'ClickHouse']}
Вывод в терминалрезультат запуска
{ "fio" : "Алина Дроздова", "skills" : [ "SQL", "Python", "ClickHouse", "Power BI", "Excel" ] }
{ "fio" : "Тимур Валиев", "skills" : [ "PostgreSQL", "MongoDB", "Bash", "Zabbix" ] }
{ "fio" : "Марк Ефремов", "skills" : [ "Python", "FastAPI", "MongoDB", "Git" ] }
{ "fio" : "Ольга Пирогова", "skills" : [ "SQL", "Excel", "Python", "ClickHouse" ] }
Вывод в терминалрезультат запуска
{"fio":"Алина Дроздова","skills":["SQL","Python","ClickHouse","Power BI","Excel"]}
{"fio":"Тимур Валиев","skills":["PostgreSQL","MongoDB","Bash","Zabbix"]}
{"fio":"Марк Ефремов","skills":["Python","FastAPI","MongoDB","Git"]}
{"fio":"Ольга Пирогова","skills":["SQL","Excel","Python","ClickHouse"]}
Вывод в терминалрезультат запуска
{"fio"=>"Алина Дроздова", "skills"=>["SQL", "Python", "ClickHouse", "Power BI", "Excel"]}
{"fio"=>"Тимур Валиев", "skills"=>["PostgreSQL", "MongoDB", "Bash", "Zabbix"]}
{"fio"=>"Марк Ефремов", "skills"=>["Python", "FastAPI", "MongoDB", "Git"]}
{"fio"=>"Ольга Пирогова", "skills"=>["SQL", "Excel", "Python", "ClickHouse"]}

Четыре резюме. У Тимура Валиева и Марка Ефремова в навыках есть MongoDB, у Алины Дроздовой и Ольги Пироговой — ClickHouse. Одного совпадения достаточно; остальные навыки на результат не влияют. Условие «в резюме есть все навыки из списка» записывается другим оператором — $all, он разобран в главе 2.6.

Compass, коллекция hh.resumes: фильтр skills in MongoDB, ClickHouse, счётчик 1 – 4 of 4, у первого резюме массив skills раскрыт
Compass · hh.resumes, фильтр { skills: { $in: ["MongoDB", "ClickHouse"] } } — у первого резюме массив навыков раскрыт

У Алины Дроздовой в массиве skills пять строк, и из списка фильтра среди них есть только "ClickHouse". Этого одного совпадения хватило, чтобы документ попал в результат. Счётчик 1 – 4 of 4 совпадает с выводом программы выше.

§4$nin: ни одного из значений

Оператор $nin — от not in — отбирает документы, у которых значение поля не совпадает ни с одним элементом списка. Для поля-массива условие строже: ни один элемент массива не должен совпасть ни с одним значением списка.

Python · PyMongoни одного значения из списка
print("вакансии не в Москве и не в Ярославле:", vacancies.count_documents({"city": {"$nin": ["Москва", "Ярославль"]}}))

for doc in resumes.find({"skills": {"$nin": ["Python", "SQL"]}}, {"_id": 0, "fio": 1, "skills": 1}):
    print(doc)
C++ · mongocxxни одного значения из списка
std::cout << "вакансии не в Москве и не в Ярославле: " << vacancies.count_documents(make_document(kvp("city", make_document(kvp("$nin", make_array("Москва", "Ярославль")))))) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("fio", 1), kvp("skills", 1)));

for (const auto& doc : resumes.find(make_document(kvp("skills", make_document(kvp("$nin", make_array("Python", "SQL"))))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driverни одного значения из списка
gorod, _ := vacancies.CountDocuments(ctx, bson.D{{Key: "city", Value: bson.D{{Key: "$nin", Value: bson.A{"Москва", "Ярославль"}}}}})

fmt.Println("вакансии не в Москве и не в Ярославле:", gorod)

cursor, _ := resumes.Find(ctx, bson.D{{Key: "skills", Value: bson.D{{Key: "$nin", Value: bson.A{"Python", "SQL"}}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "fio", Value: 1}, {Key: "skills", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongoни одного значения из списка
puts "вакансии не в Москве и не в Ярославле: " + vacancies.count_documents({ "city" => { "$nin" => ["Москва", "Ярославль"] } }).to_s

resumes.find({ "skills" => { "$nin" => ["Python", "SQL"] } }, projection: { "_id" => 0, "fio" => 1, "skills" => 1 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
вакансии не в Москве и не в Ярославле: 2
{'fio': 'Игорь Самойлов', 'skills': ['C#', '.NET', 'PostgreSQL', 'RabbitMQ', 'Docker']}
{'fio': 'Дарья Нечаева', 'skills': ['JavaScript', 'React', 'HTML', 'CSS']}
{'fio': 'Тимур Валиев', 'skills': ['PostgreSQL', 'MongoDB', 'Bash', 'Zabbix']}
Вывод в терминалрезультат запуска
вакансии не в Москве и не в Ярославле: 2
{ "fio" : "Игорь Самойлов", "skills" : [ "C#", ".NET", "PostgreSQL", "RabbitMQ", "Docker" ] }
{ "fio" : "Дарья Нечаева", "skills" : [ "JavaScript", "React", "HTML", "CSS" ] }
{ "fio" : "Тимур Валиев", "skills" : [ "PostgreSQL", "MongoDB", "Bash", "Zabbix" ] }
Вывод в терминалрезультат запуска
вакансии не в Москве и не в Ярославле: 2
{"fio":"Игорь Самойлов","skills":["C#",".NET","PostgreSQL","RabbitMQ","Docker"]}
{"fio":"Дарья Нечаева","skills":["JavaScript","React","HTML","CSS"]}
{"fio":"Тимур Валиев","skills":["PostgreSQL","MongoDB","Bash","Zabbix"]}
Вывод в терминалрезультат запуска
вакансии не в Москве и не в Ярославле: 2
{"fio"=>"Игорь Самойлов", "skills"=>["C#", ".NET", "PostgreSQL", "RabbitMQ", "Docker"]}
{"fio"=>"Дарья Нечаева", "skills"=>["JavaScript", "React", "HTML", "CSS"]}
{"fio"=>"Тимур Валиев", "skills"=>["PostgreSQL", "MongoDB", "Bash", "Zabbix"]}

Вакансий не в Москве и не в Ярославле две — в Казани и Санкт-Петербурге. Резюме без Python и без SQL три: в каждом из выведенных массивов нет ни одной из этих строк. Резюме, где есть хотя бы один из двух навыков, $nin исключил.

Compass, коллекция hh.resumes: фильтр skills nin Python, SQL, счётчик 1 – 3 of 3, массивы навыков раскрыты
Compass · hh.resumes, фильтр { skills: { $nin: ["Python", "SQL"] } } — три резюме с раскрытыми навыками

Счётчик 1 – 3 of 3. Массивы skills раскрыты целиком: ни в одном нет ни "Python", ни "SQL". Проекция оставляет ФИО и навыки.

§5$nin и отсутствующее поле

С отсутствующим полем $nin ведёт себя так же, как $ne из главы 2.2: документ без поля не совпадает ни с одним значением списка, и сервер его возвращает. В коллекции резюме уровень образования записан в поле education.level, и у одного резюме вложенного документа education нет вовсе.

Python · PyMongo$nin и отсутствующее поле
print("education.level $nin [СПО]:", resumes.count_documents({"education.level": {"$nin": ["СПО"]}}))
print("education.level Высшее:    ", resumes.count_documents({"education.level": "Высшее"}))

for doc in resumes.find({"education.level": {"$nin": ["СПО"]}}, {"_id": 0, "fio": 1, "education.level": 1}):
    print(doc)
C++ · mongocxx$nin и отсутствующее поле
std::cout << "education.level $nin [СПО]: " << resumes.count_documents(make_document(kvp("education.level", make_document(kvp("$nin", make_array("СПО")))))) << std::endl;
std::cout << "education.level Высшее:     " << resumes.count_documents(make_document(kvp("education.level", "Высшее"))) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("_id", 0), kvp("fio", 1), kvp("education.level", 1)));

for (const auto& doc : resumes.find(make_document(kvp("education.level", make_document(kvp("$nin", make_array("СПО"))))), options)) {
    std::cout << bsoncxx::to_json(doc, bsoncxx::ExtendedJsonMode::k_relaxed) << std::endl;
}
Go · mongo-driver$nin и отсутствующее поле
neSpo, _ := resumes.CountDocuments(ctx, bson.D{{Key: "education.level", Value: bson.D{{Key: "$nin", Value: bson.A{"СПО"}}}}})
vysshee, _ := resumes.CountDocuments(ctx, bson.D{{Key: "education.level", Value: "Высшее"}})

fmt.Println("education.level $nin [СПО]:", neSpo)
fmt.Println("education.level Высшее:    ", vysshee)

cursor, _ := resumes.Find(ctx, bson.D{{Key: "education.level", Value: bson.D{{Key: "$nin", Value: bson.A{"СПО"}}}}},
    options.Find().SetProjection(bson.D{{Key: "_id", Value: 0}, {Key: "fio", Value: 1}, {Key: "education.level", Value: 1}}))
for cursor.Next(ctx) {
    var doc bson.D
    cursor.Decode(&doc)
    out, _ := bson.MarshalExtJSON(doc, false, false)
    fmt.Println(string(out))
}
Ruby · mongo$nin и отсутствующее поле
puts "education.level $nin [СПО]: " + resumes.count_documents({ "education.level" => { "$nin" => ["СПО"] } }).to_s
puts "education.level Высшее:     " + resumes.count_documents({ "education.level" => "Высшее" }).to_s

resumes.find({ "education.level" => { "$nin" => ["СПО"] } }, projection: { "_id" => 0, "fio" => 1, "education.level" => 1 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
education.level $nin [СПО]: 5
education.level Высшее:     4
{'fio': 'Анна Белова'}
{'fio': 'Алина Дроздова', 'education': {'level': 'Высшее'}}
{'fio': 'Игорь Самойлов', 'education': {'level': 'Высшее'}}
{'fio': 'Тимур Валиев', 'education': {'level': 'Высшее'}}
{'fio': 'Ольга Пирогова', 'education': {'level': 'Высшее'}}
Вывод в терминалрезультат запуска
education.level $nin [СПО]: 5
education.level Высшее:     4
{ "fio" : "Анна Белова" }
{ "fio" : "Алина Дроздова", "education" : { "level" : "Высшее" } }
{ "fio" : "Игорь Самойлов", "education" : { "level" : "Высшее" } }
{ "fio" : "Тимур Валиев", "education" : { "level" : "Высшее" } }
{ "fio" : "Ольга Пирогова", "education" : { "level" : "Высшее" } }
Вывод в терминалрезультат запуска
education.level $nin [СПО]: 5
education.level Высшее:     4
{"fio":"Анна Белова"}
{"fio":"Алина Дроздова","education":{"level":"Высшее"}}
{"fio":"Игорь Самойлов","education":{"level":"Высшее"}}
{"fio":"Тимур Валиев","education":{"level":"Высшее"}}
{"fio":"Ольга Пирогова","education":{"level":"Высшее"}}
Вывод в терминалрезультат запуска
education.level $nin [СПО]: 5
education.level Высшее:     4
{"fio"=>"Анна Белова"}
{"fio"=>"Алина Дроздова", "education"=>{"level"=>"Высшее"}}
{"fio"=>"Игорь Самойлов", "education"=>{"level"=>"Высшее"}}
{"fio"=>"Тимур Валиев", "education"=>{"level"=>"Высшее"}}
{"fio"=>"Ольга Пирогова", "education"=>{"level"=>"Высшее"}}

Условие «уровень не СПО» дало пять резюме, условие «уровень — высшее» — четыре. Пятое резюме — Анны Беловой: в выводе у него только ФИО, потому что поля education в документе нет, и проекции нечего было показать. Если задача — найти соискателей с высшим образованием, её записывают равенством; $nin подходит, когда документы без поля тоже нужны.

Compass, коллекция hh.resumes: фильтр education.level nin СПО, счётчик 1 – 5 of 5, у первого резюме нет поля education
Compass · hh.resumes, фильтр { "education.level": { $nin: ["СПО"] } } — первое резюме без поля education

У четырёх документов после ФИО стоит свёрнутый education, у первого — Анны Беловой — только ФИО: вложенного документа нет, и $nin его пропустил. Счётчик 1 – 5 of 5.

§6null в списке

Условие равенства с null — {"ready_to_move": null} — отбирает документы, где поле равно null, и документы, где поля нет. Для сервера отсутствующее поле при сравнении с null считается равным ему. Внутри $in значение null работает так же.

Python · PyMongonull в списке значений
print("ready_to_move null:             ", resumes.count_documents({"ready_to_move": None}))
print("ready_to_move $in [false, null]:", resumes.count_documents({"ready_to_move": {"$in": [False, None]}}))
C++ · mongocxxnull в списке значений
std::cout << "ready_to_move null:              " << resumes.count_documents(make_document(kvp("ready_to_move", bsoncxx::types::b_null{}))) << std::endl;
std::cout << "ready_to_move $in [false, null]: " << resumes.count_documents(make_document(kvp("ready_to_move", make_document(kvp("$in", make_array(false, bsoncxx::types::b_null{})))))) << std::endl;
Go · mongo-drivernull в списке значений
pusto, _ := resumes.CountDocuments(ctx, bson.D{{Key: "ready_to_move", Value: nil}})
netIliPusto, _ := resumes.CountDocuments(ctx, bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$in", Value: bson.A{false, nil}}}}})

fmt.Println("ready_to_move null:             ", pusto)
fmt.Println("ready_to_move $in [false, null]:", netIliPusto)
Ruby · mongonull в списке значений
puts "ready_to_move null:              " + resumes.count_documents({ "ready_to_move" => nil }).to_s
puts "ready_to_move $in [false, null]: " + resumes.count_documents({ "ready_to_move" => { "$in" => [false, nil] } }).to_s
Вывод в терминалрезультат запуска
ready_to_move null:              1
ready_to_move $in [false, null]: 5
Вывод в терминалрезультат запуска
ready_to_move null:              1
ready_to_move $in [false, null]: 5
Вывод в терминалрезультат запуска
ready_to_move null:              1
ready_to_move $in [false, null]: 5
Вывод в терминалрезультат запуска
ready_to_move null:              1
ready_to_move $in [false, null]: 5

Поля ready_to_move нет в одном резюме, и равенство с null его нашло. Условие $in: [false, null] читается «ответ „нет“ или ответа нет» и даёт те же пять резюме, что $ne: true в главе 2.2. Разница в записи: здесь оба допустимых случая перечислены явно. Отличить поле со значением null от отсутствующего поля позволяют операторы $exists и $type из главы 2.5.

Compass, коллекция hh.resumes: фильтр ready_to_move in false, null, счётчик 1 – 5 of 5
Compass · hh.resumes, фильтр { ready_to_move: { $in: [false, null] } } — ответ «нет» или ответа нет

Первый документ — без поля ready_to_move, у остальных четырёх оно равно false. Счётчик 1 – 5 of 5, как у $ne: true в главе 2.2.

§7Пустой список

Список для $in часто собирает программа — например, из городов, которые пользователь отметил в форме поиска. Если пользователь не отметил ни одного города, список окажется пустым.

Python · PyMongoпустой список
print("$in []: ", vacancies.count_documents({"city": {"$in": []}}))
print("$nin []:", vacancies.count_documents({"city": {"$nin": []}}))
C++ · mongocxxпустой список
std::cout << "$in []:  " << vacancies.count_documents(make_document(kvp("city", make_document(kvp("$in", make_array()))))) << std::endl;
std::cout << "$nin []: " << vacancies.count_documents(make_document(kvp("city", make_document(kvp("$nin", make_array()))))) << std::endl;
Go · mongo-driverпустой список
vSpiske, _ := vacancies.CountDocuments(ctx, bson.D{{Key: "city", Value: bson.D{{Key: "$in", Value: bson.A{}}}}})
neVSpiske, _ := vacancies.CountDocuments(ctx, bson.D{{Key: "city", Value: bson.D{{Key: "$nin", Value: bson.A{}}}}})

fmt.Println("$in []: ", vSpiske)
fmt.Println("$nin []:", neVSpiske)
Ruby · mongoпустой список
puts "$in []:  " + vacancies.count_documents({ "city" => { "$in" => [] } }).to_s
puts "$nin []: " + vacancies.count_documents({ "city" => { "$nin" => [] } }).to_s
Вывод в терминалрезультат запуска
$in []:  0
$nin []: 8
Вывод в терминалрезультат запуска
$in []:  0
$nin []: 8
Вывод в терминалрезультат запуска
$in []:  0
$nin []: 8
Вывод в терминалрезультат запуска
$in []:  0
$nin []: 8

С пустым списком $in не находит ничего: нет значения, с которым поле могло бы совпасть. $nin с пустым списком находит все восемь вакансий: исключать нечего. Для формы поиска обычно нужно третье поведение — «город не выбран, значит, город не важен». Его получают в программе: если список пустой, условие на город в фильтр не добавляют совсем.

Compass, коллекция hh.vacancies: фильтр city in пустой список, счётчик 0 – 0 of 0, No results
Compass · hh.vacancies, фильтр { city: { $in: [] } } — пустой результат без ошибки

Compass выполнил запрос без предупреждения и показал No results, хотя вкладка Documents подписана числом 8. Так же пустой список выглядит и в программе: ошибки нет, документов нет.

§8Списки в Compass

Кадры в разделах выше сняты в Compass на тех же запросах, что и примеры. Список значений в поле фильтра пишется в квадратных скобках, как массив в JSON: { city: { $in: ["Москва", "Казань"] } }. Пустой список записывается как [], значение null — словом null без кавычек.

§9Частые ошибки

Что написаноЧто происходитКак правильно
{"city": {"$in": "Москва"}}Ошибка сервера: $in needs an array{"$in": ["Москва"]}
{"skills": ["Python", "SQL"]}Пусто: массив в фильтре сравнивается с полем целиком, вместе с порядком элементов{"$in": ["Python", "SQL"]}
{"education.level": {"$nin": ["СПО"]}}Кроме «Высшее» находит документ, в котором поля нетРавенство "Высшее"; отсутствующее поле — глава 2.5
Результат ожидается в порядке списка $inДокументы идут в порядке коллекцииsort или упорядочить в программе
Список для $in собран из формы, и пользователь ничего не выбрал$in с пустым списком не находит ничего, $nin — находит всёПроверить пустой выбор до запроса и не добавлять условие
Что написаноЧто происходитКак правильно
make_document(kvp("city", make_document(kvp("$in", "Москва"))))Ошибка сервера: $in needs an arraymake_document(kvp("$in", make_array("Москва")))
make_document(kvp("skills", make_array("Python", "SQL")))Пусто: массив в фильтре сравнивается с полем целиком, вместе с порядком элементовmake_document(kvp("$in", make_array("Python", "SQL")))
make_document(kvp("education.level", make_document(kvp("$nin", make_array("СПО")))))Кроме «Высшее» находит документ, в котором поля нетРавенство "Высшее"; отсутствующее поле — глава 2.5
Результат ожидается в порядке списка $inДокументы идут в порядке коллекцииsort или упорядочить в программе
Список для $in собран из формы, и пользователь ничего не выбрал$in с пустым списком не находит ничего, $nin — находит всёПроверить пустой выбор до запроса и не добавлять условие
Что написаноЧто происходитКак правильно
bson.D{{Key: "city", Value: bson.D{{Key: "$in", Value: "Москва"}}}}Ошибка сервера: $in needs an arraybson.D{{Key: "$in", Value: bson.A{"Москва"}}}
bson.D{{Key: "skills", Value: bson.A{"Python", "SQL"}}}Пусто: массив в фильтре сравнивается с полем целиком, вместе с порядком элементовbson.D{{Key: "$in", Value: bson.A{"Python", "SQL"}}}
bson.D{{Key: "education.level", Value: bson.D{{Key: "$nin", Value: bson.A{"СПО"}}}}}Кроме «Высшее» находит документ, в котором поля нетРавенство "Высшее"; отсутствующее поле — глава 2.5
Результат ожидается в порядке списка $inДокументы идут в порядке коллекцииsort или упорядочить в программе
Список для $in собран из формы, и пользователь ничего не выбрал$in с пустым списком не находит ничего, $nin — находит всёПроверить пустой выбор до запроса и не добавлять условие
Что написаноЧто происходитКак правильно
{ "city" => { "$in" => "Москва" } }Ошибка сервера: $in needs an array{ "$in" => ["Москва"] }
{ "skills" => ["Python", "SQL"] }Пусто: массив в фильтре сравнивается с полем целиком, вместе с порядком элементов{ "$in" => ["Python", "SQL"] }
{ "education.level" => { "$nin" => ["СПО"] } }Кроме «Высшее» находит документ, в котором поля нетРавенство "Высшее"; отсутствующее поле — глава 2.5
Результат ожидается в порядке списка $inДокументы идут в порядке коллекцииsort или упорядочить в программе
Список для $in собран из формы, и пользователь ничего не выбрал$in с пустым списком не находит ничего, $nin — находит всёПроверить пустой выбор до запроса и не добавлять условие

§10Проверьте себя

Чем {"city": {"$in": ["Москва", "Казань"]}} отличается от {"city": "Москва", "city": "Казань"}?

Первый фильтр отбирает документы, где город — один из двух. Во втором ключ повторён: в Python и Ruby останется только последняя пара, и фильтр найдёт вакансии в Казани; в Go и C++ оба условия уйдут на сервер и соединятся по «и», и результат будет пустым.

В каком порядке придут документы по фильтру {"_id": {"$in": ["v-008", "v-001"]}}?

В порядке коллекции: сначала v-001, потом v-008. Порядок значений в списке $in на порядок результата не влияет.

Какие резюме отберёт {"skills": {"$in": ["MongoDB", "ClickHouse"]}}?

Те, где в массиве навыков есть хотя бы один из двух: MongoDB или ClickHouse. Резюме с обоими навыками тоже подходит. В базе курса таких резюме четыре.

Почему {"education.level": {"$nin": ["СПО"]}} нашёл пять резюме, а соискателей с высшим образованием четыре?

В одном резюме нет вложенного документа education. Отсутствующее поле не совпадает ни с одним значением списка, и $nin этот документ возвращает.

Что вернёт $in с пустым списком?

Ни одного документа: совпадать не с чем. $nin с пустым списком, наоборот, вернёт все документы. Если список собирается из выбора пользователя, пустой выбор обрабатывают в программе: условие на это поле в фильтр не добавляют.

§11Шпаргалка

Одно из значений {"city": {"$in": ["Москва", "Казань"]}}
По списку ключей {"_id": {"$in": ["v-001", "v-008"]}}
Хотя бы один элемент массива {"skills": {"$in": ["MongoDB", "ClickHouse"]}}
Ни одно из значений {"city": {"$nin": ["Москва", "Ярославль"]}}
Значение или нет поля {"ready_to_move": {"$in": [False, None]}}
Одно из значений make_document(kvp("city", make_document(kvp("$in", make_array("Москва", "Казань")))))
По списку ключей make_document(kvp("_id", make_document(kvp("$in", make_array("v-001", "v-008")))))
Хотя бы один элемент массива make_document(kvp("skills", make_document(kvp("$in", make_array("MongoDB", "ClickHouse")))))
Ни одно из значений make_document(kvp("city", make_document(kvp("$nin", make_array("Москва", "Ярославль")))))
Значение или нет поля make_document(kvp("ready_to_move", make_document(kvp("$in", make_array(false, bsoncxx::types::b_null{})))))
Одно из значений bson.D{{Key: "city", Value: bson.D{{Key: "$in", Value: bson.A{"Москва", "Казань"}}}}}
По списку ключей bson.D{{Key: "_id", Value: bson.D{{Key: "$in", Value: bson.A{"v-001", "v-008"}}}}}
Хотя бы один элемент массива bson.D{{Key: "skills", Value: bson.D{{Key: "$in", Value: bson.A{"MongoDB", "ClickHouse"}}}}}
Ни одно из значений bson.D{{Key: "city", Value: bson.D{{Key: "$nin", Value: bson.A{"Москва", "Ярославль"}}}}}
Значение или нет поля bson.D{{Key: "ready_to_move", Value: bson.D{{Key: "$in", Value: bson.A{false, nil}}}}}
Одно из значений { "city" => { "$in" => ["Москва", "Казань"] } }
По списку ключей { "_id" => { "$in" => ["v-001", "v-008"] } }
Хотя бы один элемент массива { "skills" => { "$in" => ["MongoDB", "ClickHouse"] } }
Ни одно из значений { "city" => { "$nin" => ["Москва", "Ярославль"] } }
Значение или нет поля { "ready_to_move" => { "$in" => [false, nil] } }

Итог главы

$in отбирает документы, у которых значение поля совпадает с одним из элементов списка, $nin — с которыми не совпадает ни один. Для поля-массива $in требует хотя бы одного совпадения среди элементов, $nin — ни одного. Порядок списка на порядок результата не влияет. $nin, как и $ne, возвращает документы без поля, а null в списке совпадает и с null, и с отсутствующим полем. Пустой список в $in не находит ничего, в $nin — находит всё. Следующая глава — о логических операторах, которые соединяют условия на разных полях: $and, $or, $not, $nor.

Глава 2.3 · Справочник по MongoDB Автор: Макаров Максим Николаевич