Представьте, что вы создаёте мощное поисковое приложение. Пользователи вводят ключевые слова, а ваш бэкенд должен отправить запрос к Manticore Search и найти подходящие результаты. Распространённый (и соблазнительный!) подход — напрямую подставлять пользовательский ввод в SQL-запросы. Например, вы можете фильтровать по числовому полю, такому как категория или идентификатор записи. Если пользователь передаёт обычное значение, например 5, запрос будет SELECT * FROM products WHERE id=5. А что, если он передаст 1 OR 1=1? Запрос станет SELECT * FROM products WHERE id=1 OR 1=1 — условие всегда истинно, поэтому вместо одной строки вернутся все. Это SQL-инъекция.
К счастью, существует более безопасный и эффективный способ: prepared statements. По сути, prepared statements отделяют ваш SQL‑код от передаваемых данных. Вместо того чтобы каждый раз собирать всю строку запроса, вы один раз задаёте структуру запроса с маркерами параметров, а затем отдельно передаёте поисковые термины. Подробнее о концепции можно узнать на Wikipedia .
Manticore Search поддерживает prepared statements через стандартный протокол MySQL, и это даёт вам удобный инструмент для создания безопасных поисковых приложений. Используя prepared statements, вы не только заметно снижаете риск SQL-инъекций, но и делаете код понятнее.
prepared statements — это не просто полезная функция; иногда без них вообще не обойтись. Например, библиотека Rust sqlx работает с MySQL-эндпоинтом только через prepared statements. Кроме того, некоторые OLE DB-коннекторы, позволяющие MS SQL работать с MySQL-сервером, тоже используют prepared statements внутри.
Зачем использовать prepared statements?
Безопасность прежде всего (SQL-инъекции): SQL-инъекция — это уязвимость веб-безопасности, которая позволяет злоумышленникам вмешиваться в запросы приложения к базе данных. Она возникает, когда пользовательский ввод некорректно включается в SQL-запрос и в результате может выполниться вредоносный код. Например, рассмотрим простой поисковый запрос, построенный конкатенацией поискового термина пользователя прямо в SQL:
// Vulnerable code example (DO NOT USE!)
$productId = $_GET['search'];
$query = "SELECT * FROM products WHERE id= " . $productId;
Если $productId содержал, например, 0 OR 1=1, запрос превратится в SELECT * FROM products WHERE id= 0 OR 1=1, эффективно обходя условие WHERE и возвращая все товары.
Подготовленные выражения предотвращают это, рассматривая пользовательский ввод строго как данные, а не как часть самого SQL-команды. Для связанных параметров драйвер и Manticore обрабатывают SQL-представление значения, поэтому оно не может изменить окружающий SQL-запрос. Обратите внимание, что значения, используемые в MATCH(), по-прежнему интерпретируются согласно синтаксису полнотекстовых запросов Manticore, для которого действуют отдельные правила экранирования. Вот тот же запрос с использованием подготовленного выражения:
// Secure code example using a prepared statement
$productId = $_GET['search'];
$stmt = $mysqli->prepare("SELECT * FROM products WHERE id= ?");
$stmt->bind_param("i", $productId);
$stmt->execute();
В этом случае, даже если $productId содержит вредоносный код, он будет рассматриваться как буквальное значение, а не как исполняемый SQL.
Как они работают
prepared statements работают по простому трёхшаговому процессу:
- Подготовка: Сначала вы отправляете в Manticore Search SQL-оператор с маркерами параметров, например
?или?VEC?. Manticore разбирает этот оператор, строит план запроса и возвращает уникальный идентификатор prepared statement. - Привязка: Затем вы отправляете фактические данные — значения для маркеров параметров — в Manticore отдельно. Именно здесь и обеспечивается безопасность: данные трактуются только как данные, а не как SQL-код.
- Выполнение: Наконец, вы даёте Manticore команду выполнить prepared statement, используя сохранённый план запроса и привязанные параметры.
Проще всего думать об этом как о шаблоне. Один раз задаёте структуру, а потом подставляете в неё разные значения каждый раз, когда хотите выполнить запрос.
Маркеры параметров: ? и ?VEC?
Manticore Search использует специальные маркеры параметров, чтобы обозначать параметры внутри prepared statements:
?обозначает один параметр — это может быть целое число, число с плавающей точкой или строка. При использовании этого заполнителя Manticore автоматически выполняет экранирование и заключение в кавычки на уровне SQL для строковых значений, защищая от SQL-инъекций и обеспечивая корректное форматирование данных.?VEC?предназначен для списков числовых значений. Он ожидает строку с числами, разделёнными запятыми и необязательными пробелами (например,1, 2.3, 4, 1e-10, INF). Важно, что к значениям внутри?VEC?не применяется ни экранирование, ни кавычки. Корректный ввод состоит только из чисел, запятых и пробелов; любые другие символы, скорее всего, вызовут ошибку. Это делает маркер параметра удобным для прямой вставки числовых векторов — как векторов с плавающей точкой, так и целочисленных MVA (многозначных атрибутов).
Полнотекстовый поиск с MATCH(?): когда экранирование все еще требуется
Подготовленные выражения обрабатывают заключение в кавычки и экранирование на уровне SQL для связанных строковых параметров, поэтому вам не нужно вручную экранировать значение, чтобы безопасно включить его в SQL-запрос.
Полнотекстовый поиск немного отличается, потому что у MATCH() есть собственный синтаксис запросов.
Например, вы можете безопасно связать полнотекстовое выражение так же, как любую другую строку:
$stmt = $mysqli->prepare("SELECT * FROM products WHERE MATCH(?)");
$search = 'wireless headphones';
$stmt->bind_param("s", $search);
$stmt->execute();
Здесь подготовленное выражение гарантирует, что $search будет воспринят как строковое значение SQL, а не как часть SQL-синтаксиса.
Однако после того как Manticore получает эту строку, MATCH() разбирает ее как полнотекстовый запрос. Такие символы, как !, ", $, ', (, ), -, /, <, @, \, ^, | и ~, имеют особое значение в языке полнотекстовых запросов.
Это означает, что нужно учитывать два разных случая.
Если значение намеренно является полнотекстовым выражением, его можно связать напрямую:
$search = 'laptop | notebook';
$stmt = $mysqli->prepare("SELECT * FROM products WHERE MATCH(?)");
$stmt->bind_param("s", $search);
$stmt->execute();
| остается оператором полнотекстового OR. Он не может повлиять на окружающий SQL-запрос, потому что значение по-прежнему передается как параметр подготовленного выражения.
Если же значение должно использоваться как буквальный поисковый текст, перед связыванием нужно экранировать полнотекстовые операторы. Например, чтобы $ не был интерпретирован как полнотекстовый оператор:
$search = '\$manticore';
$stmt = $mysqli->prepare("SELECT * FROM products WHERE MATCH(?)");
$stmt->bind_param("s", $search);
$stmt->execute();
Обратная косая черта здесь нужна для полнотекстового парсера Manticore, а не для SQL. От того, можно ли искать саму пунктуацию вроде $, также зависят настройки токенизации таблицы.
Важно различать следующее:
Подготовленные выражения обрабатывают экранирование SQL; они не отключают и не экранируют синтаксис полнотекстовых запросов Manticore внутри MATCH().
Поэтому обычно вам не нужен mysqli_real_escape_string() для значений, передаваемых через ?, но при необходимости пользовательский ввод все еще может требовать экранирования полнотекстовых операторов, если он должен трактоваться как буквальный поисковый текст, а не как полнотекстовое выражение.
См. документацию Manticore по экранированию символов в строке запроса для полного набора правил экранирования.
Пример: prepared statements в PHP
Посмотрим, как prepared statements работают на практике в PHP. Разберём и простую вставку со строковыми значениями, и более сложную вставку, включающую вектор с плавающей точкой и маркер параметра ?VEC?.
Сначала простая вставка:
<?php
// Assuming you have a valid MySQLi connection established ($mysqli)
$stmt = $mysqli->prepare("INSERT INTO products (name, description) VALUES (?, ?)");
$productName = "Awesome Widget";
$productDescription = "A truly amazing widget for all your needs.";
$stmt->bind_param("ss", $productName, $productDescription); // "ss" indicates two strings
$stmt->execute();
echo "Product added successfully!";
?>
Этот код подготавливает запрос INSERT, привязывает строковые значения для названия и описания товара, а затем выполняет запрос. SQL, который в итоге выполнит Manticore, будет выглядеть так:
INSERT INTO products (name, description) VALUES ('Awesome Widget', 'A truly amazing widget for all your needs.');
Теперь разберём пример с вектором с плавающей точкой. Что такое ?VEC?? Это маркер параметра, который используется только в prepared statements, для вектора — списка чисел, например эмбеддингов или похожих данных. В Manticore SQL векторный литерал всегда записывается в скобках: (0.1, 0.2, 0.3). Поэтому, когда вы используете prepared statement и передаёте параметр-вектор, скобки пишутся прямо в SQL-строке, а ?VEC? ставится на место самих чисел. Вы привязываете только числа, разделённые запятыми, например "0.1,0.2,0.3"; скобки ( и ) не привязываются — они остаются в запросе. Без prepared statements полный литерал (0.1, 0.2, 0.3) пришлось бы собирать вручную.
Иными словами, используйте ? для скалярных значений и полнотекстовых строк запроса, ?VEC? для числовых списков и помните, что строки, передаваемые в MATCH(?), могут дополнительно требовать полнотекстового экранирования в зависимости от того, должны ли специальные операторы интерпретироваться или восприниматься буквально.
В PHP mysqli значения ?VEC? обычно привязываются как строки, поэтому в этом примере естественный выбор — iss. Если вы хотите передавать более объёмную векторную нагрузку, параметр также можно привязать как b и отправить содержимое через send_long_data().
<?php
// Assuming you have a valid MySQLi connection established ($mysqli)
$stmt = $mysqli->prepare("INSERT INTO items (item_id, coords, features) VALUES (?, (?VEC?),(?VEC?))");
$itemId = 123;
$coordVector = "20.245,54.354,30.000"; // that is vector of floats
$featureSet = "1,4,20,456,112,3"; // that is set of integer values (MVA)
$stmt->bind_param("iss", $itemId, $coordVector, $featureSet); // "i" for integer (itemId), "s" for string
$stmt->execute();
echo "Item with feature vector added successfully!";
$itemId = 124;
$coordVector = "18.500,42.000,31.125"; // Another float vector
$featureSet = "0,6,34,665,22,3445,221,564,2232,5644,43"; // Example with more feature values
// For larger payloads you can bind the second ?VEC? as a blob and stream it.
$featurePlaceholder = "";
$stmt->bind_param("isb", $itemId, $coordVector, $featurePlaceholder); // "b" is for blob data
// bind_param() must be called before send_long_data().
$stmt->send_long_data(2, $featureSet); // zero-based index: 2 means the third bound parameter
$stmt->execute();
echo "Item with feature vector added successfully!";
?>
Обратите внимание: скобки — это часть SQL-строки в вызове prepare(). Мы привязываем только значения внутри скобок, используя маркер параметра ?VEC?. SQL, который выполнит Manticore, будет таким:
INSERT INTO items (item_id, coords, features) VALUES (123, (20.245,54.354,30.000), (1,4,20,456,112,3));
INSERT INTO items (item_id, coords, features) VALUES (124, (18.500,42.000,31.125), (0,6,34,665,22,3445,221,564,2232,5644,43));
Использование ?VEC? в prepared statement даёт те же преимущества, что и маркер параметра ?: значения вектора отправляются как данные, а не как часть SQL-текста, поэтому их нельзя интерпретировать как SQL и они не могут вызвать инъекцию. Кроме того, вам не нужно вручную собирать или экранировать векторный литерал в приложении — Manticore получает привязанные числа и сам корректно форматирует вектор, сохраняя запрос безопасным, а данные — согласованными.
Важные соображения и ограничения
Хотя prepared statements в Manticore — мощный инструмент, у них есть несколько ограничений, о которых стоит помнить.
Мульти-запросы: В рамках одного prepared statement разрешён только один SQL-оператор. Попытки использовать мульти-запросы, например SELECT ...; SHOW META, завершатся ошибкой. Если вам нужно выполнить несколько операторов, подготовьте отдельный запрос для каждого и выполняйте их последовательно в рамках одной сессии.
Числовые типы: Некоторые драйверы баз данных (например, mysql2 для Node.js) могут по умолчанию отправлять числовые параметры как DOUBLE. Это может привести к неожиданному поведению, если вам нужна строгая обработка целых чисел (например, чтобы отрицательные ID отклонялись). В таких случаях можно отправлять целые числа как строки или использовать специфичные для драйвера целочисленные типы (например, BigInt), чтобы обеспечить корректную обработку данных.
Пользователи Rust sqlx: Если вы используете crate sqlx в Rust, имейте в виду: при чтении строк результата нужно использовать индексы столбцов, а не их имена. Хотя имена столбцов присутствуют в наборе результатов, sqlx не использует их для сопоставления. Например, используйте row.try_get(0)? вместо row.try_get("id")?.
Заключение
Подготовленные выражения дают критически важное сочетание безопасности, читаемости и потенциального выигрыша в производительности при работе с Manticore Search. Разделяя SQL-логику и данные, вы резко снижаете риск SQL-инъекций, улучшаете сопровождаемость кода и потенциально ускоряете выполнение запросов. Для обычных связанных значений это также убирает необходимость в ручном SQL-экранировании. Исключение, о котором стоит помнить, - полнотекстовые запросы: MATCH(?) можно безопасно параметризовать, но его значение по-прежнему разбирается с помощью языка полнотекстовых запросов Manticore, поэтому буквальный поисковый текст может требовать полнотекстового экранирования. Мы настоятельно рекомендуем использовать подготовленные выражения в ваших приложениях с Manticore Search.
Если хотите разобраться глубже, обязательно загляните в эти материалы:
- Документация Manticore Search по prepared statements: https://manual.manticoresearch.com/Connecting_to_the_server/MySQL_protocol#Prepared-statements
- Wikipedia — prepared statements: https://en.wikipedia.org/wiki/Prepared_statement
Это руководство даёт хорошую основу для эффективного использования prepared statements в проектах на Manticore Search и помогает создавать более безопасные, эффективные и поддерживаемые приложения.
