feat(search): index compliance snapshot mappings

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-08-07 14:44:46 +08:00
parent a7b25b072b
commit da5c3cba3b
2 changed files with 100 additions and 3 deletions

View file

@ -14,6 +14,7 @@ import com.iflytek.skillhub.domain.skill.SkillRepository;
import com.iflytek.skillhub.domain.skill.SkillStatus;
import com.iflytek.skillhub.domain.skill.SkillVersion;
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
import com.iflytek.skillhub.domain.skill.metadata.ComplianceMetadataService;
import com.iflytek.skillhub.search.SearchIndexService;
import com.iflytek.skillhub.search.SearchRebuildService;
import com.iflytek.skillhub.search.SearchTextTokenizer;
@ -127,9 +128,13 @@ public class PostgresSearchRebuildService implements SearchRebuildService {
Set<String> keywords = new TreeSet<>();
resolveLatestVersion(skill)
.map(this::extractParsedMetadata)
.map(metadata -> metadata.get("frontmatter"))
.map(this::asMap)
.ifPresent(frontmatter -> appendFrontmatter(frontmatter, keywords, searchParts));
.ifPresent(metadata -> {
appendFrontmatter(asMap(metadata.get("frontmatter")), keywords, searchParts);
appendComplianceSnapshot(
asMap(metadata.get(ComplianceMetadataService.SNAPSHOT_FIELD_NAME)),
keywords,
searchParts);
});
appendLabelKeywords(skill.getId(), keywords);
return new SearchIndexPayload(
@ -197,6 +202,29 @@ public class PostgresSearchRebuildService implements SearchRebuildService {
}
}
private void appendComplianceSnapshot(Map<String, Object> snapshot,
Set<String> keywords,
List<String> searchParts) {
Object rawItems = snapshot.get("items");
if (!(rawItems instanceof Collection<?> items)) {
return;
}
for (Object rawItem : items) {
Map<String, Object> item = asMap(rawItem);
appendComplianceSearchValue(item.get("standard"), keywords, searchParts);
appendComplianceSearchValue(item.get("version"), keywords, searchParts);
appendComplianceSearchValue(item.get("controlId"), keywords, searchParts);
appendComplianceSearchValue(item.get("title"), keywords, searchParts);
}
}
private void appendComplianceSearchValue(Object value, Set<String> keywords, List<String> searchParts) {
flattenToStrings(value).forEach(text -> {
addKeyword(keywords, text);
addPart(searchParts, text);
});
}
private List<String> flattenToStrings(Object value) {
if (value == null) {
return List.of();

View file

@ -158,6 +158,75 @@ class PostgresSearchRebuildServiceTest {
assertThat(document.searchText()).doesNotContain("智能体");
}
@Test
void rebuildBySkill_shouldAppendComplianceSnapshotMappingsIntoSearchDocument() {
SkillRepository skillRepository = mock(SkillRepository.class);
NamespaceRepository namespaceRepository = mock(NamespaceRepository.class);
SkillVersionRepository skillVersionRepository = mock(SkillVersionRepository.class);
SearchIndexService searchIndexService = mock(SearchIndexService.class);
Skill skill = new Skill(7L, "terminal-agent", "owner-1", SkillVisibility.PUBLIC);
skill.setDisplayName("Terminal Agent");
skill.setSummary("Runs shell workflows safely");
skill.setLatestVersionId(104L);
Namespace namespace = new Namespace("team-ai", "Team AI", "owner-1");
SkillVersion version = new SkillVersion(1L, "1.7.0", "owner-1");
version.setParsedMetadataJson("""
{
"frontmatter": {
"keywords": ["shell"]
},
"complianceSnapshot": {
"schemaVersion": "1",
"digest": "sha256:abc123",
"items": [
{
"standard": "mitre-attack",
"version": "15",
"controlId": "T1059",
"title": "Command and Scripting Interpreter",
"evidence": [
{
"type": "packaged-file",
"path": "docs/security.md",
"sha256": "sha256:def456"
}
]
}
]
}
}
""");
when(skillRepository.findById(1L)).thenReturn(Optional.of(skill));
when(namespaceRepository.findById(7L)).thenReturn(Optional.of(namespace));
when(skillVersionRepository.findById(104L)).thenReturn(Optional.of(version));
PostgresSearchRebuildService service = newService(
skillRepository,
namespaceRepository,
skillVersionRepository,
searchIndexService
);
service.rebuildBySkill(1L);
ArgumentCaptor<SkillSearchDocument> captor = ArgumentCaptor.forClass(SkillSearchDocument.class);
verify(searchIndexService).index(captor.capture());
SkillSearchDocument document = captor.getValue();
assertThat(document.keywords()).contains("shell");
assertThat(document.keywords()).contains("mitre-attack");
assertThat(document.keywords()).contains("T1059");
assertThat(document.keywords()).contains("Command and Scripting Interpreter");
assertThat(document.searchText()).contains("mitre-attack");
assertThat(document.searchText()).contains("T1059");
assertThat(document.searchText()).contains("Command and Scripting Interpreter");
assertThat(document.searchText()).doesNotContain("docs/security.md");
assertThat(document.searchText()).doesNotContain("sha256:def456");
}
@Test
void rebuildBySkill_shouldIgnoreNullFrontmatterValues() {
SkillRepository skillRepository = mock(SkillRepository.class);