diff --git a/client/faq.go b/client/faq.go index a0fc1fbd..2d8301f7 100644 --- a/client/faq.go +++ b/client/faq.go @@ -31,6 +31,9 @@ type FAQEntry struct { Score float64 `json:"score,omitempty"` MatchType string `json:"match_type,omitempty"` ChunkType string `json:"chunk_type"` + // MatchedQuestion is the actual question text that was matched in FAQ search + // Could be the standard question or one of the similar questions + MatchedQuestion string `json:"matched_question,omitempty"` } // FAQEntryPayload is used to create or update a FAQ entry. diff --git a/client/knowledgebase.go b/client/knowledgebase.go index 6412c4b4..10660a7f 100644 --- a/client/knowledgebase.go +++ b/client/knowledgebase.go @@ -127,6 +127,9 @@ type SearchResult struct { Metadata map[string]string `json:"metadata"` KnowledgeFilename string `json:"knowledge_filename"` KnowledgeSource string `json:"knowledge_source"` + // MatchedContent is the actual content that was matched in vector search + // For FAQ: this is the matched question text (standard or similar question) + MatchedContent string `json:"matched_content,omitempty"` } // HybridSearchResponse hybrid search response diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 332cc58e..6a85b6f3 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1160,6 +1160,7 @@ export default { noResults: 'No matching FAQ entries found', score: 'Similarity', matchType: 'Match Type', + matchedQuestion: 'Matched', matchTypeEmbedding: 'Vector Match', matchTypeKeywords: 'Keyword Match', similarityThresholdLabel: 'Similarity Threshold', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index f4008c39..0e2f1e69 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -1366,6 +1366,7 @@ export default { noResults: "일치하는 FAQ 항목을 찾을 수 없습니다", score: "유사도", matchType: "매칭 유형", + matchedQuestion: "일치된 질문", matchTypeEmbedding: "벡터 매칭", matchTypeKeywords: "키워드 매칭", similarityThresholdLabel: "유사도 임계값", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 286b6824..343d8377 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -1123,6 +1123,7 @@ export default { noResults: 'Совпадающих записей FAQ не найдено', score: 'Сходство', matchType: 'Тип совпадения', + matchedQuestion: 'Совпавший вопрос', matchTypeEmbedding: 'Векторное совпадение', matchTypeKeywords: 'Совпадение ключевых слов', similarityThresholdLabel: 'Порог сходства', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index cfcc5eee..b34997ac 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1561,6 +1561,7 @@ export default { noResults: "未找到匹配的 FAQ 条目", score: "相似度", matchType: "匹配类型", + matchedQuestion: "命中问题", matchTypeEmbedding: "向量匹配", matchTypeKeywords: "关键词匹配", similarityThresholdLabel: "相似度阈值", diff --git a/frontend/src/views/knowledge/components/FAQEntryManager.vue b/frontend/src/views/knowledge/components/FAQEntryManager.vue index 10c10f69..5bfbd02b 100644 --- a/frontend/src/views/knowledge/components/FAQEntryManager.vue +++ b/frontend/src/views/knowledge/components/FAQEntryManager.vue @@ -1091,13 +1091,19 @@ >
-
- {{ index + 1 }}. - {{ result.standard_question }} +
+
+ {{ index + 1 }}. + {{ result.standard_question }} +
+
+ {{ $t('knowledgeEditor.faq.matchedQuestion') }}: + {{ result.matched_question }} +
- {{ $t('knowledgeEditor.faq.score') }}: {{ (result.score || 0).toFixed(3) }} + {{ (result.score || 0).toFixed(3) }}
entries.value.map(e => ({ .result-question-wrapper { display: flex; - align-items: center; + align-items: flex-start; gap: 10px; width: 100%; } -.result-question { +.result-main { flex: 1; min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.result-question { font-family: "PingFang SC"; font-size: 14px; font-weight: 600; @@ -5816,6 +5829,29 @@ watch(() => entries.value.map(e => ({ } } +.matched-question { + display: flex; + align-items: flex-start; + gap: 4px; + padding-left: 20px; + font-size: 12px; + line-height: 1.5; + + .matched-label { + flex-shrink: 0; + color: #E37318; + font-weight: 500; + } + + .matched-text { + color: #92400E; + background: linear-gradient(90deg, rgba(251, 191, 36, 0.15) 0%, rgba(251, 191, 36, 0.05) 100%); + padding: 1px 6px; + border-radius: 4px; + word-break: break-word; + } +} + .result-meta { display: flex; gap: 8px; diff --git a/internal/application/service/knowledge.go b/internal/application/service/knowledge.go index 139cd62a..e72bc8be 100644 --- a/internal/application/service/knowledge.go +++ b/internal/application/service/knowledge.go @@ -4937,16 +4937,18 @@ func (s *knowledgeService) SearchFAQEntries(ctx context.Context, return []*types.FAQEntry{}, nil } - // Extract chunk IDs and build score/match type maps + // Extract chunk IDs and build score/match type/matched content maps chunkIDs := make([]string, 0, len(searchResults)) chunkScores := make(map[string]float64) chunkMatchTypes := make(map[string]types.MatchType) + chunkMatchedContents := make(map[string]string) for _, result := range searchResults { // SearchResult.ID is the chunk ID chunkID := result.ID chunkIDs = append(chunkIDs, chunkID) chunkScores[chunkID] = result.Score chunkMatchTypes[chunkID] = result.MatchType + chunkMatchedContents[chunkID] = result.MatchedContent } // Batch fetch chunks @@ -5003,6 +5005,11 @@ func (s *knowledgeService) SearchFAQEntries(ctx context.Context, entry.MatchType = matchType } + // Set MatchedQuestion from search result's matched content + if matchedContent, ok := chunkMatchedContents[chunk.ID]; ok && matchedContent != "" { + entry.MatchedQuestion = matchedContent + } + entries = append(entries, entry) } diff --git a/internal/application/service/knowledgebase.go b/internal/application/service/knowledgebase.go index 6dfea6d0..92d2d599 100644 --- a/internal/application/service/knowledgebase.go +++ b/internal/application/service/knowledgebase.go @@ -998,6 +998,7 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, var chunkIDs []string chunkScores := make(map[string]float64) chunkMatchTypes := make(map[string]types.MatchType) + chunkMatchedContents := make(map[string]string) processedKnowledgeIDs := make(map[string]bool) // Collect all knowledge and chunk IDs @@ -1010,6 +1011,7 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, chunkIDs = append(chunkIDs, chunk.ChunkID) chunkScores[chunk.ChunkID] = chunk.Score chunkMatchTypes[chunk.ChunkID] = chunk.MatchType + chunkMatchedContents[chunk.ChunkID] = chunk.Content } // Batch fetch knowledge data @@ -1110,7 +1112,8 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, score := chunkScores[chunk.ID] if knowledge, ok := knowledgeMap[chunk.KnowledgeID]; ok { matchType := chunkMatchTypes[chunk.ID] - searchResults = append(searchResults, s.buildSearchResult(chunk, knowledge, score, matchType)) + matchedContent := chunkMatchedContents[chunk.ID] + searchResults = append(searchResults, s.buildSearchResult(chunk, knowledge, score, matchType, matchedContent)) addedChunkIDs[chunk.ID] = true } else { logger.Warnf(ctx, "Knowledge not found for chunk: %s, knowledge_id: %s", chunk.ID, chunk.KnowledgeID) @@ -1136,7 +1139,8 @@ func (s *knowledgeBaseService) processSearchResults(ctx context.Context, logger.Warnf(ctx, "Unkonwn match type for chunk: %s", chunkID) continue } - searchResults = append(searchResults, s.buildSearchResult(chunk, knowledge, score, matchType)) + matchedContent := chunkMatchedContents[chunkID] + searchResults = append(searchResults, s.buildSearchResult(chunk, knowledge, score, matchType, matchedContent)) } } logger.Infof(ctx, "Search results processed, total: %d", len(searchResults)) @@ -1166,6 +1170,7 @@ func (s *knowledgeBaseService) buildSearchResult(chunk *types.Chunk, knowledge *types.Knowledge, score float64, matchType types.MatchType, + matchedContent string, ) *types.SearchResult { return &types.SearchResult{ ID: chunk.ID, @@ -1185,6 +1190,7 @@ func (s *knowledgeBaseService) buildSearchResult(chunk *types.Chunk, KnowledgeFilename: knowledge.FileName, KnowledgeSource: knowledge.Source, ChunkMetadata: chunk.Metadata, + MatchedContent: matchedContent, } } diff --git a/internal/types/faq.go b/internal/types/faq.go index 6c6e3094..1a9dc452 100644 --- a/internal/types/faq.go +++ b/internal/types/faq.go @@ -194,6 +194,9 @@ type FAQEntry struct { Score float64 `json:"score,omitempty"` MatchType MatchType `json:"match_type,omitempty"` ChunkType ChunkType `json:"chunk_type"` + // MatchedQuestion is the actual question text that was matched in FAQ search + // Could be the standard question or one of the similar questions + MatchedQuestion string `json:"matched_question,omitempty"` } // FAQEntryPayload 用于创建/更新 FAQ 条目的 payload diff --git a/internal/types/search.go b/internal/types/search.go index 0158c785..b9ba42db 100644 --- a/internal/types/search.go +++ b/internal/types/search.go @@ -87,6 +87,10 @@ type SearchResult struct { // ChunkMetadata stores chunk-level metadata (e.g., generated questions) ChunkMetadata JSON `json:"chunk_metadata,omitempty"` + + // MatchedContent is the actual content that was matched in vector search + // For FAQ: this is the matched question text (standard or similar question) + MatchedContent string `json:"matched_content,omitempty"` } // SearchParams represents the search parameters