Meilisearch 集成与 RAG 内容检索总结

Meilisearch 集成与 RAG 内容检索总结

本次集成的 Meilisearch 已经不只是普通站内搜索,而是 AI Chat 的 RAG 内容检索层

当前流程是:

用户问题 → AI 提取搜索关键词 → Meilisearch 检索博客文章 → 构建 RAG 上下文 → 注入 system prompt → 大模型基于上下文回答

因此它属于 RAG 中的 Retrieval(检索)+ Context Building(上下文构建) 部分。

不过它不是向量数据库 RAG,目前使用的是 Meilisearch 的全文检索能力,而不是 embedding 相似度检索。

本次集成目标

  • 使用 meilisearch-rails 接入 Meilisearch。
  • Post 文章支持全文搜索。
  • 让 AI 聊天优先从博客内容中检索资料再回答。
  • 修复 AI 编造文章链接的问题。
  • 当 RAG 已检索到内容时,减少不必要的工具调用。
  • 支持 Kamal 生产环境部署 Meilisearch accessory。

主要改动文件

Gemfile

新增:

gem "meilisearch-rails"

用于 Rails 模型接入 Meilisearch。

config/initializers/meilisearch.rb

新增 Meilisearch 配置:

MeiliSearch::Rails.configuration = {
  meilisearch_url: ENV.fetch("MEILISEARCH_URL", "http://localhost:7700"),
  meilisearch_api_key: ENV.fetch("MEILI_MASTER_KEY", "blog_meili_key"),
  per_environment: false
}

说明:

  • 本地默认连接 http://localhost:7700
  • 生产环境通过 MEILISEARCH_URL 指向 Kamal accessory。
  • MEILI_MASTER_KEY 用于访问 Meilisearch。

app/models/post.rb

Post 模型加入:

include MeiliSearch::Rails

并配置索引:

meilisearch index_uid: "posts", synchronous: true, if: :published? do
  attributes :title, :slug, :excerpt, :content, :published_at, :sticky

  attribute :category_name do
    category&.name
  end

  attribute :category_slug do
    category&.slug
  end

  attribute :tag_names do
    tags.map(&:name)
  end

  attribute :tag_slugs do
    tags.map(&:slug)
  end

  searchable_attributes [:title, :content, :excerpt, :category_name, :tag_names]
  filterable_attributes [:category_slug, :tag_slugs, :sticky]
  sortable_attributes [:published_at, :sticky]
  displayed_attributes [:id, :title, :slug, :excerpt, :content, :category_name, :category_slug, :tag_names, :tag_slugs, :published_at, :sticky]
end

关键点:

  • if: :published?:只索引已发布文章。
  • searchable_attributes:设置可搜索字段。
  • displayed_attributes 必须包含 :id,否则 meilisearch-rails 无法把搜索结果映射回 Post 对象。

app/services/rag_retrieval_service.rb

新增 RAG 检索服务,职责:

  1. 接收用户问题。
  2. 用 AI 提取搜索关键词。
  3. 调用 Post.search(...) 使用 Meilisearch 检索文章。
  4. 将结果格式化为 RAG 上下文。
  5. 返回可注入 system prompt 的内容。

示例:

用户问题:有哪些诗歌
AI 提取关键词:诗歌
Meilisearch 检索:Post.search("诗歌")
RAG 上下文:文章标题、链接、分类、标签、摘要、正文片段
AI 回答:基于上下文列出相关文章

RAG 上下文中包含完整文章链接:

https://moli.ink/posts/{slug}

这用于避免 AI 编造类似 https://suimeng.net/article/1 的错误链接。

app/jobs/ai_chat_response_job.rb

主要改动:

1. 注入 RAG 上下文

build_messages 会取最后一条用户消息,调用:

RagRetrievalService.new.formatted_context(last_user_msg.content)

如果检索到内容,就追加到 system prompt。

2. RAG 有结果时减少工具调用

has_rag = true 时,只提供 GetPostTool

tools = has_rag ? tools_definition([ GetPostTool ]) : tools_definition

这样模型可以直接基于 RAG 上下文回答,不再重复调用:

  • search_posts
  • list_categories
  • list_posts_by_category

只有用户明确要求查看完整正文时,才调用 get_post

3. 约束链接来源

system prompt 中明确要求:

  • 如果上下文有检索内容,直接回答。
  • 引用文章链接时必须使用上下文或工具返回的 url 字段。
  • 禁止编造链接。

Tool 层改动

