cd /news/natural-language-processing/tracking-vietnamese-news-trends-with… · home › topics › natural-language-processing › article
[ARTICLE · art-164882] src=dev.to ↗ pub= topic=natural-language-processing verified=true sentiment=↑ positive

Tracking Vietnamese news trends with embeddings, HDBSCAN and an LLM

A developer built "Tin tức hôm nay có gì?", a TypeScript/Bun service that ingests roughly 95 Vietnamese RSS feeds, embeds articles with jina-embeddings-v3 (falling back to local LaBSE), and clusters them with HDBSCAN to group coverage of the same event into tracked topics. Topics persist across pipeline passes via centroids with cosine thresholds of 0.75 for joining, 0.82 for same-pass merges and 0.78 for cross-pass merges, and each topic's lifecycle is tracked by velocity and acceleration over a 2-hour window, with an LLM summarizing what drives it.

by read5 min views3 publishedNov 5, 2026

A big event is usually covered by dozens of outlets at once, under different headlines. Reading article by article, it's hard to see the overall picture. The more useful question isn't "what's in the news today", but: which stories are heating up, which are cooling off, and why?

To answer that, I built Tin tức hôm nay có gì? ("What's in the news today?"), a system that reads about 95 Vietnamese RSS feeds, groups articles about the same thing into topics, tracks each topic's lifecycle, and has an LLM summarize what is driving it. This post covers how it works and the numbers I picked along the way.

It's a single TypeScript service running on Bun, with its data in one SQLite file (bun:sqlite). The pipeline runs in passes, and each pass goes through these stages:

The same image runs as web (dashboard and API), pipeline, or both. In production the two run separately, so the dashboard isn't affected while the pipeline is busy.

News doesn't arrive evenly through the day, so the pipeline doesn't run evenly either. The schedule is computed in Vietnam time:

Time of day Every
6–9 am and 5–10 pm 15 minutes
9 am–5 pm 30 minutes
Overnight 60 minutes

Passes are aligned to round times (7:00, 7:15, 7:30…), so the dashboard can always say when the next one will run.

The same article often shows up in several feeds of one outlet (front page, section, latest news). I hash the URL into a content_hash and skip anything already in the database. It's simple and good enough, since one article nearly always keeps one URL. Two different outlets covering the same event are not duplicates, though: that's exactly the signal clustering needs.

By default I use jina-embeddings-v3 through Jina AI's API; it's multilingual and handles Vietnamese well. Without an API key the service falls back to running LaBSE locally (via @xenova/transformers). It's slower, but the pipeline doesn't depend on an outside service to keep working.

Vectors are stored with the article, and each pass only embeds new articles, up to 1,000 at a time.

With news, I don't know in advance how many topics there are, and most articles are noise: one-off stories that don't belong to anything bigger. K-means makes you pick the number of clusters and forces every point into one. HDBSCAN finds the number of clusters on its own and labels points that don't fit anywhere as -1. For this problem, that's exactly the behavior I want.

It runs in a small Python process (the hdbscan library); Bun pipes the vectors in on stdin and reads the labels from stdout. The current settings:

HDBSCAN(
    min_cluster_size=8,
    min_samples=2,
    metric="euclidean",
    cluster_selection_method="leaf",
)

A few of these choices are worth explaining:

euclidean ranks neighbors exactly as cosine would, and HDBSCAN is faster with it.leaf rather than eom. min_cluster_size=8. HDBSCAN only knows about the articles in the current pass. For a topic to live for hours, each topic stores a centroid (the mean of its vectors), and every new cluster is compared against the centroids of active topics:

Rule Cosine threshold
New cluster joins an existing topic ≥ 0.75
Merge two clusters in the same pass ≥ 0.82
Merge two topics across passes ≥ 0.78
Split a topic that got too big from 30 articles

The merge threshold is higher than the join threshold on purpose: a wrong merge is hard to undo, while leaving two clusters apart for one more pass costs almost nothing. After each merge the centroid is recomputed right away, which stops cascades (A absorbs B, then AB absorbs C only because it grew).

Each topic moves through a simple state machine driven by velocity (how fast new articles arrive, measured over a 2-hour window) and acceleration (how velocity is changing):

Only Signal through Peak count as active, which is also what limits the LLM calls.

Every transition is recorded with its reason (for example "Count >= 10 and velocity > 1.0"), so when a topic jumps around I can read its history and see why.

For ranking, each topic gets a score from 0 to 100, the sum of six parts normalized to 0–1:

Part Weight Maxes out at
Velocity (last 2 hours) 25 10 articles
Volume (24 hours) 20 150 articles
Acceleration 20 5
Distinct sources 15 8 sources
Velocity relative to the average 10 3×
Topic freshness 10 decays to 0 over 7 days

Distinct sources get their own weight because 20 articles from one outlet are very different from 8 outlets covering the same story. The score is then multiplied by a decay factor based on the time since the latest article: no decay in the first hour, then 20% after 1 hour, 40% after 6, 60% after 12, 80% after 24, and zero after 48 hours.

The LLM is called through an OpenAI-compatible API (DeepSeek by default), for two kinds of requests:

driving_force (the main cause), trend_rationale (why it's heating up or calming down), opportunity (a concrete opportunity or risk, and for which industry), watch_next (what to watch for) and an opportunity_score from 0 to 10. LLM calls cost money, so choosing which topics to analyze matters as much as the prompt:

Token usage is recorded per pass to keep an eye on cost. Setting LLM_ENABLED=false keeps the whole pipeline running, just without names and analysis.

Day to day, the service needs little attention:

There is also room for improvement:

The live version is at news.vuminhhoa.com. If you're working on something with Vietnamese news too, I'd like to hear about it: my email is on the home page.

── more in #natural-language-processing 4 stories · sorted by recency
── more on @tin tức hôm nay có gì? 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/tracking-vietnamese-…] indexed:0 read:5min 2026-11-05 · —