groupultra/telegram-search: local export and fuzzy search for Telegram history
🔍 导出并模糊搜索 Telegram 聊天记录 | Export and fuzzy search your Telegram chat history
At a glance
- What is it?
- A TypeScript project that mirrors Telegram chats into PGlite or PostgreSQL, adds tokenization and vector embeddings, and exposes search through a web UI, a bot, and a new local-first CLI. Its main constraint is that Telegram's own search is what it is replacing, so the export path and the Takeout permission model matter more than the feature list.
- Who is it for?
- Adopt it if you have multilingual or Chinese-language Telegram history that Telegram's built-in search handles poorly, and you are willing to run a Postgres or PGlite store plus MinIO for media. Skip it if you only need occasional lookups, because a full mirror with embeddings is more infrastructure than the problem deserves.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap it fills: Telegram search fails on the languages you actually write in
Telegram's server-side search is the thing being replaced here, and the README states the motivating case directly: users who cannot search Chinese chat history, or who lose a specific message inside a high-volume conversation. That is a narrower and more honest framing than a general backup tool. The project targets people whose message archive has outgrown keyword matching, either because the language does not tokenize well on Telegram's side or because the volume makes scrolling useless. The intended audience is self-hosters with a Telegram account they care about, not teams. Everything runs against a personal account's history, which is why the CLI section talks about named profiles and explicit user consent rather than service credentials. If your problem is searching a shared team archive or a support inbox, this is the wrong shape of tool.
Architecture: a mirror store, a tokenizer, and two search paths
The pipeline has three visible stages. First, messages are pulled from Telegram and written into a database, with PGlite and PostgreSQL both supported through the DATABASE_TYPE setting. Second, embedding and tokenization run during export, per the feature list, so the index is built as messages arrive rather than in a separate batch pass. Third, queries hit a combination of fuzzy matching and vector semantic search. Media is a separate concern: it can go to MinIO object storage, and the Docker image documentation notes that without MinIO parameters configured, media lands in a local data/media directory instead. There is also image embedding, which the README describes as text-to-image search. The bot is a second front end onto the same store, and it supports deep links back to the original message. Sync is described as real-time, pulling and updating recent conversations. The notable design choice is that the CLI writes to a profile-scoped PGlite instance, so the agent-facing path and the server path are not necessarily the same database.
The CLI path and its Takeout permission model
The repository now ships a local-first TypeScript CLI, and the README treats consent as a first-class part of the interface. Configuration is per named profile:
pnpm run build:packages pnpm -F @tg-search/cli build pnpm cli --profile work profile configure --apiId 123456 --apiHash abcdef pnpm cli --profile work auth login pnpm cli --profile work chats list --json pnpm cli --profile work sync --takeout --chat 123456 --from 2026-01-01 --to 2026-12-31 pnpm cli --profile work export --from 2026-01-01 --to 2026-12-31 --output ./telegram-2026
The documented boundaries are specific. chats list and messages list perform bounded remote reads and do not persist messages. Bulk sync requires prior user consent and an explicit --takeout flag; if authorization is missing, the user declines, or Takeout initialization fails, the command stops rather than falling back to a normal GetHistory call. Text sync requests only the selected chat categories, not contacts or file export. Stdout carries JSON results only, while Telegram logs, prompts and progress go to stderr. The export produces monthly JSONL plus a manifest, and deliberately excludes media binaries, login sessions, vectors and keys. That last set of exclusions is what makes the export safe to hand to another process. The trade-off is that an export is not a restorable backup: it is text and a manifest, and you still need the profile's own database for search.
Docker Compose is the intended deployment, and the env table is mostly optional
The documented path is three curl commands into an empty directory, then docker compose up -d, after which the UI answers on http://localhost:3333. The compose file pulls in the database and MinIO alongside the app. The environment table marks everything optional, with defaults if unset: TELEGRAM_API_ID and TELEGRAM_API_HASH from my.telegram.org, DATABASE_TYPE as postgres or pglite, DATABASE_URL only when DATABASE_TYPE=postgres, TELEGRAM_BOT_TOKEN from BotFather, PROXY_URL accepting formats like socks5://user:pass@host:port, PORT and HOST for the HTTP/WebSocket listener, BACKEND_URL for the Nginx reverse proxy upstream on /api and /ws, and the three MINIO_ variables. One detail worth reading twice: the README states that AI embedding and LLM settings are now configured in the application per account, under settings then API, not through .env. Anyone migrating from an older deployment should check that before assuming their previous environment variables still take effect. The single-image docker run form also works and skips MinIO, at the cost of media going to local disk.
Where it stops being the right tool
The most concrete limitation is visible in the CLI's own rules: sync depends on Telegram Takeout, and there is no fallback. If Takeout initialization fails for an account, bulk sync simply does not proceed. That is a deliberate safety choice, but it means the project inherits Takeout's availability and rate characteristics, and there is no documented workaround. Second, the export is not a backup. It contains monthly JSONL and a manifest, and excludes media binaries, sessions, vectors and keys, so restoring search from an export means re-embedding. Third, the resource profile is not small. A Postgres or PGlite store plus MinIO plus an embedding step during export means disk and compute scale with message volume and media, and the README gives no sizing guidance at all. Fourth, if you are not running this for yourself, the consent model gets awkward: the CLI assumes an agent asking a human for permission, which does not map cleanly onto a shared deployment. Finally, the roadmap items (knowledge graphs, link and image OCR indexing, Discord support) are unchecked boxes. Treat them as direction, not capability.
How it differs from Telegram's own search and from a plain archive dump
The obvious alternative is Telegram's built-in search, and the difference is architectural rather than cosmetic. Telegram searches its own servers against its own index, with no tokenization you control and no vector matching. This project copies messages into a database you operate, tokenizes them locally, and adds embeddings, which is why it can match a sentence rather than a substring and why it can search images by description. The second alternative is a plain export tool that dumps JSON and stops. That gets you portability but no query interface, no ranking, and no semantic layer. The cost of this project's approach is that you now own a database, a media bucket, and an embedding pipeline, and you have to keep them in sync with a live account. The third comparison is the hosted instance at search.lingogram.app, which the README offers as a no-deployment trial. Using it means your chat data leaves your machine, which is the exact thing the self-hosted path exists to avoid. Pick based on whether you want the index to live with you.
Licence, maintenance and what upgrading costs you
The licence is AGPL-3.0, which is the network-copyleft variant. If you modify the code and let users interact with it over a network, the licence's terms about offering the corresponding source apply. For a personal self-hosted instance that is typically a non-issue; for a hosted service built on a fork it is the central question, and it is worth reading the licence text rather than a summary. On maintenance, the release cadence visible in the material is uneven: v1.2.7 and v1.2.8 landed a day apart in late June 2026, then v1.3.1 in mid August, with the last push to main in early September 2026. The repository is not archived and CI and Docker release workflows exist. The upgrade cost that actually bites is schema and configuration drift, not the version number. The move of AI embedding and LLM settings from environment variables to per-account in-app configuration is exactly that kind of change, and it is the reason to read the release notes before pulling a new image. Back up the database and the MinIO bucket before upgrading, because re-embedding a large history is the expensive part of a bad upgrade.
Editorial conclusion
Adopt it if you have multilingual or Chinese-language Telegram history that Telegram's built-in search handles poorly, and you are willing to run a Postgres or PGlite store plus MinIO for media. Skip it if you only need occasional lookups, because a full mirror with embeddings is more infrastructure than the problem deserves. Before committing, verify three things in your own environment: that the Takeout flow completes for your account, which DATABASE_TYPE you will run and how you back it up, and that AGPL-3.0 is acceptable for how you intend to expose the service.
Community notes