涉及文件:

  • app/tools/search_posts_tool.rb
  • app/tools/get_post_tool.rb
  • app/tools/list_posts_by_category_tool.rb

主要变化:

  • search_posts 从 SQL ILIKE 改为 Post.search(...)
  • 工具返回结果中增加 url 字段。
  • URL 统一使用:
https://moli.ink/posts/{slug}

本地开发配置

文件:Procfile.dev

新增 Meilisearch 进程:

meilisearch: /home/sui/meilisearch --master-key=blog_meili_key --http-addr localhost:7700 --db-path ./tmp/meili_data

本地可使用较短的 blog_meili_key

生产部署配置

文件:config/deploy.yml

App 环境变量

env:
  secret:
    - MEILI_MASTER_KEY
  clear:
    MEILISEARCH_URL: http://javen_blog-meilisearch:7700

MEILISEARCH_URL 是 Rails app 容器访问 Meilisearch accessory 容器的 Docker 内网地址。

Meilisearch accessory

accessories:
  meilisearch:
    image: getmeili/meilisearch:v1.12
    host: 43.166.1.239
    port: "127.0.0.1:7700:7700"
    env:
      clear:
        MEILI_ENV: production
      secret:
        - MEILI_MASTER_KEY
    directories:
      - data:/meili_data

说明:

  • Meilisearch 作为 Kamal accessory 运行。
  • 数据持久化到服务器目录。
  • 端口只绑定到服务器本机 127.0.0.1,不直接暴露公网。
  • 生产环境 MEILI_MASTER_KEY 必须至少 16 字节。

生产环境部署步骤

本地执行:

export MEILI_MASTER_KEY=<至少16字节的安全随机字符串>
kamal accessory reboot meilisearch
kamal deploy

进入生产 console:

kamal console

重建索引:

Post.ms_reindex!

验证搜索:

Post.search("雪花").map { |p| [p.id, p.title, p.slug] }
Post.search("诗歌").map { |p| [p.id, p.title, p.slug] }

遇到的问题与解决

Post.reindex! 不存在

错误:

undefined method 'reindex!' for class Post

正确命令:

Post.ms_reindex!

Post.ms_reindex! 最初不存在

原因可能是代码未部署、控制台连接旧容器或 gem 未加载。

处理方式:

  • 确认生产环境已部署最新代码。
  • 退出旧 console,重新进入。
  • 确认 meilisearch-rails 已在生产镜像中安装。

DNS 解析失败

错误:

Failed to open TCP connection to javen_blog-meilisearch:7700
getaddrinfo: Temporary failure in name resolution

原因:

  • Meilisearch accessory 未正常运行。
  • app 容器与 accessory 不在同一个 kamal Docker 网络。

排查:

ssh ubuntu@43.166.xxx.xxx 'docker network inspect kamal --format "{{json .Containers}}"'

解决:

kamal accessory reboot meilisearch
kamal deploy

Meilisearch 容器反复重启

日志:

The master key must be at least 16 bytes in a production environment.

原因:

blog_meili_key

只有 14 字节,生产环境不允许。

解决:

export MEILI_MASTER_KEY=<至少16字节的安全随机字符串>
kamal accessory reboot meilisearch
kamal deploy

并确认 .kamal/secrets 中存在:

MEILI_MASTER_KEY=$MEILI_MASTER_KEY

当前能力边界

当前是基于 Meilisearch 的轻量 RAG:

  • 支持全文关键词检索。
  • 支持文章标题、正文、摘要、分类、标签搜索。
  • 支持把检索结果注入大模型上下文。
  • 支持减少 AI 编造链接。
  • 支持在 RAG 有结果时减少工具调用。

但目前还不是 embedding/vector RAG:

  • 没有使用向量数据库。
  • 没有对文章分 chunk 做 embedding。
  • 对语义相似问题的召回能力主要依赖关键词和 Meilisearch 的全文检索。

后续可优化方向

  • 将长文章切成 chunk,再按 chunk 检索,提升长文问答效果。
  • 增加向量 embedding 检索,支持更强语义召回。
  • 将 Meilisearch 检索结果和向量检索结果合并排序。
  • 将 RAG 命中来源记录到聊天消息中,便于调试。
  • 为后台文章发布、更新、删除增加索引状态提示。

AI 助手 - deepseek-v4-flash

你好!有什么可以帮你的吗?

可以问我:推荐文章、搜索主题、了解博客内容

AI 生成内容仅供参考

未播放
0:00 / 0:00