ThinkPHP项目大文本存储与检索

wen PHP项目 11

本文目录导读:

ThinkPHP项目大文本存储与检索

  1. 存储方案选择
  2. 模型层设计
  3. 检索方案实现
  4. 大文本优化策略
  5. 完整使用示例
  6. 性能优化建议

我来详细讲解ThinkPHP项目中大文本的存储与检索方案。

存储方案选择

数据库存储方案

MySQL TEXT类型选择

// 根据数据大小选择合适类型
- TINYTEXT: 255字节
- TEXT: 64KB
- MEDIUMTEXT: 16MB  
- LONGTEXT: 4GB

建表示例

CREATE TABLE `article` (
  `id` int(11) NOT NULL AUTO_INCREMENT, varchar(255) NOT NULL,
  `content` longtext,
  `content_hash` char(32) DEFAULT NULL,
  `word_count` int(11) DEFAULT '0',
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_content_hash` (`content_hash`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

文件系统存储

<?php
namespace app\common\storage;
use think\facade\Filesystem;
class TextFileStorage
{
    /**
     * 保存大文本到文件
     */
    public static function save($content, $path = 'text')
    {
        $filename = md5(uniqid() . time()) . '.txt';
        $filePath = $path . '/' . date('Y/m/d') . '/' . $filename;
        // 使用ThinkPHP文件系统
        Filesystem::put($filePath, $content);
        return [
            'path' => $filePath,
            'url'  => Filesystem::url($filePath)
        ];
    }
    /**
     * 读取文件内容
     */
    public static function read($filePath)
    {
        return Filesystem::get($filePath);
    }
    /**
     * 删除文件
     */
    public static function delete($filePath)
    {
        return Filesystem::delete($filePath);
    }
}

模型层设计

模型类实现

<?php
namespace app\common\model;
use think\Model;
use think\facade\Cache;
class Article extends Model
{
    protected $name = 'article';
    protected $pk = 'id';
    // 自动时间戳
    protected $autoWriteTimestamp = true;
    protected $createTime = 'created_at';
    protected $updateTime = 'updated_at';
    // 字段类型转换
    protected $type = [
        'id'         => 'integer',
        'word_count' => 'integer',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];
    // JSON字段
    protected $json = ['extra_data'];
    /**
     * 保存大文本内容
     */
    public function saveContent($title, $content, $options = [])
    {
        $this->title = $title;
        $this->content = $content;
        $this->content_hash = md5($content);
        $this->word_count = mb_strlen(strip_tags($content), 'utf-8');
        if (isset($options['extra_data'])) {
            $this->extra_data = $options['extra_data'];
        }
        return $this->save();
    }
    /**
     * 获取文章内容(带缓存)
     */
    public function getContentWithCache($id)
    {
        $cacheKey = 'article_content_' . $id;
        return Cache::remember($cacheKey, function() use ($id) {
            $article = self::find($id);
            return $article ? $article->content : null;
        }, 3600);
    }
    /**
     * 清空文章缓存
     */
    public function clearCache($id)
    {
        Cache::delete('article_content_' . $id);
        Cache::delete('article_detail_' . $id);
    }
}

检索方案实现

MySQL全文索引

<?php
namespace app\common\search;
use think\facade\Db;
class MysqlSearch
{
    /**
     * 创建全文索引
     */
    public function createFulltextIndex()
    {
        $sql = "ALTER TABLE article ADD FULLTEXT INDEX ft_content (title, content) WITH PARSER ngram";
        Db::execute($sql);
    }
    /**
     * 全文搜索
     */
    public function search($keyword, $page = 1, $limit = 10)
    {
        $where = "MATCH(title, content) AGAINST (:keyword IN NATURAL LANGUAGE MODE)";
        $total = Db::name('article')
            ->whereRaw($where, ['keyword' => $keyword])
            ->count();
        $list = Db::name('article')
            ->whereRaw($where, ['keyword' => $keyword])
            ->field('id, title, 
                    LEFT(content, 200) as content_preview,
                    MATCH(title, content) AGAINST (:keyword2) as relevance')
            ->bind(['keyword2' => $keyword])
            ->order('relevance DESC')
            ->page($page, $limit)
            ->select();
        return [
            'total' => $total,
            'list'  => $list
        ];
    }
}

使用Elasticsearch(推荐)

<?php
namespace app\common\search;
use Elasticsearch\ClientBuilder;
class ElasticsearchService
{
    private $client;
    public function __construct()
    {
        $config = config('elasticsearch');
        $this->client = ClientBuilder::create()
            ->setHosts($config['hosts'])
            ->build();
    }
    /**
     * 创建索引
     */
    public function createIndex($indexName = 'articles')
    {
        $params = [
            'index' => $indexName,
            'body' => [
                'settings' => [
                    'number_of_shards' => 3,
                    'number_of_replicas' => 1
                ],
                'mappings' => [
                    'properties' => [
                        'id' => ['type' => 'integer'],
                        'title' => [
                            'type' => 'text',
                            'analyzer' => 'ik_max_word',
                            'search_analyzer' => 'ik_smart'
                        ],
                        'content' => [
                            'type' => 'text',
                            'analyzer' => 'ik_max_word',
                            'search_analyzer' => 'ik_smart',
                            'index_options' => 'offsets'
                        ],
                        'created_at' => ['type' => 'date']
                    ]
                ]
            ]
        ];
        return $this->client->indices()->create($params);
    }
    /**
     * 索引文档
     */
    public function indexDocument($article)
    {
        $params = [
            'index' => 'articles',
            'id' => $article['id'],
            'body' => [
                'id' => $article['id'],
                'title' => $article['title'],
                'content' => $article['content'],
                'created_at' => $article['created_at']
            ]
        ];
        return $this->client->index($params);
    }
    /**
     * 搜索
     */
    public function search($keyword, $page = 1, $size = 10)
    {
        $params = [
            'index' => 'articles',
            'body' => [
                'from' => ($page - 1) * $size,
                'size' => $size,
                'query' => [
                    'multi_match' => [
                        'query' => $keyword,
                        'fields' => ['title^3', 'content'],
                        'operator' => 'and'
                    ]
                ],
                'highlight' => [
                    'pre_tags' => ['<em class="highlight">'],
                    'post_tags' => ['</em>'],
                    'fields' => [
                        'title' => ['fragment_size' => 100],
                        'content' => ['fragment_size' => 200, 'number_of_fragments' => 2]
                    ]
                ]
            ]
        ];
        $response = $this->client->search($params);
        return $this->formatSearchResult($response, $page, $size);
    }
    /**
     * 格式化搜索结果
     */
    private function formatSearchResult($response, $page, $size)
    {
        $hits = $response['hits'];
        $list = [];
        foreach ($hits['hits'] as $hit) {
            $item = $hit['_source'];
            $item['highlight'] = $hit['highlight'] ?? [];
            $list[] = $item;
        }
        return [
            'total' => $hits['total']['value'],
            'page'  => $page,
            'size'  => $size,
            'list'  => $list
        ];
    }
}

简易搜索封装

<?php
namespace app\common\search;
use think\facade\Db;
use app\common\search\ElasticsearchService;
class TextSearch
{
    private $searchEngine;
    public function __construct($engine = null)
    {
        $this->searchEngine = $engine ?: new ElasticsearchService();
    }
    /**
     * 综合搜索
     */
    public function search($keyword, $options = [])
    {
        // 关键词预处理
        $keyword = trim($keyword);
        if (mb_strlen($keyword) < 2) {
            return ['total' => 0, 'list' => []];
        }
        // 根据关键词长度选择搜索方式
        if (mb_strlen($keyword) <= 10) {
            // 短关键词使用ES
            return $this->searchEngine->search($keyword, 
                $options['page'] ?? 1, 
                $options['size'] ?? 10);
        } else {
            // 长文本使用MySQL LIKE查询
            return $this->mysqlSearch($keyword, $options);
        }
    }
    /**
     * MySQL搜索
     */
    private function mysqlSearch($keyword, $options)
    {
        $page = $options['page'] ?? 1;
        $limit = $options['size'] ?? 10;
        $like = '%' . $keyword . '%';
        $query = Db::name('article')
            ->where('title', 'like', $like)
            ->whereOr('content', 'like', $like);
        $total = $query->count();
        $list = $query
            ->field('id, title, LEFT(content, 200) as content_preview')
            ->page($page, $limit)
            ->select();
        return [
            'total' => $total,
            'page'  => $page,
            'size'  => $limit,
            'list'  => $list
        ];
    }
}

大文本优化策略

分块存储

<?php
namespace app\common\storage;
class TextChunk
{
    private $chunkSize = 60000; // 每块大小
    /**
     * 分块保存
     */
    public function saveChunked($content, $articleId)
    {
        // 计算分块数量
        $chunks = mb_str_split($content, $this->chunkSize);
        // 保存到数据库
        $data = [];
        foreach ($chunks as $index => $chunk) {
            $data[] = [
                'article_id' => $articleId,
                'chunk_index' => $index,
                'content' => $chunk
            ];
        }
        Db::name('article_chunks')->insertAll($data);
    }
    /**
     * 读取完整内容
     */
    public function getFullContent($articleId)
    {
        $chunks = Db::name('article_chunks')
            ->where('article_id', $articleId)
            ->order('chunk_index')
            ->column('content');
        return implode('', $chunks);
    }
}

内容压缩存储

<?php
namespace app\common\storage;
class CompressedStorage
{
    /**
     * 压缩后保存
     */
    public function saveCompressed($content, $articleId)
    {
        // GZIP压缩
        $compressed = gzcompress($content, 9);
        // Base64编码存储
        $encoded = base64_encode($compressed);
        return Db::name('article_compressed')->insert([
            'article_id' => $articleId,
            'content' => $encoded,
            'original_size' => strlen($content),
            'compressed_size' => strlen($encoded),
            'created_at' => date('Y-m-d H:i:s')
        ]);
    }
    /**
     * 读取解压
     */
    public function getContent($articleId)
    {
        $record = Db::name('article_compressed')
            ->where('article_id', $articleId)
            ->find();
        if (!$record) {
            return null;
        }
        // 解码并解压
        $compressed = base64_decode($record['content']);
        return gzuncompress($compressed);
    }
}

完整使用示例

<?php
namespace app\index\controller;
use think\Controller;
use app\common\model\Article;
use app\common\search\TextSearch;
use app\common\storage\TextFileStorage;
class ArticleController extends Controller
{
    /**
     * 保存文章
     */
    public function save()
    {
        $data = $this->request->post();
        // 验证数据
        validate(['title' => 'require|max:255', 'content' => 'require'])
            ->check($data);
        $article = new Article();
        // 保存文章
        $article->saveContent($data['title'], $data['content'], [
            'extra_data' => [
                'source' => $data['source'] ?? '',
                'author' => $data['author'] ?? ''
            ]
        ]);
        // 索引到ES
        try {
            $es = new ElasticsearchService();
            $es->indexDocument($article->toArray());
        } catch (\Exception $e) {
            // 记录日志,不影响主流程
            trace('ES索引失败: ' . $e->getMessage(), 'error');
        }
        return json(['status' => 1, 'message' => '保存成功']);
    }
    /**
     * 搜索文章
     */
    public function search()
    {
        $keyword = $this->request->get('keyword', '');
        $page = $this->request->get('page', 1);
        if (empty($keyword)) {
            return json(['status' => 0, 'message' => '关键词不能为空']);
        }
        $searchService = new TextSearch();
        $result = $searchService->search($keyword, ['page' => $page]);
        return json([
            'status' => 1,
            'data' => $result
        ]);
    }
    /**
     * 获取完整文章
     */
    public function detail($id)
    {
        $article = Article::find($id);
        if (!$article) {
            return json(['status' => 0, 'message' => '文章不存在']);
        }
        // 浏览量增加
        $article->setInc('views');
        // 获取内容(带缓存)
        $content = $article->getContentWithCache($id);
        return json([
            'status' => 1,
            'data' => [
                'id' => $article->id,
                'title' => $article->title,
                'content' => $content,
                'created_at' => $article->created_at,
                'views' => $article->views
            ]
        ]);
    }
}

性能优化建议

缓存策略

  • 使用Redis缓存热门文章内容
  • 缓存搜索结果
  • 合理的缓存过期时间

数据库优化

  • 合理使用索引
  • 分表存储大数据量文本
  • 使用读写分离

检索优化

  • 结合ES进行全文检索
  • 使用IK分词器提高中文搜索准确性
  • 优化查询语句

异步处理

  • 使用消息队列异步索引
  • 异步生成摘要

这个方案提供了完整的大文本存储与检索解决方案,可以根据实际需求选择合适的方式组合使用。

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