Quail
Demos

Civil Comments

Classify reader comments from English-language news sites.

The Civil Comments demo shows how you can use an AI filter and join to classify reader comments from English-language news sites.

The demo uses 10,000 rows from Jigsaw Civil Comments. Each row contains a comment and crowd-sourced scores for toxicity, six toxicity subtypes, and 24 identity mentions.

The dataset is labeled by human raters. A label is positive when selected by at least 50% of raters. Subjective labels can be noisy, so model predictions may not match every majority vote.

Query

The comments(comment_id, text) table contains one comment per row. The fields(field, statement) table contains one classification criterion per row. The query returns a (comment_id, field) pair when the comment is toxic and matches the field criterion.

FieldsA match means
severe_toxicityThe comment is extremely hateful, aggressive, or disrespectful.
obsceneThe comment contains profanity.
threatThe author expresses a wish or intent to cause pain, injury, or violence.
insultThe comment directly insults a person or group.
identity_attackThe comment attacks a person or group based on an identity.
sexual_explicitThe comment describes sexual acts or body parts in a sexual or lewd way.
male, female, transgender, other_genderThe comment mentions the named gender identity.
heterosexual, homosexual_gay_or_lesbian, bisexual, other_sexual_orientationThe comment mentions the named sexual orientation.
christian, jewish, muslim, hindu, buddhist, atheist, other_religionThe comment mentions the named religion or belief.
black, white, asian, latino, other_race_or_ethnicityThe comment mentions the named race or ethnicity.
physical_disability, intellectual_or_learning_disability, psychiatric_or_mental_illness, other_disabilityThe comment mentions the named disability category.

Each statement contains the field definition, one positive example, and one negative example. For example, the threat statement distinguishes “I will find you and break your legs” from a news report that says someone was attacked.

SELECT
    c.comment_id,
    f.field
FROM comments AS c
JOIN fields AS f
  ON AI.IF(
      PROMPT(:FIELD_PROMPT, c.text, f.statement)
  )
WHERE AI.IF(
    PROMPT(:TOXICITY_PROMPT, c.text)
);

The query has no selectivity hints.

Model

Quail runs DiffusionGemma 26B-A4B fp8 on one H100.

Execution

The Quail planner places the toxicity filter below the join. The relevant part of query.explain() is:

Project: c.comment_id, f.field
  AiJoin: anchor=c
    join 1 full (c, f)
    AiFilter: c
      survivors stream into the join with KV pinned
      predicate 1
      Scan comments as c
    Scan fields as f

The plan runs in two steps:

  1. AiFilter checks each row in comments for toxicity.
  2. Each matching comment enters AiJoin, which checks the comment against the 30 rows in fields.

The two steps are pipelined. AiJoin starts when AiFilter produces the first matching comment rather than waiting for all 10,000 filter checks. The join reuses the comment KV computed by the filter.

Results

Primary query time and cost exclude model startup.

BackendQuery timeTokens/sCostFilter F1Join F1
Quail, DiffusionGemma 26B-A4B fp8, 1 H10085.53 s284,022$0.09380.4130.287

Run

The source lives under demos/civil_comments/.

uv run python demos/civil_comments/quail_backend.py \
  --limit 10000 --gpus 1 \
  --output results/civil-comments/quail/local

Results are written to results/civil-comments/quail/local.

On this page