Справочник по MongoDB · модуль 2 · язык фильтров
2.9
Скрипт целиком:
подбор кандидатов
Операторы модуля 2 разбирались по одному. Здесь собрана готовая программа: она подбирает кандидатов под требования вакансии, показывает, сколько резюме проходит каждое требование по отдельности, ищет подходящие вакансии, считает собеседования за неделю и проверяет данные на пропуски и неверные типы. Глава разбирает скрипт по частям и приводит его целиком вместе с выводом.
- Категория
- Готовая программа
- Уровень
- Итоговая глава модуля
- Связанные темы
- Все главы модуля 2, практическая работа ПР-02
- Зачем это знать
- Собрать из отдельных операторов рабочий отбор
Как запускать примеры этой главы
mongodb-glava-2.9-python.zip, 58 КБmongodb-glava-2.9-ruby.zip, 57 КБmongodb-glava-2.9-go.zip, 61 КБmongodb-glava-2.9-cpp.zip, 59 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.lessons/ch2_9.py — пример вставляется в конец файлаlessons/ch2_9.rb — пример вставляется в конец файлаlessons/ch2_9.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch2_9.cpp — пример внутрь скобок в конце main, функции — выше maincd lessons, затем python ch2_9.pycd lessons, затем python3 ch2_9.py · в Docker: docker compose run --rm python lessons/ch2_9.pycd lessons, затем ruby ch2_9.rb · в Docker: docker compose run --rm ruby lessons/ch2_9.rbcd lessons, затем go run ch2_9.go · в Docker: docker compose run --rm go lessons/ch2_9.gocd lessons, затем g++ -std=c++17 ch2_9.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch2_9.cpp§1Что делает скрипт
Скрипт hh_search.py решает задачу рекрутера: по требованиям вакансии найти подходящие резюме, показать, на каком требовании отсеиваются кандидаты, подобрать вакансии под ожидание по зарплате, посчитать собеседования за неделю и проверить, всё ли в порядке с самими данными.
Восемь разделов отчёта — восемь групп операторов модуля 2.
| Раздел отчёта | Какие операторы | Глава |
|---|---|---|
| 1. Требования | значения, из которых собираются фильтры | — |
| 2. Каждое требование по отдельности | $all, $or, $lte, $elemMatch, $exists | 2.2–2.6 |
| 3. Все требования сразу | те же условия в одном фильтре, сортировка | 2.1, 2.4 |
| 4. Должности с Junior | $regex и $options | 2.7 |
| 5. Вакансии под ожидание | точечная нотация и диапазон | 2.1, 2.2 |
| 6. Собеседования за неделю | период дат полуинтервалом | 2.2 |
| 7. Проверка данных | $exists, $type, $size, $expr, $ne | 2.5, 2.6, 2.8 |
| 8. Кто не прошёл отбор | проверка на стороне программы | — |
Скрипт только читает данные: сбрасывать базы перед запуском не нужно.
§2Требования и подключение
Начало скрипта — подключение и коллекции базы hh. Требования вакансии вынесены в отдельные переменные: список навыков, город, потолок по зарплате и минимальный срок работы. Дальше в фильтрах стоят только эти имена, поэтому поменять требования можно в одном месте, не трогая запросы.
import os from datetime import datetime from pymongo import MongoClient client = MongoClient(os.environ.get("MONGO_URI", "mongodb://localhost:27017/")) hh = client["hh"] resumes = hh["resumes"] vacancies = hh["vacancies"] interviews = hh["interviews"] def title(text): print("\n" + text) print("─" * len(text)) # ── 1. Требования вакансии ──────────────────────────────────────────────── NAVYKI = ["Python", "SQL"] GOROD = "Ярославль" POTOLOK = 100000 OPYT_MESYACEV = 6 title("1. Требования") print(f" навыки: {' и '.join(NAVYKI)}") print(f" город: {GOROD} или готовность к переезду") print(f" зарплата: не больше {POTOLOK}") print(f" опыт: место работы от {OPYT_MESYACEV} месяцев")
#include <chrono> #include <cstdlib> #include <iostream> #include <string> #include <vector> #include <bsoncxx/builder/basic/array.hpp> #include <bsoncxx/builder/basic/document.hpp> #include <bsoncxx/builder/basic/kvp.hpp> #include <bsoncxx/json.hpp> #include <bsoncxx/types.hpp> #include <mongocxx/client.hpp> #include <mongocxx/collection.hpp> #include <mongocxx/instance.hpp> #include <mongocxx/options/find.hpp> #include <mongocxx/uri.hpp> using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_array; using bsoncxx::builder::basic::make_document; int simvolov(const std::string& text) { int n = 0; for (unsigned char c : text) if ((c & 0xC0) != 0x80) ++n; return n; } std::string pad(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? text + std::string(add, ' ') : text; } std::string right(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? std::string(add, ' ') + text : text; } void title(const std::string& text) { std::cout << "\n" << text << "\n"; for (int i = 0; i < simvolov(text); ++i) std::cout << "─"; std::cout << std::endl; } // Строки массива через запятую — для навыков в таблице. std::string cherez_zapyatuyu(const bsoncxx::array::view& massiv) { std::string out; for (auto&& element : massiv) { if (!out.empty()) out += ", "; out += std::string(element.get_string().value); } return out; } int main() { mongocxx::instance instance{}; const char* env = std::getenv("MONGO_URI"); mongocxx::client client{mongocxx::uri{env ? env : "mongodb://localhost:27017"}}; auto hh = client["hh"]; auto resumes = hh["resumes"]; auto vacancies = hh["vacancies"]; auto interviews = hh["interviews"]; // Требования вакансии. const std::vector<std::string> navyki = {"Python", "SQL"}; const std::string gorod = "Ярославль"; const int potolok = 100000; const int opyt_mesyacev = 6; // ── 1. Требования ───────────────────────────────────────────────── title("1. Требования"); std::cout << " навыки: " << navyki[0] << " и " << navyki[1] << std::endl; std::cout << " город: " << gorod << " или готовность к переезду" << std::endl; std::cout << " зарплата: не больше " << potolok << std::endl; std::cout << " опыт: место работы от " << opyt_mesyacev << " месяцев" << std::endl; auto spisok_navykov = make_array(navyki[0], navyki[1]); auto gorod_ili_pereezd = make_array(make_document(kvp("city", gorod)), make_document(kvp("ready_to_move", true))); auto opyt = make_document(kvp("$elemMatch", make_document(kvp("months", make_document(kvp("$gte", opyt_mesyacev))))));
package main import ( "context" "fmt" "log" "os" "strings" "time" "unicode/utf8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) // Resume — поля резюме, которые читает отбор. type Resume struct { Fio string `bson:"fio"` Position string `bson:"position"` City string `bson:"city"` Salary int `bson:"salary"` ReadyToMove bool `bson:"ready_to_move"` Skills []string `bson:"skills"` Experience []struct { Company string `bson:"company"` Role string `bson:"role"` Months int `bson:"months"` } `bson:"experience"` } // Vacancy — вакансия с вилкой зарплаты во вложенном документе. type Vacancy struct { Title string `bson:"title"` Company string `bson:"company"` City string `bson:"city"` Salary struct { From int `bson:"from"` To int `bson:"to"` } `bson:"salary"` } func pad(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return text + strings.Repeat(" ", width-n) } return text } func right(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return strings.Repeat(" ", width-n) + text } return text } func title(text string) { fmt.Println("\n" + text) fmt.Println(strings.Repeat("─", utf8.RuneCountInString(text))) } // Требования вакансии. var ( navyki = []string{"Python", "SQL"} gorod = "Ярославль" potolok = 100000 opytMesyacev = 6 ) func main() { uri := os.Getenv("MONGO_URI") if uri == "" { uri = "mongodb://localhost:27017" } client, err := mongo.Connect(options.Client().ApplyURI(uri)) if err != nil { log.Fatal(err) } ctx := context.Background() defer client.Disconnect(ctx) hh := client.Database("hh") resumes := hh.Collection("resumes") vacancies := hh.Collection("vacancies") interviews := hh.Collection("interviews") // ── 1. Требования ───────────────────────────────────────────────── title("1. Требования") fmt.Printf(" навыки: %s\n", strings.Join(navyki, " и ")) fmt.Printf(" город: %s или готовность к переезду\n", gorod) fmt.Printf(" зарплата: не больше %d\n", potolok) fmt.Printf(" опыт: место работы от %d месяцев\n", opytMesyacev) spisokNavykov := bson.A{} for _, n := range navyki { spisokNavykov = append(spisokNavykov, n) } gorodIliPereezd := bson.A{ bson.D{{Key: "city", Value: gorod}}, bson.D{{Key: "ready_to_move", Value: true}}, } opyt := bson.D{{Key: "$elemMatch", Value: bson.D{ {Key: "months", Value: bson.D{{Key: "$gte", Value: opytMesyacev}}}}}}
require "mongo" Mongo::Logger.logger.level = Logger::WARN client = Mongo::Client.new(ENV.fetch("MONGO_URI", "mongodb://localhost:27017/")) hh = client.use("hh").database resumes = hh[:resumes] vacancies = hh[:vacancies] interviews = hh[:interviews] def title(text) puts "\n" + text puts "─" * text.length end def pad(text, width) text.to_s.ljust(width) end # ── 1. Требования вакансии ──────────────────────────────────────────────── NAVYKI = ["Python", "SQL"].freeze GOROD = "Ярославль" POTOLOK = 100_000 OPYT_MESYACEV = 6 title("1. Требования") puts " навыки: #{NAVYKI.join(' и ')}" puts " город: #{GOROD} или готовность к переезду" puts " зарплата: не больше #{POTOLOK}" puts " опыт: место работы от #{OPYT_MESYACEV} месяцев"
Так пишут отбор в рабочем коде: условия приходят из формы или настроек, а фильтр собирается из них. В главе 2.2 §7 разобрано, почему значение, пришедшее извне, безопаснее подставлять через $eq, если оно может оказаться документом.
§3Каждое требование по отдельности
Прежде чем соединять условия, смотрят, сколько резюме проходит каждое из них. Тогда видно, какое требование отсекает больше всего кандидатов. Условия сложены в список парами «название — фильтр», и скрипт перебирает его одним циклом.
# ── 2. Каждое требование по отдельности ─────────────────────────────────── title("2. Сколько резюме проходит каждое требование") kriterii = [ ("навыки $all", {"skills": {"$all": NAVYKI}}), ("город или переезд $or", {"$or": [{"city": GOROD}, {"ready_to_move": True}]}), ("зарплата $lte", {"salary": {"$lte": POTOLOK}}), ("опыт $elemMatch", {"experience": {"$elemMatch": {"months": {"$gte": OPYT_MESYACEV}}}}), ("есть контакты $exists", {"contacts": {"$exists": True}}), ] for nazvanie, filtr in kriterii: print(f" {nazvanie:24} {resumes.count_documents(filtr):2} из {resumes.count_documents({})}")
// ── 2. Каждое требование по отдельности ─────────────────────────── title("2. Сколько резюме проходит каждое требование"); auto vsego = resumes.count_documents(make_document()); std::vector<std::pair<std::string, bsoncxx::document::value>> kriterii; kriterii.emplace_back("навыки $all", make_document(kvp("skills", make_document(kvp("$all", spisok_navykov))))); kriterii.emplace_back("город или переезд $or", make_document(kvp("$or", gorod_ili_pereezd))); kriterii.emplace_back("зарплата $lte", make_document(kvp("salary", make_document(kvp("$lte", potolok))))); kriterii.emplace_back("опыт $elemMatch", make_document(kvp("experience", opyt))); kriterii.emplace_back("есть контакты $exists", make_document(kvp("contacts", make_document(kvp("$exists", true))))); for (const auto& kriteriy : kriterii) { auto skolko = resumes.count_documents(kriteriy.second.view()); std::cout << " " << pad(kriteriy.first, 24) << " " << right(std::to_string(skolko), 2) << " из " << vsego << std::endl; }
// ── 2. Каждое требование по отдельности ─────────────────────────── title("2. Сколько резюме проходит каждое требование") vsego, _ := resumes.CountDocuments(ctx, bson.D{}) kriterii := []struct { nazvanie string filtr bson.D }{ {"навыки $all", bson.D{{Key: "skills", Value: bson.D{{Key: "$all", Value: spisokNavykov}}}}}, {"город или переезд $or", bson.D{{Key: "$or", Value: gorodIliPereezd}}}, {"зарплата $lte", bson.D{{Key: "salary", Value: bson.D{{Key: "$lte", Value: potolok}}}}}, {"опыт $elemMatch", bson.D{{Key: "experience", Value: opyt}}}, {"есть контакты $exists", bson.D{{Key: "contacts", Value: bson.D{{Key: "$exists", Value: true}}}}}, } for _, k := range kriterii { skolko, _ := resumes.CountDocuments(ctx, k.filtr) fmt.Printf(" %s %s из %d\n", pad(k.nazvanie, 24), right(fmt.Sprint(skolko), 2), vsego) }
# ── 2. Каждое требование по отдельности ─────────────────────────────────── title("2. Сколько резюме проходит каждое требование") kriterii = [ ["навыки $all", { "skills" => { "$all" => NAVYKI } }], ["город или переезд $or", { "$or" => [{ "city" => GOROD }, { "ready_to_move" => true }] }], ["зарплата $lte", { "salary" => { "$lte" => POTOLOK } }], ["опыт $elemMatch", { "experience" => { "$elemMatch" => { "months" => { "$gte" => OPYT_MESYACEV } } } }], ["есть контакты $exists", { "contacts" => { "$exists" => true } }], ] vsego = resumes.count_documents({}) kriterii.each do |nazvanie, filtr| puts " #{pad(nazvanie, 24)} #{resumes.count_documents(filtr).to_s.rjust(2)} из #{vsego}" end
Пять фильтров подряд — пять операторов из разных глав: $all требует оба навыка, $or допускает город или переезд, $lte ограничивает зарплату, $elemMatch ищет место работы нужной длины, $exists проверяет, что контакты заполнены. Каждый вызов count_documents возвращает число, и отчёт печатает его рядом с общим количеством резюме.
§4Все требования сразу
Теперь те же условия соединяются в один фильтр. Пары верхнего уровня работают как «и», а $or стоит среди них обычной парой и даёт выбор только между своими условиями (глава 2.4 §3).
# ── 3. Все требования сразу ─────────────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr = { "skills": {"$all": NAVYKI}, "salary": {"$lte": POTOLOK}, "experience": {"$elemMatch": {"months": {"$gte": OPYT_MESYACEV}}}, "$or": [{"city": GOROD}, {"ready_to_move": True}], } print(f" {'ФИО':18} {'город':16} {'зарплата':>9} навыки") for doc in resumes.find(filtr).sort("salary", 1): print(f" {doc['fio']:18} {doc['city']:16} {doc['salary']:>9} {', '.join(doc['skills'])}") print(f" подошло: {resumes.count_documents(filtr)}")
// ── 3. Все требования сразу ─────────────────────────────────────── title("3. Кандидаты, проходящие все требования"); auto filtr = make_document( kvp("skills", make_document(kvp("$all", spisok_navykov))), kvp("salary", make_document(kvp("$lte", potolok))), kvp("experience", opyt), kvp("$or", gorod_ili_pereezd)); std::cout << " " << pad("ФИО", 18) << " " << pad("город", 16) << " " << right("зарплата", 9) << " навыки" << std::endl; mongocxx::options::find options; options.sort(make_document(kvp("salary", 1))); for (auto&& doc : resumes.find(filtr.view(), options)) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " " << pad(std::string(doc["city"].get_string().value), 16) << " " << right(std::to_string(doc["salary"].get_int32().value), 9) << " " << cherez_zapyatuyu(doc["skills"].get_array().value) << std::endl; } std::cout << " подошло: " << resumes.count_documents(filtr.view()) << std::endl;
// ── 3. Все требования сразу ─────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr := bson.D{ {Key: "skills", Value: bson.D{{Key: "$all", Value: spisokNavykov}}}, {Key: "salary", Value: bson.D{{Key: "$lte", Value: potolok}}}, {Key: "experience", Value: opyt}, {Key: "$or", Value: gorodIliPereezd}, } fmt.Printf(" %s %s %s навыки\n", pad("ФИО", 18), pad("город", 16), right("зарплата", 9)) cursor, _ := resumes.Find(ctx, filtr, options.Find().SetSort(bson.D{{Key: "salary", Value: 1}})) var podoshli []Resume cursor.All(ctx, &podoshli) for _, r := range podoshli { fmt.Printf(" %s %s %s %s\n", pad(r.Fio, 18), pad(r.City, 16), right(fmt.Sprint(r.Salary), 9), strings.Join(r.Skills, ", ")) } fmt.Println(" подошло:", len(podoshli))
# ── 3. Все требования сразу ─────────────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr = { "skills" => { "$all" => NAVYKI }, "salary" => { "$lte" => POTOLOK }, "experience" => { "$elemMatch" => { "months" => { "$gte" => OPYT_MESYACEV } } }, "$or" => [{ "city" => GOROD }, { "ready_to_move" => true }], } puts " #{pad('ФИО', 18)} #{pad('город', 16)} #{'зарплата'.rjust(9)} навыки" resumes.find(filtr).sort({ "salary" => 1 }).each do |doc| puts " #{pad(doc['fio'], 18)} #{pad(doc['city'], 16)} #{doc['salary'].to_s.rjust(9)} #{doc['skills'].join(', ')}" end puts " подошло: #{resumes.count_documents(filtr)}"
Результат выводится таблицей: ФИО, город, зарплата и навыки в одну строку. Сортировка по зарплате ставит наверх тех, кто просит меньше. Под таблицей — число подошедших, его берёт тот же фильтр через count_documents.
§5Текст и подбор вакансий
Четвёртый раздел ищет по части строки: шаблон ^junior с параметром "i" находит должности, которые начинаются со слова «Junior» в любом регистре. Пятый подбирает вакансии под ожидание кандидата: нижняя граница вилки не выше ожидания, верхняя — не ниже.
# ── 4. Поиск по части строки ────────────────────────────────────────────── title("4. Должности, начинающиеся с Junior") for doc in resumes.find({"position": {"$regex": "^junior", "$options": "i"}}, {"_id": 0, "fio": 1, "position": 1}): print(f" {doc['fio']:18} {doc['position']}") # ── 5. Подходящие вакансии ──────────────────────────────────────────────── title("5. Вакансии под ожидание 90 000") OZHIDANIE = 90000 vilka = {"salary.from": {"$lte": OZHIDANIE}, "salary.to": {"$gte": OZHIDANIE}} for doc in vacancies.find(vilka, {"_id": 0, "title": 1, "company": 1, "city": 1, "salary": 1}): zarplata = doc["salary"] print(f" {doc['title']:26} {doc['company']:16} {doc['city']:16} " f"{zarplata['from']}–{zarplata['to']}") print(f" подходит вакансий: {vacancies.count_documents(vilka)}")
// ── 4. Поиск по части строки ────────────────────────────────────── title("4. Должности, начинающиеся с Junior"); auto shablon = make_document(kvp("position", make_document(kvp("$regex", "^junior"), kvp("$options", "i")))); for (auto&& doc : resumes.find(shablon.view())) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " " << std::string(doc["position"].get_string().value) << std::endl; } // ── 5. Подходящие вакансии ──────────────────────────────────────── title("5. Вакансии под ожидание 90 000"); const int ozhidanie = 90000; auto vilka = make_document( kvp("salary.from", make_document(kvp("$lte", ozhidanie))), kvp("salary.to", make_document(kvp("$gte", ozhidanie)))); for (auto&& doc : vacancies.find(vilka.view())) { auto zarplata = doc["salary"].get_document().value; std::cout << " " << pad(std::string(doc["title"].get_string().value), 26) << " " << pad(std::string(doc["company"].get_string().value), 16) << " " << pad(std::string(doc["city"].get_string().value), 16) << " " << zarplata["from"].get_int32().value << "–" << zarplata["to"].get_int32().value << std::endl; } std::cout << " подходит вакансий: " << vacancies.count_documents(vilka.view()) << std::endl;
// ── 4. Поиск по части строки ────────────────────────────────────── title("4. Должности, начинающиеся с Junior") cursor, _ = resumes.Find(ctx, bson.D{{Key: "position", Value: bson.D{ {Key: "$regex", Value: "^junior"}, {Key: "$options", Value: "i"}}}}) var junior []Resume cursor.All(ctx, &junior) for _, r := range junior { fmt.Printf(" %s %s\n", pad(r.Fio, 18), r.Position) } // ── 5. Подходящие вакансии ──────────────────────────────────────── title("5. Вакансии под ожидание 90 000") ozhidanie := 90000 vilka := bson.D{ {Key: "salary.from", Value: bson.D{{Key: "$lte", Value: ozhidanie}}}, {Key: "salary.to", Value: bson.D{{Key: "$gte", Value: ozhidanie}}}, } cursor, _ = vacancies.Find(ctx, vilka) var podhodyat []Vacancy cursor.All(ctx, &podhodyat) for _, v := range podhodyat { fmt.Printf(" %s %s %s %d–%d\n", pad(v.Title, 26), pad(v.Company, 16), pad(v.City, 16), v.Salary.From, v.Salary.To) } fmt.Println(" подходит вакансий:", len(podhodyat))
# ── 4. Поиск по части строки ────────────────────────────────────────────── title("4. Должности, начинающиеся с Junior") resumes.find({ "position" => { "$regex" => "^junior", "$options" => "i" } }, projection: { "_id" => 0, "fio" => 1, "position" => 1 }) .each { |doc| puts " #{pad(doc['fio'], 18)} #{doc['position']}" } # ── 5. Подходящие вакансии ──────────────────────────────────────────────── title("5. Вакансии под ожидание 90 000") OZHIDANIE = 90_000 vilka = { "salary.from" => { "$lte" => OZHIDANIE }, "salary.to" => { "$gte" => OZHIDANIE } } vacancies.find(vilka, projection: { "_id" => 0, "title" => 1, "company" => 1, "city" => 1, "salary" => 1 }) .each do |doc| zarplata = doc["salary"] puts " #{pad(doc['title'], 26)} #{pad(doc['company'], 16)} #{pad(doc['city'], 16)} " \ "#{zarplata['from']}–#{zarplata['to']}" end puts " подходит вакансий: #{vacancies.count_documents(vilka)}"
Вилка лежит во вложенном документе, поэтому условия стоят на именах с точкой — salary.from и salary.to. В выводе обе границы печатаются через тире: значения берутся из того же вложенного документа.
§6Собеседования за неделю
Шестой раздел считает собеседования за неделю с 21 по 27 сентября. Период задан полуинтервалом: от начала 21-го числа до начала 28-го, иначе события последнего дня потерялись бы (глава 2.2 §4).
# ── 6. Собеседования за неделю ──────────────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") nedelya = {"when": {"$gte": datetime(2026, 9, 21), "$lt": datetime(2026, 9, 28)}} print(f" всего за неделю: {interviews.count_documents(nedelya)}") for etap in ("скрининг", "техническое", "финальное"): print(f" {etap:12} {interviews.count_documents({**nedelya, 'stage': etap}):2}") print(f" очно: {interviews.count_documents({**nedelya, 'format': 'очно'})}, " f"видеозвонком: {interviews.count_documents({**nedelya, 'format': 'видеозвонок'})}")
// ── 6. Собеседования за неделю ──────────────────────────────────── title("6. Собеседования с 21 по 27 сентября"); auto sep21 = bsoncxx::types::b_date{std::chrono::milliseconds{1789948800000}}; // 21.09.2026 00:00 UTC auto sep28 = bsoncxx::types::b_date{std::chrono::milliseconds{1790553600000}}; // 28.09.2026 00:00 UTC auto nedelya = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28)))); std::cout << " всего за неделю: " << interviews.count_documents(nedelya.view()) << std::endl; for (const std::string& etap : {"скрининг", "техническое", "финальное"}) { auto filtr_etapa = make_document( kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("stage", etap)); std::cout << " " << pad(etap, 12) << " " << right(std::to_string(interviews.count_documents(filtr_etapa.view())), 2) << std::endl; } auto ochno = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("format", "очно")); auto video = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("format", "видеозвонок")); std::cout << " очно: " << interviews.count_documents(ochno.view()) << ", видеозвонком: " << interviews.count_documents(video.view()) << std::endl;
// ── 6. Собеседования за неделю ──────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") sep21 := time.Date(2026, 9, 21, 0, 0, 0, 0, time.UTC) sep28 := time.Date(2026, 9, 28, 0, 0, 0, 0, time.UTC) nedelya := bson.D{{Key: "when", Value: bson.D{{Key: "$gte", Value: sep21}, {Key: "$lt", Value: sep28}}}} zaNedelyu, _ := interviews.CountDocuments(ctx, nedelya) fmt.Println(" всего за неделю:", zaNedelyu) // К общему фильтру добавляем условие копией: append по общему срезу // может перезаписать данные соседних вызовов. sUsloviem := func(key, value string) bson.D { return append(append(bson.D{}, nedelya...), bson.E{Key: key, Value: value}) } for _, etap := range []string{"скрининг", "техническое", "финальное"} { skolko, _ := interviews.CountDocuments(ctx, sUsloviem("stage", etap)) fmt.Printf(" %s %s\n", pad(etap, 12), right(fmt.Sprint(skolko), 2)) } ochno, _ := interviews.CountDocuments(ctx, sUsloviem("format", "очно")) video, _ := interviews.CountDocuments(ctx, sUsloviem("format", "видеозвонок")) fmt.Printf(" очно: %d, видеозвонком: %d\n", ochno, video)
# ── 6. Собеседования за неделю ──────────────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") nedelya = { "when" => { "$gte" => Time.utc(2026, 9, 21), "$lt" => Time.utc(2026, 9, 28) } } puts " всего за неделю: #{interviews.count_documents(nedelya)}" ["скрининг", "техническое", "финальное"].each do |etap| puts " #{pad(etap, 12)} #{interviews.count_documents(nedelya.merge('stage' => etap)).to_s.rjust(2)}" end puts " очно: #{interviews.count_documents(nedelya.merge('format' => 'очно'))}, " \ "видеозвонком: #{interviews.count_documents(nedelya.merge('format' => 'видеозвонок'))}"
К периоду добавляются условия по этапу и формату — те же пары фильтра, соединённые по «и». Общее число и суммы по этапам считаются отдельными запросами, поэтому их можно сверить: скрининг, техническое и финальное вместе дают столько же, сколько строка «всего за неделю».
§7Проверка данных
Седьмой раздел ищет дефекты в самих данных: пропущенные поля, значения не того типа, пустые массивы и несогласованные границы вилки. Такие проверки запускают по расписанию или перед выгрузкой, чтобы ошибка не дошла до отчёта.
# ── 7. Проверка данных ──────────────────────────────────────────────────── title("7. Проверка данных") proverki = [ ("резюме без даты обновления", resumes, {"updated": {"$exists": False}}), ("зарплата записана не числом", resumes, {"salary": {"$not": {"$type": "number"}}}), ("пустой список мест работы", resumes, {"experience": {"$size": 0}}), ("нет ни одного навыка", resumes, {"skills.0": {"$exists": False}}), ("перевёрнутая вилка", vacancies, {"$expr": {"$gt": ["$salary.from", "$salary.to"]}}), ("вакансия закрыта", vacancies, {"is_open": {"$ne": True}}), ] for nazvanie, col, filtr in proverki: skolko = col.count_documents(filtr) print(f" {nazvanie:30} {skolko:2}" + (" ← проверить" if skolko else ""))
// ── 7. Проверка данных ──────────────────────────────────────────── title("7. Проверка данных"); std::vector<std::tuple<std::string, mongocxx::collection, bsoncxx::document::value>> proverki; proverki.emplace_back("резюме без даты обновления", resumes, make_document(kvp("updated", make_document(kvp("$exists", false))))); proverki.emplace_back("зарплата записана не числом", resumes, make_document(kvp("salary", make_document(kvp("$not", make_document(kvp("$type", "number"))))))); proverki.emplace_back("пустой список мест работы", resumes, make_document(kvp("experience", make_document(kvp("$size", 0))))); proverki.emplace_back("нет ни одного навыка", resumes, make_document(kvp("skills.0", make_document(kvp("$exists", false))))); proverki.emplace_back("перевёрнутая вилка", vacancies, make_document(kvp("$expr", make_document(kvp("$gt", make_array("$salary.from", "$salary.to")))))); proverki.emplace_back("вакансия закрыта", vacancies, make_document(kvp("is_open", make_document(kvp("$ne", true))))); for (auto& proverka : proverki) { auto skolko = std::get<1>(proverka).count_documents(std::get<2>(proverka).view()); std::cout << " " << pad(std::get<0>(proverka), 30) << " " << right(std::to_string(skolko), 2) << (skolko ? " ← проверить" : "") << std::endl; }
// ── 7. Проверка данных ──────────────────────────────────────────── title("7. Проверка данных") proverki := []struct { nazvanie string col *mongo.Collection filtr bson.D }{ {"резюме без даты обновления", resumes, bson.D{{Key: "updated", Value: bson.D{{Key: "$exists", Value: false}}}}}, {"зарплата записана не числом", resumes, bson.D{{Key: "salary", Value: bson.D{{Key: "$not", Value: bson.D{{Key: "$type", Value: "number"}}}}}}}, {"пустой список мест работы", resumes, bson.D{{Key: "experience", Value: bson.D{{Key: "$size", Value: 0}}}}}, {"нет ни одного навыка", resumes, bson.D{{Key: "skills.0", Value: bson.D{{Key: "$exists", Value: false}}}}}, {"перевёрнутая вилка", vacancies, bson.D{{Key: "$expr", Value: bson.D{{Key: "$gt", Value: bson.A{"$salary.from", "$salary.to"}}}}}}, {"вакансия закрыта", vacancies, bson.D{{Key: "is_open", Value: bson.D{{Key: "$ne", Value: true}}}}}, } for _, p := range proverki { skolko, _ := p.col.CountDocuments(ctx, p.filtr) pometka := "" if skolko > 0 { pometka = " ← проверить" } fmt.Printf(" %s %s%s\n", pad(p.nazvanie, 30), right(fmt.Sprint(skolko), 2), pometka) }
# ── 7. Проверка данных ──────────────────────────────────────────────────── title("7. Проверка данных") proverki = [ ["резюме без даты обновления", resumes, { "updated" => { "$exists" => false } }], ["зарплата записана не числом", resumes, { "salary" => { "$not" => { "$type" => "number" } } }], ["пустой список мест работы", resumes, { "experience" => { "$size" => 0 } }], ["нет ни одного навыка", resumes, { "skills.0" => { "$exists" => false } }], ["перевёрнутая вилка", vacancies, { "$expr" => { "$gt" => ["$salary.from", "$salary.to"] } }], ["вакансия закрыта", vacancies, { "is_open" => { "$ne" => true } }], ] proverki.each do |nazvanie, col, filtr_proverki| skolko = col.count_documents(filtr_proverki) puts " #{pad(nazvanie, 30)} #{skolko.to_s.rjust(2)}#{skolko.zero? ? '' : ' ← проверить'}" end
Каждая строка — отдельный оператор из модуля 2: $exists находит резюме без даты обновления, $not с $type — зарплату, записанную не числом, $size — пустой список мест работы, "skills.0" с $exists — резюме без единого навыка, $expr — перевёрнутую вилку. Ноль в строке означает, что такого дефекта в базе нет; ненулевое число помечается стрелкой.
§8Что считает программа
Фильтр отвечает на вопрос «какие документы подходят», но не объясняет, почему документ не подошёл. Последний раздел отчёта перебирает все резюме в программе и для каждого собирает список причин отказа.
# ── 8. Кто не подошёл и почему ──────────────────────────────────────────── title("8. Кто не прошёл отбор") for doc in resumes.find({}, {"_id": 0, "fio": 1, "skills": 1, "salary": 1, "city": 1, "ready_to_move": 1, "experience": 1}): prichiny = [] if not set(NAVYKI) <= set(doc.get("skills", [])): prichiny.append("нет навыков") if doc["salary"] > POTOLOK: prichiny.append("зарплата выше потолка") if doc["city"] != GOROD and not doc.get("ready_to_move"): prichiny.append("другой город без переезда") if not any(m.get("months", 0) >= OPYT_MESYACEV for m in doc.get("experience", [])): prichiny.append("мало опыта") if prichiny: print(f" {doc['fio']:18} {'; '.join(prichiny)}") client.close()
// ── 8. Кто не подошёл и почему ──────────────────────────────────── title("8. Кто не прошёл отбор"); for (auto&& doc : resumes.find(make_document())) { std::vector<std::string> prichiny; for (const auto& nuzhen : navyki) { bool est = false; for (auto&& navyk : doc["skills"].get_array().value) if (std::string(navyk.get_string().value) == nuzhen) est = true; if (!est) { prichiny.push_back("нет навыков"); break; } } if (doc["salary"].get_int32().value > potolok) prichiny.push_back("зарплата выше потолка"); bool gotov = doc["ready_to_move"] && doc["ready_to_move"].get_bool().value; if (std::string(doc["city"].get_string().value) != gorod && !gotov) prichiny.push_back("другой город без переезда"); bool hvatit_opyta = false; if (doc["experience"]) for (auto&& mesto : doc["experience"].get_array().value) if (mesto.get_document().value["months"].get_int32().value >= opyt_mesyacev) hvatit_opyta = true; if (!hvatit_opyta) prichiny.push_back("мало опыта"); if (!prichiny.empty()) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " "; for (size_t i = 0; i < prichiny.size(); ++i) std::cout << (i ? "; " : "") << prichiny[i]; std::cout << std::endl; } } }
// ── 8. Кто не подошёл и почему ──────────────────────────────────── title("8. Кто не прошёл отбор") cursor, _ = resumes.Find(ctx, bson.D{}) var vse []Resume cursor.All(ctx, &vse) for _, r := range vse { var prichiny []string for _, nuzhen := range navyki { est := false for _, navyk := range r.Skills { if navyk == nuzhen { est = true } } if !est { prichiny = append(prichiny, "нет навыков") break } } if r.Salary > potolok { prichiny = append(prichiny, "зарплата выше потолка") } if r.City != gorod && !r.ReadyToMove { prichiny = append(prichiny, "другой город без переезда") } hvatitOpyta := false for _, mesto := range r.Experience { if mesto.Months >= opytMesyacev { hvatitOpyta = true } } if !hvatitOpyta { prichiny = append(prichiny, "мало опыта") } if len(prichiny) > 0 { fmt.Printf(" %s %s\n", pad(r.Fio, 18), strings.Join(prichiny, "; ")) } } }
# ── 8. Кто не подошёл и почему ──────────────────────────────────────────── title("8. Кто не прошёл отбор") resumes.find({}, projection: { "_id" => 0, "fio" => 1, "skills" => 1, "salary" => 1, "city" => 1, "ready_to_move" => 1, "experience" => 1 }).each do |doc| prichiny = [] prichiny << "нет навыков" unless (NAVYKI - (doc["skills"] || [])).empty? prichiny << "зарплата выше потолка" if doc["salary"] > POTOLOK prichiny << "другой город без переезда" if doc["city"] != GOROD && !doc["ready_to_move"] prichiny << "мало опыта" unless (doc["experience"] || []).any? { |m| (m["months"] || 0) >= OPYT_MESYACEV } puts " #{pad(doc['fio'], 18)} #{prichiny.join('; ')}" unless prichiny.empty? end client.close
Здесь нет запросов с условиями: скрипт читает все девять резюме и сравнивает поля средствами языка. Так делают, когда к результату нужно объяснение — например, чтобы показать рекрутеру, какое требование смягчить. На больших коллекциях так не поступают: туда приходит слишком много документов, и отбор оставляют серверу.
§9Скрипт целиком
Ниже — тот же файл без пропусков, а под ним вывод его запуска на стенде курса.
"""Подбор кандидатов: что умеет язык фильтров из модуля 2. Скрипт решает прикладную задачу на базе hh: подобрать кандидатов под требования вакансии, посмотреть, сколько резюме проходит каждое требование по отдельности, найти подходящие вакансии и проверить данные на пропуски, неверные типы и несогласованные поля. cd scripts python3 hh_search.py docker compose run --rm python scripts/hh_search.py # в контейнере """ import os from datetime import datetime from pymongo import MongoClient client = MongoClient(os.environ.get("MONGO_URI", "mongodb://localhost:27017/")) hh = client["hh"] resumes = hh["resumes"] vacancies = hh["vacancies"] interviews = hh["interviews"] def title(text): print("\n" + text) print("─" * len(text)) # ── 1. Требования вакансии ──────────────────────────────────────────────── NAVYKI = ["Python", "SQL"] GOROD = "Ярославль" POTOLOK = 100000 OPYT_MESYACEV = 6 title("1. Требования") print(f" навыки: {' и '.join(NAVYKI)}") print(f" город: {GOROD} или готовность к переезду") print(f" зарплата: не больше {POTOLOK}") print(f" опыт: место работы от {OPYT_MESYACEV} месяцев") # ── 2. Каждое требование по отдельности ─────────────────────────────────── title("2. Сколько резюме проходит каждое требование") kriterii = [ ("навыки $all", {"skills": {"$all": NAVYKI}}), ("город или переезд $or", {"$or": [{"city": GOROD}, {"ready_to_move": True}]}), ("зарплата $lte", {"salary": {"$lte": POTOLOK}}), ("опыт $elemMatch", {"experience": {"$elemMatch": {"months": {"$gte": OPYT_MESYACEV}}}}), ("есть контакты $exists", {"contacts": {"$exists": True}}), ] for nazvanie, filtr in kriterii: print(f" {nazvanie:24} {resumes.count_documents(filtr):2} из {resumes.count_documents({})}") # ── 3. Все требования сразу ─────────────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr = { "skills": {"$all": NAVYKI}, "salary": {"$lte": POTOLOK}, "experience": {"$elemMatch": {"months": {"$gte": OPYT_MESYACEV}}}, "$or": [{"city": GOROD}, {"ready_to_move": True}], } print(f" {'ФИО':18} {'город':16} {'зарплата':>9} навыки") for doc in resumes.find(filtr).sort("salary", 1): print(f" {doc['fio']:18} {doc['city']:16} {doc['salary']:>9} {', '.join(doc['skills'])}") print(f" подошло: {resumes.count_documents(filtr)}") # ── 4. Поиск по части строки ────────────────────────────────────────────── title("4. Должности, начинающиеся с Junior") for doc in resumes.find({"position": {"$regex": "^junior", "$options": "i"}}, {"_id": 0, "fio": 1, "position": 1}): print(f" {doc['fio']:18} {doc['position']}") # ── 5. Подходящие вакансии ──────────────────────────────────────────────── title("5. Вакансии под ожидание 90 000") OZHIDANIE = 90000 vilka = {"salary.from": {"$lte": OZHIDANIE}, "salary.to": {"$gte": OZHIDANIE}} for doc in vacancies.find(vilka, {"_id": 0, "title": 1, "company": 1, "city": 1, "salary": 1}): zarplata = doc["salary"] print(f" {doc['title']:26} {doc['company']:16} {doc['city']:16} " f"{zarplata['from']}–{zarplata['to']}") print(f" подходит вакансий: {vacancies.count_documents(vilka)}") # ── 6. Собеседования за неделю ──────────────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") nedelya = {"when": {"$gte": datetime(2026, 9, 21), "$lt": datetime(2026, 9, 28)}} print(f" всего за неделю: {interviews.count_documents(nedelya)}") for etap in ("скрининг", "техническое", "финальное"): print(f" {etap:12} {interviews.count_documents({**nedelya, 'stage': etap}):2}") print(f" очно: {interviews.count_documents({**nedelya, 'format': 'очно'})}, " f"видеозвонком: {interviews.count_documents({**nedelya, 'format': 'видеозвонок'})}") # ── 7. Проверка данных ──────────────────────────────────────────────────── title("7. Проверка данных") proverki = [ ("резюме без даты обновления", resumes, {"updated": {"$exists": False}}), ("зарплата записана не числом", resumes, {"salary": {"$not": {"$type": "number"}}}), ("пустой список мест работы", resumes, {"experience": {"$size": 0}}), ("нет ни одного навыка", resumes, {"skills.0": {"$exists": False}}), ("перевёрнутая вилка", vacancies, {"$expr": {"$gt": ["$salary.from", "$salary.to"]}}), ("вакансия закрыта", vacancies, {"is_open": {"$ne": True}}), ] for nazvanie, col, filtr in proverki: skolko = col.count_documents(filtr) print(f" {nazvanie:30} {skolko:2}" + (" ← проверить" if skolko else "")) # ── 8. Кто не подошёл и почему ──────────────────────────────────────────── title("8. Кто не прошёл отбор") for doc in resumes.find({}, {"_id": 0, "fio": 1, "skills": 1, "salary": 1, "city": 1, "ready_to_move": 1, "experience": 1}): prichiny = [] if not set(NAVYKI) <= set(doc.get("skills", [])): prichiny.append("нет навыков") if doc["salary"] > POTOLOK: prichiny.append("зарплата выше потолка") if doc["city"] != GOROD and not doc.get("ready_to_move"): prichiny.append("другой город без переезда") if not any(m.get("months", 0) >= OPYT_MESYACEV for m in doc.get("experience", [])): prichiny.append("мало опыта") if prichiny: print(f" {doc['fio']:18} {'; '.join(prichiny)}") client.close()
// Подбор кандидатов: что умеет язык фильтров из модуля 2. // // Скрипт решает прикладную задачу на базе hh: подобрать кандидатов под // требования вакансии, посмотреть, сколько резюме проходит каждое требование // по отдельности, найти подходящие вакансии и проверить данные на пропуски, // неверные типы и несогласованные поля. // // cd scripts // g++ -std=c++17 hh_search.cpp -o hh_search $(pkg-config --cflags --libs libmongocxx1) && ./hh_search // // docker compose run --rm cpp scripts/hh_search.cpp # в контейнере #include <chrono> #include <cstdlib> #include <iostream> #include <string> #include <vector> #include <bsoncxx/builder/basic/array.hpp> #include <bsoncxx/builder/basic/document.hpp> #include <bsoncxx/builder/basic/kvp.hpp> #include <bsoncxx/json.hpp> #include <bsoncxx/types.hpp> #include <mongocxx/client.hpp> #include <mongocxx/collection.hpp> #include <mongocxx/instance.hpp> #include <mongocxx/options/find.hpp> #include <mongocxx/uri.hpp> using bsoncxx::builder::basic::kvp; using bsoncxx::builder::basic::make_array; using bsoncxx::builder::basic::make_document; int simvolov(const std::string& text) { int n = 0; for (unsigned char c : text) if ((c & 0xC0) != 0x80) ++n; return n; } std::string pad(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? text + std::string(add, ' ') : text; } std::string right(const std::string& text, int width) { int add = width - simvolov(text); return add > 0 ? std::string(add, ' ') + text : text; } void title(const std::string& text) { std::cout << "\n" << text << "\n"; for (int i = 0; i < simvolov(text); ++i) std::cout << "─"; std::cout << std::endl; } // Строки массива через запятую — для навыков в таблице. std::string cherez_zapyatuyu(const bsoncxx::array::view& massiv) { std::string out; for (auto&& element : massiv) { if (!out.empty()) out += ", "; out += std::string(element.get_string().value); } return out; } int main() { mongocxx::instance instance{}; const char* env = std::getenv("MONGO_URI"); mongocxx::client client{mongocxx::uri{env ? env : "mongodb://localhost:27017"}}; auto hh = client["hh"]; auto resumes = hh["resumes"]; auto vacancies = hh["vacancies"]; auto interviews = hh["interviews"]; // Требования вакансии. const std::vector<std::string> navyki = {"Python", "SQL"}; const std::string gorod = "Ярославль"; const int potolok = 100000; const int opyt_mesyacev = 6; // ── 1. Требования ───────────────────────────────────────────────── title("1. Требования"); std::cout << " навыки: " << navyki[0] << " и " << navyki[1] << std::endl; std::cout << " город: " << gorod << " или готовность к переезду" << std::endl; std::cout << " зарплата: не больше " << potolok << std::endl; std::cout << " опыт: место работы от " << opyt_mesyacev << " месяцев" << std::endl; auto spisok_navykov = make_array(navyki[0], navyki[1]); auto gorod_ili_pereezd = make_array(make_document(kvp("city", gorod)), make_document(kvp("ready_to_move", true))); auto opyt = make_document(kvp("$elemMatch", make_document(kvp("months", make_document(kvp("$gte", opyt_mesyacev)))))); // ── 2. Каждое требование по отдельности ─────────────────────────── title("2. Сколько резюме проходит каждое требование"); auto vsego = resumes.count_documents(make_document()); std::vector<std::pair<std::string, bsoncxx::document::value>> kriterii; kriterii.emplace_back("навыки $all", make_document(kvp("skills", make_document(kvp("$all", spisok_navykov))))); kriterii.emplace_back("город или переезд $or", make_document(kvp("$or", gorod_ili_pereezd))); kriterii.emplace_back("зарплата $lte", make_document(kvp("salary", make_document(kvp("$lte", potolok))))); kriterii.emplace_back("опыт $elemMatch", make_document(kvp("experience", opyt))); kriterii.emplace_back("есть контакты $exists", make_document(kvp("contacts", make_document(kvp("$exists", true))))); for (const auto& kriteriy : kriterii) { auto skolko = resumes.count_documents(kriteriy.second.view()); std::cout << " " << pad(kriteriy.first, 24) << " " << right(std::to_string(skolko), 2) << " из " << vsego << std::endl; } // ── 3. Все требования сразу ─────────────────────────────────────── title("3. Кандидаты, проходящие все требования"); auto filtr = make_document( kvp("skills", make_document(kvp("$all", spisok_navykov))), kvp("salary", make_document(kvp("$lte", potolok))), kvp("experience", opyt), kvp("$or", gorod_ili_pereezd)); std::cout << " " << pad("ФИО", 18) << " " << pad("город", 16) << " " << right("зарплата", 9) << " навыки" << std::endl; mongocxx::options::find options; options.sort(make_document(kvp("salary", 1))); for (auto&& doc : resumes.find(filtr.view(), options)) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " " << pad(std::string(doc["city"].get_string().value), 16) << " " << right(std::to_string(doc["salary"].get_int32().value), 9) << " " << cherez_zapyatuyu(doc["skills"].get_array().value) << std::endl; } std::cout << " подошло: " << resumes.count_documents(filtr.view()) << std::endl; // ── 4. Поиск по части строки ────────────────────────────────────── title("4. Должности, начинающиеся с Junior"); auto shablon = make_document(kvp("position", make_document(kvp("$regex", "^junior"), kvp("$options", "i")))); for (auto&& doc : resumes.find(shablon.view())) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " " << std::string(doc["position"].get_string().value) << std::endl; } // ── 5. Подходящие вакансии ──────────────────────────────────────── title("5. Вакансии под ожидание 90 000"); const int ozhidanie = 90000; auto vilka = make_document( kvp("salary.from", make_document(kvp("$lte", ozhidanie))), kvp("salary.to", make_document(kvp("$gte", ozhidanie)))); for (auto&& doc : vacancies.find(vilka.view())) { auto zarplata = doc["salary"].get_document().value; std::cout << " " << pad(std::string(doc["title"].get_string().value), 26) << " " << pad(std::string(doc["company"].get_string().value), 16) << " " << pad(std::string(doc["city"].get_string().value), 16) << " " << zarplata["from"].get_int32().value << "–" << zarplata["to"].get_int32().value << std::endl; } std::cout << " подходит вакансий: " << vacancies.count_documents(vilka.view()) << std::endl; // ── 6. Собеседования за неделю ──────────────────────────────────── title("6. Собеседования с 21 по 27 сентября"); auto sep21 = bsoncxx::types::b_date{std::chrono::milliseconds{1789948800000}}; // 21.09.2026 00:00 UTC auto sep28 = bsoncxx::types::b_date{std::chrono::milliseconds{1790553600000}}; // 28.09.2026 00:00 UTC auto nedelya = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28)))); std::cout << " всего за неделю: " << interviews.count_documents(nedelya.view()) << std::endl; for (const std::string& etap : {"скрининг", "техническое", "финальное"}) { auto filtr_etapa = make_document( kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("stage", etap)); std::cout << " " << pad(etap, 12) << " " << right(std::to_string(interviews.count_documents(filtr_etapa.view())), 2) << std::endl; } auto ochno = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("format", "очно")); auto video = make_document(kvp("when", make_document(kvp("$gte", sep21), kvp("$lt", sep28))), kvp("format", "видеозвонок")); std::cout << " очно: " << interviews.count_documents(ochno.view()) << ", видеозвонком: " << interviews.count_documents(video.view()) << std::endl; // ── 7. Проверка данных ──────────────────────────────────────────── title("7. Проверка данных"); std::vector<std::tuple<std::string, mongocxx::collection, bsoncxx::document::value>> proverki; proverki.emplace_back("резюме без даты обновления", resumes, make_document(kvp("updated", make_document(kvp("$exists", false))))); proverki.emplace_back("зарплата записана не числом", resumes, make_document(kvp("salary", make_document(kvp("$not", make_document(kvp("$type", "number"))))))); proverki.emplace_back("пустой список мест работы", resumes, make_document(kvp("experience", make_document(kvp("$size", 0))))); proverki.emplace_back("нет ни одного навыка", resumes, make_document(kvp("skills.0", make_document(kvp("$exists", false))))); proverki.emplace_back("перевёрнутая вилка", vacancies, make_document(kvp("$expr", make_document(kvp("$gt", make_array("$salary.from", "$salary.to")))))); proverki.emplace_back("вакансия закрыта", vacancies, make_document(kvp("is_open", make_document(kvp("$ne", true))))); for (auto& proverka : proverki) { auto skolko = std::get<1>(proverka).count_documents(std::get<2>(proverka).view()); std::cout << " " << pad(std::get<0>(proverka), 30) << " " << right(std::to_string(skolko), 2) << (skolko ? " ← проверить" : "") << std::endl; } // ── 8. Кто не подошёл и почему ──────────────────────────────────── title("8. Кто не прошёл отбор"); for (auto&& doc : resumes.find(make_document())) { std::vector<std::string> prichiny; for (const auto& nuzhen : navyki) { bool est = false; for (auto&& navyk : doc["skills"].get_array().value) if (std::string(navyk.get_string().value) == nuzhen) est = true; if (!est) { prichiny.push_back("нет навыков"); break; } } if (doc["salary"].get_int32().value > potolok) prichiny.push_back("зарплата выше потолка"); bool gotov = doc["ready_to_move"] && doc["ready_to_move"].get_bool().value; if (std::string(doc["city"].get_string().value) != gorod && !gotov) prichiny.push_back("другой город без переезда"); bool hvatit_opyta = false; if (doc["experience"]) for (auto&& mesto : doc["experience"].get_array().value) if (mesto.get_document().value["months"].get_int32().value >= opyt_mesyacev) hvatit_opyta = true; if (!hvatit_opyta) prichiny.push_back("мало опыта"); if (!prichiny.empty()) { std::cout << " " << pad(std::string(doc["fio"].get_string().value), 18) << " "; for (size_t i = 0; i < prichiny.size(); ++i) std::cout << (i ? "; " : "") << prichiny[i]; std::cout << std::endl; } } }
// Подбор кандидатов: что умеет язык фильтров из модуля 2. // // Скрипт решает прикладную задачу на базе hh: подобрать кандидатов под // требования вакансии, посмотреть, сколько резюме проходит каждое требование // по отдельности, найти подходящие вакансии и проверить данные на пропуски, // неверные типы и несогласованные поля. // // cd scripts // go run hh_search.go // // docker compose run --rm go scripts/hh_search.go # в контейнере package main import ( "context" "fmt" "log" "os" "strings" "time" "unicode/utf8" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) // Resume — поля резюме, которые читает отбор. type Resume struct { Fio string `bson:"fio"` Position string `bson:"position"` City string `bson:"city"` Salary int `bson:"salary"` ReadyToMove bool `bson:"ready_to_move"` Skills []string `bson:"skills"` Experience []struct { Company string `bson:"company"` Role string `bson:"role"` Months int `bson:"months"` } `bson:"experience"` } // Vacancy — вакансия с вилкой зарплаты во вложенном документе. type Vacancy struct { Title string `bson:"title"` Company string `bson:"company"` City string `bson:"city"` Salary struct { From int `bson:"from"` To int `bson:"to"` } `bson:"salary"` } func pad(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return text + strings.Repeat(" ", width-n) } return text } func right(text string, width int) string { if n := utf8.RuneCountInString(text); n < width { return strings.Repeat(" ", width-n) + text } return text } func title(text string) { fmt.Println("\n" + text) fmt.Println(strings.Repeat("─", utf8.RuneCountInString(text))) } // Требования вакансии. var ( navyki = []string{"Python", "SQL"} gorod = "Ярославль" potolok = 100000 opytMesyacev = 6 ) func main() { uri := os.Getenv("MONGO_URI") if uri == "" { uri = "mongodb://localhost:27017" } client, err := mongo.Connect(options.Client().ApplyURI(uri)) if err != nil { log.Fatal(err) } ctx := context.Background() defer client.Disconnect(ctx) hh := client.Database("hh") resumes := hh.Collection("resumes") vacancies := hh.Collection("vacancies") interviews := hh.Collection("interviews") // ── 1. Требования ───────────────────────────────────────────────── title("1. Требования") fmt.Printf(" навыки: %s\n", strings.Join(navyki, " и ")) fmt.Printf(" город: %s или готовность к переезду\n", gorod) fmt.Printf(" зарплата: не больше %d\n", potolok) fmt.Printf(" опыт: место работы от %d месяцев\n", opytMesyacev) spisokNavykov := bson.A{} for _, n := range navyki { spisokNavykov = append(spisokNavykov, n) } gorodIliPereezd := bson.A{ bson.D{{Key: "city", Value: gorod}}, bson.D{{Key: "ready_to_move", Value: true}}, } opyt := bson.D{{Key: "$elemMatch", Value: bson.D{ {Key: "months", Value: bson.D{{Key: "$gte", Value: opytMesyacev}}}}}} // ── 2. Каждое требование по отдельности ─────────────────────────── title("2. Сколько резюме проходит каждое требование") vsego, _ := resumes.CountDocuments(ctx, bson.D{}) kriterii := []struct { nazvanie string filtr bson.D }{ {"навыки $all", bson.D{{Key: "skills", Value: bson.D{{Key: "$all", Value: spisokNavykov}}}}}, {"город или переезд $or", bson.D{{Key: "$or", Value: gorodIliPereezd}}}, {"зарплата $lte", bson.D{{Key: "salary", Value: bson.D{{Key: "$lte", Value: potolok}}}}}, {"опыт $elemMatch", bson.D{{Key: "experience", Value: opyt}}}, {"есть контакты $exists", bson.D{{Key: "contacts", Value: bson.D{{Key: "$exists", Value: true}}}}}, } for _, k := range kriterii { skolko, _ := resumes.CountDocuments(ctx, k.filtr) fmt.Printf(" %s %s из %d\n", pad(k.nazvanie, 24), right(fmt.Sprint(skolko), 2), vsego) } // ── 3. Все требования сразу ─────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr := bson.D{ {Key: "skills", Value: bson.D{{Key: "$all", Value: spisokNavykov}}}, {Key: "salary", Value: bson.D{{Key: "$lte", Value: potolok}}}, {Key: "experience", Value: opyt}, {Key: "$or", Value: gorodIliPereezd}, } fmt.Printf(" %s %s %s навыки\n", pad("ФИО", 18), pad("город", 16), right("зарплата", 9)) cursor, _ := resumes.Find(ctx, filtr, options.Find().SetSort(bson.D{{Key: "salary", Value: 1}})) var podoshli []Resume cursor.All(ctx, &podoshli) for _, r := range podoshli { fmt.Printf(" %s %s %s %s\n", pad(r.Fio, 18), pad(r.City, 16), right(fmt.Sprint(r.Salary), 9), strings.Join(r.Skills, ", ")) } fmt.Println(" подошло:", len(podoshli)) // ── 4. Поиск по части строки ────────────────────────────────────── title("4. Должности, начинающиеся с Junior") cursor, _ = resumes.Find(ctx, bson.D{{Key: "position", Value: bson.D{ {Key: "$regex", Value: "^junior"}, {Key: "$options", Value: "i"}}}}) var junior []Resume cursor.All(ctx, &junior) for _, r := range junior { fmt.Printf(" %s %s\n", pad(r.Fio, 18), r.Position) } // ── 5. Подходящие вакансии ──────────────────────────────────────── title("5. Вакансии под ожидание 90 000") ozhidanie := 90000 vilka := bson.D{ {Key: "salary.from", Value: bson.D{{Key: "$lte", Value: ozhidanie}}}, {Key: "salary.to", Value: bson.D{{Key: "$gte", Value: ozhidanie}}}, } cursor, _ = vacancies.Find(ctx, vilka) var podhodyat []Vacancy cursor.All(ctx, &podhodyat) for _, v := range podhodyat { fmt.Printf(" %s %s %s %d–%d\n", pad(v.Title, 26), pad(v.Company, 16), pad(v.City, 16), v.Salary.From, v.Salary.To) } fmt.Println(" подходит вакансий:", len(podhodyat)) // ── 6. Собеседования за неделю ──────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") sep21 := time.Date(2026, 9, 21, 0, 0, 0, 0, time.UTC) sep28 := time.Date(2026, 9, 28, 0, 0, 0, 0, time.UTC) nedelya := bson.D{{Key: "when", Value: bson.D{{Key: "$gte", Value: sep21}, {Key: "$lt", Value: sep28}}}} zaNedelyu, _ := interviews.CountDocuments(ctx, nedelya) fmt.Println(" всего за неделю:", zaNedelyu) // К общему фильтру добавляем условие копией: append по общему срезу // может перезаписать данные соседних вызовов. sUsloviem := func(key, value string) bson.D { return append(append(bson.D{}, nedelya...), bson.E{Key: key, Value: value}) } for _, etap := range []string{"скрининг", "техническое", "финальное"} { skolko, _ := interviews.CountDocuments(ctx, sUsloviem("stage", etap)) fmt.Printf(" %s %s\n", pad(etap, 12), right(fmt.Sprint(skolko), 2)) } ochno, _ := interviews.CountDocuments(ctx, sUsloviem("format", "очно")) video, _ := interviews.CountDocuments(ctx, sUsloviem("format", "видеозвонок")) fmt.Printf(" очно: %d, видеозвонком: %d\n", ochno, video) // ── 7. Проверка данных ──────────────────────────────────────────── title("7. Проверка данных") proverki := []struct { nazvanie string col *mongo.Collection filtr bson.D }{ {"резюме без даты обновления", resumes, bson.D{{Key: "updated", Value: bson.D{{Key: "$exists", Value: false}}}}}, {"зарплата записана не числом", resumes, bson.D{{Key: "salary", Value: bson.D{{Key: "$not", Value: bson.D{{Key: "$type", Value: "number"}}}}}}}, {"пустой список мест работы", resumes, bson.D{{Key: "experience", Value: bson.D{{Key: "$size", Value: 0}}}}}, {"нет ни одного навыка", resumes, bson.D{{Key: "skills.0", Value: bson.D{{Key: "$exists", Value: false}}}}}, {"перевёрнутая вилка", vacancies, bson.D{{Key: "$expr", Value: bson.D{{Key: "$gt", Value: bson.A{"$salary.from", "$salary.to"}}}}}}, {"вакансия закрыта", vacancies, bson.D{{Key: "is_open", Value: bson.D{{Key: "$ne", Value: true}}}}}, } for _, p := range proverki { skolko, _ := p.col.CountDocuments(ctx, p.filtr) pometka := "" if skolko > 0 { pometka = " ← проверить" } fmt.Printf(" %s %s%s\n", pad(p.nazvanie, 30), right(fmt.Sprint(skolko), 2), pometka) } // ── 8. Кто не подошёл и почему ──────────────────────────────────── title("8. Кто не прошёл отбор") cursor, _ = resumes.Find(ctx, bson.D{}) var vse []Resume cursor.All(ctx, &vse) for _, r := range vse { var prichiny []string for _, nuzhen := range navyki { est := false for _, navyk := range r.Skills { if navyk == nuzhen { est = true } } if !est { prichiny = append(prichiny, "нет навыков") break } } if r.Salary > potolok { prichiny = append(prichiny, "зарплата выше потолка") } if r.City != gorod && !r.ReadyToMove { prichiny = append(prichiny, "другой город без переезда") } hvatitOpyta := false for _, mesto := range r.Experience { if mesto.Months >= opytMesyacev { hvatitOpyta = true } } if !hvatitOpyta { prichiny = append(prichiny, "мало опыта") } if len(prichiny) > 0 { fmt.Printf(" %s %s\n", pad(r.Fio, 18), strings.Join(prichiny, "; ")) } } }
# Подбор кандидатов: что умеет язык фильтров из модуля 2. # # Скрипт решает прикладную задачу на базе hh: подобрать кандидатов под # требования вакансии, посмотреть, сколько резюме проходит каждое требование # по отдельности, найти подходящие вакансии и проверить данные на пропуски, # неверные типы и несогласованные поля. # # cd scripts # ruby hh_search.rb # # docker compose run --rm ruby scripts/hh_search.rb # в контейнере require "mongo" Mongo::Logger.logger.level = Logger::WARN client = Mongo::Client.new(ENV.fetch("MONGO_URI", "mongodb://localhost:27017/")) hh = client.use("hh").database resumes = hh[:resumes] vacancies = hh[:vacancies] interviews = hh[:interviews] def title(text) puts "\n" + text puts "─" * text.length end def pad(text, width) text.to_s.ljust(width) end # ── 1. Требования вакансии ──────────────────────────────────────────────── NAVYKI = ["Python", "SQL"].freeze GOROD = "Ярославль" POTOLOK = 100_000 OPYT_MESYACEV = 6 title("1. Требования") puts " навыки: #{NAVYKI.join(' и ')}" puts " город: #{GOROD} или готовность к переезду" puts " зарплата: не больше #{POTOLOK}" puts " опыт: место работы от #{OPYT_MESYACEV} месяцев" # ── 2. Каждое требование по отдельности ─────────────────────────────────── title("2. Сколько резюме проходит каждое требование") kriterii = [ ["навыки $all", { "skills" => { "$all" => NAVYKI } }], ["город или переезд $or", { "$or" => [{ "city" => GOROD }, { "ready_to_move" => true }] }], ["зарплата $lte", { "salary" => { "$lte" => POTOLOK } }], ["опыт $elemMatch", { "experience" => { "$elemMatch" => { "months" => { "$gte" => OPYT_MESYACEV } } } }], ["есть контакты $exists", { "contacts" => { "$exists" => true } }], ] vsego = resumes.count_documents({}) kriterii.each do |nazvanie, filtr| puts " #{pad(nazvanie, 24)} #{resumes.count_documents(filtr).to_s.rjust(2)} из #{vsego}" end # ── 3. Все требования сразу ─────────────────────────────────────────────── title("3. Кандидаты, проходящие все требования") filtr = { "skills" => { "$all" => NAVYKI }, "salary" => { "$lte" => POTOLOK }, "experience" => { "$elemMatch" => { "months" => { "$gte" => OPYT_MESYACEV } } }, "$or" => [{ "city" => GOROD }, { "ready_to_move" => true }], } puts " #{pad('ФИО', 18)} #{pad('город', 16)} #{'зарплата'.rjust(9)} навыки" resumes.find(filtr).sort({ "salary" => 1 }).each do |doc| puts " #{pad(doc['fio'], 18)} #{pad(doc['city'], 16)} #{doc['salary'].to_s.rjust(9)} #{doc['skills'].join(', ')}" end puts " подошло: #{resumes.count_documents(filtr)}" # ── 4. Поиск по части строки ────────────────────────────────────────────── title("4. Должности, начинающиеся с Junior") resumes.find({ "position" => { "$regex" => "^junior", "$options" => "i" } }, projection: { "_id" => 0, "fio" => 1, "position" => 1 }) .each { |doc| puts " #{pad(doc['fio'], 18)} #{doc['position']}" } # ── 5. Подходящие вакансии ──────────────────────────────────────────────── title("5. Вакансии под ожидание 90 000") OZHIDANIE = 90_000 vilka = { "salary.from" => { "$lte" => OZHIDANIE }, "salary.to" => { "$gte" => OZHIDANIE } } vacancies.find(vilka, projection: { "_id" => 0, "title" => 1, "company" => 1, "city" => 1, "salary" => 1 }) .each do |doc| zarplata = doc["salary"] puts " #{pad(doc['title'], 26)} #{pad(doc['company'], 16)} #{pad(doc['city'], 16)} " \ "#{zarplata['from']}–#{zarplata['to']}" end puts " подходит вакансий: #{vacancies.count_documents(vilka)}" # ── 6. Собеседования за неделю ──────────────────────────────────────────── title("6. Собеседования с 21 по 27 сентября") nedelya = { "when" => { "$gte" => Time.utc(2026, 9, 21), "$lt" => Time.utc(2026, 9, 28) } } puts " всего за неделю: #{interviews.count_documents(nedelya)}" ["скрининг", "техническое", "финальное"].each do |etap| puts " #{pad(etap, 12)} #{interviews.count_documents(nedelya.merge('stage' => etap)).to_s.rjust(2)}" end puts " очно: #{interviews.count_documents(nedelya.merge('format' => 'очно'))}, " \ "видеозвонком: #{interviews.count_documents(nedelya.merge('format' => 'видеозвонок'))}" # ── 7. Проверка данных ──────────────────────────────────────────────────── title("7. Проверка данных") proverki = [ ["резюме без даты обновления", resumes, { "updated" => { "$exists" => false } }], ["зарплата записана не числом", resumes, { "salary" => { "$not" => { "$type" => "number" } } }], ["пустой список мест работы", resumes, { "experience" => { "$size" => 0 } }], ["нет ни одного навыка", resumes, { "skills.0" => { "$exists" => false } }], ["перевёрнутая вилка", vacancies, { "$expr" => { "$gt" => ["$salary.from", "$salary.to"] } }], ["вакансия закрыта", vacancies, { "is_open" => { "$ne" => true } }], ] proverki.each do |nazvanie, col, filtr_proverki| skolko = col.count_documents(filtr_proverki) puts " #{pad(nazvanie, 30)} #{skolko.to_s.rjust(2)}#{skolko.zero? ? '' : ' ← проверить'}" end # ── 8. Кто не подошёл и почему ──────────────────────────────────────────── title("8. Кто не прошёл отбор") resumes.find({}, projection: { "_id" => 0, "fio" => 1, "skills" => 1, "salary" => 1, "city" => 1, "ready_to_move" => 1, "experience" => 1 }).each do |doc| prichiny = [] prichiny << "нет навыков" unless (NAVYKI - (doc["skills"] || [])).empty? prichiny << "зарплата выше потолка" if doc["salary"] > POTOLOK prichiny << "другой город без переезда" if doc["city"] != GOROD && !doc["ready_to_move"] prichiny << "мало опыта" unless (doc["experience"] || []).any? { |m| (m["months"] || 0) >= OPYT_MESYACEV } puts " #{pad(doc['fio'], 18)} #{prichiny.join('; ')}" unless prichiny.empty? end client.close
1. Требования
─────────────
навыки: Python и SQL
город: Ярославль или готовность к переезду
зарплата: не больше 100000
опыт: место работы от 6 месяцев
2. Сколько резюме проходит каждое требование
────────────────────────────────────────────
навыки $all 4 из 9
город или переезд $or 8 из 9
зарплата $lte 5 из 9
опыт $elemMatch 6 из 9
есть контакты $exists 8 из 9
3. Кандидаты, проходящие все требования
───────────────────────────────────────
ФИО город зарплата навыки
Пётр Ковалёв Ярославль 70000 Python, SQL, Git, Docker
подошло: 1
4. Должности, начинающиеся с Junior
───────────────────────────────────
Анна Белова Junior Python-разработчик
Пётр Ковалёв Junior Python-разработчик
Дарья Нечаева Junior Frontend-разработчик
Марк Ефремов Junior Python-разработчик
5. Вакансии под ожидание 90 000
───────────────────────────────
Junior Python-разработчик Тензор Ярославль 60000–90000
Тестировщик МедСофт Казань 70000–95000
Frontend-разработчик Retail Lab Санкт-Петербург 90000–140000
Инженер сопровождения БД ЯрКом Ярославль 80000–120000
подходит вакансий: 4
6. Собеседования с 21 по 27 сентября
────────────────────────────────────
всего за неделю: 22
скрининг 4
техническое 7
финальное 11
очно: 12, видеозвонком: 10
7. Проверка данных
──────────────────
резюме без даты обновления 1 ← проверить
зарплата записана не числом 0
пустой список мест работы 1 ← проверить
нет ни одного навыка 0
перевёрнутая вилка 0
вакансия закрыта 0
8. Кто не прошёл отбор
──────────────────────
Анна Белова нет навыков; мало опыта
Алина Дроздова зарплата выше потолка
Игорь Самойлов нет навыков; зарплата выше потолка
Дарья Нечаева нет навыков; мало опыта
Тимур Валиев нет навыков; зарплата выше потолка; другой город без переезда
Ксения Лапина мало опыта
Марк Ефремов нет навыков
Ольга Пирогова зарплата выше потолка
1. Требования
─────────────
навыки: Python и SQL
город: Ярославль или готовность к переезду
зарплата: не больше 100000
опыт: место работы от 6 месяцев
2. Сколько резюме проходит каждое требование
────────────────────────────────────────────
навыки $all 4 из 9
город или переезд $or 8 из 9
зарплата $lte 5 из 9
опыт $elemMatch 6 из 9
есть контакты $exists 8 из 9
3. Кандидаты, проходящие все требования
───────────────────────────────────────
ФИО город зарплата навыки
Пётр Ковалёв Ярославль 70000 Python, SQL, Git, Docker
подошло: 1
4. Должности, начинающиеся с Junior
───────────────────────────────────
Анна Белова Junior Python-разработчик
Пётр Ковалёв Junior Python-разработчик
Дарья Нечаева Junior Frontend-разработчик
Марк Ефремов Junior Python-разработчик
5. Вакансии под ожидание 90 000
───────────────────────────────
Junior Python-разработчик Тензор Ярославль 60000–90000
Тестировщик МедСофт Казань 70000–95000
Frontend-разработчик Retail Lab Санкт-Петербург 90000–140000
Инженер сопровождения БД ЯрКом Ярославль 80000–120000
подходит вакансий: 4
6. Собеседования с 21 по 27 сентября
────────────────────────────────────
всего за неделю: 22
скрининг 4
техническое 7
финальное 11
очно: 12, видеозвонком: 10
7. Проверка данных
──────────────────
резюме без даты обновления 1 ← проверить
зарплата записана не числом 0
пустой список мест работы 1 ← проверить
нет ни одного навыка 0
перевёрнутая вилка 0
вакансия закрыта 0
8. Кто не прошёл отбор
──────────────────────
Анна Белова нет навыков; мало опыта
Алина Дроздова зарплата выше потолка
Игорь Самойлов нет навыков; зарплата выше потолка
Дарья Нечаева нет навыков; мало опыта
Тимур Валиев нет навыков; зарплата выше потолка; другой город без переезда
Ксения Лапина мало опыта
Марк Ефремов нет навыков
Ольга Пирогова зарплата выше потолка
1. Требования
─────────────
навыки: Python и SQL
город: Ярославль или готовность к переезду
зарплата: не больше 100000
опыт: место работы от 6 месяцев
2. Сколько резюме проходит каждое требование
────────────────────────────────────────────
навыки $all 4 из 9
город или переезд $or 8 из 9
зарплата $lte 5 из 9
опыт $elemMatch 6 из 9
есть контакты $exists 8 из 9
3. Кандидаты, проходящие все требования
───────────────────────────────────────
ФИО город зарплата навыки
Пётр Ковалёв Ярославль 70000 Python, SQL, Git, Docker
подошло: 1
4. Должности, начинающиеся с Junior
───────────────────────────────────
Анна Белова Junior Python-разработчик
Пётр Ковалёв Junior Python-разработчик
Дарья Нечаева Junior Frontend-разработчик
Марк Ефремов Junior Python-разработчик
5. Вакансии под ожидание 90 000
───────────────────────────────
Junior Python-разработчик Тензор Ярославль 60000–90000
Тестировщик МедСофт Казань 70000–95000
Frontend-разработчик Retail Lab Санкт-Петербург 90000–140000
Инженер сопровождения БД ЯрКом Ярославль 80000–120000
подходит вакансий: 4
6. Собеседования с 21 по 27 сентября
────────────────────────────────────
всего за неделю: 22
скрининг 4
техническое 7
финальное 11
очно: 12, видеозвонком: 10
7. Проверка данных
──────────────────
резюме без даты обновления 1 ← проверить
зарплата записана не числом 0
пустой список мест работы 1 ← проверить
нет ни одного навыка 0
перевёрнутая вилка 0
вакансия закрыта 0
8. Кто не прошёл отбор
──────────────────────
Анна Белова нет навыков; мало опыта
Алина Дроздова зарплата выше потолка
Игорь Самойлов нет навыков; зарплата выше потолка
Дарья Нечаева нет навыков; мало опыта
Тимур Валиев нет навыков; зарплата выше потолка; другой город без переезда
Ксения Лапина мало опыта
Марк Ефремов нет навыков
Ольга Пирогова зарплата выше потолка
1. Требования
─────────────
навыки: Python и SQL
город: Ярославль или готовность к переезду
зарплата: не больше 100000
опыт: место работы от 6 месяцев
2. Сколько резюме проходит каждое требование
────────────────────────────────────────────
навыки $all 4 из 9
город или переезд $or 8 из 9
зарплата $lte 5 из 9
опыт $elemMatch 6 из 9
есть контакты $exists 8 из 9
3. Кандидаты, проходящие все требования
───────────────────────────────────────
ФИО город зарплата навыки
Пётр Ковалёв Ярославль 70000 Python, SQL, Git, Docker
подошло: 1
4. Должности, начинающиеся с Junior
───────────────────────────────────
Анна Белова Junior Python-разработчик
Пётр Ковалёв Junior Python-разработчик
Дарья Нечаева Junior Frontend-разработчик
Марк Ефремов Junior Python-разработчик
5. Вакансии под ожидание 90 000
───────────────────────────────
Junior Python-разработчик Тензор Ярославль 60000–90000
Тестировщик МедСофт Казань 70000–95000
Frontend-разработчик Retail Lab Санкт-Петербург 90000–140000
Инженер сопровождения БД ЯрКом Ярославль 80000–120000
подходит вакансий: 4
6. Собеседования с 21 по 27 сентября
────────────────────────────────────
всего за неделю: 22
скрининг 4
техническое 7
финальное 11
очно: 12, видеозвонком: 10
7. Проверка данных
──────────────────
резюме без даты обновления 1 ← проверить
зарплата записана не числом 0
пустой список мест работы 1 ← проверить
нет ни одного навыка 0
перевёрнутая вилка 0
вакансия закрыта 0
8. Кто не прошёл отбор
──────────────────────
Анна Белова нет навыков; мало опыта
Алина Дроздова зарплата выше потолка
Игорь Самойлов нет навыков; зарплата выше потолка
Дарья Нечаева нет навыков; мало опыта
Тимур Валиев нет навыков; зарплата выше потолка; другой город без переезда
Ксения Лапина мало опыта
Марк Ефремов нет навыков
Ольга Пирогова зарплата выше потолка
§10Как запустить
Скрипт лежит в репозитории практик, в папке scripts. Данные он не меняет, поэтому запускать его можно в любой момент.
python3 scripts/hh_search.py
ruby scripts/hh_search.rb
go run scripts/hh_search.go
g++ -std=c++17 scripts/hh_search.cpp -o search $(pkg-config --cflags --libs libmongocxx1) && ./search
docker compose run --rm python scripts/hh_search.py docker compose run --rm ruby scripts/hh_search.rb docker compose run --rm go scripts/hh_search.go docker compose run --rm cpp scripts/hh_search.cpp
Чтобы подобрать кандидатов под другую вакансию, меняют четыре переменные в начале файла: список навыков, город, потолок зарплаты и срок работы. Остальной код остаётся прежним.
§11Частые ошибки
| Что написано | Что происходит | Как правильно |
|---|---|---|
{"skills": НАВЫКИ} для «оба навыка» | Массив сравнивается целиком, вместе с порядком: результат пустой | {"skills": {"$all": НАВЫКИ}} |
Два условия на массив без $elemMatch | Условия выполняются разными элементами массива | $elemMatch, как в разделе с опытом |
Конец недели задан $lte с датой без времени | Теряются события последнего дня | $lt с началом следующего дня |
| Значение из формы подставлено прямо в фильтр | Документ {"$ne": null} вместо строки превращается в условие | Проверить тип или сравнивать через $eq |
| Все резюме вычитываются, чтобы отобрать их в программе | По сети едут все документы коллекции | Отбор — фильтром на сервере; в программе — только объяснение причин |
| Что написано | Что происходит | Как правильно |
|---|---|---|
| Фильтр собран во временном значении и передан дальше | Обращение к освобождённой памяти | Держать auto filtr = make_document(…) живым до запроса |
Два условия на массив без $elemMatch | Условия выполняются разными элементами массива | $elemMatch, как в разделе с опытом |
std::setw для столбцов с кириллицей | Столбцы разъезжаются: в UTF-8 на букву приходится два байта | Считать ширину по символам — функция pad в скрипте |
get_int32 на поле, которого нет | Программа падает на резюме без поля | Сначала проверить, что поле есть: if (doc["experience"]) |
| Что написано | Что происходит | Как правильно |
|---|---|---|
append(obshiy, …) к общему фильтру | Добавленное условие перезаписывает данные соседнего запроса | Копия: append(append(bson.D{}, obshiy…), …) |
Два условия на массив без $elemMatch | Условия выполняются разными элементами массива | $elemMatch, как в разделе с опытом |
%-18s для столбцов с кириллицей | Столбцы разъезжаются: ширина считается в байтах | Считать ширину по символам — функция pad в скрипте |
Ошибка запроса присвоена _ | Пустой результат выглядит как «никто не подошёл» | Проверять второе значение, которое возвращает драйвер |
| Что написано | Что происходит | Как правильно |
|---|---|---|
{ "skills" => НАВЫКИ } для «оба навыка» | Массив сравнивается целиком, вместе с порядком: результат пустой | { "skills" => { "$all" => НАВЫКИ } } |
Два условия на массив без $elemMatch | Условия выполняются разными элементами массива | $elemMatch, как в разделе с опытом |
Результат find перебран дважды | Запрос выполняется заново при каждом обходе | Сохранить в массив: .to_a |
Ключи документа — символы (:skills) | Поля не находятся: в документах имена строковые | Писать ключи строками |
§12Проверьте себя
Зачем считать каждое требование по отдельности, если нужен итоговый список?
Чтобы видеть, какое требование отсекает кандидатов. В базе курса навыкам отвечают четыре резюме, а всем требованиям сразу — одно: видно, какое требование смягчать в первую очередь.
Почему условие по опыту записано через $elemMatch?
Потому что опыт — массив документов, и без $elemMatch условия проверялись бы на разных местах работы: роль нашлась бы в одном, срок — в другом. $elemMatch требует, чтобы одно место работы отвечало всем условиям сразу.
Что покажет раздел проверки данных, если в резюме появится зарплата строкой?
Строка «зарплата записана не числом» получит единицу и пометку. Фильтр {"salary": {"$not": {"$type": "number"}}} находит любое значение, которое не относится к числовым типам.
Почему последний раздел не использует фильтры?
Фильтр отвечает, какие документы подходят, но не объясняет причину отказа. Чтобы назвать причину, скрипт читает резюме и сравнивает поля в программе. На больших коллекциях так не делают: отбор оставляют серверу.
Как подобрать кандидатов под другую вакансию?
Поменять значения четырёх переменных в начале файла: навыки, город, потолок зарплаты и срок работы. Все фильтры собраны из этих переменных, поэтому запросы менять не нужно.
§13Шпаргалка
навыки, город, потолок, срок
фильтры собираются из них
счётчик на каждое требование
видно, что отсекает кандидатов
пары фильтра — «и»
$or — одна из пар
$gte начало, $lt следующий день
$exists, $type, $size, $expr
ноль — дефектов нет
её считает программа,
отбор остаётся на сервере
Итог главы
Скрипт собирает операторы модуля 2 в рабочий отбор: требования лежат в переменных, из них собираются фильтры, счётчики показывают, какое требование отсекает кандидатов, один общий фильтр даёт итоговый список. Рядом — поиск по части строки, подбор вакансий по вилке, период дат полуинтервалом и проверка данных на пропуски, типы и несогласованные поля. Причина отказа считается в программе, потому что фильтр её не возвращает. На этом модуль 2 закончен: дальше практическая работа ПР-02, где такой отбор нужно написать самому.