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.
| Fields | A match means |
|---|---|
severe_toxicity | The comment is extremely hateful, aggressive, or disrespectful. |
obscene | The comment contains profanity. |
threat | The author expresses a wish or intent to cause pain, injury, or violence. |
insult | The comment directly insults a person or group. |
identity_attack | The comment attacks a person or group based on an identity. |
sexual_explicit | The comment describes sexual acts or body parts in a sexual or lewd way. |
male, female, transgender, other_gender | The comment mentions the named gender identity. |
heterosexual, homosexual_gay_or_lesbian, bisexual, other_sexual_orientation | The comment mentions the named sexual orientation. |
christian, jewish, muslim, hindu, buddhist, atheist, other_religion | The comment mentions the named religion or belief. |
black, white, asian, latino, other_race_or_ethnicity | The comment mentions the named race or ethnicity. |
physical_disability, intellectual_or_learning_disability, psychiatric_or_mental_illness, other_disability | The 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 fThe plan runs in two steps:
AiFilterchecks each row incommentsfor toxicity.- Each matching comment enters
AiJoin, which checks the comment against the 30 rows infields.
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.
| Backend | Query time | Tokens/s | Cost | Filter F1 | Join F1 |
|---|---|---|---|---|---|
| Quail, DiffusionGemma 26B-A4B fp8, 1 H100 | 85.53 s | 284,022 | $0.0938 | 0.413 | 0.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/localResults are written to results/civil-comments/quail/local.