Представьте, что вы создаёте мощное поисковое приложение. Пользователи вводят ключевые слова, а ваш бэкенд должен отправить запрос к 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 и возвращая все товары.

prepared statements предотвращают это, потому что пользовательский ввод рассматривается строго как данные, а не как часть SQL-команды. Драйвер базы данных сам обрабатывает экранирование и кавычки, нейтрализуя потенциально опасные символы. Ниже — тот же запрос, но уже с prepared statement:

// 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 работают по простому трёхшаговому процессу:

  1. Подготовка: Сначала вы отправляете в Manticore Search SQL-оператор с маркерами параметров, например ? или ?VEC?. Manticore разбирает этот оператор, строит план запроса и возвращает уникальный идентификатор prepared statement.

  2. Привязка: Затем вы отправляете фактические данные — значения для маркеров параметров — в Manticore отдельно. Именно здесь и обеспечивается безопасность: данные трактуются только как данные, а не как SQL-код.

  3. Выполнение: Наконец, вы даёте Manticore команду выполнить prepared statement, используя сохранённый план запроса и привязанные параметры.

Проще всего думать об этом как о шаблоне. Один раз задаёте структуру, а потом подставляете в неё разные значения каждый раз, когда хотите выполнить запрос.

Маркеры параметров: ? и ?VEC?

Manticore Search использует специальные маркеры параметров, чтобы обозначать параметры внутри prepared statements:

  • ? означает один параметр — это может быть целое число, число с плавающей точкой или строка. При использовании этого маркера параметра Manticore автоматически выполняет экранирование и расставляет кавычки для строковых значений, защищая от SQL-инъекций и обеспечивая корректное форматирование данных.

  • ?VEC? предназначен для списков числовых значений. Он ожидает строку с числами, разделёнными запятыми и необязательными пробелами (например, 1, 2.3, 4, 1e-10, INF). Важно, что к значениям внутри ?VEC? не применяется ни экранирование, ни кавычки. Корректный ввод состоит только из чисел, запятых и пробелов; любые другие символы, скорее всего, вызовут ошибку. Это делает маркер параметра удобным для прямой вставки числовых векторов — как векторов с плавающей точкой, так и целочисленных MVA (многозначных атрибутов).

Пример: 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) пришлось бы собирать вручную.

В 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")?.

Заключение

prepared statements дают важное сочетание безопасности, читаемости и потенциального прироста производительности при работе с Manticore Search. Отделяя SQL-логику от данных, вы заметно снижаете риск SQL-инъекций, упрощаете поддержку кода и потенциально ускоряете выполнение запросов. Мы настоятельно рекомендуем использовать prepared statements в ваших приложениях на базе Manticore Search.

Если хотите разобраться глубже, обязательно загляните в эти материалы:

Это руководство даёт хорошую основу для эффективного использования prepared statements в проектах на Manticore Search и помогает создавать более безопасные, эффективные и поддерживаемые приложения.

