怎样在PHP项目中实现搜索功能?

wen java案例 2

本文目录导读:

怎样在PHP项目中实现搜索功能?

  1. MySQL LIKE 模糊搜索(小型项目)
  2. MySQL FULLTEXT 全文索引(中等规模)
  3. Elasticsearch 专业搜索引擎(大型项目)
  4. Sphinx 搜索引擎(中等规模高性能)
  5. 中文分词处理
  6. 性能优化建议
  7. 完整前端搜索交互
  8. 选择推荐

在PHP项目中实现搜索功能,根据项目规模和需求的不同,有多种实现方案,以下从简单到复杂,介绍几种主流方法。

MySQL LIKE 模糊搜索(小型项目)

最基础的方式,适合数据量小(万级以内)的场景。

// 搜索逻辑
function searchUsers($keyword) {
    $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
    $keyword = '%' . $keyword . '%';
    // 使用预处理语句防止SQL注入
    $stmt = $pdo->prepare("SELECT * FROM users 
                          WHERE name LIKE :keyword 
                          OR email LIKE :keyword 
                          ORDER BY id DESC 
                          LIMIT 20");
    $stmt->execute([':keyword' => $keyword]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 调用示例
$results = searchUsers($_GET['q'] ?? '');

优点:实现简单,无需额外服务
缺点:性能差(全表扫描),不支持中文分词,不支持模糊排序

MySQL FULLTEXT 全文索引(中等规模)

适合10万级数据量,需要精确匹配关键字的场景。

-- 先创建全文索引
ALTER TABLE articles ADD FULLTEXT INDEX idx_content (title, content);
function searchArticles($keyword) {
    $pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'root', '');
    // BOOLEAN MODE 支持 +(必须包含) -(排除) *(通配符)
    $sql = "SELECT *, MATCH(title, content) AGAINST(:keyword IN BOOLEAN MODE) AS relevance 
            FROM articles 
            WHERE MATCH(title, content) AGAINST(:keyword IN BOOLEAN MODE)
            ORDER BY relevance DESC 
            LIMIT 20";
    $stmt = $pdo->prepare($sql);
    $stmt->execute([':keyword' => $keyword . '*']); // 通配符允许部分匹配
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

注意

  • 需要 MySQL 5.6+ 支持中文全文索引(ngram)
  • 创建表时指定:ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  • 配置最小词长:ft_min_word_len=1(中文)

Elasticsearch 专业搜索引擎(大型项目)

适合百万级以上数据,需要复杂搜索、聚合分析、实时搜索的场景。

环境准备

# 安装 Elasticsearch(需要Java环境)
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.11.0-linux-x86_64.tar.gz
tar -xzf elasticsearch-8.11.0-linux-x86_64.tar.gz
cd elasticsearch-8.11.0/bin
./elasticsearch
# 安装 PHP Elasticsearch 客户端
composer require elasticsearch/elasticsearch

使用示例

<?php
require_once __DIR__ . '/vendor/autoload.php';
use Elasticsearch\ClientBuilder;
class SearchService {
    private $client;
    public function __construct() {
        $this->client = ClientBuilder::create()
            ->setHosts(['http://localhost:9200'])
            ->build();
    }
    // 索引文档(将数据存入ES)
    public function indexArticle($id, $title, $content) {
        $params = [
            'index' => 'articles',
            'id'    => $id,
            'body'  => [
                'title'   => $title,
                'content' => $content,
                'created_at' => date('Y-m-d H:i:s')
            ]
        ];
        return $this->client->index($params);
    }
    // 搜索文档
    public function search($keyword, $page = 1, $size = 20) {
        $params = [
            'index' => 'articles',
            'body'  => [
                'query' => [
                    'bool' => [
                        'should' => [
                            ['match' => ['title' => ['query' => $keyword, 'boost' => 3]]],
                            ['match' => ['content' => $keyword]]
                        ]
                    ]
                ],
                'highlight' => [
                    'fields' => [
                        'title' => ['pre_tags' => ['<em>'], 'post_tags' => ['</em>']],
                        'content' => ['fragment_size' => 100, 'number_of_fragments' => 3]
                    ]
                ],
                'from' => ($page - 1) * $size,
                'size' => $size,
                'sort' => ['_score' => 'desc']
            ]
        ];
        $response = $this->client->search($params);
        // 格式化结果
        $results = [];
        foreach ($response['hits']['hits'] as $hit) {
            $source = $hit['_source'];
            $highlight = $hit['highlight'] ?? [];
            $results[] = [
                'id' => $hit['_id'],
                'title' => $highlight['title'][0] ?? $source['title'],
                'content' => implode('...', $highlight['content'] ?? [$source['content']]),
                'score' => $hit['_score']
            ];
        }
        return [
            'total' => $response['hits']['total']['value'],
            'results' => $results,
            'page' => $page,
            'size' => $size
        ];
    }
}
// 使用示例
$searchService = new SearchService();
// 索引数据
$searchService->indexArticle(1, 'PHP搜索功能实现', '本文介绍如何在PHP中实现高效搜索...');
// 搜索
$result = $searchService->search('PHP搜索');
echo "共找到 {$result['total']} 条结果:";
foreach ($result['results'] as $item) {
    echo "标题:{$item['title']}\n";
    echo "{$item['content']}\n\n";
}

Sphinx 搜索引擎(中等规模高性能)

适合需要高性能全文搜索但不想引入ES复杂性的场景。

// 安装 Sphinx PHP 扩展
// pecl install sphinx
$sphinx = new SphinxClient();
$sphinx->setServer('localhost', 9312);
$sphinx->setMatchMode(SPH_MATCH_EXTENDED2);
$sphinx->setLimits(0, 20);
$sphinx->setSortMode(SPH_SORT_RELEVANCE);
// 设置筛选条件
$sphinx->setFilter('category_id', [1, 2, 3]);
$sphinx->setFilterRange('price', 100, 500);
// 执行搜索
$result = $sphinx->query('PHP搜索', 'articles');
if ($result && $result['total'] > 0) {
    // 从 MySQL 获取完整记录
    $ids = array_keys($result['matches']);
    $stmt = $pdo->prepare("SELECT * FROM articles WHERE id IN (" . implode(',', $ids) . ")");
    $stmt->execute();
    $articles = $stmt->fetchAll();
}

中文分词处理

对于中文搜索,分词至关重要:

使用 SCWS(简易中文分词系统)

// 安装 scws 扩展
// pecl install scws
$sh = scws_open();
scws_set_charset($sh, 'utf8');
scws_set_dict($sh, '/usr/local/scws/etc/dict.utf8.xdb');
scws_set_rule($sh, '/usr/local/scws/etc/rules.utf8.ini');
$text = '这是一个中文搜索测试';
scws_send_text($sh, $text);
$words = [];
while ($tmp = scws_get_result($sh)) {
    foreach ($tmp as $word) {
        $words[] = $word['word'];
    }
}
scws_close($sh);
print_r($words);
// 输出: ['这是', '一个', '中文', '搜索', '测试']

集成到搜索中

function tokenizeSearch($keyword) {
    $tokens = tokenize($keyword); // 自定义分词函数
    // 构建布尔查询
    $conditions = [];
    $params = [];
    foreach ($tokens as $i => $token) {
        $conditions[] = "title LIKE :search{$i} OR content LIKE :search{$i}";
        $params[":search{$i}"] = "%{$token}%";
    }
    $where = implode(' AND ', $conditions); // AND表示必须所有词都出现
    $sql = "SELECT * FROM articles WHERE {$where} ORDER BY relevance DESC LIMIT 20";
    $stmt = $pdo->prepare($sql);
    $stmt->execute($params);
    return $stmt->fetchAll();
}

性能优化建议

缓存策略

// 1. 查询结果缓存(Redis/Memcached)
$cacheKey = 'search:' . md5($keyword . $page);
$cached = $redis->get($cacheKey);
if ($cached) {
    return json_decode($cached, true);
}
$results = performSearch($keyword);
$redis->setex($cacheKey, 300, json_encode($results)); // 缓存5分钟
// 2. 热门搜索缓存
$redis->zIncrBy('hot_queries', 1, $keyword);
// 3. 搜索建议缓存
$suggestions = $redis->get('suggestions');
if (!$suggestions) {
    $suggestions = buildAutocompleteIndex();
    $redis->setex('suggestions', 3600, $suggestions);
}

数据库优化

-- 1. 添加复合索引
ALTER TABLE articles ADD INDEX idx_search (title, created_at);
-- 2. 使用覆盖索引
SELECT id, LEFT(title, 100) as title_fragment 
FROM articles 
WHERE title LIKE '%keyword%';
-- 3. 分表策略(按时间)
CREATE TABLE articles_2024_01 ...;
CREATE TABLE articles_2024_02 ...;

完整前端搜索交互

<!-- HTML 搜索框 -->
<div class="search-box">
    <input type="text" id="searchInput" placeholder="搜索..." autocomplete="off">
    <div id="searchResults" class="dropdown-menu"></div>
</div>
<script>
// 防抖搜索
const debounce = (fn, delay = 300) => {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
    };
};
document.getElementById('searchInput').addEventListener('input', debounce(async (e) => {
    const query = e.target.value.trim();
    if (query.length < 2) {
        document.getElementById('searchResults').innerHTML = '';
        return;
    }
    try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();
        renderResults(data);
    } catch (error) {
        console.error('搜索失败:', error);
    }
}));
function renderResults(results) {
    const container = document.getElementById('searchResults');
    if (results.length === 0) {
        container.innerHTML = '<div class="no-results">未找到相关结果</div>';
        return;
    }
    container.innerHTML = results.map(item => `
        <a href="/article/${item.id}" class="search-result-item">
            <div class="result-title">${item.highlightTitle || item.title}</div>
            <div class="result-snippet">${item.snippet}</div>
        </a>
    `).join('');
}
</script>

选择推荐

方案 适合场景 数据量 实现难度
LIKE 简单CMS、后台管理 <1万
FULLTEXT 博客、论坛 <10万
Sphinx 电商、垂直搜索 <100万
Elasticsearch 综合搜索、日志分析 >100万

建议

  • 小项目从 MySQL FULLTEXT 起步
  • 成长型项目直接上 Elasticsearch(后续不需要迁移)
  • 始终做好 SQL 注入防护和 XSS 过滤

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