Language detection with CLD3 (#19756)

* Language detection with CLD3 POC

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update app/models/article.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Clean up tests

* Clean up tests

* Move language detection to service

* rubocop

* Update app/services/languages/detection.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* More linting cleanup

* Remove allow_any_instance_of

* Update tests to better exercise different paths

* Fix flaky spec

* Update spec/services/languages/detection_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/models/article_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/languages/detection_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/languages/detection_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/languages/detection_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Update spec/services/languages/detection_spec.rb

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Ben Halpern 2023-07-17 08:57:20 -04:00 committed by GitHub
parent 77148d8687
commit 8335a37315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 145 additions and 1 deletions

View file

@ -21,6 +21,7 @@ gem "blazer", "~> 2.6" # Allows admins to query data
gem "bootsnap", ">= 1.1.0", require: false # Boot large ruby/rails apps faster
gem "carrierwave", "~> 2.2" # Upload files in your Ruby applications, map them to a range of ORMs, store them on different backends
gem "carrierwave-bombshelter", "~> 0.2" # Protect your carrierwave from image bombs
gem "cld3", "~> 3.5" # Ruby interface for Compact Language Detector v3
gem "cloudinary", "~> 1.23" # Client library for easily using the Cloudinary service
gem "counter_culture", "~> 3.2" # counter_culture provides turbo-charged counter caches that are kept up-to-date
gem "ddtrace", "~> 1.3.0" # ddtrace is Datadogs tracing client for Ruby.

View file

@ -158,6 +158,7 @@ GEM
fastimage
cgi (0.3.6)
chartkick (4.2.1)
cld3 (3.5.3)
cloudinary (1.26.0)
aws_cf_signer
rest-client (>= 2.0.0)
@ -982,6 +983,7 @@ DEPENDENCIES
carrierwave (~> 2.2)
carrierwave-bombshelter (~> 0.2)
cgi (~> 0.3.6)
cld3 (~> 3.5)
cloudinary (~> 1.23)
counter_culture (~> 3.2)
cuprite (~> 0.13)

View file

@ -198,6 +198,7 @@ class Article < ApplicationRecord
before_save :calculate_base_scores
before_save :fetch_video_duration
before_save :set_caches
before_save :detect_language
before_create :create_password
before_destroy :before_destroy_actions, prepend: true
@ -625,6 +626,12 @@ class Article < ApplicationRecord
collection.destroy
end
def detect_language
return unless title_changed? || body_markdown_changed?
self.language = Languages::Detection.call("#{title}. #{body_text}")
end
def search_score
comments_score = (comments_count * 3).to_i
partial_score = (comments_score + (public_reactions_count.to_i * 300 * user.reputation_modifier * score.to_i))

View file

@ -0,0 +1,22 @@
module Languages
class Detection
attr_reader :text
PROBABILITY_THRESHOLD = 0.5
def self.call(...)
new(...).call
end
def initialize(text)
@text = text
end
def call(identifier: CLD3::NNetLanguageIdentifier.new(0, 1000))
language_outcome = identifier.find_language(text)
return unless language_outcome.probability > PROBABILITY_THRESHOLD && language_outcome.reliable?
language_outcome.language
end
end
end

View file

@ -0,0 +1,5 @@
class AddHumanLanguageToArticles < ActiveRecord::Migration[7.0]
def change
add_column :articles, :language, :string
end
end

View file

@ -0,0 +1,6 @@
class AddLanguageIndexToArticles < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
add_index :articles, :language, algorithm: :concurrently
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_06_27_154435) do
ActiveRecord::Schema[7.0].define(version: 2023_07_13_150940) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_trgm"
@ -105,6 +105,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_27_154435) do
t.boolean "featured", default: false
t.string "feed_source_url"
t.integer "hotness_score", default: 0
t.string "language"
t.datetime "last_comment_at", precision: nil, default: "2017-01-01 05:00:00"
t.datetime "last_experience_level_rating_at", precision: nil
t.string "main_image"
@ -160,6 +161,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_06_27_154435) do
t.index ["feed_source_url"], name: "index_articles_on_feed_source_url_unscoped"
t.index ["hotness_score", "comments_count"], name: "index_articles_on_hotness_score_and_comments_count"
t.index ["hotness_score"], name: "index_articles_on_hotness_score"
t.index ["language"], name: "index_articles_on_language"
t.index ["path"], name: "index_articles_on_path"
t.index ["public_reactions_count"], name: "index_articles_on_public_reactions_count", order: :desc
t.index ["published"], name: "index_articles_on_published"

View file

@ -1420,4 +1420,27 @@ RSpec.describe Article do
end
end
end
describe "#detect_language" do
let(:detected_language) { :kl } # kl for Klingon
before do
allow(Languages::Detection).to receive(:call).and_return(detected_language)
end
it "detects language using title and body for newly created articles" do
article = create(:article)
expect(Languages::Detection).to have_received(:call).with("#{article.title}. #{article.body_text}")
end
it "detects language using title and body for updated articles" do
article.update(body_markdown: "---title: This is a new english article\n---\n\n# Hello World")
expect(Languages::Detection).to have_received(:call).with("#{article.title}. #{article.body_text}")
end
it "does not call detection when title and body_markdown are unchanged" do
article.language = "es"
article.update(nth_published_by_author: 5)
expect(Languages::Detection).not_to have_received(:call)
end
end
end

View file

@ -0,0 +1,76 @@
require "rails_helper"
RSpec.describe Languages::Detection, type: :service do
subject(:language_detection) { described_class.call(text) }
context "when the text is clearly identifiable as English" do
let(:text) { "This is clearly English text." }
it "returns en" do
expect(language_detection).to eq(:en)
end
end
context "when the text is clearly identifiable as Spanish" do
let(:text) { "Esto es claramente un texto en español." }
it "returns es" do
expect(language_detection).to eq(:es)
end
end
context "when probability and reliability vary" do
let(:text) { "This is some dummy text." }
let(:identifier) { instance_double(CLD3::NNetLanguageIdentifier) }
before do
allow(CLD3::NNetLanguageIdentifier).to receive(:new).and_return(identifier)
allow(identifier).to receive(:find_language).with(text).and_return(language_outcome)
end
context "when probability is low" do
let(:language_outcome) do
instance_double(
CLD3::NNetLanguageIdentifier::Result,
language: :es,
probability: 0.4,
reliable?: true
)
end
it "returns nil" do
expect(described_class.call(text)).to eq(nil)
end
end
context "when reliability is low" do
let(:language_outcome) do
instance_double(
'CLD3::NNetLanguageIdentifier::Result',
language: :es,
probability: 0.9,
reliable?: false
)
end
it "returns nil" do
expect(described_class.call(text)).to be(nil)
end
end
context "when probability and reliability are high" do
let(:language_outcome) do
instance_double(
CLD3::NNetLanguageIdentifier::Result,
language: :es,
probability: 0.9,
reliable?: true,
)
end
it "returns es" do
expect(described_class.call(text)).to eq(:es)
end
end
end
end

BIN
vendor/cache/cld3-3.5.3.gem vendored Normal file

Binary file not shown.