← ブログに戻る

1つのAPIキー、多数のテナント:顧客間でDeepL翻訳を分離する方法

Rasepiは、すべてのテナントに対して単一のDeepL APIキーを使用します。ここでは、顧客ごとの用語集、スタイルルール、キャッシュされた翻訳、ブロックレベルの分離を漏れなく処理する方法を紹介します。

Thinking out loud
1つのAPIキー、多数のテナント:顧客間でDeepL翻訳を分離する方法

Rasepiの翻訳アーキテクチャを他の開発者に説明するたびに出てくる質問があります。すべてのテナントが1つのDeepL APIキーを共有しているのですか?

これは正しい質問です。その答えには、予想以上に多くの設計作業が含まれます。

ブロックレベルのハッシュ化、オーケストレーター、ドキュメントの保存から翻訳された出力までの全体の流れなど、完全な翻訳パイプラインについては以前の投稿で取り上げました。この投稿では、マルチテナンシーという特定の問題にズームインします。テナントの概念を持たないサードパーティAPIをどのように利用し、その上にテナント分離を構築するか。

問題:DeepLはあなたの顧客について知らない

DeepLのAPIは、単一のAPIキーで認証されます。そのキーの下で作成されたすべてのもの、用語集、スタイルルールリスト、翻訳履歴は、同じアカウントに属します。DeepL側には、"この用語集はテナントAに属する" という概念はありません。

CODEBLOCK_2__ を呼び出すと、すべてのテナントからすべての用語集が取得されます。スタイル・ルール・リストを作成すると、それは他のすべてのテナントのスタイル・ルールと同じネームスペースに存在します。APIはフラットです。

すべての顧客が独自の DeepL キーで独自のインスタンスを実行するセルフホスト型製品の場合、これは問題ありません。当社がインフラストラクチャを管理するマルチテナント型SaaSの場合は?分離レイヤが必要です。

データベースは真実のソースです。

私たちの中核となる設計上の決定:**データベースは、すべての用語集コンテンツおよびスタイル・ルール構成を所有します。DeepL は実行時のターゲットであり、それ以上ではありません。

すべてのTenantGlossaryおよびTenantStyleRuleListエンティティはITenantScopedを実装しています。これは、EF Coreグローバル・クエリ・フィルタがすべての読み取りを現在のテナントに自動的にスコープすることを意味します。テナントAのリクエストコンテキストにある用語集に対するクエリは、テナントBのエントリを決して返しません。これはORMレベルで強制される、Rasepiのあらゆる場所で使用している同じ分離パターンです。

これが興味深い点です。テナントが用語集を編集しても、すぐにDeepLを呼び出すわけではありません。データベースの行を更新し、IsDirty = trueを設定します。これだけです。実際の DeepL 用語集は、次の翻訳で必要になる直前に、遅延的に作成 (または再作成) されます。

CODEBLOCK_0__

CODEBLOCK_7__ のクエリ・フィルタが分離を行います。CODEBLOCK_8__フラグが遅延同期を行います。また、命名規則 (rasepi-{glossary.Id}) は、DeepL ダッシュボードでのデバッグのためだけであり、機能的な目的はありません。

