Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,31 @@

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.util.HtmlUtils;

import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Component
public class FewShotSearchTextBuilder {
static final int MAX_IDENTIFIER_LENGTH = 100;
static final int MAX_SHORT_FIELD_LENGTH = 200;
static final int MAX_REQUIREMENTS_LENGTH = 1_200;
static final int MAX_QUESTION_LENGTH = 600;
static final int MAX_ANSWER_LENGTH = 2_000;
static final int MAX_TAGS_LENGTH = 500;

private static final Pattern SCRIPT_STYLE_PATTERN = Pattern.compile(
"(?is)<(script|style)\\b[^>]*>.*?</\\1\\s*>"
);
private static final Pattern BLOCK_HTML_TAG_PATTERN = Pattern.compile(
"(?is)</?(?:br|p|div|li|ul|ol|h[1-6]|tr|td|th|section|article)\\b[^>]*>"
);
private static final Pattern HTML_COMMENT_PATTERN = Pattern.compile("(?s)<!--.*?-->");
private static final Pattern HTML_TAG_PATTERN = Pattern.compile("(?is)</?[a-z][a-z0-9:-]*\\b[^>]*>");
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\p{Z}\\s]+");

public String buildQueryText(FewShotSearchQuery query) {
return """
[JOB_CATEGORY]
Expand All @@ -27,12 +47,12 @@ public String buildQueryText(FewShotSearchQuery query) {
[ANSWER]
%s
""".formatted(
value(query.jobCategory()),
value(query.jobTitle()),
lines(query.mainTasks()),
lines(query.qualifications()),
value(query.question()),
value(query.answer())
value(query.jobCategory(), MAX_SHORT_FIELD_LENGTH),
value(query.jobTitle(), MAX_SHORT_FIELD_LENGTH),
lines(query.mainTasks(), MAX_REQUIREMENTS_LENGTH),
lines(query.qualifications(), MAX_REQUIREMENTS_LENGTH),
value(query.question(), MAX_QUESTION_LENGTH),
value(query.answer(), MAX_ANSWER_LENGTH)
);
}

Expand Down Expand Up @@ -63,31 +83,63 @@ public String buildCandidateDocument(FewShotCase fewShotCase) {
[TAGS]
%s
""".formatted(
value(fewShotCase.id()),
value(fewShotCase.id(), MAX_IDENTIFIER_LENGTH),
fewShotCase.source(),
value(fewShotCase.jobCategory()),
value(fewShotCase.jobTitle()),
lines(fewShotCase.mainTasks()),
lines(fewShotCase.qualifications()),
value(fewShotCase.question()),
value(fewShotCase.sanitizedAnswer()),
String.join(", ", fewShotCase.tags())
value(fewShotCase.jobCategory(), MAX_SHORT_FIELD_LENGTH),
value(fewShotCase.jobTitle(), MAX_SHORT_FIELD_LENGTH),
lines(fewShotCase.mainTasks(), MAX_REQUIREMENTS_LENGTH),
lines(fewShotCase.qualifications(), MAX_REQUIREMENTS_LENGTH),
value(fewShotCase.question(), MAX_QUESTION_LENGTH),
value(fewShotCase.sanitizedAnswer(), MAX_ANSWER_LENGTH),
values(fewShotCase.tags(), MAX_TAGS_LENGTH)
);
}

private static String lines(List<String> values) {
private static String lines(List<String> values, int maxLength) {
if (values == null || values.isEmpty()) {
return "";
}
return values.stream()
String joined = values.stream()
.filter(StringUtils::hasText)
.map(FewShotSearchTextBuilder::normalize)
.filter(StringUtils::hasText)
.map(String::trim)
.map(line -> line.startsWith("-") ? line : "- " + line)
.reduce((left, right) -> left + "\n" + right)
.orElse("");
.collect(Collectors.joining("\n"));
return truncate(joined, maxLength);
}

private static String values(List<String> values, int maxLength) {
if (values == null || values.isEmpty()) {
return "";
}
String joined = values.stream()
.filter(StringUtils::hasText)
.map(FewShotSearchTextBuilder::normalize)
.filter(StringUtils::hasText)
.collect(Collectors.joining(", "));
return truncate(joined, maxLength);
}

private static String value(String value, int maxLength) {
return truncate(normalize(value), maxLength);
}

private static String normalize(String value) {
if (!StringUtils.hasText(value)) {
return "";
}
String decoded = HtmlUtils.htmlUnescape(value);
String withoutExecutableContent = SCRIPT_STYLE_PATTERN.matcher(decoded).replaceAll(" ");
String withoutComments = HTML_COMMENT_PATTERN.matcher(withoutExecutableContent).replaceAll(" ");
String withBlockSeparators = BLOCK_HTML_TAG_PATTERN.matcher(withoutComments).replaceAll(" ");
String withoutTags = HTML_TAG_PATTERN.matcher(withBlockSeparators).replaceAll("");
return WHITESPACE_PATTERN.matcher(withoutTags).replaceAll(" ").trim();
}

private static String value(String value) {
return StringUtils.hasText(value) ? value.trim() : "";
private static String truncate(String value, int maxLength) {
if (value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength).trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,50 @@ void buildsCandidateDocument() {
.contains("Spring Boot 경험")
.doesNotContain("embedding");
}

@Test
@DisplayName("HTML을 제거하고 반복 공백을 정규화한 뒤 검색 텍스트를 구성한다")
void normalizesHtmlAndWhitespace() {
String text = builder.buildQueryText(new FewShotSearchQuery(
"EV-01",
" 백엔드 개발 ",
"<b>Backend</b>&nbsp;Engineer",
List.of("<p>Spring Boot</p> API 개발"),
List.of("Java\n\t개발 경험"),
"지원 <strong>직무</strong> 경험",
"JPA로 API를 <em>개발</em>했습니다.<script>ignore()</script>"
));

assertThat(text)
.contains("[JOB_CATEGORY]\n백엔드 개발")
.contains("[JOB_TITLE]\nBackend Engineer")
.contains("[MAIN_TASKS]\n- Spring Boot API 개발")
.contains("[QUALIFICATIONS]\n- Java 개발 경험")
.contains("[QUESTION]\n지원 직무 경험")
.contains("[ANSWER]\nJPA로 API를 개발했습니다.")
.doesNotContain("<b>", "<script>", "ignore()", "&nbsp;");
}

@Test
@DisplayName("HTML 제거와 공백 정규화 후 답변 섹션 길이를 제한한다")
void truncatesAnswerAfterNormalization() {
String answer = "<b>가</b> ".repeat(FewShotSearchTextBuilder.MAX_ANSWER_LENGTH + 100);

String text = builder.buildQueryText(new FewShotSearchQuery(
"EV-01",
"백엔드 개발",
"Backend Engineer",
List.of(),
List.of(),
"직무 경험",
answer
));
String answerSection = text.substring(text.indexOf("[ANSWER]\n") + "[ANSWER]\n".length()).trim();

assertThat(answerSection)
.doesNotContain("<b>", " ");
assertThat(answerSection.length())
.isPositive()
.isLessThanOrEqualTo(FewShotSearchTextBuilder.MAX_ANSWER_LENGTH);
}
}
Loading