Комментарии (7)


  1. sadiquemannan
    09.04.2026 05:24

    Great breakdown of prepared statements in Manticore Search. I especially like how you showed the difference between raw SQL and the parameterized version. I’m curious: how does the performance of prepared statements in Manticore compare to plain queries under heavy read/write load, and are there any best practices for batching parameterized queries?


    1. ManticoreSearch Автор
      09.04.2026 05:24

      We didn't benchmark it, but it's an interesting question. We'd appreciate it if you could do that and share your findings. What exactly do you mean by "batching"?


      1. sadiquemannan
        09.04.2026 05:24

        Thanks for the clarification, that makes sense. By “batching” I meant sending many parameter sets for the same prepared statement in one go (for example, inserting/updating a large number of rows in a single round‑trip instead of one by one) to see how much overhead the prepare/bind/execute cycle adds compared to plain SQL. I’ll try to set up a small benchmark with Manticore Load or a custom script to compare single‑row vs multi‑row/batched inserts and SELECTs using prepared statements vs normal queries, and will share the results once I have something meaningful.


  1. Akina
    09.04.2026 05:24

    Manticore Search поддерживает prepared statements через стандартный протокол MySQL, и это даёт вам удобный инструмент для создания безопасных поисковых приложений.

    "Prepared statements через стандартный протокол MySQL", на который вы ссылаетесь, предполагает, что на сервер MySQL передаются запросы:

    PREPARE stmt FROM {строка с текстом запроса с плейсхолдерами, 
                       или определённая пользователем переменная с текстом запроса};
    EXECUTE stmt USING ( {набор параметров № 1} );
    EXECUTE stmt USING ( {набор параметров № 2} );
    ...
    DROP PREPARE stmt;

    Однако всё, написанное далее, говорит о том, что Manticore Search ИГНОРИРУЕТ стандартные prepared statements в MySQL и выполняет все операции САМОСТОЯТЕЛЬНО. Экранирует переданные значения, конкатенирует их в текст запроса на место плейсхолдеров, и затем передаёт полученный текст запроса на MySQL. Самого обычного запроса. То есть это вовсе даже не серверный prepared statement, а его эмуляция на стороне клиента.

    Но проблема в том, что Manticore Search - не MySQL. И он выполняет разнообразные экранирования по своему разумению, а не так, как это понимает MySQL. Нет, я понимаю, что разработчики не лохи, исследовали исходники MySQL вдоль и поперёк, сто раз всё проверили. Но все мы люди, все человеки, а потому потенция ошибки остаётся. И я, к примеру, не знаю, как будет обработана специально созданная текстовая строка в какой-нибудь экзотической или вообще несуществующей кодировке. Вы готовы гарантировать, что невозможно создание такой строки, которая после обработки кодом Manticore и конкатенации в текст запроса создаст-таки инъекцию? Я лично - нет.

    PS. К слову, стандартные подготовленные запросы в MySQL поддерживают исключительно скалярные параметры предопределённых типов, соответственно никакие списки не допускаются, ибо такого типа данных в MySQL просто нет.

    PPS. Стандартный prepared statement в MySQL прекрасно защитит от инъекции, даже если передаваемые в EXECUTE значения параметров будут злонамеренно модифицированы где-то в середине при прохождении трафика от клиента. Тогда как реализация на клиенте от такого не защищает.


    1. klirichek
      09.04.2026 05:24

      MySQL поддерживает два типа prepared statements. Один - это через протокол. Буквально, в бинарном протоколе есть подмножество команд, которые реализуют perepare, execute и опционально long_data. Через консольный mysql клиент "пощупать" их невозможно, он эти команды не шлёт. Но вот некоторые другие клиенты, как оказалось, буквально ВСЮ работу делают через prepared statements. Даже какой-нибудь "show status" они шлют как prepare + execute + delete prepared. Собственно, именно эта часть протокола и реализована.

      А то, о чём вы говорите - это другой тип, "синтаксический сахар" поверх обычного текстового протокола. Этот тип не поддерживается, и его не реализовывали.


      1. Akina
        09.04.2026 05:24

        Повторю ещё раз:

        стандартные подготовленные запросы в MySQL поддерживают исключительно скалярные параметры предопределённых типов, соответственно никакие списки не допускаются, ибо такого типа данных в MySQL просто нет. ,

        В статье же рассказывается о вставке списка в текст. Через подготовленные запросы MySQL этого не сделать - хоть в текстовой форме, хоть через протокол, о котором вы говорите. То есть список вставляется в текст запроса до отправки на сервер БД - другого варианта осуществить такую вставку я не вижу. Нет, если говорить строго, то существует ненулевая вероятность, что в протоколе такая возможность есть, а из текстовой формы нет - но вы не представляете, как такое мне лично сомнительно..

        А что самое неприятное - то, что автор статьи в очень неуверенной форме говорит: "Корректный ввод состоит только из чисел, запятых и пробелов; любые другие символы, скорее всего, вызовут ошибку". Я это понимаю однозначно - может и не вызвать. А дальше можно накрутить и до инъекции.

        К слову, у MySQL есть такая штука как General Log. Если его включить, то мы увидим все поступившие на MySQl запросы, причём строго в той форме, в какой они поступили. Если бы автор статьи озаботился получением этих данных, вопросов, непониманий и разночтений бы возникало куда как меньше.

        Вот автор пишет:

        Этот код подготавливает запрос INSERT, привязывает строковые значения для названия и описания товара, а затем выполняет запрос. SQL, который в итоге выполнит Manticore, будет выглядеть так

        Вот откуда он этот текст запроса взял? придумал? прикинул, как оно должно быть? вычитал из лога? Вот если бы в логе по показанному коду мы увидели показанный запрос, можно бы было смело утверждать, что prepared statement в Manticore есть эмуляция и фикция. А сейчас есть только вульгарное "а хрен знает"...


        1. klirichek
          09.04.2026 05:24

          Синтаксис в разных БД разный. У Mysql строго вопросики, у Pgsql иной. В мантикоре из-за наличия списков сделали ещё ?VEC?. Протоколу всё равно, он просто передаёт символы, не углубляясь в детали.

          Список не вставляется в текст запроса. Он отправляется либо в пэйлоад команды execute, либо заранее, через команду long_blob. В этом можно убедиться, например, рассматривая фактический обмен данными с помощью tcpdump/wireshark.

          У MySQL свои собственные подготовленные запросы. Но мантикора - не MySQL. Никто не мешает использовать протокол MySQL, но слать через него собственные вариации, что, собственно, и сделано.

          Списки валидируются - там, действительно, допускаются ТОЛЬКО числа (включая "не-числа" inf, nan), запятые и пробелы.

          Про General Log - это частная особенность MySQL. В мантикоре для этого есть query.log, и можно при желании увеличить verbosity общего лога. Но надо понимать, бинарный протокол всё же разнообразнее, чем содержимое, прилетающее с пэйлоадом команды MYSQL_COM_QUERY(3) - т.е. текстовые запросы.

          Prepared statements - это про MYSQL_COM_STMT_PREPARE, MYSQL_COM_STMT_EXECUTE, MYSQL_COM_STMT_SEND_LONG_DATA, MYSQL_COM_STMT_CLOSE и MYSQL_COM_STMT_RESET. Речь именно об этих командах протокола