本文目录导读:

在PHP项目中实现全文搜索,可以根据项目规模和需求选择不同的方案,以下是几种常见且实用的实现方式:
MySQL 内置全文搜索(中小型项目)
创建全文索引
-- 建表时创建全文索引
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,VARCHAR(200),
content TEXT,
FULLTEXT INDEX ft_index (title, content)
) ENGINE=InnoDB;
-- 或给已有表添加
ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title, content);
PHP 查询示例
class FullTextSearch {
private $pdo;
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function search($keyword, $page = 1, $perPage = 10) {
// 防止SQL注入
$keyword = trim($keyword);
if (empty($keyword)) return [];
$offset = ($page - 1) * $perPage;
// 自然语言模式搜索
$sql = "SELECT id, title,
MATCH(title, content) AGAINST(:keyword IN NATURAL LANGUAGE MODE) AS relevance,
SUBSTRING(content, LOCATE(:keyword2, content), 200) AS snippet
FROM articles
WHERE MATCH(title, content) AGAINST(:keyword3 IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC
LIMIT :limit OFFSET :offset";
$stmt = $this->pdo->prepare($sql);
$stmt->bindValue(':keyword', $keyword);
$stmt->bindValue(':keyword2', $keyword);
$stmt->bindValue(':keyword3', $keyword);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function searchCount($keyword) {
$sql = "SELECT COUNT(*) as total
FROM articles
WHERE MATCH(title, content) AGAINST(:keyword IN NATURAL LANGUAGE MODE)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':keyword' => $keyword]);
return $stmt->fetch(PDO::FETCH_ASSOC)['total'];
}
}
布尔模式搜索(支持高级语法)
public function advancedSearch($query) {
// 支持: +必须包含 -排除 *通配符 ""精确短语
$sql = "SELECT * FROM articles
WHERE MATCH(title, content) AGAINST(:query IN BOOLEAN MODE)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':query' => $query]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 用法
$results = $search->advancedSearch('+PHP -MySQL "全文搜索"');
Elasticsearch 全文搜索(大型项目)
安装与配置
# 安装 Elasticsearch 客户端 composer require elasticsearch/elasticsearch
PHP 实现
require 'vendor/autoload.php';
use Elasticsearch\ClientBuilder;
class ElasticSearchService {
private $client;
public function __construct() {
$this->client = ClientBuilder::create()
->setHosts(['http://localhost:9200'])
->build();
}
// 创建索引
public function createIndex($indexName) {
$params = [
'index' => $indexName,
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'my_analyzer' => [
'type' => 'standard',
'tokenizer' => 'ik_smart' // 中文分词
]
]
]
],
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
'analyzer' => 'ik_max_word',
'search_analyzer' => 'ik_smart'
],
'content' => [
'type' => 'text',
'analyzer' => 'ik_max_word',
'search_analyzer' => 'ik_smart'
],
'category' => ['type' => 'keyword'],
'created_at' => ['type' => 'date']
]
]
]
];
return $this->client->indices()->create($params);
}
// 索引文档
public function indexDocument($index, $id, $document) {
$params = [
'index' => $index,
'id' => $id,
'body' => $document
];
return $this->client->index($params);
}
// 批量索引
public function bulkIndex($index, $documents) {
$params = ['body' => []];
foreach ($documents as $id => $doc) {
$params['body'][] = [
'index' => [
'_index' => $index,
'_id' => $id
]
];
$params['body'][] = $doc;
}
return $this->client->bulk($params);
}
// 搜索
public function search($index, $query, $page = 1, $perPage = 10) {
$from = ($page - 1) * $perPage;
$params = [
'index' => $index,
'body' => [
'query' => [
'bool' => [
'must' => [
'multi_match' => [
'query' => $query,
'fields' => ['title^3', 'content'], // title权重更高
'type' => 'best_fields',
'fuzziness' => 'AUTO' // 模糊搜索
]
],
'filter' => [
// 可以添加过滤条件
// 'term' => ['category' => '技术']
]
]
],
'highlight' => [
'pre_tags' => ['<mark>'],
'post_tags' => ['</mark>'],
'fields' => [
'title' => ['fragment_size' => 50],
'content' => ['fragment_size' => 200, 'number_of_fragments' => 2]
]
],
'from' => $from,
'size' => $perPage,
'sort' => [
'_score' => ['order' => 'desc']
]
]
];
$response = $this->client->search($params);
return [
'total' => $response['hits']['total']['value'],
'results' => $this->formatResults($response['hits']['hits'])
];
}
private function formatResults($hits) {
$results = [];
foreach ($hits as $hit) {
$result = $hit['_source'];
$result['_score'] = $hit['_score'];
$result['highlight'] = $hit['highlight'] ?? [];
$results[] = $result;
}
return $results;
}
}
TNTSearch(轻量级替代方案)
安装
composer require team-tnt/tntsearch
实现
use TeamTNT\TNTSearch\TNTSearch;
class TNTSearchService {
private $tnt;
public function __construct() {
$this->tnt = new TNTSearch();
$this->tnt->loadConfig([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'your_db',
'username' => 'root',
'password' => '',
'storage' => '/path/to/index/storage'
]);
}
// 创建索引
public function createIndex($table, $fields) {
$indexer = $this->tnt->createIndex("{$table}.index");
$indexer->query("SELECT id, " . implode(',', $fields) . " FROM {$table}");
$indexer->run();
}
// 搜索
public function search($query, $page = 1, $perPage = 10) {
$this->tnt->selectIndex("articles.index");
$this->tnt->fuzziness = true; // 启用模糊搜索
$this->tnt->fuzzy_distance = 2; // 编辑距离
$res = $this->tnt->search($query, $perPage);
return [
'total' => $res['hits'],
'ids' => $res['ids'],
'suggestions' => $res['suggestions'] ?? []
];
}
}
Sphinx 搜索引擎(高性能)
配置示例
# /etc/sphinx/sphinx.conf
source article_src
{
type = mysql
sql_host = localhost
sql_user = root
sql_pass =
sql_db = your_db
sql_query = SELECT id, title, content, created_at FROM articles
sql_attr_timestamp = created_at
}
index article_index
{
source = article_src
path = /var/lib/sphinx/data/article_index
charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
}
PHP 调用
class SphinxSearch {
private $client;
public function __construct() {
$this->client = new mysqli('localhost', '', '', '', 9306);
if ($this->client->connect_error) {
throw new Exception("Sphinx连接失败: " . $this->client->connect_error);
}
}
public function search($query, $page = 1, $perPage = 10) {
$offset = ($page - 1) * $perPage;
// 转义特殊字符
$query = str_replace(['\\', '|', '(', ')', '-', '!', '@', '~', '"', '&', '/', '^', '$', '='],
['\\\\', '\\|', '\\(', '\\)', '\\-', '\\!', '\\@', '\\~', '\\"', '\\&', '\\/', '\\^', '\\$', '\\='],
$query);
$sql = "SELECT *, WEIGHT() as weight
FROM article_index
WHERE MATCH('$query')
ORDER BY weight DESC
LIMIT $offset, $perPage";
$result = $this->client->query($sql);
return $result->fetch_all(MYSQLI_ASSOC);
}
}
完整搜索系统示例
// 搜索服务接口
interface SearchInterface {
public function index($id, $data);
public function search($query, $page, $perPage);
public function delete($id);
public function update($id, $data);
}
// 工厂模式
class SearchFactory {
public static function create($type = 'mysql') {
switch ($type) {
case 'elasticsearch':
return new ElasticSearchService();
case 'tntsearch':
return new TNTSearchService();
case 'sphinx':
return new SphinxSearch();
default:
return new FullTextSearch($pdo);
}
}
}
// 控制器示例
class SearchController {
private $searchService;
public function __construct($searchType = 'mysql') {
$this->searchService = SearchFactory::create($searchType);
}
public function search(Request $request) {
$keyword = $request->input('q');
$page = $request->input('page', 1);
$perPage = $request->input('per_page', 20);
try {
$results = $this->searchService->search($keyword, $page, $perPage);
return [
'success' => true,
'data' => $results,
'meta' => [
'query' => $keyword,
'page' => $page,
'total' => $results['total'] ?? 0
]
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
public function reindex() {
// 从数据库获取所有数据
$articles = Article::all();
foreach ($articles as $article) {
$this->searchService->index($article->id, [
'title' => $article->title,
'content' => $article->content,
'category' => $article->category,
'created_at' => $article->created_at->timestamp
]);
}
return ['success' => true, 'message' => '索引更新完成'];
}
}
选择建议
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| MySQL FULLTEXT | 中小型项目(<10万数据) | 无需额外服务,简单 | 性能有限,不支持中文分词 |
| Elasticsearch | 大型项目、高并发 | 分布式、实时、功能强大 | 需要额外维护集群 |
| TNTSearch | 轻量级、嵌入式 | 无需外部依赖 | 功能相对简单 |
| Sphinx | 高性能、大数据量 | 索引速度快 | 配置复杂 |
最佳实践
- 中文搜索:建议使用 Elasticsearch + IK 分词器,或 MySQL + 中文分词插件
- 搜索优化:实现搜索建议、拼写纠正、相关搜索
- 缓存策略:对热门搜索进行缓存
- 性能监控:记录搜索日志,分析搜索行为
- 数据同步:建立索引更新机制,保持数据一致性
选择哪种方案取决于你的项目规模、技术栈和性能要求,对于大多数中小型项目,MySQL全文搜索加上一些优化措施就足够了。