CarefreeCMS 文档CarefreeCMS 文档
指南
  • 内容管理
  • 多站点管理
  • AI文章生成
  • SEO优化
  • 静态化生成
API
  • FAQ
  • 更新日志
  • 贡献指南
  • v1.3.0
  • v1.2.0
  • v1.1.0
GitHub
指南
  • 内容管理
  • 多站点管理
  • AI文章生成
  • SEO优化
  • 静态化生成
API
  • FAQ
  • 更新日志
  • 贡献指南
  • v1.3.0
  • v1.2.0
  • v1.1.0
GitHub
  • 开始使用

    • 介绍
    • 安装指南
    • 快速开始
    • 系统配置
  • 基础功能

    • 文章管理
    • 分类管理
    • 标签管理
    • 单页管理
    • 媒体库
  • 高级功能

    • 模板开发
    • 静态化生成
    • 搜索功能
    • 权限管理
    • 用户管理
  • AI 功能

    • AI 服务商配置
    • AI 模型配置
    • 提示词工程
  • 系统管理

    • 定时任务
    • 日志管理
    • 安全指南
    • 性能优化

搜索功能

CarefreeCMS 提供强大的全文搜索功能,支持多种搜索方式,帮助用户快速找到所需内容。

搜索概述

搜索类型

基础搜索

  • 标题搜索
  • 内容搜索
  • 标签搜索
  • 作者搜索

高级搜索

  • 分类筛选
  • 时间范围
  • 标签组合
  • 自定义字段

全文搜索

  • 中文分词
  • 关键词高亮
  • 相关度排序
  • 搜索建议

搜索引擎

内置搜索

  • MySQL LIKE 查询
  • MySQL 全文索引
  • 简单快速

Elasticsearch

  • 分布式搜索
  • 强大的分词
  • 高性能
  • 适合大数据量

Meilisearch

  • 轻量级搜索引擎
  • 易于部署
  • 搜索速度快
  • 中文支持好

基础搜索

前台搜索框

头部搜索

<div class="header-search">
    <form action="/search" method="get">
        <input type="text"
               name="keyword"
               placeholder="搜索文章..."
               autocomplete="off">
        <button type="submit">
            <i class="icon-search"></i>
        </button>
    </form>
</div>

搜索页面

<div class="search-page">
    <div class="search-box">
        <form action="/search" method="get">
            <input type="text"
                   name="keyword"
                   value="{{ keyword }}"
                   placeholder="输入关键词搜索">
            <button type="submit">搜索</button>
        </form>
    </div>

    <div class="search-results">
        <p class="search-info">
            找到 <strong>{{ total }}</strong> 个结果
        </p>

        {% for article in articles %}
        <div class="result-item">
            <h3>
                <a href="/article/{{ article.id }}">
                    {{ article.title|highlight(keyword)|raw }}
                </a>
            </h3>
            <p class="summary">
                {{ article.summary|highlight(keyword)|raw }}
            </p>
            <div class="meta">
                <span>{{ article.create_time }}</span>
                <span>{{ article.category.name }}</span>
            </div>
        </div>
        {% endfor %}
    </div>

    <!-- 分页 -->
    {{ pagination|raw }}
</div>

后端实现

搜索控制器

<?php
namespace app\controller\index;

use app\model\Article;

class Search
{
    public function index()
    {
        $keyword = input('keyword', '');
        $page = input('page', 1);
        $pageSize = 20;

        if (empty($keyword)) {
            return view('search/index', [
                'keyword' => '',
                'articles' => [],
                'total' => 0
            ]);
        }

        // 搜索文章
        $query = Article::where('status', 1);

        // 标题或内容包含关键词
        $query->where(function($q) use ($keyword) {
            $q->whereOr('title', 'like', "%{$keyword}%")
              ->whereOr('content', 'like', "%{$keyword}%");
        });

        // 获取总数
        $total = $query->count();

        // 分页查询
        $articles = $query->page($page, $pageSize)
            ->order('create_time', 'desc')
            ->select();

        return view('search/index', [
            'keyword' => $keyword,
            'articles' => $articles,
            'total' => $total,
            'page' => $page
        ]);
    }
}

