add event clustering feature to group articles by similarity and time proximity

This commit is contained in:
ImBenji
2026-04-21 12:45:22 +01:00
parent cb819e77ee
commit 3c238fb5f7
8 changed files with 239 additions and 102 deletions
+38 -1
View File
@@ -23,7 +23,7 @@ On startup the server:
1. Opens the SQLite database and runs any pending migrations.
2. Registers routes.
3. Starts the HTTP server.
4. Launches continuous background loops for each source, content backfill, and embedding backfill.
4. Launches continuous background loops for each source, content backfill, embedding backfill, and event clustering.
When a new article is inserted:
@@ -31,6 +31,7 @@ When a new article is inserted:
- `content` starts as `null`
- content backfill workers pick it up asynchronously — plain HTTP first, Playwright fallback for JS-heavy sites
- vector embeddings are generated after title, description, and content are all available
- the clustering worker assigns the article to an event once it has an embedding
- only articles with content + embedding are exposed via the API
Content backfill prioritises recent articles (`pub_date_effective DESC`) so newest content surfaces first regardless of ingestion order.
@@ -107,6 +108,41 @@ Semantic and similarity results also include `"distance": 0.1234`.
Returns one article by numeric ID. Same usability filter as the list endpoint — returns `404` if the article exists but has no content or embedding.
### `GET /events`
Returns a single event and its articles.
#### Query params
| Param | Description |
|---|---|
| `id` | Event ID (required) |
#### Response shape
```json
{
"id": 1,
"title": "...",
"created_at": "2025-01-01T12:35:10.000Z",
"articles": [
{
"id": 123,
"title": "...",
"description": "...",
"content": "...",
"url": "...",
"normalized_title": "...",
"source": "rss:BBC",
"pub_date": "2025-01-01T12:34:56.000Z",
"ingested_at": "2025-01-01T12:35:10.000Z"
}
]
}
```
Returns `404` if the event ID does not exist.
### `GET /status`
Returns archive summary. Cached for 30 seconds.
@@ -149,3 +185,4 @@ Use `domains[].policy` to diagnose why a source has high `skipped` or `failed` c
- Embeddings use OpenRouter and are indexed in `sqlite-vec` for ANN search.
- Query embeddings are cached in SQLite to avoid redundant API calls.
- SEC requests use the `User-Agent` from `config.json`.
- Event clustering groups articles by embedding similarity (cosine distance ≤ `config.clustering.distanceThreshold`, default `0.25`) and time proximity (within `config.clustering.windowHours`, default `72`). Articles outside the time window are never grouped together even if embeddings are close.