Skip to content

fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL|Loc dedup (#3852)#3853

Merged
koala73 merged 9 commits into
koala73:mainfrom
shubham1206agra:fix-gdelt-gkg-events
May 26, 2026
Merged

fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL|Loc dedup (#3852)#3853
koala73 merged 9 commits into
koala73:mainfrom
shubham1206agra:fix-gdelt-gkg-events

Conversation

@shubham1206agra

@shubham1206agra shubham1206agra commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #3852

Root issue: GDELT v1 GKG GeoJSON only accepts one theme tag per call and uses uppercase QUERY/MAXROWS. The previous free-text keyword queries (protest OR riot OR ..., breakthrough OR discovery OR ...) weren't matching GDELT's tag-based index well, and sending the unfiltered response without any errors.

Both GDELT GKG seeders moved from free-text keyword queries to single-theme fan-out, since v1 GKG only accepts one theme tag per call and uses uppercase QUERY/MAXROWS params.

scripts/ais-relay.cjs (positive events):

  • Replaced 3 keyword OR-queries with 6 theme tags: SOC_INNOVATION, EDUCATION, MEDICAL, TOURISM, WB_1765_CULTURE_HERITAGE_AND_SUSTAINABLE_TOURISM, PEACEKEEPING.
  • Fixed param casing (QUERY/MAXROWS).
  • Added urltone > 2 gate so only positive-tone articles pass.
  • Added cross-call URL|Loc dedup (seenUrlLocs) so a multi-theme article doesn't inflate counts.

scripts/seed-unrest-events.mjs (unrest events):

  • Replaced the single keyword OR-query with 3 theme tags: PROTEST, STRIKE, VIOLENT_UNREST.
  • Fixed param casing.
  • Fanned out one request per theme into a shared locationMap, merging counts and source URLs.
  • Added cross-call URL|Loc dedup.
  • Tolerates partial failure: proceeds if at least one theme call succeeds; only throws if all fail.

Type of change

  • Bug fix
  • New feature
  • New data source / feed
  • New map layer
  • Refactor / code cleanup
  • Documentation
  • CI / Build / Infrastructure

Affected areas

  • Map / Globe
  • News panels / RSS feeds
  • AI Insights / World Brief
  • Market Radar / Crypto
  • Desktop app (Tauri)
  • API endpoints (/api/*)
  • Config / Settings
  • Other:

Checklist

  • Tested on worldmonitor.app variant
  • Tested on tech.worldmonitor.app variant (if applicable)
  • New RSS feed domains added to api/rss-proxy.js allowlist (if adding feeds)
  • No API keys or secrets committed
  • TypeScript compiles without errors (npm run typecheck)

Screenshots

…dedup

v1 GKG GeoJSON only accepts one theme tag per call, and the previous
free-text keyword queries weren't matching GDELT's tag-based index,
returning non-filtered events which is useless for the usecase. Fan
out both seeders (ais-relay positive events, seed-unrest-events)
across themes, merge into a shared locationMap, and dedup by article
URL across calls so multi-theme articles don't inflate counts. Also
fix param casing (QUERY/MAXROWS) and add a urltone > 5 gate for the
positive-events path. The unrest seeder now tolerates partial theme
failures and only throws if all calls fail.
@vercel

vercel Bot commented May 21, 2026

Copy link
Copy Markdown

@shubham1206agra is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the trust:safe Brin: contributor trust score safe label May 21, 2026
@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73 This is how I came up with themes which can give maximum throughput.

GDELT GKG GeoJSON v1 — Positive-tone yield by theme

Probed via QUERY=<theme>&TIMESPAN=1440 (24h window, ~99% of features carry urltone).
Probe date: 2026-05-21, ~24h windows during US business hours.

Theme Features URLs tone>5 tone>10 %pos
WB_1765_CULTURE_HERITAGE_AND_SUSTAINABLE_TOURISM 303 91 132 13 43.6%
SOC_INNOVATION 2,500 1,017 864 48 34.6%
PEACEKEEPING 418 72 103 0 24.6%
EDUCATION 2,500 723 583 5 23.3%
MEDICAL 2,500 975 578 54 23.1%
WB_1950_AGRICULTURE_TECHNOLOGY 58 21 13 0 22.4%
TOURISM 2,500 636 467 15 18.7%
WB_588_CONSERVATION_FINANCE 81 32 13 0 16.0%
WB_1100_SUSTAINABLE_GROWTH 1,499 548 230 9 15.3%
AGRICULTURE 2,500 946 290 16 11.6%
SLFID_PEACE_BUILDING 61 19 6 0 9.8%
WB_1041_PATENTS 819 265 70 11 8.5%
ENV_GREEN 2,070 618 162 0 7.8%
ENV_GEOTHERMAL 287 62 21 0 7.3%
SOC_TECHNOLOGYSECTOR 125 54 9 0 7.2%
WB_1614_NUTRITIONAL_PROGRAMS 621 306 42 5 6.8%
WB_2084_BIODIVERSITY 870 359 42 6 4.8%
WB_1057_SUSTAINABLE_FOREST_MANAGEMENT 22 10 1 0 4.5%
ENV_SOLAR 2,005 607 88 1 4.4%
ENV_CARBONCAPTURE 79 24 2 0 2.5%
ENV_HYDRO 467 148 11 0 2.4%
HEALTH_VACCINATION 2,500 869 32 2 1.3%
ENV_WINDPOWER 460 166 1 0 0.2%
SOC_AIDASSISTANCE 543 99 0 0 0.0%
WB_605_WILDLIFE_RESOURCES 21 7 0 0 0.0%
LITERACY empty
TAX_AIDGROUPS empty
WB_1903_SCIENCE_TECHNOLOGY_AND_INNOVATION empty
WB_1716_POST_DISASTER_RECOVERY_PLANNING empty
WB_1162_POVERTY_NUTRITION_AND_FOOD_SECURITY empty

Note

  • %pos = (features with urltone > 5) / total features × 100
  • Themes hitting the 2,500-feature cap have far more data behind it — the actual positive count is a lower bound
  • Themes with very few features (<100) are noise-prone; treat percentages as indicative only
  • "empty" = the theme returned 0 features in the 24h window probed (may have data in other windows)
  • tone>10 = strongly positive subset (high-precision filter)

Selected for production POSITIVE_QUERIES (6 themes)

Balances absolute positive volume vs. precision; covers the 6 classifier categories:

Theme Maps to category Positive features
SOC_INNOVATION innovation-tech 864
EDUCATION culture-community 583
MEDICAL science-health 578
TOURISM (broad coverage) 467
WB_1765_CULTURE_HERITAGE_AND_SUSTAINABLE_TOURISM culture-community 132
PEACEKEEPING humanity-kindness 103

Note

Used AI to generate this formatted text

@shubham1206agra shubham1206agra changed the title fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL dedup fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL dedup (#3852) May 21, 2026
@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two GDELT GKG seeders that were sending free-text keyword queries the v1 API doesn't index well, replacing them with single-theme fan-out calls (one request per theme tag), fixing QUERY/MAXROWS param casing, and adding cross-call URL dedup so a multi-theme article can't inflate location counts.

  • scripts/ais-relay.cjs: 3 OR-queries → 6 theme tags; adds a urltone > 5 gate to keep only positive-tone articles; seenUrls dedup is correctly scoped across the sequential calls.
  • scripts/seed-unrest-events.mjs: 1 OR-query → 3 theme tags (PROTEST, STRIKE, VIOLENT_UNREST); introduces a shared locationMap and seenUrls across themes; adds partial-failure tolerance — but the console.log after the fan-out loop references features, now a block-scoped variable inside the loop, causing a ReferenceError on every successful run.

Confidence Score: 3/5

The unrest seeder will throw a ReferenceError on every successful run, silently discarding all aggregated events — this path needs a fix before merging.

The features variable is declared with const inside the fan-out loop body in seed-unrest-events.mjs, making it block-scoped to each iteration. The console.log statement that follows the loop still references features.length; as an ES module the file runs in strict mode, so every successful call to fetchGdeltEvents ends with a ReferenceError: features is not defined instead of returning the aggregated events. The positive-events changes in ais-relay.cjs are clean.

scripts/seed-unrest-events.mjs — the post-loop console.log references a block-scoped variable from the loop

Important Files Changed

Filename Overview
scripts/seed-unrest-events.mjs Fans out one GDELT GKG query into 3 single-theme calls (PROTEST, STRIKE, VIOLENT_UNREST), merges into a shared locationMap with cross-call URL dedup and partial-failure tolerance — but introduces a ReferenceError on every successful run because features is declared block-scoped inside the loop yet referenced in a console.log after the loop.
scripts/ais-relay.cjs Replaces 3 free-text OR-queries with 6 GDELT theme tags, fixes QUERY/MAXROWS param casing, adds a urltone > 5 gate, and adds cross-call seenUrls dedup — logic and flow are correct; only a minor naming ambiguity around POSITIVE_TONE_MIN.

Sequence Diagram

sequenceDiagram
    participant S as Seeder
    participant G as GDELT GKG v1 API
    participant R as Redis / Store

    Note over S: seed-unrest-events.mjs
    loop for each theme in [PROTEST, STRIKE, VIOLENT_UNREST]
        S->>G: "GET ?QUERY=THEME&MAXROWS=2500"
        G-->>S: GeoJSON features
        S->>S: skip if fUrl in seenUrls
        S->>S: merge into shared locationMap
        S->>S: add fUrl to seenUrls
        Note over S: wait 5.5s (rate limit)
    end
    S->>S: "filter count >= 5"
    S->>R: store aggregated events

    Note over S: ais-relay.cjs (positive events)
    loop for each theme in [SOC_INNOVATION, EDUCATION, MEDICAL, TOURISM, ...]
        S->>G: "GET ?QUERY=THEME&MAXROWS=500"
        G-->>S: GeoJSON features
        S->>S: skip if url in seenUrls
        S->>S: "skip if urltone <= 5"
        S->>S: merge into local locationMap
        S->>S: add url to seenUrls
        Note over S: wait 5.5s (rate limit)
    end
    S->>S: "filter count >= 3, dedup by name"
    S->>R: store positive events
Loading

Comments Outside Diff (1)

  1. scripts/seed-unrest-events.mjs, line 354 (link)

    P1 features is out of scope — ReferenceError on every successful run

    After the fan-out refactor, const features = data?.features || [] is declared inside the for (let i = 0; ...) loop body, making it block-scoped to each iteration. The console.log below the loop still references features.length, but that binding is gone by the time execution reaches it. Because .mjs files are always in strict mode, this throws ReferenceError: features is not defined on every successful invocation of fetchGdeltEvents, preventing any events from being returned to the caller.

Reviews (1): Last reviewed commit: "fix(seeders): switch GDELT GKG queries t..." | Re-trigger Greptile

Comment thread scripts/ais-relay.cjs Outdated
Update const POSITIVE_TONE_MIN to POSITIVE_TONE_THRESHOLD to better reflect the usage of the constant

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Comment thread scripts/ais-relay.cjs Outdated
@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73

koala73 commented May 21, 2026

Copy link
Copy Markdown
Owner

Code Review — switch GDELT GKG queries to single-theme fan-out + URL dedup

The core diagnosis (param casing + single-theme constraint) is sound and the partial-failure handling is a genuine improvement. But the PR ships non-trivial new logic with zero test changes, and one existing locked contract is broken.

🔴 Blocking

  • Breaks existing test tests/seed-unrest-gdelt-fetch.test.mjs (case Fetch AIS signals from API instead of hardcoded data #6).
    That test mocks _proxyFetcher to throw once then succeed, and asserts calls === 2. With 3-theme fan-out the fetcher is now invoked 4 times (1 throw + 3 successful theme calls), so assert.equal(calls, 2, 'should retry exactly once after the 522') fails. The test header explicitly says it "locks the behavioural contract" — this is CI breakage. The test must be updated to the fan-out model.

  • No test coverage for any of the new behavior. Both changed files are production scripts. Nothing exercises: the QUERY/MAXROWS casing fix, theme fan-out + locationMap merge, cross-call seenUrls dedup, the anyThemeSucceeded partial-failure path (all-fail → throw, one-succeeds → proceed), or the positive urltone gate. At minimum add: all-themes-fail → throws with lastError; one-theme-fails → still aggregates; same-URL across two themes counted once.

🟡 Should fix

  • URL-only dedup collapses multi-location articles. GKG v1 GeoJSON emits one feature per (article, location) mention (hence the original existing.count++ and the "individual mentions, not aggregated counts" comment). seenUrls keys on properties.url alone, so an article mentioning Cairo + Alexandria now contributes to only the first location — and this fires within a single theme call, not just across calls. The intended fix (don't double-count an article carrying multiple theme tags) needs a composite key:

    const dedupKey = url ? `${url}|${key}` : null;   // key = `${lat.toFixed(1)}:${lon.toFixed(1)}`

    As written, the change also silently shifts count semantics from "total mentions at a location" to "distinct articles at a location," lowering values against the count < 5 floor and the count > 20 HIGH-confidence threshold in the unrest seeder — fewer events qualify. If that re-tune is intentional, call it out; otherwise the composite key restores prior behavior.

  • Dedup field mismatch with extractGdeltSourceUrls. Dedup checks only feature.properties?.url, but extractGdeltSourceUrls recognizes 7 URL fields (source_url, document_url, article_url, …). A feature exposing only source_url escapes dedup entirely. Test Fetch AIS signals from API instead of hardcoded data #6's own mock has features[1] carrying just source_url, so the inconsistency is already latent in the fixture. Use the same URL-resolution logic for the dedup key as for source extraction.

  • Positive urltone > 5 gate is aggressive. GDELT urltone is roughly −10..+10; strictly > 5 is a small slice, and themes like MEDICAL/EDUCATION skew neutral-to-negative. Combined with the theme switch, the positive-events panel could go sparse or empty. Verify live volume before merge and consider a lower threshold (e.g. > 2).

🟢 Minor / notes

  • Runtime grows: positive seeder is now 6 themes × 5.5s spacing (~+28s) plus per-call proxy timeouts/retries; unrest +11s. Fine for a cron seeder, but worth noting if either has a wall-clock budget.
  • Good: seenUrls.add is placed after coordinate validation in both files, so invalid features don't poison the dedup set.
  • The load-bearing claim — v1 GKG requires uppercase QUERY/MAXROWS and ignored the old lowercase params — is plausible and matches GDELT's case-sensitive v1 APIs, but it's the entire premise of the fix. A one-line note on before/after live response volume would close that loop.

Verdict

Right root cause and reasonable structure, but not mergeable as-is: it breaks an existing test and adds substantial untested logic. Update seed-unrest-gdelt-fetch.test.mjs for fan-out, add coverage for dedup + partial-failure, and fix the URL-only dedup key (composite url|location) before merge.

@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73 Thanks for the review. But can you confirm if the positive themes are fine with you?

@shubham1206agra

Copy link
Copy Markdown
Contributor Author

Let me address #3853 (comment) and I will let you know when this is ready

…ution

GKG v1 emits one feature per (article, location), so URL-only dedup
collapses multi-location articles into a single location. Key dedup on
(url, lat/lon bucket) instead, and resolve the URL via the same
multi-field walk (extractGdeltSourceUrls / gkgFeatureUrl) used for
source attribution — features carrying only source_url were bypassing
dedup entirely.

Lower POSITIVE_TONE_THRESHOLD from 5 → 2. Live volume check showed
> 5 yielded ~1 positive event vs ~9 at > 2; the panel was going
effectively empty.

Add test coverage for the fan-out contract introduced in this PR:
counts merge across themes at the same location, partial-failure
tolerance (one theme dies, others still aggregate), all-themes-fail
throws lastError, composite-key dedup preserves multi-location
articles, and source_url-only features are deduped across themes.
@shubham1206agra

Copy link
Copy Markdown
Contributor Author

A one-line note on before/after live response volume would close that loop.

@koala73 Please see the following table for confirmation

# Call bytes features
0 no params (baseline default) 1,312,455 2500
1 lowercase: query=protest, maxrows=2500 1,312,455 2500
2 uppercase: QUERY=protest, MAXROWS=2500 1,245,695 1830
3 OLD unrest: query=protest OR riot OR demonstration OR strike 1,312,455 2500
4 NEW unrest theme: QUERY=PROTEST 1,245,695 1830
5 OLD positive: query=(breakthrough OR discovery OR …), maxrows=500 1,312,455 2500
6 NEW positive theme: QUERY=SOC_INNOVATION, MAXROWS=500 264,978 500

@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73 This is ready for 2nd round of review

@koala73

koala73 commented May 21, 2026

Copy link
Copy Markdown
Owner

Re-review (after fan-out/dedup updates)

Thanks for the revisions — the earlier blocking items are resolved. Verified by checking out the PR head and running node --test tests/seed-unrest-gdelt-fetch.test.mjs11/11 pass.

✅ Resolved

🟡 Should fix — new CI-time regression

The inter-theme rate-limit delay at seed-unrest-events.mjs:279 is not injectable:

if (i > 0) await new Promise((r) => setTimeout(r, 5_500)); // GDELT rate limit

The proxy-retry path has _sleep/_jitter seams, but this new fan-out delay has none. Result: each of the 6 fetchGdeltEvents tests sleeps a real 2 × 5.5 s = 11 s — I measured tests #6#11 at ~11 000 ms each, ~66 s of pure sleep added to every CI run. Add a _themeDelayMs opt (default 5500) — or route the delay through the existing _sleep seam — so tests can pass 0. The positive seeder's 5×5.5 s loop has the same untestable delay.

🟢 Minor / nits

  • Positive uses gkgFeatureUrl() (raw first-match of 7 fields); unrest uses extractGdeltSourceUrls(...)[0] (normalized). Both correct internally, but a single shared helper would avoid drift.
  • fetchGdeltGeoPositive in ais-relay.cjs got the same dedup/tone/URL logic but has zero test coverageais-relay.cjs exports nothing, so it's structurally untestable. Not blocking, but the positive path is now unverified; consider extracting the GKG aggregation into a shared tested module.
  • Stale assertion messages in test Fetch AIS signals from API instead of hardcoded data #6: assert.equal(calls, 4, 'should retry exactly once after the 522') (4 isn't intuitively "once") and 'Fifteen mentions' (11 after dedup).
  • const url = gkgFeatureUrl(...) — double space after =.

Verdict

Core logic is correct and now well-tested on the unrest side. Recommend adding the inter-theme delay test seam before merge (66 s of CI sleep is a real cost the PR's own tests incur); the rest are nits.

The unrest fan-out previously hardcoded a 5.5s setTimeout between theme
calls, which forced fan-out tests to wait real-time (~11s each). Pull
_sleep and _jitter out of _proxyOpts (same defaults pattern fetchGdelt-
ViaProxy uses), so tests can pass noSleep/noJitter and run instantly.
Production pacing is now 1.5-3s jittered between themes.

Also clean up test koala73#6's stale assertion messages — calls === 4 now
documents the breakdown ("theme 1: 1 throw + 1 retry; themes 2,3: 1
call each"), and "Fifteen mentions" is corrected to 11 (composite
dedup blocks the two shared URLs across themes: 5 + 3 + 3). Adds a
/11 reports/ match to lock the post-dedup count.
@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73 Thanks for the review. I have made the changes

@shubham1206agra shubham1206agra changed the title fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL dedup (#3852) fix(seeders): switch GDELT GKG queries to single-theme fan-out + URL|Loc dedup (#3852) May 21, 2026
@shubham1206agra

Copy link
Copy Markdown
Contributor Author

@koala73 This is ready for review

@vercel

vercel Bot commented May 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment May 26, 2026 9:34am

Request Review

@koala73
koala73 merged commit 75f3939 into koala73:main May 26, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trust:safe Brin: contributor trust score safe

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong Protest events getting displayed due to corrupted GDELT events data

2 participants