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'})
自动补全接口只返回 id 和 title,响应体极小。用户在输入框中打字时,每按一个键就触发一次自动补全,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 更务实的选择。
本文基于 甘蔗知行阁 博客项目的实际架构编写