本文概述
如果你的Symfony 5应用程序使用MySQL作为默认数据库管理器, 那么你将知道在数据库中开发模糊搜索功能是多么困难, 而在使用Doctrine时, 更糟。最好, 最有效的解决方案是将需要搜索的数据迁移到elasticsearch, 但是, 如果你不想在项目中实施新技术, 则可以坚持使用默认的和众所周知的技术。就像有一些工具可以使PHP仅使用PHP原生生成PDF(例如TCPDF)一样, 在PHP项目中实现模糊搜索也存在TNTSearch。
TNTSearch是一个完全用PHP编写的功能齐全的全文搜索引擎。其简单的配置使你可以在短短几分钟内为你的网站添加出色的搜索体验。它还具有地理搜索功能和文本分类器。该项目最著名的功能是:
- 模糊搜索
- 当你键入功能时
- 地理搜索
- 文本分类
- 发芽
- 自定义标记器
- bm25排名算法(是非常重要的科学知识, 我们最初可能会忽略它们)
- 布尔搜索
- 结果突出显示
在本文中, 我们将向你介绍如何在应用程序中实现此模糊搜索功能
1.安装TNTSearch
与Symfony 5一样, TNTSearch库需要在系统上安装以下扩展:
- PHP> = 7.1
- PDO PHP扩展
- SQLite PHP扩展
- mbstring PHP扩展
在安装过程中, composer仍将验证所提及的扩展在已安装的PHP版本上是否可用, 并将进行安装。打开一个终端, 切换到symfony项目的根目录, 并使用以下命令安装该库:
composer require teamtnt/tntsearch
该软件包具有很多辅助功能, 例如jaro-winkler和余弦相似度, 用于距离计算。它支持英语, 克罗地亚语, 阿拉伯语, 意大利语, 俄语, 葡萄牙语和乌克兰语的词干。安装软件包后, 我们将能够继续创建模糊搜索引擎。
有关此项目的更多信息, 请访问Github上的官方资源库。
2.创建模糊搜索索引文件
现在开始, 你需要基本了解库功能的工作原理。首先, 你需要创建一个索引文件, 其中包含要公开进行模糊搜索的数据库的所有数据。例如, 在本文中, 我们在MySQL数据库中将有一个表, 该表对应于世界知名音乐家的集合, 例如, 可以通过PHPMyAdmin查看该表:
现在我们知道索引将包含什么, 我们将开始在项目中编写一些代码来生成它。本质上, TNTSearch库需要直接连接到MySQL数据库, 如以下示例所示:
use TeamTNT\TNTSearch\TNTSearch;
$tnt = new TNTSearch;
$tnt->loadConfig([
'driver' => 'mysql', 'host' => 'localhost', 'database' => 'dbname', 'username' => 'user', 'password' => 'pass', 'storage' => '/var/www/tntsearch/examples/', 'stemmer' => \TeamTNT\TNTSearch\Stemmer\PorterStemmer::class//optional
]);
但是, 在使用Symfony 5项目时, 你知道数据库配置存储在项目根目录下的.env文件中, 该行如下所示:
DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
因此, 为了获得Symfony中的信息, 我们将创建以下方法(本教程将在单个控制器内进行, 仅出于学习目的, 你可以自由修改逻辑, 或者通过命令公开索引的生成如果不需要不断修改它):
<?php
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DefaultController extends AbstractController
{
/**
* Returns an array with the configuration of TNTSearch with the
* database used by the Symfony project.
*
* @return type
*/
private function getTNTSearchConfiguration(){
$databaseURL = $_ENV['DATABASE_URL'];
$databaseParameters = parse_url($databaseURL);
$config = [
'driver' => $databaseParameters["scheme"], 'host' => $databaseParameters["host"], 'database' => str_replace("/", "", $databaseParameters["path"]), 'username' => $databaseParameters["user"], 'password' => $databaseParameters["pass"], // In Windows:
// C:\\xampp738\\htdocs\\myproject/fuzzy_storage
// Or Linux:
// /var/www/vhosts/myproject/fuzzy_storage
//
// Create the fuzzy_storage directory in your project to store the index file
'storage' => '/var/www/vhosts/myproject/fuzzy_storage/', // A stemmer is optional
'stemmer' => \TeamTNT\TNTSearch\Stemmer\PorterStemmer::class
];
return $config;
}
}
正如方法所解释的, 它将返回具有数据库配置的数组, 我们将使用该数据库通过TNTSearch创建索引。我们还提到了你需要在本教程中创建的项目的根目录中的Fuzzy_storage目录, 因为我们会将索引保存在其中, 但是你可以根据需要随意更改它。现在有了配置对象, 我们将继续在控制器中创建第一个路由, 这将允许你创建索引文件。我们的路线将是/ generate-index, 并且将具有以下代码(在示例中, 我们将省略getTNTSearchConfiguration方法, 但是应该存在):
<?php
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// Import TNTSearch
use TeamTNT\TNTSearch\TNTSearch;
class DefaultController extends AbstractController
{
/**
* @Route("/generate-index", name="app_generate-index")
*/
public function generate_index()
{
$tnt = new TNTSearch;
// Obtain and load the configuration that can be generated with the previous described method
$configuration = $this->getTNTSearchConfiguration();
$tnt->loadConfig($configuration);
// The index file will have the following name, feel free to change it as you want
$indexer = $tnt->createIndex('artists.index');
// The result with all the rows of the query will be the data
// that the engine will use to search, in our case we only want 2 columns
// (note that the primary key needs to be included)
$indexer->query('SELECT id, name, slug FROM artists;');
// Generate index file !
$indexer->run();
return new Response(
'<html><body>Index succesfully generated !</body></html>'
);
}
/// ... ///
}
结果, 在Web浏览器http:// app / generate-index中访问该项目将返回以下输出(因为表中有7400行):
Processed 1000 rows
Processed 2000 rows
Processed 3000 rows
Processed 4000 rows
Processed 5000 rows
Processed 6000 rows
Processed 7000 rows
Total rows 7435 Index succesfully generated !
并且在提到的目录中, 你将找到可用于模糊搜索数据的索引文件:
现在有了索引, 我们可以开始模糊搜索了。
3.模糊搜索
我们快到了!如前所述, 现在有了索引, 我们可以轻松搜索用户正在寻找的内容。在我们的示例控制器中, 我们将有一个/ search路由, 它将显示TNTSearch的搜索方法返回的结果。这可以通过使用配置简单地初始化TNTSearch实例, 使用selectIndex方法选择索引文件, 然后从我们的实例中运行search方法来实现。此方法期望将搜索字符串作为参数, 并将结果限制作为第二个参数, 默认情况下为100。
我们将按照以下控制器中的描述将结果作为JSON字符串返回:
<?php
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// Import TNTSearch
use TeamTNT\TNTSearch\TNTSearch;
use Symfony\Component\HttpFoundation\JsonResponse;
class DefaultController extends AbstractController
{
/**
* @Route("/search", name="app_search")
*/
public function search()
{
$tnt = new TNTSearch;
// Obtain and load the configuration that can be generated with the previous described method
$configuration = $this->getTNTSearchConfiguration();
$tnt->loadConfig($configuration);
// Use the generated index in the previous step
$tnt->selectIndex('artists.index');
$maxResults = 20;
// Search for a band named like "Guns n' roses"
$results = $tnt->search("Gans n rosas", $maxResults);
return new JsonResponse($results);
}
// ... //
}
该控制器的响应将是TNTSearch的搜索方法返回的数组, 该数组具有以下结构:
{
"ids": [
946, 2990, 4913, 5564, 5751, 1924, 4794, 5541, 5560, 5725, 5757, 6581, 7370
], "hits": 13, "execution_time": "1.087 ms"
}
如你所见, TNTSearch仅返回最匹配搜索条件的数据库表中行的主键的集合。通常, 在Symfony项目中, 你将使用Doctrine来操纵数据库中的数据, 因此你将需要通过Doctrine通过其主键搜索项目。例如, 在我们的项目中, 我们有一个实体, 其中数据库的所有字段都标识为:
<?php
// /src/Entity/Artists.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Artists
*
* @ORM\Table(name="artists", indexes={@ORM\Index(name="first_character", columns={"first_character"})})
* @ORM\Entity
*/
class Artists
{
/**
* @var int
*
* @ORM\Column(name="id", type="bigint", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* @var string
*
* @ORM\Column(name="slug", type="string", length=255, nullable=false)
*/
private $slug;
/**
* @var string|null
*
* @ORM\Column(name="first_character", type="string", length=1, nullable=true, options={"default"="NULL"})
*/
private $firstCharacter = 'NULL';
/**
* @var string|null
*
* @ORM\Column(name="description", type="text", length=65535, nullable=true, options={"default"="NULL"})
*/
private $description = 'NULL';
/**
* @var string|null
*
* @ORM\Column(name="image", type="string", length=255, nullable=true, options={"default"="NULL"})
*/
private $image = 'NULL';
public function getId(): ?string
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
public function getFirstCharacter(): ?string
{
return $this->firstCharacter;
}
public function setFirstCharacter(?string $firstCharacter): self
{
$this->firstCharacter = $firstCharacter;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getImage(): ?string
{
return $this->image;
}
public function setImage(?string $image): self
{
$this->image = $image;
return $this;
}
}
因此, 我们可以简单地通过存储库的ID搜索每个项目。请记住, TNTSearch首先按最佳匹配返回结果, 因此这就是为什么我们搜索每个项目并将它们存储到数组中的原因, 如以下代码中所述:
<?php
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// Import TNTSearch
use TeamTNT\TNTSearch\TNTSearch;
use Symfony\Component\HttpFoundation\JsonResponse;
use App\Entity\Artists;
class DefaultController extends AbstractController
{
/**
* @Route("/search", name="app_search")
*/
public function search()
{
$em = $this->getDoctrine()->getManager();
$tnt = new TNTSearch;
// Obtain and load the configuration that can be generated with the previous described method
$configuration = $this->getTNTSearchConfiguration();
$tnt->loadConfig($configuration);
// Use the generated index in the previous step
$tnt->selectIndex('artists.index');
$maxResults = 20;
// Search for a band named like "Guns n' roses"
$results = $tnt->search("Gans n rosas", $maxResults);
// Keep a reference to the Doctrine repository of artists
$artistsRepository = $em->getRepository(Artists::class);
// Store the results in an array
$rows = [];
foreach($results["ids"] as $id){
// You can optimize this by using the FIELD function of MySQL if you are using mysql
// more info at: https://ourcodeworld.com/articles/read/1162/how-to-order-a-doctrine-2-query-result-by-a-specific-order-of-an-array-using-mysql-in-symfony-5
$artist = $artistsRepository->find($id);
$rows[] = [
'id' => $artist->getId(), 'name' => $artist->getName()
];
}
// Return the results to the user
return new JsonResponse($rows);
}
}
然后, 在搜索路径中, 我们将收到如下响应:
[
{
"id": "946", "name": "Black N Blue"
}, {
"id": "2990", "name": "Guns N' Roses"
}, {
"id": "4913", "name": "Nigrino, N"
}, {
"id": "5564", "name": "Rage N Rox"
}, {
"id": "5751", "name": "Robin N Looza"
}, {
"id": "1924", "name": "Demon n'Angel"
}, {
"id": "4794", "name": "N.E.R.D"
}, {
"id": "5541", "name": "R.I.S.E.N."
}, {
"id": "5560", "name": "Rag'n'Bone Man"
}, {
"id": "5725", "name": "Rínon Nínqueon"
}, {
"id": "5757", "name": "Rock'n'Roll Soldiers"
}, {
"id": "6581", "name": "T.N.F."
}, {
"id": "7370", "name": "Youssou N'Dour feat. Neneh Cherry"
}
]
不出所料, 即使我们用错误的方式写了Guns’n玫瑰, 在我们的最佳成绩中, 正确的乐队也在名单中!借助该库, 你将在几分钟内仅使用PHP即可在项目中实现模糊搜索。
编码愉快!
评论前必须登录!
注册