Semantic search over your Telegram memes. Ask in Russian or English — find the meme you half-remember.
Pipeline: Telegram export → vision captions (bilingual) + OCR → multilingual embeddings (bge-m3, local) → PostgreSQL/pgvector hybrid search (dense + full-text, RRF) → cross-encoder reranking. Exposed as a CLI, a web UI and an MCP server, so your AI assistant can search your memes too.
See the design spec for the full architecture.
cp .env.example .env # fill in OpenRouter API key
make up # PostgreSQL + pgvector on :5433
make test
uv run memgrep ingest # load data/result.json + photos into the db
uv run memgrep caption --limit 400 # describe images via a vision model (pilot)
uv run memgrep embed # local bge-m3 embeddings
uv run memgrep search "this is fine" # CLI search; --open shows files in Preview
uv run memgrep serve # web UI on localhost:8000
uv run memgrep evals # golden-set metrics for the current config
uv run memgrep sync # ingest + caption + embed for new exports
uv run memgrep watch # follow the channel via Bot APIThe Telegram export goes to data/ (Telegram Desktop → Export chat history → Photos, JSON format). Memes and exported data stay local — data/ is gitignored; only code is published.
Measured on a 21-query golden set collected through the UI feedback button ("это он ✓"), pilot corpus of 400 images. Each retrieval layer was added only after it proved itself:
| Layer | hit@5 | hit@10 | MRR |
|---|---|---|---|
| dense vectors only | 0.857 | 0.905 | 0.768 |
| + full-text search, RRF fusion | 0.905 | 0.952 | 0.815 |
| + cross-encoder reranker | 0.905 | 1.000 | 0.839 |
Captioning-model comparison on the same corpus and golden set: qwen3-vl-8b matched or beat qwen3-vl-235b on retrieval metrics at a third of the price, so the full corpus is indexed with the 8b model (~$3.6 for ~11k images). Caveat documented in the evals history: the golden set was collected on top of the 8b index, which biases the comparison toward it.
Search config is env-driven: MEMGREP_SEARCH_MODE=vector|hybrid, MEMGREP_RERANK_ENABLED=true|false. memgrep evals writes a timestamped JSON per configuration into evals/results/ so runs stay comparable.
Let Claude (or any MCP client) search your memes:
claude mcp add memgrep -- uv run --directory /absolute/path/to/memgrep python -m memgrep.mcp_serverTools: search_memes(query, k) returns matches with file paths and captions; get_meme(sha256) returns the image itself.
The memgrep watch command follows a Telegram channel in real-time via Bot API long polling. Set up the bot as a channel admin, configure MEMGREP_TG_BOT_TOKEN and MEMGREP_TG_CHANNEL_ID, and run the watcher service. New photos posted to the channel are automatically indexed: they get captioned, embedded, and searchable within minutes. Reactions on recent posts are also synced back to the database for engagement tracking.
Python 3.12+, uv, PostgreSQL 17 + pgvector, sentence-transformers (BAAI/bge-m3 embeddings, BAAI/bge-reranker-v2-m3 reranker, both local), any OpenAI-compatible vision endpoint for captioning (default: OpenRouter, qwen3-vl), FastAPI, typer, MCP Python SDK. 49 tests, no network or model downloads in the test suite.