高级搜索

搜索表单

<form action="/search" method="get" class="advanced-search">
    <!-- 关键词 -->
    <div class="form-group">
        <label>关键词</label>
        <input type="text" name="keyword" value="{{ keyword }}">
    </div>

    <!-- 分类 -->
    <div class="form-group">
        <label>分类</label>
        <select name="category_id">
            <option value="">全部分类</option>
            {% for category in categories %}
            <option value="{{ category.id }}"
                    {{ category_id == category.id ? 'selected' : '' }}>
                {{ category.name }}
            </option>
            {% endfor %}
        </select>
    </div>

    <!-- 标签 -->
    <div class="form-group">
        <label>标签</label>
        <select name="tag_id">
            <option value="">全部标签</option>
            {% for tag in tags %}
            <option value="{{ tag.id }}"
                    {{ tag_id == tag.id ? 'selected' : '' }}>
                {{ tag.name }}
            </option>
            {% endfor %}
        </select>
    </div>

    <!-- 时间范围 -->
    <div class="form-group">
        <label>发布时间</label>
        <input type="date" name="start_date" value="{{ start_date }}">
        <span>至</span>
        <input type="date" name="end_date" value="{{ end_date }}">
    </div>

    <!-- 排序 -->
    <div class="form-group">
        <label>排序方式</label>
        <select name="sort">
            <option value="create_time">发布时间</option>
            <option value="view_count">浏览量</option>
            <option value="like_count">点赞数</option>
        </select>
        <select name="order">
            <option value="desc">降序</option>
            <option value="asc">升序</option>
        </select>
    </div>

    <button type="submit">搜索</button>
    <button type="reset">重置</button>
</form>

后端处理

public function advanced()
{
    $params = [
        'keyword' => input('keyword', ''),
        'category_id' => input('category_id', 0),
        'tag_id' => input('tag_id', 0),
        'start_date' => input('start_date', ''),
        'end_date' => input('end_date', ''),
        'sort' => input('sort', 'create_time'),
        'order' => input('order', 'desc'),
        'page' => input('page', 1),
    ];

    $query = Article::where('status', 1);

    // 关键词搜索
    if (!empty($params['keyword'])) {
        $query->where(function($q) use ($params) {
            $q->whereOr('title', 'like', "%{$params['keyword']}%")
              ->whereOr('content', 'like', "%{$params['keyword']}%");
        });
    }

    // 分类筛选
    if ($params['category_id'] > 0) {
        $query->where('category_id', $params['category_id']);
    }

    // 标签筛选
    if ($params['tag_id'] > 0) {
        $query->whereExists(function($q) use ($params) {
            $q->table('article_tag')
              ->whereRaw('article_tag.article_id = article.id')
              ->where('article_tag.tag_id', $params['tag_id']);
        });
    }

    // 时间范围
    if (!empty($params['start_date'])) {
        $query->where('create_time', '>=', $params['start_date']);
    }
    if (!empty($params['end_date'])) {
        $query->where('create_time', '<=', $params['end_date'] . ' 23:59:59');
    }

    // 排序
    $query->order($params['sort'], $params['order']);

    // 分页
    $total = $query->count();
    $articles = $query->page($params['page'], 20)->select();

    return view('search/advanced', [
        'params' => $params,
        'articles' => $articles,
        'total' => $total,
    ]);
}

MySQL 全文索引

创建全文索引

-- 为标题和内容创建全文索引
ALTER TABLE article
ADD FULLTEXT INDEX ft_title_content (title, content)
WITH PARSER ngram;

使用全文搜索

