在线沟通

MeiliSearch轻量级全文搜索实战

MeiliSearch:为 Flask 博客接入「开箱即用」的全文搜索

为什么不用 Elasticsearch?

Elasticsearch 功能强大,但个人博客场景下有几个痛点:

  • 内存消耗大 — 默认 JVM 堆内存 1GB 起步,个人服务器撑不住
  • 运维复杂 — 需要调 JVM 参数、管理分片、监控集群健康
  • 部署重 — 即使单节点部署,Docker 镜像也接近 1GB

我选了 MeiliSearch,一个用 Rust 写的轻量搜索引擎:

Elasticsearch MeiliSearch
语言 Java Rust
内存占用 1GB+ < 100MB
启动速度 30s+ < 1s
中文分词 需要装插件 内置支持
搜索响应 < 50ms < 5ms
配置复杂度 极低

对于个人博客这种 几千篇文章、几个并发请求 的场景,MeiliSearch 完全够用。

部署

Docker 一行命令搞定:

docker run -d \
  --name meilisearch \
  -p 7700:7700 \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch:latest \
  meilisearch --master-key="your_key"

在 Flask 中初始化:

import meilisearch

def init_search(app, index_name="articles"):
    url = app.config.get('MEILI_HOST', 'http://127.0.0.1:7700')
    key = app.config.get('MEILI_KEY', None)
    search_client = meilisearch.Client(url, key)
    app.extensions['meilisearch'] = search_client

    # 开发环境健康检查
    if app.debug:
        print(f"MeiliSearch 状态: {search_client.health()}")

搜索服务封装

class ArticleSearchService:
    def __init__(self, search_client):
        self.client = search_client
        self.index = search_client.index("articles")

    def sync_one(self, article):
        """同步单篇文章到索引"""
        preview_content = json.loads(article.preview_content)
        doc = {
            'id': article.id,
            'title': article.title,
            'content': article.content,
            'preview_mesh': preview_content.get('mesh'),    # AI 生成的知识图谱
            'preview_summary': preview_content.get('summary'), # AI 摘要
            'tags': [tag.name for tag in article.tags],
            'user_id': article.user_id,
            'author': article.user.user_name,
            'created_at': article.created_at.strftime("%Y-%m-%d %H:%M:%S")
        }
        return self.index.add_documents([doc])

    def delete_one(self, article_id):
        """从索引中删除"""
        return self.index.delete_document(article_id)

    def search(self, query, page=1, page_size=10):
        """全文搜索"""
        params = {
            'limit': page_size,
            'offset': (page - 1) * page_size,
            'attributesToCrop': ['content'],       # 对 content 字段截断
            'cropLength': 200,                     # 截断为 200 字
            'attributesToHighlight': ['title', 'preview_summary'],  # 高亮
            'highlightPreTag': '<span class="highlight">',
            'highlightPostTag': '</span>',
        }
        raw_result = self.index.search(query, params)
        return {
            'items': raw_result['hits'],
            'total': raw_result['estimatedTotalHits'],
            'time_ms': raw_result['processingTimeMs']
        }

    def search_autocomplete(self, query, page_size=8):
        """首页搜索框自动补全 — 只返回标题"""
        params = {
            'attributesToHighlight': ['title'],
            'attributesToRetrieve': ['id', 'title'],  # 只取 id + 标题
            'limit': page_size
        }
        raw_result = self.index.search(query, params)
        return {
            'items': raw_result['hits'],
            'total': raw_result['estimatedTotalHits']
        }

索引同步时机

文章发布和编辑时,同步触发索引更新:

# 发布文章后
article_celery_task.set_preview_content.delay(article.id)
# Celery 任务内部会:
#   1. AI 生成摘要
#   2. articleSS.delete_one(article_id)  ← 删旧索引
#   3. articleSS.sync_one(article)        ← 建新索引

# 删除文章后
ArticleSearchService(current_app.extensions['meilisearch']).delete_one(id)

核心逻辑:AI 摘要生成完毕 → 更新搜索索引 → 用户搜索时能搜到 AI 摘要内容

这意味着用户搜索时不仅能匹配文章标题和正文,还能匹配 AI 生成的知识图谱关键词和摘要。搜索体验比单纯 LIKE 查询好很多。

两个搜索接口:搜索页 vs 搜索框

# 搜索页 — 返回完整结果(标题 + 摘要 + 截断正文 + 高亮)
@my_index.route('/search_page')
def search_page():
    query = request.args.get('search', '').strip()
    page = request.args.get('page', 1, type=int)

    if not query:
        return render_template('search.html', pagination=empty, keyword='')

    search_client = current_app.extensions['meilisearch']
    raw_result = ArticleSearchService(search_client).search(query, page)
    # ...

# 搜索框自动补全 — 只返回标题(轻量快速)
@my_index.route('/article_search')
def article_search():
    autocomplete = request.args.get('autocomplete')
    if autocomplete:
        data = articleSS.search_autocomplete(search_text)  # 只取 id + title
    else:
        data = articleSS.search(search_text)                # 完整搜索
    return jsonify({'data': data, 'status': 'success'})

自动补全接口只返回 idtitle,响应体极小。用户在输入框中打字时,每按一个键就触发一次自动补全,MeiliSearch 5ms 级响应完全跟得上。

MeiliSearch 的核心优势总结

特性 我的实际体验
中文分词 不需要任何配置,开箱即用
容错搜索 输入"Flsk" 能搜到 "Flask",自带 typo tolerance
高亮返回 _formatted 字段直接返回带 HTML 标签的高亮文本
字段裁剪 attributesToCrop + cropLength 自动截断长文本
零配置 没有 mapping 概念,不需要提前定义字段类型
RESTful API 所有操作通过 HTTP API,调试方便

一个坑:搜索框输入中文时的性能

中文输入法打字时,每按一个键都会触发 oninput 事件,导致大量搜索请求。解决方案是在前端做 防抖

// 前端防抖
const searchInput = document.getElementById('search-input');
let debounceTimer;

searchInput.addEventListener('input', function() {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
        const query = this.value.trim();
        if (query.length > 0) {
            fetchAutocompleteResults(query);
        }
    }, 300);  // 用户停止输入 300ms 后才发请求
});

总结

对于个人项目或小团队来说,MeiliSearch 是比 Elasticsearch 更务实的选择。


本文基于 甘蔗知行阁 博客项目的实际架构编写