diff --git a/app/controllers/test_controller.rb b/app/controllers/test_controller.rb index efc12a92a..69ec29733 100644 --- a/app/controllers/test_controller.rb +++ b/app/controllers/test_controller.rb @@ -225,6 +225,39 @@ def suggest_similarity_item render_success 'suggest_similarity', pm1 end + def create_imported_standalone_fact_check + team = Team.current = Team.find(params[:team_id]) + user = User.where(email: params[:email]).last + description = params[:description] + context = params[:context] + title = params[:title] + summary = params[:summary] + url = params[:url] + language = params[:language] || 'en' + + # Create ClaimDescription + claim_description = ClaimDescription.create!( + description: description, + context: context, + user: user, + team: team + ) + + # Set up FactCheck + fact_check = FactCheck.new( + claim_description: claim_description, + title: title, + summary: summary, + url: url, + language: language, + user: user, + publish_report: true, + report_status: 'published' + ) + fact_check.save! + render_success 'fact_check', fact_check + end + def random render html: "Test #{rand(100000).to_i}Test".html_safe end diff --git a/app/graph/types/project_media_type.rb b/app/graph/types/project_media_type.rb index 794c03326..986cd4097 100644 --- a/app/graph/types/project_media_type.rb +++ b/app/graph/types/project_media_type.rb @@ -394,4 +394,16 @@ def articles_count count += 1 if object.fact_check count end + + field :relevant_articles, ::ArticleUnion.connection_type, null: true + + def relevant_articles + object.get_similar_articles + end + + field :relevant_articles_count, GraphQL::Types::Int, null: true + + def relevant_articles_count + object.get_similar_articles.count + end end diff --git a/app/models/bot/alegre.rb b/app/models/bot/alegre.rb index a1d05d895..e2b9b393b 100644 --- a/app/models/bot/alegre.rb +++ b/app/models/bot/alegre.rb @@ -256,11 +256,12 @@ def self.merge_suggested_and_confirmed(suggested_or_confirmed, confirmed, pm) end end - def self.get_matching_key_value(pm, media_type, similarity_method, automatic, model_name) + def self.get_threshold_given_model_settings(team_id, media_type, similarity_method, automatic, model_name) + tbi = nil + tbi = self.get_alegre_tbi(team_id) unless team_id.nil? similarity_level = automatic ? 'matching' : 'suggestion' generic_key = "#{media_type}_#{similarity_method}_#{similarity_level}_threshold" specific_key = "#{media_type}_#{similarity_method}_#{model_name}_#{similarity_level}_threshold" - tbi = self.get_alegre_tbi(pm&.team_id) settings = tbi.alegre_settings unless tbi.nil? outkey = "" value = nil @@ -274,17 +275,25 @@ def self.get_matching_key_value(pm, media_type, similarity_method, automatic, mo return [outkey, value] end - def self.get_threshold_for_query(media_type, pm, automatic = false) + def self.get_matching_key_value(pm, media_type, similarity_method, automatic, model_name) + self.get_threshold_given_model_settings(pm&.team_id, media_type, similarity_method, automatic, model_name) + end + + def self.get_similarity_methods_and_models_given_media_type_and_team_id(media_type, team_id, get_vector_settings) similarity_methods = media_type == 'text' ? ['elasticsearch'] : ['hash'] models = similarity_methods.dup - if media_type == 'text' && !pm.nil? - models_to_use = [self.matching_model_to_use(pm.team_id)].flatten-[Bot::Alegre::ELASTICSEARCH_MODEL] + if media_type == 'text' && get_vector_settings + models_to_use = [self.matching_model_to_use(team_id)].flatten-[Bot::Alegre::ELASTICSEARCH_MODEL] models_to_use.each do |model| similarity_methods << 'vector' models << model end end - similarity_methods.zip(models).collect do |similarity_method, model_name| + return similarity_methods.zip(models) + end + + def self.get_threshold_for_query(media_type, pm, automatic = false) + self.get_similarity_methods_and_models_given_media_type_and_team_id(media_type, pm&.team_id, !pm.nil?).collect do |similarity_method, model_name| key, value = self.get_matching_key_value(pm, media_type, similarity_method, automatic, model_name) { value: value.to_f, key: key, automatic: automatic, model: model_name} end diff --git a/app/models/concerns/smooch_search.rb b/app/models/concerns/smooch_search.rb index ca6941093..b306a4dc4 100644 --- a/app/models/concerns/smooch_search.rb +++ b/app/models/concerns/smooch_search.rb @@ -9,21 +9,22 @@ module ClassMethods def search(app_id, uid, language, message, team_id, workflow, provider = nil) platform = self.get_platform_from_message(message) begin + limit = CheckConfig.get('most_relevant_team_limit', 3, :integer) sm = CheckStateMachine.new(uid) self.get_installation(self.installation_setting_id_keys, app_id) if self.config.blank? RequestStore.store[:smooch_bot_provider] = provider unless provider.blank? query = self.get_search_query(uid, message) - results = self.get_search_results(uid, query, team_id, language).collect{ |pm| Relationship.confirmed_parent(pm) }.uniq + results = self.get_search_results(uid, query, team_id, language, limit).collect{ |pm| Relationship.confirmed_parent(pm) }.uniq reports = results.select{ |pm| pm.report_status == 'published' }.collect{ |pm| pm.get_dynamic_annotation('report_design') }.reject{ |r| r.nil? }.collect{ |r| r.report_design_to_tipline_search_result }.select{ |r| r.should_send_in_language?(language) } # Extract explainers from matched media if they don't have published fact-checks but they have explainers - reports = results.collect{ |pm| pm.explainers.to_a }.flatten.uniq.first(3).map(&:as_tipline_search_result) if !results.empty? && reports.empty? + reports = results.collect{ |pm| pm.explainers.to_a }.flatten.uniq.first(limit).map(&:as_tipline_search_result) if !results.empty? && reports.empty? # Search for explainers if fact-checks were not found if reports.empty? && query['type'] == 'text' - explainers = self.search_for_explainers(uid, query['text'], team_id, language).first(3).select{ |explainer| explainer.as_tipline_search_result.should_send_in_language?(language) } + explainers = self.search_for_explainers(uid, query['text'], team_id, limit, language).select{ |explainer| explainer.as_tipline_search_result.should_send_in_language?(language) } Rails.logger.info "[Smooch Bot] Text similarity search got #{explainers.count} explainers while looking for '#{query['text']}' for team #{team_id}" - results = explainers.collect{ |explainer| explainer.project_medias.to_a }.flatten.uniq.reject{ |pm| pm.blank? }.first(3) + results = explainers.collect{ |explainer| explainer.project_medias.to_a }.flatten.uniq.reject{ |pm| pm.blank? }.first(limit) reports = explainers.map(&:as_tipline_search_result) end @@ -100,9 +101,9 @@ def reject_temporary_results(results) end end - def parse_search_results_from_alegre(results, after = nil, feed_id = nil, team_ids = nil) + def parse_search_results_from_alegre(results, limit, after = nil, feed_id = nil, team_ids = nil) pms = reject_temporary_results(results).sort_by{ |a| [a[1][:model] != Bot::Alegre::ELASTICSEARCH_MODEL ? 1 : 0, a[1][:score]] }.to_h.keys.reverse.collect{ |id| Relationship.confirmed_parent(ProjectMedia.find_by_id(id)) } - filter_search_results(pms, after, feed_id, team_ids).uniq(&:id).sort_by{ |pm| pm.report_status == 'published' ? 0 : 1 }.first(3) + filter_search_results(pms, after, feed_id, team_ids).uniq(&:id).first(limit) end def date_filter(team_id) @@ -127,14 +128,14 @@ def get_search_query(uid, last_message) self.bundle_list_of_messages(list, last_message, true) end - def get_search_results(uid, message, team_id, language) + def get_search_results(uid, message, team_id, language, limit) results = [] begin type = message['type'] after = self.date_filter(team_id) query = message['text'] query = CheckS3.rewrite_url(message['mediaUrl']) unless type == 'text' - results = self.search_for_similar_published_fact_checks(type, query, [team_id], after, nil, language).select{ |pm| is_a_valid_search_result(pm) } + results = self.search_for_similar_published_fact_checks(type, query, [team_id], limit, after, nil, language).select{ |pm| is_a_valid_search_result(pm) } rescue StandardError => e self.handle_search_error(uid, e, language) end @@ -148,19 +149,19 @@ def normalized_query_hash(type, query, team_ids, after, feed_id, language) # "type" is text, video, audio or image # "query" is either a piece of text of a media URL - def search_for_similar_published_fact_checks(type, query, team_ids, after = nil, feed_id = nil, language = nil, skip_cache = false) + def search_for_similar_published_fact_checks(type, query, team_ids, limit, after = nil, feed_id = nil, language = nil, skip_cache = false) if skip_cache - self.search_for_similar_published_fact_checks_no_cache(type, query, team_ids, after, feed_id, language) + self.search_for_similar_published_fact_checks_no_cache(type, query, team_ids, limit, after, feed_id, language) else Rails.cache.fetch("smooch:search_results:#{self.normalized_query_hash(type, query, team_ids, after, feed_id, language)}", expires_in: 2.hours) do - self.search_for_similar_published_fact_checks_no_cache(type, query, team_ids, after, feed_id, language) + self.search_for_similar_published_fact_checks_no_cache(type, query, team_ids, limit, after, feed_id, language) end end end # "type" is text, video, audio or image # "query" is either a piece of text of a media URL - def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, after = nil, feed_id = nil, language = nil) + def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, limit, after = nil, feed_id = nil, language = nil) results = [] pm = nil pm = ProjectMedia.new(team_id: team_ids[0]) if team_ids.size == 1 # We'll use the settings of a team instead of global settings when there is only one team @@ -179,10 +180,10 @@ def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, aft words = text.split(/\s+/) Rails.logger.info "[Smooch Bot] Search query (text): #{text}" if Bot::Alegre.get_number_of_words(text) <= self.max_number_of_words_for_keyword_search - results = self.search_by_keywords_for_similar_published_fact_checks(words, after, team_ids, feed_id, language) + results = self.search_by_keywords_for_similar_published_fact_checks(words, after, team_ids, limit, feed_id, language) else alegre_results = Bot::Alegre.get_merged_similar_items(pm, [{ value: self.get_text_similarity_threshold }], Bot::Alegre::ALL_TEXT_SIMILARITY_FIELDS, text, team_ids) - results = self.parse_search_results_from_alegre(alegre_results, after, feed_id, team_ids) + results = self.parse_search_results_from_alegre(alegre_results, limit, after, feed_id, team_ids) Rails.logger.info "[Smooch Bot] Text similarity search got #{results.count} results while looking for '#{text}' after date #{after.inspect} for teams #{team_ids}" end else @@ -192,7 +193,7 @@ def search_for_similar_published_fact_checks_no_cache(type, query, team_ids, aft media_url = self.save_locally_and_return_url(media_url, type, feed_id) threshold = Bot::Alegre.get_threshold_for_query(type, pm)[0][:value] alegre_results = Bot::Alegre.get_items_with_similar_media_v2(media_url: media_url, threshold: [{ value: threshold }], team_ids: team_ids, type: type) - results = self.parse_search_results_from_alegre(alegre_results, after, feed_id, team_ids) + results = self.parse_search_results_from_alegre(alegre_results, limit, after, feed_id, team_ids) Rails.logger.info "[Smooch Bot] Media similarity search got #{results.count} results while looking for '#{query}' after date #{after.inspect} for teams #{team_ids}" end results @@ -245,11 +246,11 @@ def should_restrict_by_language?(team_ids) !!tbi&.alegre_settings&.dig('single_language_fact_checks_enabled') end - def search_by_keywords_for_similar_published_fact_checks(words, after, team_ids, feed_id = nil, language = nil) + def search_by_keywords_for_similar_published_fact_checks(words, after, team_ids, limit, feed_id = nil, language = nil) types = CheckSearch::MEDIA_TYPES.clone.push('blank') search_fields = %w(title description fact_check_title fact_check_summary extracted_text url claim_description_content) - filters = { keyword: words.join('+'), keyword_fields: { fields: search_fields }, sort: 'recent_activity', eslimit: 3, show: types } - filters.merge!({ fc_language: [language] }) if should_restrict_by_language?(team_ids) + filters = { keyword: words.join('+'), keyword_fields: { fields: search_fields }, sort: 'recent_activity', eslimit: limit, show: types } + filters.merge!({ fc_language: [language] }) if !language.blank? && should_restrict_by_language?(team_ids) filters.merge!({ sort: 'score' }) if words.size > 1 # We still want to be able to return the latest fact-checks if a meaninful query is not passed feed_id.blank? ? filters.merge!({ report_status: ['published'] }) : filters.merge!({ feed_id: feed_id }) filters.merge!({ range: { updated_at: { start_time: after.strftime('%Y-%m-%dT%H:%M:%S.%LZ') } } }) unless after.blank? @@ -304,19 +305,19 @@ def ask_for_feedback_when_all_search_results_are_received(app_id, language, work end end - def search_for_explainers(uid, query, team_id, language) + def search_for_explainers(uid, query, team_id, limit, language = nil) results = nil begin text = ::Bot::Smooch.extract_claim(query) if Bot::Alegre.get_number_of_words(text) == 1 results = Explainer.where(team_id: team_id).where('description ILIKE ? OR title ILIKE ?', "%#{text}%", "%#{text}%") - results = results.where(language: language) if should_restrict_by_language?([team_id]) + results = results.where(language: language) if !language.nil? && should_restrict_by_language?([team_id]) results = results.order('updated_at DESC') else - results = Explainer.search_by_similarity(text, language, team_id) + results = Explainer.search_by_similarity(text, language, team_id, limit) end rescue StandardError => e - self.handle_search_error(uid, e, language) + self.handle_search_error(uid, e, language) unless uid.blank? end results.joins(:project_medias) end diff --git a/app/models/explainer.rb b/app/models/explainer.rb index aa25ea9e4..6303c157e 100644 --- a/app/models/explainer.rb +++ b/app/models/explainer.rb @@ -1,12 +1,6 @@ class Explainer < ApplicationRecord include Article - # FIXME: Read from workspace settings - ALEGRE_MODELS_AND_THRESHOLDS = { - # Bot::Alegre::ELASTICSEARCH_MODEL => 0.8 # Sometimes this is easier for local development - Bot::Alegre::PARAPHRASE_MULTILINGUAL_MODEL => 0.7 - } - belongs_to :team has_annotations @@ -71,13 +65,14 @@ def self.update_paragraphs_in_alegre(id, previous_paragraphs_count, timestamp) explainer_id: explainer.id } + models_thresholds = Explainer.get_alegre_models_and_thresholds(explainer.team_id).keys # Index title params = { content_hash: Bot::Alegre.content_hash_for_value(explainer.title), doc_id: Digest::MD5.hexdigest(['explainer', explainer.id, 'title'].join(':')), context: base_context.merge({ field: 'title' }), text: explainer.title, - models: ALEGRE_MODELS_AND_THRESHOLDS.keys, + models: models_thresholds, } Bot::Alegre.index_async_with_params(params, "text") @@ -90,7 +85,7 @@ def self.update_paragraphs_in_alegre(id, previous_paragraphs_count, timestamp) doc_id: Digest::MD5.hexdigest(['explainer', explainer.id, 'paragraph', count].join(':')), context: base_context.merge({ paragraph: count }), text: paragraph.strip, - models: ALEGRE_MODELS_AND_THRESHOLDS.keys, + models: models_thresholds, } Bot::Alegre.index_async_with_params(params, "text") end @@ -107,23 +102,35 @@ def self.update_paragraphs_in_alegre(id, previous_paragraphs_count, timestamp) end end - def self.search_by_similarity(text, language, team_id) + def self.search_by_similarity(text, language, team_id, limit) + models_thresholds = Explainer.get_alegre_models_and_thresholds(team_id) + context = { + type: 'explainer', + team: Team.find(team_id).slug + } + context[:language] = language unless language.nil? params = { text: text, - models: ALEGRE_MODELS_AND_THRESHOLDS.keys, - per_model_threshold: ALEGRE_MODELS_AND_THRESHOLDS, - context: { - type: 'explainer', - team: Team.find(team_id).slug, - language: language - } + models: models_thresholds.keys, + per_model_threshold: models_thresholds, + context: context + } response = Bot::Alegre.query_sync_with_params(params, "text") results = response['result'].to_a.sort_by{ |result| result['_score'] } - explainer_ids = results.collect{ |result| result.dig('context', 'explainer_id').to_i }.uniq.first(3) + explainer_ids = results.collect{ |result| result.dig('context', 'explainer_id').to_i }.uniq.first(limit) explainer_ids.empty? ? Explainer.none : Explainer.where(team_id: team_id, id: explainer_ids) end + def self.get_alegre_models_and_thresholds(team_id) + models_thresholds = {} + Bot::Alegre.get_similarity_methods_and_models_given_media_type_and_team_id("text", team_id, true).map do |similarity_method, model_name| + _, value = Bot::Alegre.get_threshold_given_model_settings(team_id, "text", similarity_method, true, model_name) + models_thresholds[model_name] = value + end + models_thresholds + end + private def set_team diff --git a/app/models/project_media.rb b/app/models/project_media.rb index ab169298d..e29adddb8 100644 --- a/app/models/project_media.rb +++ b/app/models/project_media.rb @@ -455,6 +455,22 @@ def replace_with_blank_media self.save! end + def get_similar_articles + # Get search query based on Media type + # Quote for Claim + # Transcription for UploadedVideo , UploadedAudio and UploadedImage + # Title and/or description for Link + media = self.media + search_query = case media.type + when 'Claim' + media.quote + when 'UploadedVideo', 'UploadedAudio', 'UploadedImage' + self.transcription + end + search_query ||= self.title + self.team.search_for_similar_articles(search_query, self) + end + protected def add_extra_elasticsearch_data(ms) diff --git a/app/models/team.rb b/app/models/team.rb index 329b044e6..8b9e76274 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -563,6 +563,30 @@ def filter_by_keywords(query, filters, type = 'FactCheck') query.where(Arel.sql("#{tsvector} @@ #{tsquery}")) end + def search_for_similar_articles(query, pm = nil) + # query: expected to be text + # pm: to request a most relevant to specific item and also include both FactCheck & Explainer + limit = pm.nil? ? CheckConfig.get('most_relevant_team_limit', 3, :integer) : CheckConfig.get('most_relevant_item_limit', 10, :integer) + result_ids = Bot::Smooch.search_for_similar_published_fact_checks_no_cache('text', query, [self.id], limit).map(&:id) + items = [] + unless result_ids.blank? + # I depend on FactCheck to filter result instead of report_design + items = FactCheck.where(report_status: 'published') + .joins(claim_description: :project_media) + .where('project_medias.id': result_ids) + # Exclude the ones already applied to a target item if exsits + items = items.where.not('fact_checks.id' => pm.fact_check_id) unless pm&.fact_check_id.nil? + end + if items.blank? || !pm.nil? + # Get Explainers if no fact-check returned or get similar_articles for a ProjectMedia + ex_items = Bot::Smooch.search_for_explainers(nil, query, self.id, limit) + # Exclude the ones already applied to a target item + ex_items = ex_items.where.not(id: pm.explainer_ids) unless pm&.explainer_ids.blank? + items = items + ex_items + end + items + end + # private # # Please add private methods to app/models/concerns/team_private.rb diff --git a/app/resources/api/v2/feed_resource.rb b/app/resources/api/v2/feed_resource.rb index ffabf372a..83f4c0212 100644 --- a/app/resources/api/v2/feed_resource.rb +++ b/app/resources/api/v2/feed_resource.rb @@ -38,26 +38,27 @@ def self.records(options = {}, skip_save_request = false, skip_cache = false) skip_cache = skip_cache || filters.dig(:skip_cache, 0) == 'true' return ProjectMedia.none if team_ids.blank? || query.blank? - + limit = CheckConfig.get('most_relevant_team_limit', 3, :integer) if feed_id > 0 - return get_results_from_feed_teams(team_ids, feed_id, query, type, after, webhook_url, skip_save_request, skip_cache) + return get_results_from_feed_teams(team_ids, feed_id, query, type, after, webhook_url, skip_save_request, skip_cache, limit) elsif ApiKey.current - return get_results_from_api_key_teams(type, query, after, skip_cache) + return get_results_from_api_key_teams(type, query, after, skip_cache, limit) end end - def self.get_results_from_api_key_teams(type, query, after, skip_cache) + def self.get_results_from_api_key_teams(type, query, after, skip_cache, limit) RequestStore.store[:pause_database_connection] = true # Release database connection during Bot::Alegre.request_api team_ids = ApiKey.current.bot_user.team_ids - Bot::Smooch.search_for_similar_published_fact_checks(type, query, team_ids, after, skip_cache) + limit = CheckConfig.get('most_relevant_team_limit', 3, :integer) + Bot::Smooch.search_for_similar_published_fact_checks(type, query, team_ids, limit, after, skip_cache) end - def self.get_results_from_feed_teams(team_ids, feed_id, query, type, after, webhook_url, skip_save_request, skip_cache) + def self.get_results_from_feed_teams(team_ids, feed_id, query, type, after, webhook_url, skip_save_request, skip_cache, limit) return ProjectMedia.none unless can_read_feed?(feed_id, team_ids) feed = Feed.find(feed_id) RequestStore.store[:pause_database_connection] = true # Release database connection during Bot::Alegre.request_api RequestStore.store[:smooch_bot_settings] = feed.get_smooch_bot_settings.to_h - results = Bot::Smooch.search_for_similar_published_fact_checks(type, query, feed.team_ids, after, feed_id, skip_cache) + results = Bot::Smooch.search_for_similar_published_fact_checks(type, query, feed.team_ids, limit, after, feed_id, skip_cache) Feed.delay({ retry: 0, queue: 'feed' }).save_request(feed_id, type, query, webhook_url, results.to_a.map(&:id)) unless skip_save_request results end diff --git a/config/routes.rb b/config/routes.rb index e4bc0fa4e..e2c6f3d51 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,5 +79,6 @@ match '/test/suggest_similarity' => 'test#suggest_similarity_item', via: :get match '/test/install_bot' => 'test#install_bot', via: :get match '/test/add_team_user' => 'test#add_team_user', via: :get + match '/test/create_imported_standalone_fact_check' => 'test#create_imported_standalone_fact_check', via: :get match '/test/random' => 'test#random', via: :get end diff --git a/lib/relay.idl b/lib/relay.idl index b672574b3..f65bcb1b2 100644 --- a/lib/relay.idl +++ b/lib/relay.idl @@ -11666,6 +11666,28 @@ type ProjectMedia implements Node { published: String pusher_channel: String quote: String + relevant_articles( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): ArticleUnionConnection + relevant_articles_count: Int report_status: String report_type: String requests( diff --git a/public/relay.json b/public/relay.json index 2fc923085..c2387c828 100644 --- a/public/relay.json +++ b/public/relay.json @@ -61591,6 +61591,81 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "relevant_articles", + "description": null, + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "ArticleUnionConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "relevant_articles_count", + "description": null, + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "report_status", "description": null, diff --git a/test/controllers/feeds_controller_test.rb b/test/controllers/feeds_controller_test.rb index bced9ac47..534872db7 100644 --- a/test/controllers/feeds_controller_test.rb +++ b/test/controllers/feeds_controller_test.rb @@ -26,7 +26,7 @@ def setup @f = create_feed published: true @f.teams = [@t1, @t2] FeedTeam.update_all(shared: true) - Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false).returns([@pm1, @pm2]) + Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false).returns([@pm1, @pm2]) end def teardown @@ -44,7 +44,7 @@ def teardown b.api_key = a b.save! create_team_user team: @t1, user: b - Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id], nil, false).returns([@pm1]) + Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id], 3, nil, false).returns([@pm1]) authenticate_with_token a get :index, params: { filter: { type: 'text', query: 'Foo' } } @@ -55,7 +55,7 @@ def teardown test "should request feed data" do Bot::Smooch.stubs(:search_for_similar_published_fact_checks) - .with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false) + .with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false) .returns([@pm1, @pm2]) authenticate_with_token @a get :index, params: { filter: { type: 'text', query: 'Foo', feed_id: @f.id } } @@ -68,13 +68,13 @@ def teardown Sidekiq::Testing.fake! do authenticate_with_token @a - Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false).returns([@pm1, @pm2]) + Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false).returns([@pm1, @pm2]) get :index, params: { filter: { type: 'text', query: 'Foo', feed_id: @f.id } } assert_response :success assert_equal 'Foo', json_response['data'][0]['attributes']['organization'] assert_equal 'Bar', json_response['data'][1]['attributes']['organization'] - Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false).returns([@pm2, @pm1]) + Bot::Smooch.stubs(:search_for_similar_published_fact_checks).with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false).returns([@pm2, @pm1]) get :index, params: { filter: { type: 'text', query: 'Foo', feed_id: @f.id } } assert_response :success assert_equal 'Bar', json_response['data'][0]['attributes']['organization'] @@ -111,7 +111,7 @@ def teardown test "should save request query" do Bot::Alegre.stubs(:request).returns({}) Bot::Smooch.stubs(:search_for_similar_published_fact_checks) - .with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false) + .with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false) .returns([@pm1, @pm2]) Sidekiq::Testing.inline! authenticate_with_token @a @@ -129,7 +129,7 @@ def teardown test "should save relationship between request and results" do Bot::Alegre.stubs(:request).returns({}) Bot::Smooch.stubs(:search_for_similar_published_fact_checks) - .with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false) + .with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false) .returns([@pm1, @pm2]) Sidekiq::Testing.inline! authenticate_with_token @a @@ -143,7 +143,7 @@ def teardown test "should not save request when skip_save_request is true" do Bot::Alegre.stubs(:request).returns({}) Bot::Smooch.stubs(:search_for_similar_published_fact_checks) - .with('text', 'Foo', [@t1.id, @t2.id], nil, @f.id, false) + .with('text', 'Foo', [@t1.id, @t2.id], 3, nil, @f.id, false) .returns([@pm1, @pm2]) Sidekiq::Testing.inline! authenticate_with_token @a diff --git a/test/controllers/graphql_controller_5_test.rb b/test/controllers/graphql_controller_5_test.rb index e85a1918b..78e778827 100644 --- a/test/controllers/graphql_controller_5_test.rb +++ b/test/controllers/graphql_controller_5_test.rb @@ -405,6 +405,25 @@ def setup assert_equal 'Test', JSON.parse(@response.body)['data']['createTag']['tag_text_object']['text'] end + test "should get relevant articles for ProjectMedia item" do + t = create_team + pm = create_project_media quote: 'Foo Bar', team: t + ex = create_explainer language: 'en', team: t, title: 'Foo Bar' + pm.explainers << ex + cd = create_claim_description description: pm.title, project_media: pm + fc = create_fact_check claim_description: cd, title: pm.title + items = FactCheck.where(id: fc.id) + Explainer.where(id: ex.id) + ProjectMedia.any_instance.stubs(:get_similar_articles).returns(items) + query = "query { project_media(ids: \"#{pm.id}\") { relevant_articles_count, relevant_articles { edges { node { ... on FactCheck { dbid }, ... on Explainer { dbid } } } } } }" + post :create, params: { query: query, team: t.slug } + assert_response :success + data = JSON.parse(@response.body)['data']['project_media'] + assert_equal 2, data['relevant_articles_count'] + item_ids = data['relevant_articles']['edges'].collect{ |i| i['node']['dbid'] } + assert_equal items.map(&:id).sort, item_ids.sort + ProjectMedia.any_instance.unstub(:get_similar_articles) + end + protected def assert_error_message(expected) diff --git a/test/controllers/test_controller_test.rb b/test/controllers/test_controller_test.rb index eb9fef1b8..94eaed620 100644 --- a/test/controllers/test_controller_test.rb +++ b/test/controllers/test_controller_test.rb @@ -493,6 +493,53 @@ class TestControllerTest < ActionController::TestCase Rails.unstub(:env) end + test "should create standalone fact check and associate with the team" do + # Test setup + team = create_team + user = create_user + create_team_user(user: user, team: team) + + assert_difference 'FactCheck.count' do + get :create_imported_standalone_fact_check, params: { + team_id: team.id, + email: user.email, + description: 'Test description', + context: 'Test context', + title: 'Test title', + summary: 'Test summary', + url: 'http://example.com', + language: 'en' + } + end + + assert_response :success + end + + test "should not create standalone fact check and associate with the team" do + Rails.stubs(:env).returns('development') + + # Test setup + team = create_team + user = create_user + create_team_user(user: user, team: team) + + assert_no_difference 'FactCheck.count' do + get :create_imported_standalone_fact_check, params: { + team_id: team.id, + email: user.email, + description: 'Test description', + context: 'Test context', + title: 'Test title', + summary: 'Test summary', + url: 'http://example.com', + language: 'en' + } + end + + assert_response 400 + Rails.unstub(:env) + end + test "should get a random number in HTML" do get :random assert_response :success diff --git a/test/models/bot/smooch_3_test.rb b/test/models/bot/smooch_3_test.rb index 843b40602..f3cb76cdd 100644 --- a/test/models/bot/smooch_3_test.rb +++ b/test/models/bot/smooch_3_test.rb @@ -691,10 +691,10 @@ def teardown 'Segurança das urna', 'Seguranca das urnas' ].each do |query| - assert_equal [pm1.id], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id]).to_a.map(&:id) + assert_equal [pm1.id], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], 3).to_a.map(&:id) end - assert_equal [], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Segurando', [t.id]).to_a.map(&:id) + assert_equal [], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Segurando', [t.id], 3).to_a.map(&:id) end test "should get turn.io installation" do diff --git a/test/models/bot/smooch_4_test.rb b/test/models/bot/smooch_4_test.rb index 34167fa5f..611b0b07a 100644 --- a/test/models/bot/smooch_4_test.rb +++ b/test/models/bot/smooch_4_test.rb @@ -671,10 +671,10 @@ def teardown uid = random_string query = Bot::Smooch.get_search_query(uid, {}) - assert_equal [pm2], Bot::Smooch.get_search_results(uid, query, t.id, 'en') + assert_equal [pm2], Bot::Smooch.get_search_results(uid, query, t.id, 'en', 3) Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'text', 'text' => "Test #{url}" }) query = Bot::Smooch.get_search_query(uid, {}) - assert_equal [pm1], Bot::Smooch.get_search_results(uid, query, t.id, 'en') + assert_equal [pm1], Bot::Smooch.get_search_results(uid, query, t.id, 'en', 3) ProjectMedia.any_instance.unstub(:report_status) CheckSearch.any_instance.unstub(:medias) @@ -694,7 +694,7 @@ def teardown assert_nothing_raised do with_current_user_and_team(nil, t) do - Bot::Smooch.search_for_similar_published_fact_checks('text', 'https://projetocomprova.com.br/publicações/tuite-engana-ao-dizer-que-o-stf-decidiu-que-voto-impresso-e-inconstitucional/ ', [t.id], nil, f.id) + Bot::Smooch.search_for_similar_published_fact_checks('text', 'https://projetocomprova.com.br/publicações/tuite-engana-ao-dizer-que-o-stf-decidiu-que-voto-impresso-e-inconstitucional/ ', [t.id], 3, nil, f.id) end end end diff --git a/test/models/bot/smooch_5_test.rb b/test/models/bot/smooch_5_test.rb index 93eef03af..49f90522d 100644 --- a/test/models/bot/smooch_5_test.rb +++ b/test/models/bot/smooch_5_test.rb @@ -68,16 +68,16 @@ def teardown # and for each team participating in the feed with_current_user_and_team(u, t1) do # Keyword search - result = Bot::Smooch.search_for_similar_published_fact_checks('text', 'Test', [t1.id, t2.id, t3.id, t4.id], nil, f1.id).map(&:id) + result = Bot::Smooch.search_for_similar_published_fact_checks('text', 'Test', [t1.id, t2.id, t3.id, t4.id], 3, nil, f1.id).map(&:id) assert_equal [pm1a.id, pm1f.id, pm2a.id].sort, result.sort # Text similarity search - result = Bot::Smooch.search_for_similar_published_fact_checks('text', 'This is a test', [t1.id, t2.id, t3.id, t4.id], nil, f1.id).map(&:id) + result = Bot::Smooch.search_for_similar_published_fact_checks('text', 'This is a test', [t1.id, t2.id, t3.id, t4.id], 3, nil, f1.id).map(&:id) assert_equal [pm1a.id, pm1d.id, pm2a.id].sort, result.sort # Media similarity search - result = Bot::Smooch.search_for_similar_published_fact_checks('image', random_url, [t1.id, t2.id, t3.id, t4.id], nil, f1.id).map(&:id) + result = Bot::Smooch.search_for_similar_published_fact_checks('image', random_url, [t1.id, t2.id, t3.id, t4.id], 3, nil, f1.id).map(&:id) assert_equal [pm1a.id, pm1d.id, pm2a.id], result.sort # URL search - result = Bot::Smooch.search_for_similar_published_fact_checks('text', "Test with URL: #{url}", [t1.id, t2.id, t3.id, t4.id], nil, f1.id).map(&:id) + result = Bot::Smooch.search_for_similar_published_fact_checks('text', "Test with URL: #{url}", [t1.id, t2.id, t3.id, t4.id], 3, nil, f1.id).map(&:id) assert_equal [pm1g.id, pm2b.id].sort, result.sort end diff --git a/test/models/bot/smooch_7_test.rb b/test/models/bot/smooch_7_test.rb index 8aee9c2af..10217674c 100644 --- a/test/models/bot/smooch_7_test.rb +++ b/test/models/bot/smooch_7_test.rb @@ -219,7 +219,7 @@ def teardown uid = random_string query = Bot::Smooch.get_search_query(uid, {}) - assert_equal [pm], Bot::Smooch.get_search_results(uid, query, pm.team_id, 'en') + assert_equal [pm], Bot::Smooch.get_search_results(uid, query, pm.team_id, 'en', 3) Bot::Smooch.unstub(:bundle_list_of_messages) CheckSearch.any_instance.unstub(:medias) @@ -242,7 +242,7 @@ def teardown uid = random_string query = Bot::Smooch.get_search_query(uid, {}) - assert_equal [pm], Bot::Smooch.get_search_results(uid, query, pm.team_id, 'en') + assert_equal [pm], Bot::Smooch.get_search_results(uid, query, pm.team_id, 'en', 3) Bot::Smooch.unstub(:bundle_list_of_messages) ProjectMedia.any_instance.unstub(:report_status) @@ -266,7 +266,7 @@ def teardown Bot::Alegre.stubs(:get_items_with_similar_media_v2).returns({ pm.id => { score: 0.9, model: 'elasticsearch', context: {foo: :bar} } }) CheckS3.stubs(:rewrite_url).returns(random_url) - assert_equal [pm], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en') + assert_equal [pm], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en', 3) Bot::Smooch.unstub(:bundle_list_of_messages) ProjectMedia.any_instance.unstub(:report_status) @@ -316,7 +316,7 @@ def teardown # Create more project media if needed results = { pm1.id => { model: 'elasticsearch', score: 10.8, context: {foo: :bar}}, pm2.id => { model: 'elasticsearch', score: 15.2, context: {foo: :bar}}, pm3.id => { model: 'anything-else', score: 1.98, context: {foo: :bar}}, pm4.id => { model: 'anything-else', score: 1.8, context: {foo: :bar}}} - assert_equal [pm3, pm4, pm2], Bot::Smooch.parse_search_results_from_alegre(results, t.id) + assert_equal [pm3, pm4, pm2], Bot::Smooch.parse_search_results_from_alegre(results, 3, t.id) ProjectMedia.any_instance.unstub(:report_status) end @@ -330,7 +330,7 @@ def teardown # Create more project media if needed results = { pm1.id => { model: 'elasticsearch', score: 10.8, context: {blah: 1} }, pm2.id => { model: 'elasticsearch', score: 15.2, context: {blah: 1} }, pm3.id => { model: 'anything-else', score: 1.98, context: {temporary_media: true} }, pm4.id => { model: 'anything-else', score: 1.8, context: {temporary_media: false}}} - assert_equal [pm4, pm2, pm1], Bot::Smooch.parse_search_results_from_alegre(results, t.id) + assert_equal [pm4, pm2, pm1], Bot::Smooch.parse_search_results_from_alegre(results, 3, t.id) ProjectMedia.any_instance.unstub(:report_status) end @@ -346,7 +346,7 @@ def teardown Bot::Smooch.stubs(:bundle_list_of_messages).returns({ 'type' => 'text', 'text' => url }) CheckSearch.any_instance.stubs(:medias).returns([pm]) - assert_equal [], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en') + assert_equal [], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en', 3) ProjectMedia.any_instance.unstub(:report_status) CheckSearch.any_instance.unstub(:medias) @@ -362,11 +362,11 @@ def teardown query = 'foo bar' assert_queries '>', 1 do - assert_equal [pm], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], nil) + assert_equal [pm], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], 3, nil) end assert_queries '=', 0 do - assert_equal [pm], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], nil) + assert_equal [pm], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], 3, nil) end ProjectMedia.any_instance.unstub(:report_status) @@ -389,7 +389,7 @@ def teardown 'ward', #Fuzzy match (non-emoji) '🤣 ward', #Fuzzy match (non-emoji) ].each do |query| - assert_equal [pm.id], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id]).to_a.map(&:id) + assert_equal [pm.id], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], 3).to_a.map(&:id) end [ @@ -397,7 +397,7 @@ def teardown '🌞', #No match '🤣 🌞' #No match (we only perform AND) ].each do |query| - assert_equal [], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id]).to_a.map(&:id) + assert_equal [], Bot::Smooch.search_for_similar_published_fact_checks('text', query, [t.id], 3).to_a.map(&:id) end end @@ -412,9 +412,9 @@ def teardown [pm1, pm2, pm3].each { |pm| publish_report(pm) } sleep 2 # Wait for ElasticSearch to index content - assert_equal [pm1.id, pm2.id, pm3.id], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Foo Bar', [t.id]).to_a.map(&:id) + assert_equal [pm1.id, pm2.id, pm3.id], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Foo Bar', [t.id], 3).to_a.map(&:id) # Calling wiht skip_cache true - assert_equal [pm1.id, pm2.id, pm3.id], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Foo Bar', [t.id], nil, nil, nil, true).to_a.map(&:id) + assert_equal [pm1.id, pm2.id, pm3.id], Bot::Smooch.search_for_similar_published_fact_checks('text', 'Foo Bar', [t.id], 3, nil, nil, nil, true).to_a.map(&:id) end test "should store media" do @@ -441,7 +441,7 @@ def teardown test "should not return cache search result if report is not published anymore" do pm = create_project_media Bot::Smooch.stubs(:search_for_similar_published_fact_checks).returns([pm]) - assert_equal [], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en') + assert_equal [], Bot::Smooch.get_search_results(random_string, {}, pm.team_id, 'en', 3) end test "should store sent tipline message in background" do @@ -609,12 +609,12 @@ def teardown m = create_uploaded_image pm = create_project_media team: t, media: m, disable_es_callbacks: false query = "Claim content" - results = Bot::Smooch.search_by_keywords_for_similar_published_fact_checks(query.split(), nil, [t.id]) + results = Bot::Smooch.search_by_keywords_for_similar_published_fact_checks(query.split(), nil, [t.id], 3) assert_empty results cd = create_claim_description project_media: pm, description: query publish_report(pm) assert_equal query, pm.claim_description_content - results = Bot::Smooch.search_by_keywords_for_similar_published_fact_checks(query.split(), nil, [t.id]) + results = Bot::Smooch.search_by_keywords_for_similar_published_fact_checks(query.split(), nil, [t.id], 3) assert_equal [pm.id], results.map(&:id) end diff --git a/test/models/explainer_test.rb b/test/models/explainer_test.rb index 7d191e1e7..c694152dd 100644 --- a/test/models/explainer_test.rb +++ b/test/models/explainer_test.rb @@ -153,4 +153,10 @@ def setup end end end + + test "should get alegre models_and_thresholds in hash format" do + ex = create_explainer + models_thresholds = Explainer.get_alegre_models_and_thresholds(ex.team_id) + assert_kind_of Hash, models_thresholds + end end diff --git a/test/models/project_media_7_test.rb b/test/models/project_media_7_test.rb index 73553e8b8..d83e68c18 100644 --- a/test/models/project_media_7_test.rb +++ b/test/models/project_media_7_test.rb @@ -115,4 +115,52 @@ def setup 2.times { create_project_media(team: t, set_original_claim: 'This is a claim.') } end end + + test "should search for item similar articles" do + RequestStore.store[:skip_cached_field_update] = false + setup_elasticsearch + t = create_team + pm1 = create_project_media quote: 'Foo Bar', team: t + pm2 = create_project_media quote: 'Foo Bar Test', team: t + pm3 = create_project_media quote: 'Foo Bar Test Testing', team: t + ex1 = create_explainer language: 'en', team: t, title: 'Foo Bar' + ex2 = create_explainer language: 'en', team: t, title: 'Foo Bar Test' + ex3 = create_explainer language: 'en', team: t, title: 'Foo Bar Test Testing' + pm1.explainers << ex1 + pm2.explainers << ex2 + pm3.explainers << ex3 + ex_ids = [ex1.id, ex2.id, ex3.id] + Bot::Smooch.stubs(:search_for_explainers).returns(Explainer.where(id: ex_ids)) + # Should get explainer + assert_equal [ex2.id, ex3.id], pm1.get_similar_articles.map(&:id).sort + fact_checks = [] + [pm1, pm2, pm3].each do |pm| + cd = create_claim_description description: pm.title, project_media: pm + fc = create_fact_check claim_description: cd, title: pm.title + fact_checks << fc.id + end + [pm1, pm2, pm3].each { |pm| publish_report(pm) } + sleep 1 + fact_checks.delete(pm1.fact_check_id) + # Should get both explainer and FactCheck + assert_equal fact_checks.concat([ex2.id, ex3.id]).sort, pm1.get_similar_articles.map(&:id).sort + Bot::Smooch.unstub(:search_for_explainers) + # Test with media item + json_schema = { + type: 'object', + required: ['job_name'], + properties: { + text: { type: 'string' }, + job_name: { type: 'string' }, + last_response: { type: 'object' } + } + } + create_annotation_type_and_fields('Transcription', {}, json_schema) + img = create_uploaded_image + pm_i = create_project_media team: t, media: img + data = { 'job_status' => 'COMPLETED', 'transcription' => 'Foo Bar'} + a = create_dynamic_annotation annotation_type: 'transcription', annotated: pm_i, set_fields: { text: 'Foo Bar', job_name: '0c481e87f2774b1bd41a0a70d9b70d11', last_response: data }.to_json + sleep 1 + assert_equal [pm1.fact_check_id, pm2.fact_check_id, pm3.fact_check_id].sort, pm_i.get_similar_articles.map(&:id).sort + end end diff --git a/test/models/team_2_test.rb b/test/models/team_2_test.rb index 5b2df9544..82a0f237f 100644 --- a/test/models/team_2_test.rb +++ b/test/models/team_2_test.rb @@ -1549,4 +1549,38 @@ def setup assert_equal 1, t.filtered_explainers(text: 'Foo Bar Alpha').count assert_equal 0, t.filtered_fact_checks(text: 'Foo Bar Delta').count end + + test "should search for similar articles" do + RequestStore.store[:skip_cached_field_update] = false + setup_elasticsearch + t = create_team + pm1 = create_project_media quote: 'Foo Bar', team: t + pm2 = create_project_media quote: 'Foo Bar Test', team: t + pm3 = create_project_media quote: 'Foo Bar Test Testing', team: t + ex1 = create_explainer language: 'en', team: t, title: 'Foo Bar' + ex2 = create_explainer language: 'en', team: t, title: 'Foo Bar Test' + ex3 = create_explainer language: 'en', team: t, title: 'Foo Bar Test Testing' + pm1.explainers << ex1 + pm2.explainers << ex2 + pm3.explainers << ex3 + ex_ids = [ex1.id, ex2.id, ex3.id] + Bot::Smooch.stubs(:search_for_explainers).returns(Explainer.where(id: ex_ids)) + # Return Explainer if no FactCheck exists + assert_equal ex_ids, t.search_for_similar_articles('Foo Bar').map(&:id).sort + fact_checks = [] + [pm1, pm2, pm3].each do |pm| + cd = create_claim_description description: pm.title, project_media: pm + fc = create_fact_check claim_description: cd, title: pm.title + fact_checks << fc.id + end + [pm1, pm2, pm3].each { |pm| publish_report(pm) } + sleep 2 + # Should return FactCheck even there is an Explainer exists + assert_equal fact_checks.sort, t.search_for_similar_articles('Foo Bar').map(&:id).sort + # Verirfy limit option + stub_configs({ 'most_relevant_team_limit' => 1 }) do + assert_equal [fact_checks.first], t.search_for_similar_articles('Foo Bar').map(&:id).sort + end + Bot::Smooch.unstub(:search_for_explainers) + end end