public function fulltext()
{
    $keyword = input('keyword', '');

    if (empty($keyword)) {
        return json(['code' => 400, 'message' => '请输入关键词']);
    }

    // 使用全文搜索
    $articles = Article::where('status', 1)
        ->whereRaw("MATCH(title, content) AGAINST(? IN NATURAL LANGUAGE MODE)", [$keyword])
        ->order('create_time', 'desc')
        ->select();

    return json([
        'code' => 200,
        'data' => $articles
    ]);
}

布尔模式搜索

// 必须包含 "Vue" 和 "教程"
$keyword = '+Vue +教程';

// 必须包含 "Vue",但不能包含 "React"
$keyword = '+Vue -React';

// 包含 "Vue" 或 "React"
$keyword = 'Vue React';

$articles = Article::whereRaw(
    "MATCH(title, content) AGAINST(? IN BOOLEAN MODE)",
    [$keyword]
)->select();

Elasticsearch 集成

安装配置

安装 Elasticsearch

# Docker 安装
docker run -d \
  --name elasticsearch \
  -p 9200:9200 \
  -p 9300:9300 \
  -e "discovery.type=single-node" \
  elasticsearch:8.11.0

安装中文分词插件

# 进入容器
docker exec -it elasticsearch bash

# 安装 ik 分词器
elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.11.0/elasticsearch-analysis-ik-8.11.0.zip

# 重启
docker restart elasticsearch

安装 PHP 客户端

composer require elasticsearch/elasticsearch

配置索引

<?php
namespace app\service;

use Elasticsearch\ClientBuilder;

class ElasticsearchService
{
    private $client;

    public function __construct()
    {
        $this->client = ClientBuilder::create()
            ->setHosts(['localhost:9200'])
            ->build();
    }

    // 创建索引
    public function createIndex()
    {
        $params = [
            'index' => 'articles',
            'body' => [
                'settings' => [
                    'number_of_shards' => 1,
                    'number_of_replicas' => 0,
                    'analysis' => [
                        'analyzer' => [
                            'ik_smart' => [
                                'type' => 'custom',
                                'tokenizer' => 'ik_smart'
                            ]
                        ]
                    ]
                ],
                'mappings' => [
                    'properties' => [
                        'id' => ['type' => 'integer'],
                        'title' => [
                            'type' => 'text',
                            'analyzer' => 'ik_smart',
                            'search_analyzer' => 'ik_smart'
                        ],
                        'content' => [
                            'type' => 'text',
                            'analyzer' => 'ik_smart',
                            'search_analyzer' => 'ik_smart'
                        ],
                        'category_id' => ['type' => 'integer'],
                        'tags' => ['type' => 'keyword'],
                        'create_time' => ['type' => 'date']
                    ]
                ]
            ]
        ];

        return $this->client->indices()->create($params);
    }

    // 添加文档
    public function addDocument($article)
    {
        $params = [
            'index' => 'articles',
            'id' => $article->id,
            'body' => [
                'id' => $article->id,
                'title' => $article->title,
                'content' => strip_tags($article->content),
                'category_id' => $article->category_id,
                'tags' => $article->tags->column('name'),
                'create_time' => $article->create_time
            ]
        ];

        return $this->client->index($params);
    }

    // 搜索
    public function search($keyword, $page = 1, $pageSize = 20)
    {
        $params = [
            'index' => 'articles',
            'body' => [
                'from' => ($page - 1) * $pageSize,
                'size' => $pageSize,
                'query' => [
                    'multi_match' => [
                        'query' => $keyword,
                        'fields' => ['title^3', 'content'],
                        'type' => 'best_fields'
                    ]
                ],
                'highlight' => [
                    'pre_tags' => ['<em class="highlight">'],
                    'post_tags' => ['</em>'],
                    'fields' => [
                        'title' => new \stdClass(),
                        'content' => [
                            'fragment_size' => 150,
                            'number_of_fragments' => 3
                        ]
                    ]
                ]
            ]
        ];

        $response = $this->client->search($params);

        return [
            'total' => $response['hits']['total']['value'],
            'hits' => $response['hits']['hits']
        ];
    }
}

