---
title: "$and, $or, $regex: сложные фильтры"
description: "$and, $or и $regex в mongo из Go: неявный AND, порядок в bson.D, regex поиск mongodb по части строки, ловушка дублирующегося ключа и цена case-insensitive."
course: "MongoDB в Go: официальный драйвер mongo-driver v2"
url: "https://golangmaster.ru/courses/mongo-driver/lessons/14-slozhnye-filtry/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# $and, $or, $regex: сложные фильтры

*Курс «MongoDB в Go: официальный драйвер mongo-driver v2», урок 14 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/mongo-driver/lessons/14-slozhnye-filtry/*

$and, $or и $regex в mongo из Go закрывают поиск по частичному совпадению и комбинации условий: «активные или отложенные», «имя начинается с А», «цена в диапазоне и не архив». Разберём неявный AND, явный $and через bson.D, ловушку дублирующегося ключа и цену регистронезависимого regex поиска mongodb.

Фильтры восьмого урока работали с одним полем: статус, цена, попадание в список. Реальный поиск устроен сложнее. Панель администратора собирает запрос из трёх полей, любое из которых может быть пустым: статус, минимальная цена, подстрока в названии. Поиск по каталогу ищет фразу и в названии, и в описании — «или» по двум полям. Отчёт исключает архив и черновики, оставляя всё остальное. Это уже не одиночные условия, а их комбинации, и MongoDB описывает их тремя операторами: неявный AND соберёт условия по разным полям, $or объявит альтернативы, $and разрешит конфликты, а $regex добавит частичное совпадение строк. В Go всё это собирается из тех же bson.M и bson.D, но у композиции есть ловушка, которая не ловится ни компилятором, ни сервером: карта в Go хранит один ключ один раз, и второй $or молча затирает первый. У композиции цена небрежности выше, чем у любого одиночного условия: ошибка не ломает запрос — она меняет его смысл. Начнём с простого и дойдём до этой ловушки по шагам, а к концу соберём панельный фильтр, который не боится пустых полей и пользовательского ввода, — в виде именованной функции, которую не страшно читать через полгода.

### Неявный AND и когда нужен явный $and

Фильтр с несколькими ключами — уже конъюнкция: bson.M{"status": "active", "stock": bson.M{"$gt": 0}} находит документы, где статус активен и остаток положителен. Это неявный AND, и он покрывает большинство запросов, пока условия лежат на разных полях. Явный $and нужен в двух случаях. Первый: одно поле должно удовлетворить нескольким условиям, которые не помещаются в один операторный документ — два диапазона с границами «строго больше» и «не больше» записываются как {"$and": [{"qty": {"$gt": 5}}, {"qty": {"$lte": 20}}]}. Второй: нужно скомбинировать две $or-группы — об этом отдельный блок ниже. Записывается $and массивом условий: bson.D{{Key: "$and", Value: []bson.M{...}}}; порядок элементов массива сервер сохраняет, поэтому для сложных конъюнкций берут bson.D, а не карту — карта и здесь рискует перезаписью ключей. Внутри массива каждый элемент — обычный фильтр: равенства, операторы, вложенные $or — всё то же самое на один уровень глубже. Явный $and с единственным условием избыточен: такое условие спокойно живёт в одной карте рядом с другими, и оператор оправдан, только когда без него не собрать.

### Динамический фильтр панели

```go
func adminFilter(q AdminQuery) bson.D {
        conds := bson.D{}

        if q.Status != "" {
                conds = append(conds, bson.E{Key: "status", Value: q.Status})
        }
        if q.MinPrice > 0 {
                conds = append(conds, bson.E{Key: "price", Value: bson.M{"$gte": q.MinPrice}})
        }
        if q.Search != "" {
                pattern := regexp.QuoteMeta(q.Search)
                conds = append(conds, bson.E{Key: "name", Value: bson.M{
                        "$regex": pattern, "$options": "i",
                }})
        }

        return conds // неявный AND: ключи не пересекаются
}
```

Динамический фильтр собирается срезом bson.E: пустые поля панели просто не попадают в условие, и неявный AND собирает остальные в один запрос без лишних веток. Пользовательский ввод проходит regexp.QuoteMeta: точка или скобка в поиске превращаются в литералы, а не в части регэкспа, поэтому инъекция паттерна исключена ещё на сборке. Возвращённый срез пар передаётся прямо в Find как фильтр — сервер сам прочитает конъюнкцию.

### $or: альтернативы и ловушка второго ключа

$or принимает массив условий и находит документы, подходящие хотя бы под одно: поиск по названию или описанию записывается bson.M{"$or": []bson.M{{"name": ...}, {"description": ...}}}. В Go-коде mongo or filter чаще всего собирается именно срезом bson.M — каждый элемент самостоятельный фильтр со своими операторами. Ловушка подстерегает на уровне языка: задача «активные или отложенные, но не архивные» выглядит как два ключа $or в одной карте, но карта в Go хранит ключ один раз — второй $or перезапишет первый ещё при сборке литерала, без ошибки компиляции и без ошибки сервера. Половина условий исчезает молча, и фильтр находит не то, что задумано. Решение — упаковать обе группы в $and: bson.D{{Key: "$and", Value: []bson.M{{"$or": ...}, {"archived": ...}}}} — каждый элемент массива живёт своей жизнью. Вложенность ограничена только здравым смыслом: $or внутри $and внутри $or читается уже с трудом, и это сигнал вынести сборку фильтра в именованную функцию с комментариями. Рядом стоит зеркальный оператор $nor: он находит документы, не подходящие ни под одно условие из массива, — «не архив и не черновик» иногда читается через $nor из двух равенств, хотя чаще та же запись живёт в $nin со списком значений.

### Как собрать комбинации условий

| Приём | Запись в Go | Когда брать |
| --- | --- | --- |
| Неявный AND | bson.M{"status": "active", "stock": 1} | условия по разным полям — самый частый случай |
| $and | bson.D{{Key: "$and", Value: []bson.M{...}}} | две $or-группы вместе или условия, не помещающиеся в одну карту |
| $or | bson.M{"$or": []bson.M{...}} | любое из условий — альтернативы одного смысла |
| $ne / $nin | bson.M{"archived": bson.M{"$ne": true}} | исключение значения или списка значений |
| $regex | bson.M{"name": bson.M{"$regex": "^A"}} | частичное совпадение строки, лучше с якорем |

### $regex: поиск по части строки

Оператор $regex запускает на сервере регэксп: bson.M{"name": bson.M{"$regex": "^клавиатура", "$options": "i"}} находит имена, начинающиеся на «клавиатура», в любом регистре. Паттерн — обычная строка, опции идут отдельным ключом $options: i снимает регистр, m включает якоря по строкам, x разрешает пробелы в шаблоне. Вторая форма записи — тип bson.Regex из пакета bson: bson.Regex{Pattern: "^клавиатура", Options: "i"} вставляется прямо как значение фильтра, и кодек соберёт BSON-тип регэкспа; в v1 этот тип назывался primitive.Regex. Для regex поиска mongodb-сервер компилирует паттерн в свой движок, и синтаксис там PCRE-подобный, а не Go-шный: look-ahead в нём есть, а именованные классы пишутся по-другому — паттерн пишется для сервера, а не для пакета regexp. Цена операции зависит от якоря: паттерн с префиксом ^буквы может опираться на индекс и перебирать только его начало; паттерн без якоря заставляет сервер проверять каждое значение каждого документа коллекции. Опции парсятся однобуквенные: i снимает регистр, m включает якоря по строкам, s делает точку совпадающей с переводом строки, x разрешает пробелы в шаблоне, u включает юникод-таблицы. Самая частая — i, и её же чаще всего добавляют без нужды: регистр важен для артикулов и кодов.

> **Живое место: $regex вместо поиска.** На каталоге в два миллиона товаров поиск без якоря — {$regex: q} — уложил базу: каждое нажатие клавиши запускало полный скан коллекции с компиляцией регэкспа, а опция i удваивала работу. Правило: $regex — для админских панелей и префиксного поиска по индексу; пользовательский поиск — отдельный механизм (текстовый индекс семнадцатого урока или внешний поисковик), а ввод пользователя — всегда через regexp.QuoteMeta. Мораль та же, что и с пустым фильтром удаления: драйвер выполнит любой запрос честно, цену посчитает только мониторинг.

### Синтаксис паттернов: MongoDB против Go regexp

Два регэксп-движка в одном проекте — постоянный источник путаницы, поэтому разложим грани. Паттерн в $regex исполняет сервер MongoDB: синтаксис близок к PCRE — есть look-ahead и look-behind, классы \d и \w, а флаги живут в $options. Пакет regexp в Go исполняет RE2: look-around в нём принципиально отсутствует, зато он линеен по времени и не подвержен катастрофическому бэктрекингу. Из Go в фильтр паттерн уезжает строкой, и его пишут под серверный движок; Go-regexp нужны только для проверки на клиенте, если она вообще нужна. Вторая граница — экранирование: пользовательский ввод почти всегда содержит спецсимволы, и regexp.QuoteMeta превращает их в литералы; без этого скобка или звёздочка в запросе меняют смысл паттерна. Третья граница — сами флаги: опции сервера однобуквенные, i для регистра — самое частое; вместе с якорем ^ префиксный поиск по индексу остаётся быстрым, всё остальное — платно. Паттерн — обычная строка, поэтому его удобно отлаживать в mongosh тем же запросом: db.coll.find({ name: { $regex: "...", $options: "i" } }) — увиденный там состав выборки совпадёт с выдачей драйвера байт в байт.

### Комбинация: $and, две $or-группы, $ne

```go
// «Активные ИЛИ отложенные» — и не архивные
filter := bson.M{
        "$and": []bson.M{
                {"$or": []bson.M{
                        {"status": "active"},
                        {"status": "deferred"},
                }},
                {"archived": bson.M{"$ne": true}},
        },
}

count, err := coll.CountDocuments(ctx, filter)
```

Две $or-группы в одной карте не уживутся: второй ключ перезапишет первый молча, ещё до отправки на сервер. Обёртка $and со срезом условий решает конфликт: каждый элемент среза — самостоятельное условие со своей группой альтернатив. $ne: true исключает архив; для списка исключений есть $nin со срезом значений — приём тот же, меняется только количество.

### Чек-лист сложного фильтра

- Условия по разным полям — неявный AND без всяких операторов.
- Две $or-группы — только через $and со срезом: второй ключ карты затрёт первый.
- Одно поле с несколькими операторами — один вложенный документ или $and.
- Пользовательский ввод в $regex — сначала regexp.QuoteMeta.
- Префиксный ^паттерн — под индекс; без якоря — полный скан.
- Комбинированный фильтр собирает именованная функция — литералы на месте вызова не читаются.

### Проверка: два ключа $or

**Вопрос:** В bson.M записаны два ключа "$or" подряд. Что окажется в фильтре?

- Обе группы условий объединятся
- ✅ Останется последняя: карта молча перезапишет первый ключ
- Ошибка компиляции
- Драйвер вернёт ошибку дубликата ключа

Карта хранит один ключ один раз: второй $or затрёт первый на этапе сборки литерала, и половина условий исчезнет без единой ошибки. Два набора альтернатив комбинируют через $and со срезом условий — каждому элементу свой $or.

### Проверка: какой $regex быстрый

**Вопрос:** Какой паттерн $regex сможет опираться на индекс по полю name?

- "тура" — подстрока без якорей
- ✅ "^клавиатура" — префиксный якорь
- "тура$" — суффиксный якорь
- любой паттерн при опции i

Индекс работает, когда паттерн совпадает с префиксом: ^клавиатура перебирает только начало диапазона индекса. Подстрока и суффикс требуют проверки всех значений, а опция i дополнительно расширяет перебор — платой становится скан.

### Сколько имён найдёт фильтр?

```go
// в коллекции: alice, Bob, carol, Dave
cur, _ := coll.Find(ctx, bson.M{
        "name": bson.M{"$regex": "^a", "$options": "i"},
})
var names []bson.M
cur.All(ctx, &names)
fmt.Println(len(names))
```

**Что выведет программа?**

- 0
- ✅ 1 — только alice
- 2 — alice и carol
- 4 — опция i снимает все ограничения

Якорь ^a с опцией i находит имена, начинающиеся на a или A: из списка это только alice. Опция i снимает регистр, но не расширяет паттерн на середину строки — carol начинается с c, Bob и Dave с больших букв.

### Комбинации на практике: три типовых запроса

Соберём три запроса, которые встречаются почти в каждом сервисе. Поиск по любому из полей: $or со срезом из name и description, каждый элемент — свой $regex с QuoteMeta-вводом и опцией i; итог — «фраза встречается где угодно из двух мест». Исключение неактуального: {"archived": {"$ne": true}} и {"status": {"$nin": ["draft", "deleted"]}} рядом в неявном AND — карта справится, ключи разные. Комбо с диапазоном: цена между границами через $gte и $lte в одном вложенном документе, статус через $in со списком допустимых — и всё это внутри $and с $or-группой для поиска. Общий приём у всех трёх — сборка через именованную функцию, возвращающую bson.D или bson.M: панель с пятью необязательными полями в виде одного литерала не читается, а функция с append по условию — читается и тестируется. Фильтры из этих примеров уходят в Find и Aggregate одинаково: синтаксис условий общий, агрегации лишь оборачивают его в этап $match. Отдельная мелочь, о которой стоит знать заранее: сравнить два поля документа операторами запроса нельзя — $gte сравнивает поле с константой. Для «остаток больше зарезервировано» есть $expr: {"$expr": {"$gt": ["$stock", "$reserved"]}} запускает агрегационное выражение прямо в фильтре — это мост к конвейерам пятнадцатого урока.

> **v1 → v2: bson.Regex вместо primitive.Regex.** Тип регэкспа переехал из primitive в bson вместе со всем словарём примитивов: bson.Regex{Pattern, Options} — то, что в старых туториалах писалось как primitive.Regex. Обе формы — карта с $regex и $options и тип bson.Regex — собирают один BSON-тип; разница только в читаемости. Поля типа называются Pattern и Options, без доллара: доллар нужен только в строковой форме.

### Соберите комбинацию условий

Восстановите фильтр: альтернативы статусов, исключение архива и сортировка по новизне.

```go
filter := bson.M{
        "$___": []bson.M{
                {"status": "active"},
                {"status": "deferred"},
        },
        "archived": bson.M{"$___": true},
}
cursor, err := coll.Find(ctx, filter, options.Find().
        Set___(bson.D{{Key: "created_at", Value: -1}}))
```

Подсказка: Альтернативы — $or со срезом; исключение значения — $ne; порядок выдачи задаёт SetSort в опциях Find.

Правильные ответы в пропусках: `or`, `ne`, `Sort`

### Соберите функцию поиска

Расставьте строки метода по порядку: экранирование ввода, фильтр с регэкспом, запрос с опциями, декодирование результата.

Правильный порядок строк:

1. `func (r *ProductRepo) Search(ctx context.Context, q string) ([]Product, error) {`
2. `  pattern := regexp.QuoteMeta(q)`
3. `  filter := bson.M{"name": bson.M{"$regex": pattern, "$options": "i"}}`
4. `  cursor, err := r.coll.Find(ctx, filter,`
5. `    options.Find().SetSort(bson.D{{Key: "name", Value: 1}}).SetLimit(20))`
6. `  if err != nil { return nil, err }`
7. `  var out []Product`
8. `  return out, cursor.All(ctx, &out)`
9. `}`

### Карточки: сложные фильтры

- **$or** — Массив условий: bson.M{"$or": []bson.M{...}} — подходит хотя бы одно. Второй ключ $or в карте молча затирается: две группы комбинируют через $and.
- **$and** — Явная конъюнкция: bson.D{{Key: "$and", Value: []bson.M{...}}}. Нужен для двух $or-групп и условий, не помещающихся в одну карту.
- **$regex** — Регэксп на сервере: bson.M{"name": bson.M{"$regex": "^A", "$options": "i"}} или значение bson.Regex{Pattern, Options}. Префикс с ^ опирается на индекс.
- **$options** — Флаги регэкспа отдельным ключом: i — без регистра, m — якоря по строкам, x — пробелы в шаблоне. Самое частое — i.
- **regexp.QuoteMeta** — Экранирует спецсимволы ввода пользователя: точка, скобка, звёздочка становятся литералами. Обязательный шаг перед подстановкой в $regex.
- **$ne и $nin** — Исключение: $ne — не равно, $nin — не входит в срез значений. Ставятся рядом с другими условиями в неявном AND.

Фильтры сложного уровня готовы: комбинации через $and и $or, частичное совпадение через $regex с экранированием, сборка динамических условий срезом пар. Они уезжают в тот же Find из седьмого урока вместе с [опциями сортировки и лимитами](/courses/mongo-driver/lessons/13-proekcii-sort-limit/) — базовые условия восьмого урока остаются фундаментом, а [итоговый проект](/courses/mongo-driver/lessons/25-itogovyj-proekt/) соберёт поиск каталога именно из этих деталей. Двух места требуют индексы: префиксный regex и $match по статусу быстры только с ними — [урок про CreateOne](/courses/mongo-driver/lessons/17-indeksy/) покажет, как измерить и построить. Перед отладкой печатайте собранный фильтр: bson.MarshalExtJSON из пятого урока покажет документ ровно в том виде, в каком его увидит сервер, — и затёртый $or ловится глазом за секунду. Проверять комбинации лучше на живых данных: песочница сайта — тематическая программа на чистом Go, поэтому соберите панельный фильтр локально и посмотрите в mongosh, какие документы он находит на каждом шаге сборки.

## Коротко: что запомнить

- Несколько ключей в одной карте — неявный AND; он покрывает условия по разным полям.
- $or принимает срез условий; два ключа $or в bson.M молча перезаписываются — комбинируйте через $and.
- $and со срезом bson.M нужен для двух $or-групп и условий, не помещающихся в одну карту.
- $regex исполняет сервер, синтаксис PCRE-подобный: префиксный ^паттерн опирается на индекс, остальное — скан.
- Пользовательский ввод перед $regex проходит regexp.QuoteMeta — спецсимволы становятся литералами.
- bson.Regex{Pattern, Options} — второй способ записать регэксп; в v1 тип назывался primitive.Regex.
- Для пользовательского поиска по всему тексту $regex не предназначен: текстовый индекс или внешний поисковик.

## Домашнее задание

Соберите функцию поиска каталога: три необязательных параметра (статус, минимальная цена, подстрока названия), срез bson.E с append по условию, QuoteMeta для ввода и сортировка с лимитом из опций Find. Затем проверьте на живой базе: добавьте индекс по name и сравните в explain префиксный паттерн со сканом — разница в плане покажет цену якоря. Попутно выведите финальный фильтр через bson.MarshalExtJSON: увиденный документ — это ровно то, что уйдёт на сервер.

## Вопросы и ответы по уроку

### Почему нельзя записать две $or-группы в одной bson.M?

Карта в Go хранит каждый ключ один раз: второй $or перезапишет первый при сборке литерала — без ошибки компиляции и без ошибки сервера. Решение: $and со срезом условий, где каждая группа живёт отдельным элементом массива. Поймать такое быстро: выведите фильтр через bson.MarshalExtJSON перед запросом — в распечатке видно, что от двух $or осталась одна.

### Как искать по части слова без скана коллекции?

Честный ответ — почти никак: только префиксные паттерны с ^ опираются на индекс. Для поиска «в середине слова» MongoDB предлагает текстовый индекс по словам, а для тяжёлых требований — внешний поисковик. $regex без якоря на больших коллекциях — бюджет скана. Граница простая: если поиск нужен пользователям каталога на сотни тысяч документов — внешний поисковик или текстовый индекс; если админам для разовой чистки — $regex с якорем и QuoteMeta.

### Чем паттерн $regex отличается от Go regexp?

Паттерн исполняет сервер MongoDB со своим PCRE-подобным движком: там есть look-ahead и look-behind, которых нет в Go RE2. Паттерн пишется под серверный синтаксис, а regexp.QuoteMeta из Go используется только для экранирования ввода перед подстановкой.
