实现一个搜索引擎

为了实现搜索引擎,我们需要为搜索中包含的多个列做出规定。此外,重要的是要认识到,搜索项可能会在字段中间被发现,而用户很少会提供足够的信息来进行精确匹配。相应地,我们将严重依赖SQL LIKE %value% 子句。

如何做…

1.首先,我们定义一个基本的类来存放搜索标准。这个对象包含三个属性:key,它最终代表一个数据库列;操作符(LIKE<>等);还有一个可选的项目。之所以说项是可选的,是因为有些操作符,如IS NOT NULL,不需要特定的数据。

  1. namespace Application\Database\Search;
  2. class Criteria
  3. {
  4. public $key;
  5. public $item;
  6. public $operator;
  7. public function __construct($key, $operator, $item = NULL)
  8. {
  9. $this->key = $key;
  10. $this->operator = $operator;
  11. $this->item = $item;
  12. }
  13. }
  1. 接下来我们需要定义一个类,Application\Database\Search\Engine,并提供必要的类常量和属性。$columns$mapping之间的区别在于,$columns持有的信息将最终出现在HTML SELECT字段(或等价物)中。出于安全考虑,我们不想暴露数据库列的实际名称,因此需要另一个数组$mapping
  1. namespace Application\Database\Search;
  2. use PDO;
  3. use Application\Database\Connection;
  4. class Engine
  5. {
  6. const ERROR_PREPARE = 'ERROR: unable to prepare statement';
  7. const ERROR_EXECUTE = 'ERROR: unable to execute statement';
  8. const ERROR_COLUMN = 'ERROR: column name not on list';
  9. const ERROR_OPERATOR= 'ERROR: operator not on list';
  10. const ERROR_INVALID = 'ERROR: invalid search criteria';
  11. protected $connection;
  12. protected $table;
  13. protected $columns;
  14. protected $mapping;
  15. protected $statement;
  16. protected $sql = '';
  1. 接下来,我们定义一组我们愿意支持的操作符。键代表实际的SQL。值是将出现在表格中的内容。
  1. protected $operators = [
  2. 'LIKE' => 'Equals',
  3. '<' => 'Less Than',
  4. '>' => 'Greater Than',
  5. '<>' => 'Not Equals',
  6. 'NOT NULL' => 'Exists',
  7. ];
  1. 构造函数接受一个数据库连接实例作为参数。对于我们的目的,我们将使用Application\Database\Connection,定义在第5章,与数据库的交互。我们还需要提供数据库表的名称,以及 $columns,一个包含任意列键和标签的数组,它们将出现在HTML表单中。这将引用$mapping,其中键与$columns匹配,但值代表实际的数据库列名。
  1. public function __construct(Connection $connection,
  2. $table, array $columns, array $mapping)
  3. {
  4. $this->connection = $connection;
  5. $this->setTable($table);
  6. $this->setColumns($columns);
  7. $this->setMapping($mapping);
  8. }
  1. 在构造函数之后,我们提供了一系列有用的getter和setter。
  1. public function setColumns($columns)
  2. {
  3. $this->columns = $columns;
  4. }
  5. public function getColumns()
  6. {
  7. return $this->columns;
  8. }
  9. // etc.
  1. 最关键的方法可能是建立要准备的SQL语句。在最初的 SELECT 设置之后,我们添加一个 WHERE 子句,使用 $mapping 来添加实际的数据库列名。然后我们添加操作符,并实现switch(),根据操作符,可以添加或不添加代表搜索项的命名占位符。
  1. public function prepareStatement(Criteria $criteria)
  2. {
  3. $this->sql = 'SELECT * FROM ' . $this->table . ' WHERE ';
  4. $this->sql .= $this->mapping[$criteria->key] . ' ';
  5. switch ($criteria->operator) {
  6. case 'NOT NULL' :
  7. $this->sql .= ' IS NOT NULL OR ';
  8. break;
  9. default :
  10. $this->sql .= $criteria->operator . ' :'
  11. . $this->mapping[$criteria->key] . ' OR ';
  12. }
  1. 现在已经定义了核心的SELECT,我们删除任何尾部的OR关键字,并添加一个子句,使结果根据搜索列进行排序。然后将该语句发送到数据库中进行准备。
  1. $this->sql = substr($this->sql, 0, -4)
  2. . ' ORDER BY ' . $this->mapping[$criteria->key];
  3. $statement = $this->connection->pdo->prepare($this->sql);
  4. return $statement;
  5. }
  1. 现在我们准备好了,进入正题,search()方法。我们接受一个Application\Database\Search\Criteria对象作为参数。这确保了我们至少有一个项目键和操作符。为了安全起见,我们添加一个if()语句来检查这些属性。
  1. public function search(Criteria $criteria)
  2. {
  3. if (empty($criteria->key) || empty($criteria->operator)) {
  4. yield ['error' => self::ERROR_INVALID];
  5. return FALSE;
  6. }
  1. 然后,我们使用 try / catch 调用 prepareStatement() 来捕获错误。
  1. try {
  2. if (!$statement = $this->prepareStatement($criteria)) {
  3. yield ['error' => self::ERROR_PREPARE];
  4. return FALSE;
  5. }
  1. 接下来我们建立一个将提供给 execute() 的参数数组。键代表数据库列名,它在准备好的语句中被用作占位符。注意,我们不使用 = ,而是使用LIKE %value% construct
  1. $params = array();
  2. switch ($criteria->operator) {
  3. case 'NOT NULL' :
  4. // do nothing: already in statement
  5. break;
  6. case 'LIKE' :
  7. $params[$this->mapping[$criteria->key]] =
  8. '%' . $criteria->item . '%';
  9. break;
  10. default :
  11. $params[$this->mapping[$criteria->key]] =
  12. $criteria->item;
  13. }
  1. 执行该语句,并使用 yield 关键字返回结果,这实际上是把这个方法变成了一个生成器。
  1. $statement->execute($params);
  2. while ($row = $statement->fetch(PDO::FETCH_ASSOC)) {
  3. yield $row;
  4. }
  5. } catch (Throwable $e) {
  6. error_log(__METHOD__ . ':' . $e->getMessage());
  7. throw new Exception(self::ERROR_EXECUTE);
  8. }
  9. return TRUE;
  10. }

如何运行…

将这个示例中的代码放在 Application\Database\Search下的Criteria.phpEngine.php文件中。然后你可以定义一个调用脚本,chap_10_search_engine.php,用来设置自动加载。你可以利用第5章,与数据库的交互中讨论的 Application\Database\Connection 类,以及第6章,构建可扩展网站中涉及的表单元素类。

  1. <?php
  2. define('DB_CONFIG_FILE', '/../config/db.config.php');
  3. require __DIR__ . '/../Application/Autoload/Loader.php';
  4. Application\Autoload\Loader::init(__DIR__ . '/..');
  5. use Application\Database\Connection;
  6. use Application\Database\Search\ { Engine, Criteria };
  7. use Application\Form\Generic;
  8. use Application\Form\Element\Select;

现在可以定义哪些数据库列将出现在表格中,以及一个匹配的映射文件。

  1. $dbCols = [
  2. 'cname' => 'Customer Name',
  3. 'cbal' => 'Account Balance',
  4. 'cmail' => 'Email Address',
  5. 'clevel' => 'Level'
  6. ];
  7. $mapping = [
  8. 'cname' => 'name',
  9. 'cbal' => 'balance',
  10. 'cmail' => 'email',
  11. 'clevel' => 'level'
  12. ];

现在可以设置数据库连接并创建搜索引擎实例。

  1. $conn = new Connection(include __DIR__ . DB_CONFIG_FILE);
  2. $engine = new Engine($conn, 'customer', $dbCols, $mapping);

为了显示适当的下拉式 SELECT 元素,我们基于 Application\Form\* 类定义了包装器和元素。

  1. $wrappers = [
  2. Generic::INPUT => ['type' => 'td', 'class' => 'content'],
  3. Generic::LABEL => ['type' => 'th', 'class' => 'label'],
  4. Generic::ERRORS => ['type' => 'td', 'class' => 'error']
  5. ];
  6. // define elements
  7. $fieldElement = new Select('field',
  8. Generic::TYPE_SELECT,
  9. 'Field',
  10. $wrappers,
  11. ['id' => 'field']);
  12. $opsElement = new Select('ops',
  13. Generic::TYPE_SELECT,
  14. 'Operators',
  15. $wrappers,
  16. ['id' => 'ops']);
  17. $itemElement = new Generic('item',
  18. Generic::TYPE_TEXT,
  19. 'Searching For ...',
  20. $wrappers,
  21. ['id' => 'item','title' => 'If more than one item, separate with commas']);
  22. $submitElement = new Generic('submit',
  23. Generic::TYPE_SUBMIT,
  24. 'Search',
  25. $wrappers,
  26. ['id' => 'submit','title' => 'Click to Search', 'value' => 'Search']);

然后我们获取输入参数(如果定义了),设置表单元素选项,创建搜索条件,并运行搜索。

  1. $key = (isset($_GET['field']))
  2. ? strip_tags($_GET['field']) : NULL;
  3. $op = (isset($_GET['ops'])) ? $_GET['ops'] : NULL;
  4. $item = (isset($_GET['item'])) ? strip_tags($_GET['item']) : NULL;
  5. $fieldElement->setOptions($dbCols, $key);
  6. $itemElement->setSingleAttribute('value', $item);
  7. $opsElement->setOptions($engine->getOperators(), $op);
  8. $criteria = new Criteria($key, $op, $item);
  9. $results = $engine->search($criteria);
  10. ?>

显示逻辑主要面向渲染表单。更详细的介绍在第6章《构建可扩展网站》中讨论,但我们在这里展示的是核心逻辑。

  1. <form name="search" method="get">
  2. <table class="display" cellspacing="0" width="100%">
  3. <tr><?= $fieldElement->render(); ?></tr>
  4. <tr><?= $opsElement->render(); ?></tr>
  5. <tr><?= $itemElement->render(); ?></tr>
  6. <tr><?= $submitElement->render(); ?></tr>
  7. <tr>
  8. <th class="label">Results</th>
  9. <td class="content" colspan=2>
  10. <span style="font-size: 10pt;font-family:monospace;">
  11. <table>
  12. <?php foreach ($results as $row) : ?>
  13. <tr>
  14. <td><?= $row['id'] ?></td>
  15. <td><?= $row['name'] ?></td>
  16. <td><?= $row['balance'] ?></td>
  17. <td><?= $row['email'] ?></td>
  18. <td><?= $row['level'] ?></td>
  19. </tr>
  20. <?php endforeach; ?>
  21. </table>
  22. </span>
  23. </td>
  24. </tr>
  25. </table>
  26. </form>

以下是浏览器的输出示例。

实现一个搜索引擎 - 图1