Skip to content

Saved-search subscriptions bypass "Topics to Avoid" and "Avoid disturbing news" filters #708

Description

@mircealungu

Summary

A user's saved-search subscriptions bypass both the "Topics to Avoid" keyword list and the "Avoid disturbing news" toggle. An article that matches a saved search gets injected into the home feed even when it also matches an avoid-keyword and/or is classified as disturbing.

Concrete example

A user (Danish learner) had these settings:

  • Topics to Avoid: bitcoin, blockchain, trump, Trumps
  • Avoid disturbing news (violence, death, disasters): ✅ enabled
  • A saved search subscription for iran

Their top feed card was:

"Trump truer Iran med massivt missilangreb efter påstande om attentatplaner" (dr.dk, Politics, 🔍 iran)

This is exactly the kind of article both filters exist to suppress — the title contains the trump token, and it's clearly disturbing (missile strike, assassination plots). It appeared anyway because it arrived via the iran search injection.

Root cause

article_recommendations_for_user (zeeguu/core/content_recommender/elastic_recommender.py) builds the feed in two independent halves:

1. Organic recommendationsbuild_elastic_recommender_query (elastic_query_builder.py). This honors both filters:

  • avoid-keywords → must_not on title/content (elastic_query_builder.py:124-126)
  • disturbing → must_not: {is_disturbing: true} when enabled (elastic_query_builder.py:156-157)

2. Saved-search injection — loop at elastic_recommender.py:192-210 calls article_and_video_search_for_userbuild_elastic_search_query (elastic_query_builder.py:297). That builder filters only by search term + language + CEFR + recency. It has no avoid-keyword must_not and no disturbing must_not.

Notably, article_and_video_search_for_user (elastic_recommender.py:318-326) already computes unwanted_user_searches and filter_disturbing via _prepare_user_constraints — then never passes them to the query builder. They're silently dropped.

Because injected search articles are recent, they sort to the top by published_time and the client features one as the top card.

Other paths with the same gap (for consideration)

  • topic_filter_for_user (elastic_recommender.py:364) — single-topic feed, no avoid-keyword exclusion.
  • Standalone /search and /latest_search endpoints — use build_elastic_search_query, so no avoid filtering (arguably fine for an explicit one-off search, less fine for a persisted subscription).
  • DB fallback in /user_articles/recommended (when ES is down) — plain SQL, no avoid filtering.

Open design question (why we're filing rather than just patching)

What should win when a user's avoid-list and their search-subscription conflict? Options to weigh:

  1. Avoid-list always wins — apply unwanted_user_searches + filter_disturbing must_not to the search-injection query too. Simplest; matches the intent that "avoid" is a hard block. Downside: a subscription for iran silently drops overlapping items with no explanation.
  2. Disturbing wins, keywords don't — a subscription is an explicit opt-in to a topic, so honor keyword overlap but still never inject disturbing content. Splits the two filters.
  3. Warn/surface — inject but visibly flag "matches your avoid list", let the user decide.

Leaning toward at least (1) for the disturbing filter (a safety setting shouldn't be defeated by a topic subscription), and probably for keywords too — but worth a discussion before implementing.

Suggested fix (if we go with option 1)

Thread unwanted_user_searches and filter_disturbing into build_elastic_search_query and add the same two must_not clauses the organic recommender already uses.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions