想象一下你正在构建一个强大的搜索应用程序。用户输入关键词,你的后端需要查询Manticore Search数据库以找到匹配的结果。一种常见(且诱人的!)方法是将用户输入直接嵌入到SQL查询中。例如,你可能会通过数字字段(如类别或记录ID)进行过滤。如果用户传递一个正常值如5,查询是SELECT * FROM products WHERE id=5。但如果他们传递1 OR 1=1?查询会变成SELECT * FROM products WHERE id=1 OR 1=1——条件始终为真,因此查询会返回所有行而不是一行。这就是SQL注入。
幸运的是,有一种更安全且更高效的方法:预处理语句。本质上,预处理语句将你的SQL代码与传递的数据分离开来。你不需要每次构建整个查询字符串,而是定义一次带有占位符的查询结构,然后单独提供搜索项。你可以在Wikipedia 上了解更多关于这个概念的信息。
Manticore Search通过标准的MySQL协议支持预处理语句,这为你构建安全的搜索应用程序提供了一个强大的工具。通过使用预处理语句,你不仅会显著降低SQL注入的风险,还会提高代码的可读性。
预处理语句不仅仅是一个功能;有时它们是必需的。例如,Rust的sqlx库仅使用预处理语句与MySQL端点进行交互。此外,一些启用MS SQL与MySQL服务器通信的OLE DB连接器在内部使用预处理语句。
为什么使用预处理语句?
首要考虑:安全(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。
它们是如何工作的
预处理语句通过一个简单的三步过程进行操作:
- 准备:首先,你将带有占位符(如
?或?VEC?)的SQL语句发送到Manticore Search。Manticore解析该语句并创建查询计划。然后它返回此预处理语句的唯一标识符。 - 绑定:接下来,你将实际数据——占位符的值——单独发送到Manticore。这是安全性的关键所在;数据被纯粹视为数据,而不是SQL代码。
- 执行:最后,你指示Manticore使用存储的查询计划和绑定参数来执行预处理语句。
把它想象成创建一个模板。你一次构建结构,然后每次需要使用它时用不同的信息填充空白。
参数占位符:? & ?VEC?
Manticore Search使用特定的占位符来识别预处理语句中的参数:
?表示一个单独的参数,这个参数可以是整数、浮点数或字符串。使用这个占位符时,Manticore 会自动处理字符串值在 SQL 层面的转义和引号包裹,防止 SQL 注入,并确保数据格式正确。?VEC?专为数字值列表设计。它期望一个包含用逗号和可选空格分隔的数字的字符串(例如,1, 2.3, 4, 1e-10, INF)。关键的是,不会对?VEC?中的值进行转义或引号处理。有效输入仅包含数字、逗号和空格;任何其他字符可能会导致错误。这使其非常适合直接插入数值向量到你的数据中——包括浮点向量和整数MVAs(多值属性)。
使用 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 转义;它们不会禁用或转义 MATCH() 内部的 Manticore 全文查询语法。
所以,对于通过 ? 传入的值,你通常不需要 mysqli_real_escape_string(),但当用户输入应被解释为字面搜索文本而不是全文表达式时,你可能仍然需要对 Manticore 全文运算符进行转义。
有关完整的转义规则,请参阅 Manticore 文档中关于在查询字符串中转义字符 的说明。
示例:PHP中的预处理语句
让我们看看预处理语句在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语句,绑定产品名称和描述的字符串值,然后执行查询。Manticore执行的SQL将是:
INSERT INTO products (name, description) VALUES ('Awesome Widget', 'A truly amazing widget for all your needs.');
现在,让我们处理一个使用浮点向量的示例。什么是?VEC?? 它是一个占位符(仅在预处理语句中使用),用于表示向量——一个数字列表,例如嵌入或类似数据。在Manticore SQL中,向量字面量始终用括号编写:(0.1, 0.2, 0.3)。因此,当你使用预处理语句并有一个向量参数时,你在SQL字符串中编写这些括号,并在数字位置使用?VEC?。你只绑定逗号分隔的数字(例如,"0.1,0.2,0.3");你不绑定(和)——它们保留在查询中。没有预处理语句,你必须自己在查询字符串中构建完整的字面量(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!";
?>
请注意,在 prepare() 调用中,括号是 SQL 字符串的一部分。我们仅使用 ?VEC? 占位符绑定括号内的 值。Manticore 执行的 SQL 将是:
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? 与使用 ? 占位符一样,可以带来相同的优势:向量值作为数据发送,而不是作为 SQL 文本的一部分,因此它们不会被解释为 SQL,也不会导致注入。您还可以避免在应用程序中手动构建或转义向量字面量——Manticore 接收绑定的数字并正确格式化向量,这使查询更安全,数据更一致。
重要注意事项与限制
尽管功能强大,但 Manticore 的预处理语句有一些需要注意的限制。
多查询: 每个预处理语句只允许一个 SQL 语句。尝试使用多查询(例如 SELECT ...; SHOW META)将失败。如果需要执行多个语句,请为每个语句分别准备,并在同一个会话中按顺序执行它们。
数值类型: 某些数据库驱动(如 Node.js 的 mysql2)可能会默认将数值参数作为 DOUBLE 发送。如果您需要严格的整数行为(例如拒绝负数 ID),这可能导致意外行为。在这种情况下,考虑将整数作为字符串发送,或使用特定于驱动的整数类型(例如 BigInt)以确保正确的数据处理。
Rust sqlx 用户: 如果您在 Rust 中使用 sqlx crate,请注意在读取结果集行时,必须使用列 索引 而不是列名。虽然结果集中存在列名,但 sqlx 不会使用它们进行映射。例如,使用 row.try_get(0)? 而不是 row.try_get("id")?。
结论
在使用 Manticore Search 时,预处理语句在安全性、可读性和潜在性能提升方面提供了关键组合。通过将 SQL 逻辑与数据分离,你可以显著降低 SQL 注入攻击的风险,提高代码可维护性,并有可能加快查询执行。对于普通绑定值,这同样免去了手动 SQL 转义的需要。值得记住的例外是全文查询:MATCH(?) 可以安全地参数化,但其值仍会使用 Manticore 的全文查询语言进行解析,因此字面搜索文本可能还需要全文转义。我们强烈建议你在 Manticore Search 应用中采用预处理语句。
如需更深入的信息,请务必查阅以下资源:
- Manticore Search 预处理语句文档:https://manual.manticoresearch.com/Connecting_to_the_server/MySQL_protocol#Prepared-statements
- Wikipedia - 预处理语句:https://en.wikipedia.org/wiki/Prepared_statement
本指南为在 Manticore Search 项目中有效使用预处理语句提供了坚实的基础,从而实现更安全、高效和可维护的应用程序。
