From e426f12e22af84711c1fb6fe483f5564dce0a307 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Thu, 30 Jul 2026 17:14:51 +0800 Subject: [PATCH] fix: highlight full knowledge search terms --- .../components/KnowledgeManagementView.vue | 18 ++++++++----- .../apps/admin-web/src/types/api.ts | 1 + .../app/api/admin_knowledge_lifecycle.py | 26 +++++++++++++++++++ .../backend/tests/test_knowledge_admin.py | 22 ++++++++++++++++ 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/ai_knowledge_base_v2/apps/admin-web/src/components/KnowledgeManagementView.vue b/ai_knowledge_base_v2/apps/admin-web/src/components/KnowledgeManagementView.vue index 896c297..6edc8bb 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/components/KnowledgeManagementView.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/components/KnowledgeManagementView.vue @@ -388,20 +388,24 @@ function matchedLabel(values: string[]) { return labels.join("、"); } -function highlightSnippet(text: string) { - const keyword = contentSearch.keyword.trim(); - if (!keyword) return [{ text, match: false }]; +function highlightSnippet(text: string, terms: string[] = []) { + const keywords = Array.from(new Set([...terms, contentSearch.keyword.trim()].filter(Boolean))) + .sort((left, right) => right.length - left.length); + if (!keywords.length) return [{ text, match: false }]; const parts: Array<{ text: string; match: boolean }> = []; const source = text || ""; const lowerSource = source.toLowerCase(); - const lowerKeyword = keyword.toLowerCase(); let cursor = 0; while (cursor < source.length) { - const index = lowerSource.indexOf(lowerKeyword, cursor); - if (index < 0) { + const next = keywords + .map((keyword) => ({ keyword, index: lowerSource.indexOf(keyword.toLowerCase(), cursor) })) + .filter((item) => item.index >= 0) + .sort((left, right) => left.index - right.index || right.keyword.length - left.keyword.length)[0]; + if (!next) { parts.push({ text: source.slice(cursor), match: false }); break; } + const { index, keyword } = next; if (index > cursor) parts.push({ text: source.slice(cursor, index), match: false }); parts.push({ text: source.slice(index, index + keyword.length), match: true }); cursor = index + keyword.length; @@ -493,7 +497,7 @@ async function confirmAction( {{ item.knowledgeName }} · V{{ item.versionNo }} · {{ item.sectionKey }} · 命中{{ matchedLabel(item.matchedIn) }}
- + {{ part.text }}{{ part.text }}
diff --git a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts index 5c8bc88..16c4c65 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts +++ b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts @@ -227,6 +227,7 @@ export interface KnowledgeContentSearchItem { sectionKey: string; title: string; snippet: string; + highlightTerms?: string[]; matchedIn: Array<"title" | "content">; hasChunks: boolean; isCurrentVersion: boolean; diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge_lifecycle.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge_lifecycle.py index f8f86d3..4e0a1e8 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge_lifecycle.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge_lifecycle.py @@ -657,6 +657,9 @@ def _merge_content_search_rows(rows, keyword: str, chunk_section_ids: set[int]) for matched in _matched_in(str(row["title"] or ""), str(row["content"] or ""), keyword): if matched not in current["matchedIn"]: current["matchedIn"].append(matched) + for term in _highlight_terms(str(row["title"] or ""), str(row["content"] or ""), keyword): + if term not in current["highlightTerms"]: + current["highlightTerms"].append(term) current["itemType"] = _primary_item_type(current["itemTypes"]) return list(grouped.values()) @@ -688,6 +691,7 @@ def _content_search_item(row, keyword: str, has_chunks: bool) -> dict: "title": title, "snippet": _snippet(f"{title}\n{content}", keyword), "matchedIn": _matched_in(title, content, keyword), + "highlightTerms": _highlight_terms(title, content, keyword), "itemTypes": [row["item_type"]], "hasChunks": has_chunks, "isCurrentVersion": True, @@ -712,6 +716,28 @@ def _matched_in(title: str, content: str, keyword: str) -> list[str]: return result or ["content"] +def _highlight_terms(title: str, content: str, keyword: str) -> list[str]: + keyword = keyword.strip() + if not keyword: + return [] + text = f"{title}\n{content}".lower() + terms = [keyword] + if keyword.lower() not in text: + terms.extend(_keyword_parts(keyword)) + result: list[str] = [] + for term in sorted({item.strip() for item in terms if item.strip()}, key=len, reverse=True): + if term.lower() in text and term not in result: + result.append(term) + return result or [keyword] + + +def _keyword_parts(keyword: str) -> list[str]: + parts = [part for part in keyword.replace(",", " ").replace(",", " ").split() if part] + if parts: + return parts + return list(keyword) if len(keyword) <= 8 else [] + + def _snippet(text: str, keyword: str, radius: int = 90) -> str: compact = " ".join(text.split()) if not compact: diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_admin.py b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_admin.py index 7e8bb4b..864fc6a 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_admin.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_admin.py @@ -359,3 +359,25 @@ def test_content_search_can_exclude_closed_knowledge(): assert included["data"]["total"] == 1 assert included["data"]["items"][0]["canAgentUse"] is False assert excluded["data"]["total"] == 0 + + +def test_content_search_highlight_terms_keep_full_keyword(): + with _database() as db: + item = _knowledge(db) + db.add(KnowledgeSection( + knowledge_id=item.id, + version_id=item.current_version_id, + section_key="S0002", + title="能力心密钥", + content="能力心钥用于说明能力心的核心方向。", + source_start=0, + source_end=10, + sort_order=1, + content_hash="highlight-section", + )) + db.commit() + + response = content_search("心钥", includeClosed=True, page=1, pageSize=10, db=db, current_admin=_admin()) + + assert response["data"]["total"] == 1 + assert response["data"]["items"][0]["highlightTerms"][0] == "心钥"