使用示例

use app\service\ElasticsearchService;

class SearchController
{
    public function elasticsearch()
    {
        $keyword = input('keyword', '');
        $page = input('page', 1);

        $es = new ElasticsearchService();
        $result = $es->search($keyword, $page);

        $articles = [];
        foreach ($result['hits'] as $hit) {
            $articles[] = [
                'id' => $hit['_source']['id'],
                'title' => $hit['highlight']['title'][0] ?? $hit['_source']['title'],
                'content' => $hit['highlight']['content'][0] ?? '',
                'score' => $hit['_score']
            ];
        }

        return json([
            'code' => 200,
            'data' => [
                'articles' => $articles,
                'total' => $result['total']
            ]
        ]);
    }
}

搜索优化

关键词高亮

PHP 实现

function highlight($text, $keyword)
{
    if (empty($keyword)) {
        return $text;
    }

    $keywords = explode(' ', $keyword);
    foreach ($keywords as $word) {
        $text = preg_replace(
            '/(' . preg_quote($word, '/') . ')/ui',
            '<em class="highlight">$1</em>',
            $text
        );
    }

    return $text;
}

Twig 过滤器

use Twig\TwigFilter;

$twig->addFilter(new TwigFilter('highlight', function($text, $keyword) {
    return highlight($text, $keyword);
}));

使用

<h3>___TWIG0___</h3>
<p>___TWIG1___</p>

搜索建议

自动完成

// 前端实现
const searchInput = document.querySelector('#search-input');
const suggestionBox = document.querySelector('#suggestions');

let timeout;
searchInput.addEventListener('input', function(e) {
    clearTimeout(timeout);

    const keyword = e.target.value;
    if (keyword.length < 2) {
        suggestionBox.style.display = 'none';
        return;
    }

    timeout = setTimeout(() => {
        fetch(`/api/search/suggest?keyword=${keyword}`)
            .then(res => res.json())
            .then(data => {
                if (data.code === 200) {
                    showSuggestions(data.data);
                }
            });
    }, 300);
});

function showSuggestions(suggestions) {
    suggestionBox.innerHTML = '';

    suggestions.forEach(item => {
        const div = document.createElement('div');
        div.className = 'suggestion-item';
        div.textContent = item;
        div.onclick = () => {
            searchInput.value = item;
            suggestionBox.style.display = 'none';
        };
        suggestionBox.appendChild(div);
    });

    suggestionBox.style.display = 'block';
}

后端接口

public function suggest()
{
    $keyword = input('keyword', '');

    // 从文章标题中匹配
    $articles = Article::where('title', 'like', "%{$keyword}%")
        ->field('title')
        ->limit(10)
        ->select();

    $suggestions = $articles->column('title');

    return json([
        'code' => 200,
        'data' => $suggestions
    ]);
}

热门搜索

记录搜索关键词

use app\model\SearchLog;

public function index()
{
    $keyword = input('keyword', '');

    if (!empty($keyword)) {
        // 记录搜索
        SearchLog::create([
            'keyword' => $keyword,
            'result_count' => $total,
            'user_id' => session('user_id'),
            'ip' => request()->ip(),
            'create_time' => date('Y-m-d H:i:s')
        ]);
    }

    // 执行搜索...
}

获取热门搜索

public function hot()
{
    // 最近7天的热门搜索
    $keywords = SearchLog::where('create_time', '>=', date('Y-m-d', strtotime('-7 days')))
        ->field('keyword, COUNT(*) as count')
        ->group('keyword')
        ->order('count', 'desc')
        ->limit(10)
        ->select();

    return json([
        'code' => 200,
        'data' => $keywords
    ]);
}

前台显示

