mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-08-31 00:50:02 +08:00
feat: add matched question field to FAQ search results
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1366,6 +1366,7 @@ export default {
|
||||
noResults: "일치하는 FAQ 항목을 찾을 수 없습니다",
|
||||
score: "유사도",
|
||||
matchType: "매칭 유형",
|
||||
matchedQuestion: "일치된 질문",
|
||||
matchTypeEmbedding: "벡터 매칭",
|
||||
matchTypeKeywords: "키워드 매칭",
|
||||
similarityThresholdLabel: "유사도 임계값",
|
||||
|
||||
@@ -1123,6 +1123,7 @@ export default {
|
||||
noResults: 'Совпадающих записей FAQ не найдено',
|
||||
score: 'Сходство',
|
||||
matchType: 'Тип совпадения',
|
||||
matchedQuestion: 'Совпавший вопрос',
|
||||
matchTypeEmbedding: 'Векторное совпадение',
|
||||
matchTypeKeywords: 'Совпадение ключевых слов',
|
||||
similarityThresholdLabel: 'Порог сходства',
|
||||
|
||||
@@ -1561,6 +1561,7 @@ export default {
|
||||
noResults: "未找到匹配的 FAQ 条目",
|
||||
score: "相似度",
|
||||
matchType: "匹配类型",
|
||||
matchedQuestion: "命中问题",
|
||||
matchTypeEmbedding: "向量匹配",
|
||||
matchTypeKeywords: "关键词匹配",
|
||||
similarityThresholdLabel: "相似度阈值",
|
||||
|
||||
@@ -1091,13 +1091,19 @@
|
||||
>
|
||||
<div class="result-header" @click="toggleResult(result)">
|
||||
<div class="result-question-wrapper">
|
||||
<div class="result-question">
|
||||
<span class="result-index">{{ index + 1 }}.</span>
|
||||
{{ result.standard_question }}
|
||||
<div class="result-main">
|
||||
<div class="result-question">
|
||||
<span class="result-index">{{ index + 1 }}.</span>
|
||||
{{ result.standard_question }}
|
||||
</div>
|
||||
<div v-if="result.matched_question && result.matched_question !== result.standard_question" class="matched-question">
|
||||
<span class="matched-label">{{ $t('knowledgeEditor.faq.matchedQuestion') }}:</span>
|
||||
<span class="matched-text">{{ result.matched_question }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-meta">
|
||||
<t-tag size="small" variant="light-outline" class="score-tag">
|
||||
{{ $t('knowledgeEditor.faq.score') }}: {{ (result.score || 0).toFixed(3) }}
|
||||
{{ (result.score || 0).toFixed(3) }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<t-icon
|
||||
@@ -1195,6 +1201,7 @@ interface FAQEntry {
|
||||
showMore?: boolean
|
||||
score?: number
|
||||
match_type?: string
|
||||
matched_question?: string
|
||||
expanded?: boolean
|
||||
similarCollapsed?: boolean
|
||||
negativeCollapsed?: boolean
|
||||
@@ -5791,14 +5798,20 @@ watch(() => 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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user