From a7f8b0afcf052149eb28a42ffb7700474e180cfe Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 19 Mar 2026 18:24:59 +0800 Subject: [PATCH] fix(search): expand keyword storage for rebuild safety --- ...__expand_skill_search_keywords_storage.sql | 2 + .../infra/jpa/SkillSearchDocumentEntity.java | 6 +- .../PostgresFullTextIndexService.java | 71 +++++++++++++------ .../PostgresFullTextIndexServiceTest.java | 55 ++++++++++++++ 4 files changed, 110 insertions(+), 24 deletions(-) create mode 100644 server/skillhub-app/src/main/resources/db/migration/V30__expand_skill_search_keywords_storage.sql create mode 100644 server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexServiceTest.java diff --git a/server/skillhub-app/src/main/resources/db/migration/V30__expand_skill_search_keywords_storage.sql b/server/skillhub-app/src/main/resources/db/migration/V30__expand_skill_search_keywords_storage.sql new file mode 100644 index 00000000..566689f8 --- /dev/null +++ b/server/skillhub-app/src/main/resources/db/migration/V30__expand_skill_search_keywords_storage.sql @@ -0,0 +1,2 @@ +ALTER TABLE skill_search_document + ALTER COLUMN keywords TYPE TEXT; diff --git a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillSearchDocumentEntity.java b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillSearchDocumentEntity.java index 85f58db8..932e0532 100644 --- a/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillSearchDocumentEntity.java +++ b/server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/SkillSearchDocumentEntity.java @@ -20,7 +20,7 @@ public class SkillSearchDocumentEntity { @Column(name = "namespace_slug", nullable = false, length = 64) private String namespaceSlug; - @Column(name = "owner_id", nullable = false) + @Column(name = "owner_id", nullable = false, length = 128) private String ownerId; @Column(length = 256) @@ -38,10 +38,10 @@ public class SkillSearchDocumentEntity { @Column(name = "semantic_vector", columnDefinition = "TEXT") private String semanticVector; - @Column(nullable = false, length = 20) + @Column(nullable = false, length = 32) private String visibility; - @Column(nullable = false, length = 20) + @Column(nullable = false, length = 32) private String status; @Column(name = "updated_at", nullable = false) diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java index 6025c7a2..23a8f742 100644 --- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java +++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexService.java @@ -16,6 +16,11 @@ import java.util.Optional; */ @Service public class PostgresFullTextIndexService implements SearchIndexService { + private static final int NAMESPACE_SLUG_MAX_LENGTH = 64; + private static final int OWNER_ID_MAX_LENGTH = 128; + private static final int TITLE_MAX_LENGTH = 256; + private static final int VISIBILITY_MAX_LENGTH = 32; + private static final int STATUS_MAX_LENGTH = 32; private final SkillSearchDocumentJpaRepository repository; private final SearchEmbeddingService searchEmbeddingService; @@ -29,34 +34,35 @@ public class PostgresFullTextIndexService implements SearchIndexService { @Override @Transactional public void index(SkillSearchDocument document) { + SkillSearchDocument normalizedDocument = normalize(document); Optional existing = repository.findBySkillId(document.skillId()); if (existing.isPresent()) { SkillSearchDocumentEntity entity = existing.get(); - entity.setNamespaceId(document.namespaceId()); - entity.setNamespaceSlug(document.namespaceSlug()); - entity.setOwnerId(document.ownerId()); - entity.setTitle(document.title()); - entity.setSummary(document.summary()); - entity.setKeywords(document.keywords()); - entity.setSearchText(document.searchText()); - entity.setSemanticVector(buildSemanticVector(document)); - entity.setVisibility(document.visibility()); - entity.setStatus(document.status()); + entity.setNamespaceId(normalizedDocument.namespaceId()); + entity.setNamespaceSlug(normalizedDocument.namespaceSlug()); + entity.setOwnerId(normalizedDocument.ownerId()); + entity.setTitle(normalizedDocument.title()); + entity.setSummary(normalizedDocument.summary()); + entity.setKeywords(normalizedDocument.keywords()); + entity.setSearchText(normalizedDocument.searchText()); + entity.setSemanticVector(buildSemanticVector(normalizedDocument)); + entity.setVisibility(normalizedDocument.visibility()); + entity.setStatus(normalizedDocument.status()); repository.save(entity); } else { SkillSearchDocumentEntity entity = new SkillSearchDocumentEntity( - document.skillId(), - document.namespaceId(), - document.namespaceSlug(), - document.ownerId(), - document.title(), - document.summary(), - document.keywords(), - document.searchText(), - buildSemanticVector(document), - document.visibility(), - document.status() + normalizedDocument.skillId(), + normalizedDocument.namespaceId(), + normalizedDocument.namespaceSlug(), + normalizedDocument.ownerId(), + normalizedDocument.title(), + normalizedDocument.summary(), + normalizedDocument.keywords(), + normalizedDocument.searchText(), + buildSemanticVector(normalizedDocument), + normalizedDocument.visibility(), + normalizedDocument.status() ); repository.save(entity); } @@ -88,4 +94,27 @@ public class PostgresFullTextIndexService implements SearchIndexService { private String safe(String value) { return value == null ? "" : value; } + + private SkillSearchDocument normalize(SkillSearchDocument document) { + return new SkillSearchDocument( + document.skillId(), + document.namespaceId(), + truncate(document.namespaceSlug(), NAMESPACE_SLUG_MAX_LENGTH), + truncate(document.ownerId(), OWNER_ID_MAX_LENGTH), + truncate(document.title(), TITLE_MAX_LENGTH), + document.summary(), + document.keywords(), + document.searchText(), + document.semanticVector(), + truncate(document.visibility(), VISIBILITY_MAX_LENGTH), + truncate(document.status(), STATUS_MAX_LENGTH) + ); + } + + private String truncate(String value, int maxLength) { + if (value == null || value.length() <= maxLength) { + return value; + } + return value.substring(0, maxLength); + } } diff --git a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexServiceTest.java b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexServiceTest.java new file mode 100644 index 00000000..027c4fac --- /dev/null +++ b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextIndexServiceTest.java @@ -0,0 +1,55 @@ +package com.iflytek.skillhub.search.postgres; + +import com.iflytek.skillhub.infra.jpa.SkillSearchDocumentEntity; +import com.iflytek.skillhub.infra.jpa.SkillSearchDocumentJpaRepository; +import com.iflytek.skillhub.search.HashingSearchEmbeddingService; +import com.iflytek.skillhub.search.SkillSearchDocument; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PostgresFullTextIndexServiceTest { + + @Test + void indexShouldTruncateOnlyColumnsThatStillHaveDatabaseLimits() { + SkillSearchDocumentJpaRepository repository = mock(SkillSearchDocumentJpaRepository.class); + when(repository.findBySkillId(1L)).thenReturn(Optional.empty()); + + PostgresFullTextIndexService service = new PostgresFullTextIndexService( + repository, + new HashingSearchEmbeddingService() + ); + + SkillSearchDocument document = new SkillSearchDocument( + 1L, + 2L, + "n".repeat(80), + "o".repeat(140), + "t".repeat(300), + "summary", + "k".repeat(700), + "search text", + null, + "PUBLIC", + "ACTIVE" + ); + + service.index(document); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SkillSearchDocumentEntity.class); + verify(repository).save(captor.capture()); + + SkillSearchDocumentEntity entity = captor.getValue(); + assertThat(entity.getNamespaceSlug()).hasSize(64); + assertThat(entity.getOwnerId()).hasSize(128); + assertThat(entity.getTitle()).hasSize(256); + assertThat(entity.getKeywords()).hasSize(700); + assertThat(entity.getSearchText()).isEqualTo("search text"); + } +}