遅延の理由DeepL v2 用語集は不変](https://developers.deepl.com/docs/api-reference/glossaries) だからです。編集できません。変更は、削除と再作成を意味します。チームが 200 の用語を含む CSV をインポートし、1 つのエントリでタイプミスを修正した場合、DeepL 用語集を 2 回削除して再作成する必要はありません。この場合、IsDirty を 2 回とも設定し、次の翻訳の実行時に 1 回だけ再作成します。

スタイル・ルール: 同じパターン、異なる API

DeepL のスタイル・ルール はより新しく (v3 API) 、実際に変更可能です。CODEBLOCK_11__を使用すると、構成済みのルールをその場で更新でき、カスタム命令を個別に追加または削除できます。

しかし、同じIsDirtyパターンを使用しています。CODEBLOCK_13__ には、DeepL の実行時識別子にマップされる DeepLStyleId と、書式設定ルール用の ConfiguredRulesJson 、およびフリーテキスト翻訳ディレクティブ用の TenantCustomInstruction エントリのコレクションがあります。

本当の力はこれらのカスタム命令にあります。各命令は、DeepL の翻訳方法を形成する最大 300 文字のプレーン・ランゲージ命令です。テナントからの実際の例:

  • 常に'Sie'形式を使用し、決して'du'を使用しないでください」_ドイツの法律事務所向け
  • deployment'を'Bereitstellung'と翻訳し、決して'Deployment'とは翻訳しない」_単純な用語集のマッピングを超えるコンテキスト依存の用語の場合
  • 英国の会社では、英語の表記を使い分ける必要があります。
  • 通貨記号を数字の後に付ける」_欧州の慣習のため

各テナントは、ターゲット言語ごとに完全に異なる命令を持つことができ、すべて同じAPIキーの背後にあります。すべての翻訳呼び出しには、要求元のテナントに属するglossary_idstyle_idのみが含まれるため、隔離されます。他のテナントの DeepL リソースが参照されることはありません。

翻訳呼び出し: すべては

オーケストレータがブロックを変換するとき、すべてのテナント固有の設定を 1 つの要求にまとめます:

codeblock_1__

ここにあるすべてのパラメータはテナントにスコープされています。CODEBLOCK_19__はテナントフィルターされたクエリで解決されました。CODEBLOCK_20__も同様に解決されました。CODEBLOCK_21__はTenantLanguageConfigから来ており、これもテナント・スコープされています。CODEBLOCK_23__(翻訳品質を向上させるために送信された段落を囲むもので、課金対象ではありません)も、テナント自身の文書から来たものです。

強調したいことが 1 つあります。style_id が設定されると、DeepL は自動的に quality_optimized モデルを使用します。スタイル・ルールと latency_optimized を組み合わせることはできません。これは DeepL の制約ですが、正直なところ妥当な制約です。カスタム・スタイル・ルールに投資する場合は、おそらく最高品質の出力が必要です。

ブロックレベルのキャッシュ: 翻訳メモリとしてのデータベース

変更のないブロックに対して DeepL を呼び出すことはありません。キャッシュ・メカニズムは、TranslationBlock テーブルそのものです。

すべてのソースEntryBlockには、ContentHashがあります。これは、意味的コンテンツのSHA256です (blockIddeletedなどのメタデータ属性は除去されています)。すべてのTranslationBlockは、その変換が行われた時のSourceContentHashを格納します。ソースブロックが変更されると、そのハッシュも変更されます。オーケストレータはハッシュを比較し、不一致のあるブロックだけをキューに入れます。

各ブロックのデシジョンツリーは次のようになります:

1.ハッシュが一致し、翻訳が存在する = スキップ (キャッシュ、最新) 2.ハッシュが変更、機械翻訳、ロックされていない = 自動的に再翻訳 3.ハッシュが変更された、人間が編集した、またはロックされた = 古いとマーク、上書きしない

この3番目のケースが重要です。ドイツ語翻訳者が手作業で段落を修正した場合、英語ソースが変更されたからといって、その段落を削除することはありません。ドイツ語翻訳者にレビューが必要であることを認識させるために、staleとしてフラグを立てますが、翻訳されたテキストはそのまま残ります。

実際の結果: 30 段落のドキュメントの 1 つの段落を編集すると、DeepL API 呼び出しがちょうど 1 回呼び出されます (ただし、1 つのブロックを含む 1 つのバッチ)。他の 29 の段落は、すべての言語にわたって、すでにキャッシュされており、コストはかかりません。

テナントごとに別のキーを使用しませんか?

真剣に検討しました。各テナントに独自のDeepL APIキーを与えれば、分離の問題は完全になくなります。

そうしなかった3つの理由:

1.**すべてのテナントに独自の DeepL サブスクリプションまたはサブアカウントをプロビジョニングする方法が必要です。DeepL は、マルチテナント鍵管理をネイティブで提供していません。 2.**インフラストラクチャの共有は、料金の上限とボリューム割引の共有を意味します。当社の使用量を合計することで、より良い価格設定が可能になります。 3.**ローテーションするキーは1つ、監視するクォータは1つ、保守する統合は1つです。

トレードオフは、私が説明した分離レイヤーが必要だということです。しかし、システム内の他の全てのものに対して既にテナント・スコープのEF Coreクエリを持っていることを考えると、用語集やスタイル・ルールにそれを追加することは簡単でした。パターンはすでにそこにありました。

実際にあなたを守るもの

分離保証をまとめると

  • 用語集エントリ** は TenantGlossary (ITenantScoped の実装) に格納され、EF Core のグローバル・クエリ・フィルタによってフィルタリングされます。DeepL 用語集 ID は、テナント・コンテキスト内でのみ解決される不透明な参照です。
  • スタイル・ルールおよびカスタム命令**は、TenantStyleRuleListを通じて同じパターンに従います。
  • 翻訳されたコンテンツ**はTranslationBlockにあり、親であるEntryHubチェーンを経由してスコープされます。
  • CODEBLOCK_40__ガード**は、新しいエンティティに自動的にTenantIdを設定し、クロステナント書き込み時にスローします。
  • 本番コードではIgnoreQueryFilters()**を使用しません。これまで

設計原則は単純です:DeepLはリソースIDを見ます。Rasepiはテナント・スコープのエンティティを参照します。マッピングを解決するクエリは物理的に別のテナントのデータを返すことができないため、両者間のマッピングがテナントの境界を越えることはありません。

ネイティブのテナントサポートを持たないサードパーティAPIと統合するマルチテナントSaaSを構築する場合、このパターンはうまく機能します。外部APIをステートレスな実行エンジンとして扱い、全てのコンフィギュレーションを独自のテナント・スコープ・データベースに保持し、遅延同期を行い、分離のために外部リソース・リストを決して信用しないようにします。

Smoower logo Smoower

Know what your DevRel team should ship next

Smoower watches your repos, releases, and issues, plus what devs say about you on Reddit, Hacker News, and YouTube. Each teammate gets the next task worth doing and a draft worth shipping, with the source signal attached.

See a sample draft →