discourse/plugins/discourse-ai/spec/lib/translation/topic_localizer_spec.rb
Rafael dos Santos Silva 0b73d7ac1a
PERF: Cache LlmModel lookups in backfill jobs (#36359)
Backfill jobs were fetching the same LlmModel on every loop iteration,
causing N+1 queries. For a batch of 100 items, this meant 100+ identical
SQL queries to populate the same LlmModel object.

## Changes

- Cache LlmModel once per job execution in summaries and translation
backfills
- Pass cached model through to summarization and translation methods via
optional parameters
- Add tests verifying models are queried once per batch instead of once
per item

## Impact

**Before:** For 100 items, 100+ `LlmModel.find_by()` queries  
**After:** For 100 items, 1-2 queries total

All changes are backward compatible - new parameters are optional with
nil defaults, so existing callers continue to work unchanged.
2025-12-01 17:04:25 -03:00

133 lines
4.4 KiB
Ruby
Vendored

# frozen_string_literal: true
describe DiscourseAi::Translation::TopicLocalizer do
before { enable_current_plugin }
describe ".localize" do
fab!(:topic) do
Fabricate(
:topic,
title: "this is a cat topic :)",
excerpt: "cats are great. how many do you have?",
)
end
let(:translator) { mock }
let(:translated_title) { "これは猫の話題です :)" }
let(:translated_excerpt) { "猫は素晴らしいですね。何匹飼っていますか?" }
let(:fancy_title) { "これは猫の話題です :slight_smile:" }
let(:target_locale) { "ja" }
def topic_title_translator_stub(opts)
mock = instance_double(DiscourseAi::Translation::TopicTitleTranslator)
allow(DiscourseAi::Translation::TopicTitleTranslator).to receive(:new).with(
text: opts[:text],
target_locale: opts[:target_locale],
topic: opts[:topic] || topic,
llm_model: be_nil,
).and_return(mock)
allow(mock).to receive(:translate).and_return(opts[:translated])
end
def post_raw_translator_stub(opts)
mock = instance_double(DiscourseAi::Translation::PostRawTranslator)
allow(DiscourseAi::Translation::PostRawTranslator).to receive(:new).with(
text: opts[:text],
target_locale: opts[:target_locale],
topic: opts[:topic] || topic,
llm_model: be_nil,
).and_return(mock)
allow(mock).to receive(:translate).and_return(opts[:translated])
end
it "returns nil if topic is blank" do
expect(described_class.localize(nil, "ja")).to eq(nil)
end
it "returns nil if target_locale is blank" do
expect(described_class.localize(topic, nil)).to eq(nil)
expect(described_class.localize(topic, "")).to eq(nil)
end
it "returns nil if target_locale is same as topic locale" do
topic.locale = "en"
expect(described_class.localize(topic, "en")).to eq(nil)
end
it "translates with topic and locale" do
topic_title_translator_stub(
{ text: topic.title, target_locale: "ja", translated: translated_title },
)
post_raw_translator_stub(
{ text: topic.excerpt, target_locale: "ja", translated: translated_excerpt },
)
described_class.localize(topic, "ja")
end
it "normalizes dashes to underscores and symbol type for locale" do
topic_title_translator_stub(
{ text: topic.title, target_locale: "zh_CN", translated: "这是一个猫主题 :)" },
)
post_raw_translator_stub(
{ text: topic.excerpt, target_locale: "zh_CN", translated: "这是一个猫主题 :)" },
)
described_class.localize(topic, "zh-CN")
end
it "finds or creates a TopicLocalization and sets its fields" do
topic_title_translator_stub(
{ text: topic.title, target_locale: "ja", translated: translated_title },
)
post_raw_translator_stub(
{ text: topic.excerpt, target_locale: "ja", translated: translated_excerpt },
)
expect {
res = described_class.localize(topic, target_locale)
expect(res).to be_a(TopicLocalization)
expect(res).to have_attributes(
topic_id: topic.id,
locale: target_locale,
title: translated_title,
excerpt: translated_excerpt,
fancy_title: fancy_title,
localizer_user_id: Discourse.system_user.id,
)
}.to change { TopicLocalization.count }.by(1)
end
it "updates an existing TopicLocalization if present" do
topic_title_translator_stub(
{ text: topic.title, target_locale: "ja", translated: translated_title },
)
post_raw_translator_stub(
{ text: topic.excerpt, target_locale: "ja", translated: translated_excerpt },
)
localization =
Fabricate(
:topic_localization,
topic:,
locale: "ja",
title: "old title",
excerpt: "old excerpt",
fancy_title: "old_fancy_title",
)
expect {
expect(described_class.localize(topic, "ja")).to have_attributes(
id: localization.id,
title: translated_title,
fancy_title: fancy_title,
excerpt: translated_excerpt,
)
expect(localization.reload).to have_attributes(
title: translated_title,
fancy_title: fancy_title,
excerpt: translated_excerpt,
)
}.to_not change { TopicLocalization.count }
end
end
end