怎样在PHP项目中实现全文搜索?

wen java案例 3

本文目录导读:

怎样在PHP项目中实现全文搜索?

  1. MySQL 内置全文搜索(中小型项目)
  2. Elasticsearch 全文搜索(大型项目)
  3. TNTSearch(轻量级替代方案)
  4. Sphinx 搜索引擎(高性能)
  5. 完整搜索系统示例
  6. 选择建议
  7. 最佳实践

在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 高性能、大数据量 索引速度快 配置复杂

最佳实践

  1. 中文搜索:建议使用 Elasticsearch + IK 分词器,或 MySQL + 中文分词插件
  2. 搜索优化:实现搜索建议、拼写纠正、相关搜索
  3. 缓存策略:对热门搜索进行缓存
  4. 性能监控:记录搜索日志,分析搜索行为
  5. 数据同步:建立索引更新机制,保持数据一致性

选择哪种方案取决于你的项目规模、技术栈和性能要求,对于大多数中小型项目,MySQL全文搜索加上一些优化措施就足够了。

抱歉,评论功能暂时关闭!