From 5026ab52d0ff39845bcdbfa6296728ec395f3835 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Thu, 14 Nov 2024 12:45:40 -0300 Subject: [PATCH] FEATURE: Order by emotion on /filter (#913) --- lib/sentiment/emotion_filter_order.rb | 76 +++++++ lib/sentiment/entry_point.rb | 2 + .../sentiment/emotion_filter_order_spec.rb | 210 ++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 lib/sentiment/emotion_filter_order.rb create mode 100644 spec/lib/modules/sentiment/emotion_filter_order_spec.rb diff --git a/lib/sentiment/emotion_filter_order.rb b/lib/sentiment/emotion_filter_order.rb new file mode 100644 index 000000000..4fffce320 --- /dev/null +++ b/lib/sentiment/emotion_filter_order.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module DiscourseAi + module Sentiment + class EmotionFilterOrder + def self.register!(plugin) + emotions = %w[ + admiration + amusement + anger + annoyance + approval + caring + confusion + curiosity + desire + disappointment + disapproval + disgust + embarrassment + excitement + fear + gratitude + grief + joy + love + nervousness + neutral + optimism + pride + realization + relief + remorse + sadness + surprise + ] + + emotions.each do |emotion| + filter_order_emotion = ->(scope, order_direction) do + emotion_clause = <<~SQL + SUM( + CASE + WHEN (classification_results.classification::jsonb->'#{emotion}')::float > 0.1 + THEN 1 + ELSE 0 + END + )::float / COUNT(posts.id) + SQL + scope + .joins(:posts) + .joins(<<~SQL) + INNER JOIN classification_results + ON classification_results.target_id = posts.id + AND classification_results.target_type = 'Post' + AND classification_results.model_used = 'SamLowe/roberta-base-go_emotions' + SQL + .where(<<~SQL) + topics.archetype = 'regular' + AND topics.deleted_at IS NULL + AND posts.deleted_at IS NULL + AND posts.post_type = 1 + SQL + .select(<<~SQL) + topics.*, + #{emotion_clause} AS emotion_#{emotion} + SQL + .group("1") + .having("#{emotion_clause} > 0.05") + .order("#{emotion_clause} #{order_direction}") + end + plugin.add_filter_custom_filter("order:emotion_#{emotion}", &filter_order_emotion) + end + end + end + end +end diff --git a/lib/sentiment/entry_point.rb b/lib/sentiment/entry_point.rb index 6cf688e9f..106b1dab2 100644 --- a/lib/sentiment/entry_point.rb +++ b/lib/sentiment/entry_point.rb @@ -14,6 +14,8 @@ def inject_into(plugin) plugin.on(:post_created, &sentiment_analysis_cb) plugin.on(:post_edited, &sentiment_analysis_cb) + EmotionFilterOrder.register!(plugin) + plugin.add_report("overall_sentiment") do |report| report.modes = [:stacked_chart] threshold = 0.6 diff --git a/spec/lib/modules/sentiment/emotion_filter_order_spec.rb b/spec/lib/modules/sentiment/emotion_filter_order_spec.rb new file mode 100644 index 000000000..c49a472dc --- /dev/null +++ b/spec/lib/modules/sentiment/emotion_filter_order_spec.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe DiscourseAi::Sentiment::EmotionFilterOrder do + let(:plugin) { Plugin::Instance.new } + let(:model_used) { "SamLowe/roberta-base-go_emotions" } + let(:post_1) { Fabricate(:post) } + let(:post_2) { Fabricate(:post) } + let(:post_3) { Fabricate(:post) } + let(:classification_1) do + { + love: 0.9444406, + admiration: 0.013724019, + surprise: 0.010188869, + excitement: 0.007888741, + curiosity: 0.006301749, + joy: 0.004060776, + confusion: 0.0028238264, + approval: 0.0018160914, + realization: 0.001174849, + neutral: 0.0008561869, + amusement: 0.00075853954, + disapproval: 0.0006987994, + disappointment: 0.0006166883, + anger: 0.0006000542, + annoyance: 0.0005615011, + desire: 0.00046368592, + fear: 0.00045117878, + sadness: 0.00041727215, + gratitude: 0.00041727215, + optimism: 0.00037112957, + disgust: 0.00035552034, + nervousness: 0.00022954118, + embarrassment: 0.0002049572, + caring: 0.00017737568, + remorse: 0.00011407586, + grief: 0.0001006716, + pride: 0.00009681493, + relief: 0.00008919009, + } + end + let(:classification_2) do + { + love: 0.8444406, + admiration: 0.113724019, + surprise: 0.010188869, + excitement: 0.007888741, + curiosity: 0.006301749, + joy: 0.004060776, + confusion: 0.0028238264, + approval: 0.0018160914, + realization: 0.001174849, + neutral: 0.0008561869, + amusement: 0.00075853954, + disapproval: 0.0006987994, + disappointment: 0.0006166883, + anger: 0.0006000542, + annoyance: 0.0005615011, + desire: 0.00046368592, + fear: 0.00045117878, + sadness: 0.00041727215, + gratitude: 0.00041727215, + optimism: 0.00037112957, + disgust: 0.00035552034, + nervousness: 0.00022954118, + embarrassment: 0.0002049572, + caring: 0.00017737568, + remorse: 0.00011407586, + grief: 0.0001006716, + pride: 0.00009681493, + relief: 0.00008919009, + } + end + let(:classification_3) do + { + anger: 0.8503682, + annoyance: 0.08113059, + disgust: 0.020593312, + disapproval: 0.013718102, + neutral: 0.0074148285, + disappointment: 0.005785964, + sadness: 0.0028253668, + curiosity: 0.0028253668, + confusion: 0.0023885092, + surprise: 0.001524171, + embarrassment: 0.0012784768, + love: 0.001177788, + admiration: 0.0010892758, + realization: 0.001080799, + approval: 0.00102328, + fear: 0.00097261387, + amusement: 0.0007724123, + excitement: 0.00059921003, + gratitude: 0.00055852515, + joy: 0.00054986606, + optimism: 0.00050458545, + desire: 0.00046849172, + caring: 0.00037205798, + remorse: 0.00028415458, + grief: 0.00025973833, + nervousness: 0.00024305031, + pride: 0.00011661681, + relief: 0.00007470753, + } + end + let!(:classification_result_1) do + Fabricate( + :sentiment_classification, + target: post_1, + model_used: model_used, + classification: classification_1, + ) + end + let!(:classification_result_2) do + Fabricate( + :sentiment_classification, + target: post_2, + model_used: model_used, + classification: classification_2, + ) + end + let!(:classification_result_3) do + Fabricate( + :sentiment_classification, + target: post_3, + model_used: model_used, + classification: classification_3, + ) + end + + before { described_class.register!(plugin) } + + it "registers emotion filters" do + emotions = %w[ + disappointment + sadness + annoyance + neutral + disapproval + realization + nervousness + approval + joy + anger + embarrassment + caring + remorse + disgust + grief + confusion + relief + desire + admiration + optimism + fear + love + excitement + curiosity + amusement + surprise + gratitude + pride + ] + + filters = DiscoursePluginRegistry.custom_filter_mappings.reduce(Hash.new, :merge) + + emotions.each { |emotion| expect(filters).to include("order:emotion_#{emotion}") } + end + + it "filters topics by emotion" do + emotion = "joy" + scope = Topic.all + order_direction = "desc" + + filter = + DiscoursePluginRegistry + .custom_filter_mappings + .find { _1.keys.include? "order:emotion_#{emotion}" } + .values + .first + result = filter.call(scope, order_direction) + + expect(result.to_sql).to include("INNER JOIN classification_results") + expect(result.to_sql).to include( + "classification_results.model_used = 'SamLowe/roberta-base-go_emotions'", + ) + expect(result.to_sql).to include("topics.archetype = 'regular'") + expect(result.to_sql).to include("ORDER BY") + expect(result.to_sql).to include("->'#{emotion}'") + expect(result.to_sql).to include("desc") + end + + it "sorts emotion in ascending order" do + expect( + TopicsFilter + .new(guardian: Guardian.new) + .filter_from_query_string("order:emotion_love-asc") + .pluck(:id), + ).to contain_exactly(post_2.topic.id, post_1.topic.id) + end + it "sorts emotion in default descending order" do + expect( + TopicsFilter + .new(guardian: Guardian.new) + .filter_from_query_string("order:emotion_love") + .pluck(:id), + ).to contain_exactly(post_1.topic.id, post_2.topic.id) + end +end