Sfoglia il codice sorgente

实现了 Query Expansion (查询扩展/Multi-Query) 和 HyDE (假设文档嵌入) 功能。

shang-chunyu 3 settimane fa
parent
commit
9a0edf734b

+ 10 - 4
server/src/chat/chat.controller.ts

@@ -25,8 +25,10 @@ class StreamChatDto {
   temperature?: number; // 新增:temperature 参数
   maxTokens?: number; // 新增:maxTokens 参数
   topK?: number; // 新增:topK 参数
-  similarityThreshold?: number; // 新增:similarityThreshold 参数
-  rerankSimilarityThreshold?: number; // 新增:rerankSimilarityThreshold 参数
+  similarityThreshold?: number; // 新増:similarityThreshold 参数
+  rerankSimilarityThreshold?: number; // 新増:rerankSimilarityThreshold 参数
+  enableQueryExpansion?: boolean; // 新增
+  enableHyDE?: boolean; // 新增
 }
 
 @Controller('chat')
@@ -45,7 +47,7 @@ export class ChatController {
   ) {
     try {
       console.log('Full Request Body:', JSON.stringify(body, null, 2));
-      const { message, history = [], userLanguage = 'zh', selectedEmbeddingId, selectedLLMId, selectedGroups, selectedFiles, historyId, enableRerank, selectedRerankId, temperature, maxTokens, topK, similarityThreshold, rerankSimilarityThreshold } = body;
+      const { message, history = [], userLanguage = 'zh', selectedEmbeddingId, selectedLLMId, selectedGroups, selectedFiles, historyId, enableRerank, selectedRerankId, temperature, maxTokens, topK, similarityThreshold, rerankSimilarityThreshold, enableQueryExpansion, enableHyDE } = body;
       const userId = req.user.id;
 
       console.log('=== 聊天调试信息 ===');
@@ -62,6 +64,8 @@ export class ChatController {
       console.log('Top K:', topK);
       console.log('Similarity Threshold:', similarityThreshold);
       console.log('Rerank Similarity Threshold:', rerankSimilarityThreshold);
+      console.log('Query Expansion:', enableQueryExpansion);
+      console.log('HyDE:', enableHyDE);
 
       // 获取用户的LLM模型配置
       const models = await this.modelConfigService.findAll(userId);
@@ -114,7 +118,9 @@ export class ChatController {
         maxTokens, // 传递 maxTokens 参数
         topK, // 传递 topK 参数
         similarityThreshold, // 传递 similarityThreshold 参数
-        rerankSimilarityThreshold // 传递 rerankSimilarityThreshold 参数
+        rerankSimilarityThreshold, // 传递 rerankSimilarityThreshold 参数
+        enableQueryExpansion, // 传递 enableQueryExpansion
+        enableHyDE // 传递 enableHyDE
       );
 
       for await (const chunk of stream) {

+ 8 - 2
server/src/chat/chat.service.ts

@@ -58,7 +58,9 @@ export class ChatService {
     maxTokens?: number, // 新規: maxTokens パラメータ
     topK?: number, // 新規: topK パラメータ
     similarityThreshold?: number, // 新規: similarityThreshold パラメータ
-    rerankSimilarityThreshold?: number // 新規: rerankSimilarityThreshold パラメータ
+    rerankSimilarityThreshold?: number, // 新規: rerankSimilarityThreshold パラメータ
+    enableQueryExpansion?: boolean, // 新規
+    enableHyDE?: boolean // 新規
   ): AsyncGenerator<{ type: 'content' | 'sources' | 'historyId'; data: any }> {
     console.log('=== ChatService.streamChat ===');
     console.log('ユーザーID:', userId);
@@ -72,6 +74,8 @@ export class ChatService {
     console.log('Top K:', topK);
     console.log('類似度しきい値:', similarityThreshold);
     console.log('Rerankしきい値:', rerankSimilarityThreshold);
+    console.log('Query拡張:', enableQueryExpansion);
+    console.log('HyDE:', enableHyDE);
     console.log('モデル設定:', {
       name: modelConfig.name,
       modelId: modelConfig.modelId,
@@ -164,7 +168,9 @@ export class ChatService {
           selectedRerankId,
           undefined, // selectedGroups
           effectiveFileIds,
-          rerankSimilarityThreshold
+          rerankSimilarityThreshold,
+          enableQueryExpansion,
+          enableHyDE
         );
 
         // RagSearchResult を ChatService が必要とする形式 (any[]) に変換

+ 3 - 0
server/src/defaults.ts

@@ -18,5 +18,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
   enableFullTextSearch: false,
   hybridVectorWeight: 0.7,
 
+  enableQueryExpansion: false,
+  enableHyDE: false,
+
   language: 'ja',
 };

+ 6 - 0
server/src/i18n/messages.ts

@@ -348,6 +348,8 @@ export const statusMessages = {
     ragSource: '### 来源:{fileName}',
     ragSegment: '片段 {index} (相似度: {score}):',
     ragNoDocumentFound: '未找到相关文档。',
+    queryExpansionPrompt: '您是一个搜索助手。请为以下用户查询生成3个不同的演变版本,以帮助在向量搜索中获得更好的结果。每个版本应包含不同的关键词或表达方式,但保持原始意思。直接输出3行查询,不要有数字或编号:\n\n查询:{query}',
+    hydePrompt: '请为以下用户问题写一段简短、事实性的假设回答(约100字)。不要包含任何引导性文字(如“基于我的分析...”),直接输出答案内容。\n\n问题:{query}',
   },
   ja: {
     searching: 'ナレッジベースを検索中...',
@@ -438,6 +440,8 @@ export const statusMessages = {
     ragSource: '### ソース:{fileName}',
     ragSegment: 'セグメント {index} (類似度: {score}):',
     ragNoDocumentFound: '関連するドキュメントが見つかりませんでした。',
+    queryExpansionPrompt: 'あなたは検索アシスタントです。以下のユーザーのクエリに対して、ベクトル検索でより良い結果を得るために、3つの異なるバリエーションを生成してください。各バリエーションは異なるキーワードや表現を使用しつつ、元の意味を維持する必要があります。数字やプレフィックスなしで、3行のクエリを直接出力してください:\n\nクエリ:{query}',
+    hydePrompt: '以下のユーザーの質問に対して、簡潔で事実に基づいた仮説的な回答(約200文字)を書いてください。「私の分析によると...」などの導入文は含めず、回答内容のみを直接出力してください。\n\n質問:{query}',
   },
   en: {
     searching: 'Searching knowledge base...',
@@ -528,5 +532,7 @@ export const statusMessages = {
     ragSource: '### Source: {fileName}',
     ragSegment: 'Segment {index} (Similarity: {score}):',
     ragNoDocumentFound: 'No relevant documents found.',
+    queryExpansionPrompt: 'You are a search assistant. Please generate 3 different variations of the following user query to help get better results in vector search. Each variation should use different keywords or phrasing while maintaining the original meaning. Output the 3 queries directly as 3 lines, without numbers or prefixes:\n\nQuery: {query}',
+    hydePrompt: 'Please write a brief, factual hypothetical answer (about 100 words) to the following user question. Do not include any introductory text (like "Based on my analysis..."), just output the answer content directly.\n\nQuestion: {query}',
   }
 };

+ 147 - 43
server/src/rag/rag.service.ts

@@ -6,6 +6,8 @@ import { ModelConfigService } from '../model-config/model-config.service';
 import { RerankService } from './rerank.service';
 import { I18nService } from '../i18n/i18n.service';
 import { UserSettingService } from '../user-setting/user-setting.service';
+import { ChatOpenAI } from '@langchain/openai';
+import { ModelConfig } from '../types';
 
 export interface RagSearchResult {
   content: string;
@@ -51,6 +53,8 @@ export class RagService {
     selectedGroups?: string[],
     effectiveFileIds?: string[],
     rerankSimilarityThreshold: number = 0.5, // Rerankのしきい値(デフォルト0.5)
+    enableQueryExpansion?: boolean,
+    enableHyDE?: boolean,
   ): Promise<RagSearchResult[]> {
     // 1. グローバル設定の取得
     const globalSettings = await this.userSettingService.getGlobalSettings();
@@ -64,64 +68,71 @@ export class RagService {
     const effectiveEmbeddingId = embeddingModelId || globalSettings.selectedEmbeddingId;
     const effectiveRerankId = rerankModelId || globalSettings.selectedRerankId;
     const effectiveHybridWeight = globalSettings.hybridVectorWeight ?? 0.7;
+    const effectiveEnableQueryExpansion = enableQueryExpansion !== undefined ? enableQueryExpansion : globalSettings.enableQueryExpansion;
+    const effectiveEnableHyDE = enableHyDE !== undefined ? enableHyDE : globalSettings.enableHyDE;
 
     this.logger.log(
-      `RAG search: query="${query}", topK=${effectiveTopK}, vectorThreshold=${effectiveVectorThreshold}, rerankThreshold=${effectiveRerankThreshold}, hybridWeight=${effectiveHybridWeight}`,
+      `RAG search: query="${query}", topK=${effectiveTopK}, vectorThreshold=${effectiveVectorThreshold}, rerankThreshold=${effectiveRerankThreshold}, hybridWeight=${effectiveHybridWeight}, QueryExpansion=${effectiveEnableQueryExpansion}, HyDE=${effectiveEnableHyDE}`,
     );
 
     try {
-      // 1. クエリベクトルの取得
+      // 1. クエリの準備(拡張または HyDE)
+      let queriesToSearch = [query];
+
+      if (effectiveEnableHyDE) {
+        const hydeDoc = await this.generateHyDE(query, userId);
+        queriesToSearch = [hydeDoc]; // HyDE の場合は仮想ドキュメントをクエリとして使用
+      } else if (effectiveEnableQueryExpansion) {
+        const expanded = await this.expandQuery(query, userId);
+        queriesToSearch = [...new Set([query, ...expanded])];
+      }
+
+      // 埋め込みモデルIDが提供されているか確認
       if (!effectiveEmbeddingId) {
         throw new Error('埋め込みモデルIDが提供されていません');
       }
 
-      const queryEmbedding = await this.embeddingService.getEmbeddings(
-        [query],
-        userId,
-        effectiveEmbeddingId,
-      );
-      const queryVector = queryEmbedding[0];
-
-      this.logger.log(`使用するベクトル次元数: ${queryVector?.length}`);
-
-      // 2. 設定に基づいた検索戦略の選択
-      let searchResults;
-      if (effectiveEnableFullText) {
-        // ハイブリッド検索
-        // 重要: ここでの 0.7 はハイブリッドの重み。閾値フィルタリングは後で「生のスコア」に対して行う。
-        // ElasticsearchService.hybridSearch は内部でベクトル検索と全文検索それぞれのスコアを持つ
-        searchResults = await this.elasticsearchService.hybridSearch(
-          queryVector,
-          query,
-          userId,
-          effectiveTopK * 4, // Rerankのために少し多めに取得
-          effectiveHybridWeight, // vectorWeight
-          undefined,
-          effectiveFileIds
-        );
-      } else {
-        // ベクトル検索のみ
-        let vectorSearchResults = await this.elasticsearchService.searchSimilar(
-          queryVector,
+      // 2. 複数のクエリに対して並列検索
+      const searchTasks = queriesToSearch.map(async (searchQuery) => {
+        // クエリベクトルの取得
+        const queryEmbedding = await this.embeddingService.getEmbeddings(
+          [searchQuery],
           userId,
-          effectiveTopK * 4 // Rerankのために少し多めに取得
+          effectiveEmbeddingId,
         );
-
-        // フィルタリング
-        if (effectiveFileIds && effectiveFileIds.length > 0) {
-          searchResults = vectorSearchResults.filter(r => effectiveFileIds.includes(r.fileId));
+        const queryVector = queryEmbedding[0];
+
+        // 設定に基づいた検索戦略の選択
+        let results;
+        if (effectiveEnableFullText) {
+          results = await this.elasticsearchService.hybridSearch(
+            queryVector,
+            searchQuery,
+            userId,
+            effectiveTopK * 4,
+            effectiveHybridWeight,
+            undefined,
+            effectiveFileIds
+          );
         } else {
-          searchResults = vectorSearchResults;
+          let vectorSearchResults = await this.elasticsearchService.searchSimilar(
+            queryVector,
+            userId,
+            effectiveTopK * 4
+          );
+          if (effectiveFileIds && effectiveFileIds.length > 0) {
+            results = vectorSearchResults.filter(r => effectiveFileIds.includes(r.fileId));
+          } else {
+            results = vectorSearchResults;
+          }
         }
-      }
+        return results;
+      });
 
-      // 初回の類似度フィルタリング
-      // 修正: ハイブリッド検索の場合、各要素の raw スコア(加重計算前)でチェックするのが理想だが、
-      // 現状の hybridSearch は combinedScore を .score に入れている。
-      // ただし、もし vectorWeight=0.7 の場合、相似度 0.4 のものは 0.28 になり、0.3 閾値で消えてしまう。
-      // これを避けるため、閾値チェックを「加重計算の影響を考慮した値」または「加重計算前」に行う必要がある。
-      // ここでは、ユーザーの期待に合わせるため、フィルタリングロジックを微調整する。
+      const allResultsRaw = await Promise.all(searchTasks);
+      let searchResults = this.deduplicateResults(allResultsRaw.flat());
 
+      // 初回の類似度フィルタリング
       const initialCount = searchResults.length;
 
       // ログ出力
@@ -255,4 +266,97 @@ ${answerHeader}`;
     });
     return Array.from(uniqueFiles);
   }
+
+  /**
+   * 検索結果の重複排除
+   */
+  private deduplicateResults(results: any[]): any[] {
+    const unique = new Map<string, any>();
+    results.forEach(r => {
+      const key = `${r.fileId}_${r.chunkIndex}`;
+      if (!unique.has(key) || unique.get(key)!.score < r.score) {
+        unique.set(key, r);
+      }
+    });
+    return Array.from(unique.values()).sort((a, b) => b.score - a.score);
+  }
+
+  /**
+   * クエリを拡張してバリエーションを生成
+   */
+  async expandQuery(query: string, userId: string): Promise<string[]> {
+    try {
+      const llm = await this.getInternalLlm(userId);
+      if (!llm) return [query];
+
+      const userSettings = await this.userSettingService.findOrCreate(userId);
+      const lang = userSettings.language || 'ja';
+      const prompt = this.i18nService.formatMessage('queryExpansionPrompt', { query }, lang);
+
+      const response = await llm.invoke(prompt);
+      const content = String(response.content);
+
+      const expandedQueries = content
+        .split('\n')
+        .map(q => q.trim())
+        .filter(q => q.length > 0)
+        .slice(0, 3); // 最大3つに制限
+
+      this.logger.log(`Query expanded: "${query}" -> [${expandedQueries.join(', ')}]`);
+      return expandedQueries.length > 0 ? expandedQueries : [query];
+    } catch (error) {
+      this.logger.error('Query expansion failed:', error);
+      return [query];
+    }
+  }
+
+  /**
+   * 仮想的なドキュメント(HyDE)を生成
+   */
+  async generateHyDE(query: string, userId: string): Promise<string> {
+    try {
+      const llm = await this.getInternalLlm(userId);
+      if (!llm) return query;
+
+      const userSettings = await this.userSettingService.findOrCreate(userId);
+      const lang = userSettings.language || 'ja';
+      const prompt = this.i18nService.formatMessage('hydePrompt', { query }, lang);
+
+      const response = await llm.invoke(prompt);
+      const hydeDoc = String(response.content).trim();
+
+      this.logger.log(`HyDE generated for: "${query}" (length: ${hydeDoc.length})`);
+      return hydeDoc || query;
+    } catch (error) {
+      this.logger.error('HyDE generation failed:', error);
+      return query;
+    }
+  }
+
+  /**
+   * 内部タスク用の LLM インスタンスを取得
+   */
+  private async getInternalLlm(userId: string): Promise<ChatOpenAI | null> {
+    try {
+      const models = await this.modelConfigService.findAll(userId);
+      const defaultLlm = models.find(m => m.type === 'llm' && m.isDefault && m.isEnabled !== false);
+
+      if (!defaultLlm) {
+        this.logger.warn('No default LLM configured for internal tasks');
+        return null;
+      }
+
+      return new ChatOpenAI({
+        apiKey: defaultLlm.apiKey || 'ollama',
+        temperature: 0.3,
+        modelName: defaultLlm.modelId,
+        configuration: {
+          baseURL: defaultLlm.baseUrl || 'http://localhost:11434/v1',
+        },
+      });
+    } catch (error) {
+      this.logger.error('Failed to get internal LLM:', error);
+      return null;
+    }
+  }
 }

+ 4 - 0
server/src/types.ts

@@ -44,6 +44,10 @@ export interface AppSettings {
   enableFullTextSearch: boolean;
   hybridVectorWeight: number;
 
+  // Search Enhancement
+  enableQueryExpansion: boolean;
+  enableHyDE: boolean;
+
   // Language
   language: string;
 }

+ 8 - 0
server/src/user-setting/dto/create-user-setting.dto.ts

@@ -63,6 +63,14 @@ export class CreateUserSettingDto {
   @IsOptional()
   hybridVectorWeight: number = DEFAULT_SETTINGS.hybridVectorWeight;
 
+  @IsBoolean()
+  @IsOptional()
+  enableQueryExpansion: boolean = DEFAULT_SETTINGS.enableQueryExpansion;
+
+  @IsBoolean()
+  @IsOptional()
+  enableHyDE: boolean = DEFAULT_SETTINGS.enableHyDE;
+
   @IsString()
   @IsOptional()
   coachKbId?: string;

+ 6 - 0
server/src/user-setting/user-setting.entity.ts

@@ -58,6 +58,12 @@ export class UserSetting {
   @Column({ type: 'real', default: 0.7 })
   hybridVectorWeight: number;
 
+  @Column({ type: 'boolean', default: false })
+  enableQueryExpansion: boolean;
+
+  @Column({ type: 'boolean', default: false })
+  enableHyDE: boolean;
+
   @Column({ type: 'text', nullable: true })
   defaultVisionModelId: string;
 

+ 3 - 1
web/components/ChatInterface.tsx

@@ -217,7 +217,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
         settings.maxTokens, // 最大トークン数を渡す
         settings.topK, // Top-Kパラメータを渡す
         settings.similarityThreshold, // 類似度しきい値を渡す
-        settings.rerankSimilarityThreshold // Rerankしきい値を渡す
+        settings.rerankSimilarityThreshold, // Rerankしきい値を渡す
+        settings.enableQueryExpansion, // クエリ拡張を渡す
+        settings.enableHyDE // HyDEを渡す
       );
 
       for await (const chunk of stream) {

+ 3 - 1
web/components/views/ChatView.tsx

@@ -114,7 +114,9 @@ export const ChatView: React.FC<ChatViewProps> = ({
                 similarityThreshold: globalSettings.similarityThreshold ?? userSettings.similarityThreshold ?? DEFAULT_SETTINGS.similarityThreshold,
                 rerankSimilarityThreshold: globalSettings.rerankSimilarityThreshold ?? userSettings.rerankSimilarityThreshold ?? DEFAULT_SETTINGS.rerankSimilarityThreshold,
                 enableFullTextSearch: globalSettings.enableFullTextSearch ?? userSettings.enableFullTextSearch ?? DEFAULT_SETTINGS.enableFullTextSearch,
-                hybridVectorWeight: globalSettings.hybridVectorWeight ?? userSettings.hybridVectorWeight ?? DEFAULT_SETTINGS.hybridVectorWeight
+                hybridVectorWeight: globalSettings.hybridVectorWeight ?? userSettings.hybridVectorWeight ?? DEFAULT_SETTINGS.hybridVectorWeight,
+                enableQueryExpansion: globalSettings.enableQueryExpansion ?? userSettings.enableQueryExpansion ?? DEFAULT_SETTINGS.enableQueryExpansion,
+                enableHyDE: globalSettings.enableHyDE ?? userSettings.enableHyDE ?? DEFAULT_SETTINGS.enableHyDE
             }
             setSettings(appSettings)
         } catch (error) {

+ 6 - 2
web/services/chatService.ts

@@ -28,7 +28,9 @@ export class ChatService {
     maxTokens?: number, // 追加: maxTokens パラメータ
     topK?: number, // 追加: topK パラメータ
     similarityThreshold?: number, // 追加: similarityThreshold パラメータ
-    rerankSimilarityThreshold?: number // 追加: rerankSimilarityThreshold パラメータ
+    rerankSimilarityThreshold?: number, // 追加: rerankSimilarityThreshold パラメータ
+    enableQueryExpansion?: boolean, // 追加
+    enableHyDE?: boolean // 追加
   ): AsyncGenerator<{ type: 'content' | 'sources' | 'error' | 'historyId'; data: any }> {
     try {
       const response = await fetch('/api/chat/stream', {
@@ -53,7 +55,9 @@ export class ChatService {
           maxTokens, // maxTokens パラメータを渡す
           topK, // topK パラメータを渡す
           similarityThreshold, // similarityThreshold パラメータを渡す
-          rerankSimilarityThreshold // rerankSimilarityThreshold パラメータを渡す
+          rerankSimilarityThreshold, // rerankSimilarityThreshold パラメータを渡す
+          enableQueryExpansion, // enableQueryExpansion を渡す
+          enableHyDE // enableHyDE を渡す
         }),
       });
 

+ 6 - 1
web/types.ts

@@ -225,6 +225,10 @@ export interface AppSettings {
   enableFullTextSearch: boolean; // 全文検索を有効にするかどうか
   hybridVectorWeight: number; // ハイブリッド検索のベクトル重み
 
+  // Search Enhancement
+  enableQueryExpansion: boolean;
+  enableHyDE: boolean;
+
   // Language
   language?: string;
 
@@ -249,7 +253,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
   rerankSimilarityThreshold: 0.5, // デフォルトのリランク類似度しきい値
   enableFullTextSearch: false, // デフォルトで全文検索をオフにする
   hybridVectorWeight: 0.7, // ハイブリッド検索のベクトル重み
-
+  enableQueryExpansion: false,
+  enableHyDE: false,
   language: 'ja',
 };
 

+ 18 - 3
web/utils/translations.ts

@@ -110,7 +110,12 @@ export const translations = {
     noteCreatedFailed: "笔记创建失败",
     fullTextSearch: "全文检索",
     hybridVectorWeight: "混合检索向量权重",
-    hybridVectorWeightDesc: "设置向量检索与全文检索的权重比例 (0 为全文本,1 为全向量)",
+    hybridVectorWeightDesc: "向量平衡: 1.0 = 纯向量, 0.0 = 纯全文",
+    lblQueryExpansion: "查询扩展 (Multi-Query)",
+    lblHyDE: "HyDE (假设文档嵌入)",
+    lblQueryExpansionDesc: "生成多个查询变体以提高覆盖率",
+    lblHyDEDesc: "生成假设回答以改善语义搜索",
+    // Model Management
     apiKeyValidationFailed: "API Key 验证失败",
     keepOriginalKey: "留空保持原 API Key,输入新值则替换",
     leaveEmptyNoChange: "留空不修改",
@@ -712,7 +717,12 @@ export const translations = {
     filterLowResults: "Results below this value will be filtered",
     fullTextSearch: "Full Text Search",
     hybridVectorWeight: "Hybrid Vector Weight",
-    hybridVectorWeightDesc: "Set the weight ratio between vector and full-text search (0 is text only, 1 is vector only)",
+    hybridVectorWeightDesc: "Weight: 1.0 = Pure Vector, 0.0 = Pure Keyword",
+    lblQueryExpansion: "Query Expansion (Multi-Query)",
+    lblHyDE: "HyDE (Hypothetical Doc)",
+    lblQueryExpansionDesc: "Generate multiple query variations for better coverage",
+    lblHyDEDesc: "Generate a hypothetical answer to improve semantic search",
+    // Model Management
     apiKeyValidationFailed: "API Key validation failed",
     keepOriginalKey: "Leave empty to keep original API Key, input new value to replace",
     leaveEmptyNoChange: "Leave empty to keep unchanged",
@@ -1256,7 +1266,12 @@ export const translations = {
     noteCreatedFailed: "ノートの作成に失敗しました",
     fullTextSearch: "全文検索",
     hybridVectorWeight: "ハイブリッド検索ベクトル重み",
-    hybridVectorWeightDesc: "ベクトル検索と全文検索の重み比率を設定します (0は全文のみ、1はベクトルのみ)",
+    hybridVectorWeightDesc: "重み: 1.0 = ベクトルのみ, 0.0 = キーワードのみ",
+    lblQueryExpansion: "クエリ拡張 (Multi-Query)",
+    lblHyDE: "HyDE (仮想ドキュメント埋め込み)",
+    lblQueryExpansionDesc: "検索カバレッジ向上のために複数のクエリを生成",
+    lblHyDEDesc: "セマンティック検索改善のために仮想回答を生成",
+    // Model Management
     apiKeyValidationFailed: "API Key検証に失敗しました",
     keepOriginalKey: "空のままにすると元のAPI Keyを保持、新しい値を入力すると置換",
     leaveEmptyNoChange: "空のままで変更なし",