<div class="hot-search">
    <h4>热门搜索</h4>
    <div class="tags">
        {% for item in hot_keywords %}
        <a href="/search?keyword={{ item.keyword }}" class="tag">
            {{ item.keyword }}
        </a>
        {% endfor %}
    </div>
</div>

搜索统计

统计面板

public function statistics()
{
    // 总搜索次数
    $total = SearchLog::count();

    // 今日搜索次数
    $today = SearchLog::whereDay('create_time', 'today')->count();

    // 搜索关键词排行
    $topKeywords = SearchLog::field('keyword, COUNT(*) as count')
        ->group('keyword')
        ->order('count', 'desc')
        ->limit(20)
        ->select();

    // 无结果搜索
    $noResults = SearchLog::where('result_count', 0)
        ->field('keyword, COUNT(*) as count')
        ->group('keyword')
        ->order('count', 'desc')
        ->limit(20)
        ->select();

    return view('admin/search/statistics', [
        'total' => $total,
        'today' => $today,
        'topKeywords' => $topKeywords,
        'noResults' => $noResults
    ]);
}

API 接口

搜索接口

请求

GET /api/search?keyword=Vue&page=1&page_size=20

响应

{
  "code": 200,
  "message": "搜索成功",
  "data": {
    "list": [
      {
        "id": 1,
        "title": "Vue 3 教程",
        "summary": "Vue 3 入门教程...",
        "highlight": {
          "title": "<em>Vue</em> 3 教程",
          "content": "...详细的<em>Vue</em>教程..."
        }
      }
    ],
    "total": 15,
    "page": 1,
    "page_size": 20
  }
}

搜索建议接口

请求

GET /api/search/suggest?keyword=Vue

响应

{
  "code": 200,
  "data": [
    "Vue 3 教程",
    "Vue Router",
    "Vue Composition API"
  ]
}

最佳实践

搜索性能

使用索引

-- 为常用搜索字段添加索引
CREATE INDEX idx_title ON article(title);
CREATE INDEX idx_status_time ON article(status, create_time);

缓存结果

use think\facade\Cache;

public function search($keyword)
{
    $cacheKey = 'search:' . md5($keyword);

    // 尝试从缓存获取
    $result = Cache::get($cacheKey);
    if ($result) {
        return $result;
    }

    // 执行搜索
    $result = $this->doSearch($keyword);

    // 缓存5分钟
    Cache::set($cacheKey, $result, 300);

    return $result;
}

搜索体验

输入防抖

// 防止频繁请求
let searchTimeout;
input.addEventListener('input', function(e) {
    clearTimeout(searchTimeout);
    searchTimeout = setTimeout(() => {
        performSearch(e.target.value);
    }, 300);
});

搜索历史

// 保存搜索历史
function saveSearchHistory(keyword) {
    let history = JSON.parse(localStorage.getItem('searchHistory') || '[]');

    // 去重并添加到开头
    history = history.filter(item => item !== keyword);
    history.unshift(keyword);

    // 只保留最近10条
    history = history.slice(0, 10);

    localStorage.setItem('searchHistory', JSON.stringify(history));
}

常见问题

中文搜索不准确?

使用 ngram 分词器

ALTER TABLE article
ADD FULLTEXT INDEX ft_content (content)
WITH PARSER ngram;

或使用 Elasticsearch + IK 分词

搜索速度慢?

优化方案:

  1. 添加数据库索引
  2. 限制搜索范围(只搜索标题)
  3. 使用全文索引
  4. 使用 Elasticsearch
  5. 缓存搜索结果

如何过滤敏感词?

public function filterSensitiveWords($keyword)
{
    $sensitiveWords = ['敏感词1', '敏感词2'];

    foreach ($sensitiveWords as $word) {
        $keyword = str_replace($word, '***', $keyword);
    }

    return $keyword;
}

相关功能

  • 文章管理
  • 分类管理
  • 标签管理
  • SEO 优化
在 GitHub 上编辑此页
Prev
静态化生成
Next
权限管理