AI 表情包语义搜索系统 — Go 后端 + React 前端 + Expo 移动端 monorepo
Emomo 让你用自然语言搜表情包。系统由 Go 后端(搜索 + 本地静态图片目录摄入)、React 前端(Web 用户界面)和 Expo React Native 移动端组成。当前默认检索链路以 Qwen3-VL 多模态 image embedding 为主:导入时直接为图片生成 image 向量,并为 OCR/描述/tags 写入 keyword/BM25 sparse-only 向量;搜索时 image route 权重 0.7,keyword route 权重 0.3。VLM 描述和 OCR 作为展示元数据与 keyword 辅助信号保留;dense caption embedding 仍默认关闭,待 caption 策略验证后再启用。
资源约束:表情包资源只支持静态图片;GIF 文件不再支持,也不会被摄入。
当前关系库收敛为四张核心表:memes、meme_annotations、meme_vectors,外加只记录来源/出处、不参与检索的 meme_metadata。protobuf message schema 定义在 backend/proto/emomo/v1/,拆为 types.proto / meme.proto / api.proto。在本项目里,protobuf 的边界是 API DTO、前后端生成类型、跨边界封闭枚举,以及少量结构化 DB JSON 值(当前仅 memes.image_info、meme_annotations.labels);关系表结构、迁移、运行时配置和 UI 状态不归 protobuf 管。生成代码集中在 backend/gen/(Go)与 frontend/gen/(TS),均以 linguist-generated=true 标记。数据库结构详见 docs/DATABASE_SCHEMA.md。
Supabase/PostgreSQL 部署中这四张核心表不启用 Row Level Security;前端不直接访问 Supabase 表,而是通过 Go API 访问数据,访问控制在服务端数据库连接层完成。
emomo/
├── backend/ # Go + Gin + Qdrant + GORM,REST API + 摄入流水线
├── frontend/ # React 19 + Vite + Framer Motion,单页应用
├── mobile/ # Expo + React Native,iOS / Android 搜索 App
├── deployments/ # 跨服务的 Docker Compose 编排(API + Grafana Alloy)
├── docs/ # 跨服务设计与使用文档
├── scripts/
│ └── start.sh # 本机一键起后端 + 前端
├── render.yaml # Render 部署配置(rootDir: backend)
└── railway.json # Railway 部署配置(dockerfilePath: backend/Dockerfile)
每个子项目都有自己的 README.md / AGENTS.md / CLAUDE.md / GEMINI.md,说明该子项目的本地开发与约定:
./scripts/start.sh脚本会先启 backend(go run ./cmd/api,端口 8080),再启 frontend(npm run dev,端口 5173)。需要先在 backend/.env 填好 API keys(Qdrant、对象存储、VLM、embedding 等)。
# 后端
cd backend
cp .env.example .env # 首次:填好 API keys
go run ./cmd/api
# 前端
cd frontend
cp .env.example .env # 首次:默认指向 http://localhost:8080/api/v1
npm install
npm run dev
# 移动端
cd mobile
npm install
npm run gen
EXPO_PUBLIC_API_BASE=http://localhost:8080/api/v1 npm run start数据导入只支持 backend/scripts/import-data.sh 这一种入口。默认配置会使用 qwen3vl profile 写入 image 向量和 keyword/BM25 sparse-only 向量;caption dense 向量可以通过显式 -e qwen3vl_caption 做实验性回填,但不是默认导入链路:
cd backend
./scripts/import-data.sh -p ./data/memes
# 或显式指定 profile:
./scripts/import-data.sh -p ./data/memes --profile qwen3vl详见 docs/MULTI_EMBEDDING.md 与 backend/configs/config.yaml。
修改 backend/proto/emomo/v1/ 下任意 .proto 后,需要同时重新生成后端 Go 与前端 TS:
# Go → backend/gen/
cd backend && GOTOOLCHAIN=go1.26.2 go run github.com/bufbuild/buf/cmd/buf@v1.69.0 generate
# TS → frontend/gen/
cd frontend && npm run gen
# TS → mobile/gen/
cd mobile && npm run gen| 子项目 | 关键技术 |
|---|---|
| backend | Go 1.26.2, Gin, GORM, Qdrant (gRPC), S3/R2, Qwen3-VL 多模态 embeddings, OpenAI-compatible VLM/OCR 辅助分析, BM25 hybrid 检索, Grafana Alloy + Loki |
| frontend | React 19, TypeScript, Vite 7, Framer Motion, Playwright e2e |
| mobile | Expo SDK 54, React Native 0.81, React 19, TypeScript, AsyncStorage, Expo MediaLibrary/Sharing/FileSystem |
- Docker Compose(本机):
docker compose --env-file backend/.env -f deployments/docker-compose.yml up -d,会起 API 容器 + Grafana Alloy 日志采集(Qdrant 与对象存储需自备)。 - Render:根的 render.yaml 把后端服务的 rootDir 设为
backend/。 - Railway:根的 railway.json 指向
backend/Dockerfile。 - Hugging Face Space:
.github/workflows/sync_to_hf.yml在每次 push 到 main 时把backend/子树拆出来 force-push 到 Space 的main分支,所以 Space 看到的根就是backend/。
- 提交信息使用 Conventional Commits(
feat:、fix:、chore:等);跨子项目的改动在正文里按目录分点说明。 - AI agents 协作约定见各子项目的 AGENTS.md / frontend/AGENTS.md 与本仓库根的 AGENTS.md。
- 不提交 secrets,使用各子项目下的
.env与.env.example。