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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.apache.httpcomponents.client5:httpclient5'
implementation 'org.apache.poi:poi-ooxml:5.4.1'
implementation 'com.pgvector:pgvector:0.1.6'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.jobdri.jobdri_api.domain.analysis.service.ai.FewShotPromptProvider;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService;
import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting;
import com.jobdri.jobdri_api.global.cohere.CohereProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -40,10 +41,10 @@ public class AnalysisInputFingerprintProvider {
public AnalysisInputFingerprintProvider(
ObjectMapper objectMapper,
FewShotPromptProvider fewShotPromptProvider,
CohereProperties cohereProperties,
@Value("${openai.model.cover-letter-analysis:gpt-4o-mini}") String analysisModel,
@Value("${analysis.two-pass.enabled:false}") boolean twoPassEnabled,
@Value("${analysis.mode:}") String analysisMode,
@Value("${app.corpus.embedding.model:embed-v4.0}") String embeddingModel,
@Value("${app.analysis.retrieval.jd-limit:3}") int jdLimit,
@Value("${app.analysis.retrieval.question-limit:5}") int questionLimit
) {
Expand All @@ -52,7 +53,7 @@ public AnalysisInputFingerprintProvider(
this.analysisModel = analysisModel;
this.twoPassEnabled = twoPassEnabled;
this.analysisMode = analysisMode;
this.embeddingModel = embeddingModel;
this.embeddingModel = cohereProperties.embedding().model();
this.jdLimit = jdLimit;
this.questionLimit = questionLimit;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,115 +1,22 @@
package com.jobdri.jobdri_api.domain.corpus.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jobdri.jobdri_api.global.cohere.CohereEmbeddingClient;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.util.List;

@Component
@RequiredArgsConstructor
public class CohereCorpusEmbeddingClient implements CorpusEmbeddingClient {

private final RestClient.Builder restClientBuilder;
private final ObjectMapper objectMapper;

@Value("${cohere.api.key:}")
private String cohereApiKey;

@Value("${app.corpus.embedding.model:embed-v4.0}")
private String embeddingModel;

@Value("${app.corpus.embedding.output-dimension:1024}")
private int outputDimension;
private final CohereEmbeddingClient cohereEmbeddingClient;

@Override
public List<float[]> embed(List<String> texts, InputType inputType) {
if (!StringUtils.hasText(cohereApiKey)) {
throw new IllegalStateException("Cohere API 키가 설정되지 않았습니다.");
}
if (texts == null || texts.isEmpty()) {
return List.of();
}

SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(Duration.ofSeconds(5));
requestFactory.setReadTimeout(Duration.ofSeconds(10));

RestClient client = restClientBuilder
.baseUrl("https://api.cohere.com")
.requestFactory(requestFactory)
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + cohereApiKey)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();

String responseBody = client.post()
.uri("/v2/embed")
.body(new EmbedRequest(
texts,
embeddingModel,
inputType.value(),
outputDimension,
List.of("float")
))
.retrieve()
.body(String.class);

return parseEmbeddings(responseBody);
}

private float[] toFloatArray(List<Double> values) {
float[] array = new float[values.size()];
for (int i = 0; i < values.size(); i++) {
array[i] = values.get(i).floatValue();
if (inputType == InputType.SEARCH_QUERY) {
return List.of(cohereEmbeddingClient.embedQuery(texts == null || texts.isEmpty() ? null : texts.getFirst()));
}
return array;
return cohereEmbeddingClient.embedDocuments(texts);
}

private List<float[]> parseEmbeddings(String responseBody) {
if (!StringUtils.hasText(responseBody)) {
throw new IllegalStateException("Cohere 임베딩 응답이 비어 있습니다.");
}

try {
JsonNode root = objectMapper.readTree(responseBody);
JsonNode floatEmbeddings = root.path("embeddings").path("float");
if (!floatEmbeddings.isArray()) {
throw new IllegalStateException("Cohere 임베딩 응답 형식이 예상과 다릅니다.");
}

List<float[]> result = new java.util.ArrayList<>();
for (JsonNode embeddingNode : floatEmbeddings) {
if (!embeddingNode.isArray()) {
throw new IllegalStateException("Cohere 임베딩 벡터 형식이 예상과 다릅니다.");
}

float[] vector = new float[embeddingNode.size()];
for (int i = 0; i < embeddingNode.size(); i++) {
vector[i] = embeddingNode.get(i).floatValue();
}
result.add(vector);
}
return result;
} catch (Exception e) {
throw new IllegalStateException("Cohere 임베딩 응답 파싱에 실패했습니다.", e);
}
}

private record EmbedRequest(
List<String> texts,
String model,
String input_type,
Integer output_dimension,
List<String> embedding_types
) {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.jobdri.jobdri_api.domain.corpus.entity.MockQuestionCorpus;
import com.jobdri.jobdri_api.domain.corpus.repository.MockJobPostingCorpusRepository;
import com.jobdri.jobdri_api.domain.corpus.repository.MockQuestionCorpusRepository;
import com.jobdri.jobdri_api.global.cohere.CohereProperties;
import com.pgvector.PGvector;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -46,12 +47,10 @@ ON CONFLICT (corpus_id)
updated_at = EXCLUDED.updated_at
""";

@Value("${app.corpus.embedding.model:embed-v4.0}")
private String embeddingModel;

@Value("${app.corpus.embedding.batch-size:32}")
private int batchSize;

private final CohereProperties cohereProperties;
private final MockJobPostingCorpusRepository mockJobPostingCorpusRepository;
private final MockQuestionCorpusRepository mockQuestionCorpusRepository;
private final CorpusEmbeddingClient corpusEmbeddingClient;
Expand All @@ -61,7 +60,7 @@ ON CONFLICT (corpus_id)
public CorpusEmbeddingSyncResponse syncAll(Integer limit) {
int jobPostingCount = syncJobPostingEmbeddings(limit);
int questionCount = syncQuestionEmbeddings(limit);
return new CorpusEmbeddingSyncResponse(jobPostingCount, questionCount, embeddingModel);
return new CorpusEmbeddingSyncResponse(jobPostingCount, questionCount, cohereProperties.embedding().model());
}

@Transactional
Expand Down Expand Up @@ -116,7 +115,7 @@ private void upsertVectors(String sql, List<Long> ids, List<float[]> embeddings)
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
for (int i = 0; i < ids.size(); i++) {
statement.setLong(1, ids.get(i));
statement.setString(2, embeddingModel);
statement.setString(2, cohereProperties.embedding().model());
statement.setObject(3, new PGvector(embeddings.get(i)));
statement.setTimestamp(4, now);
statement.setTimestamp(5, now);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jobdri.jobdri_api.domain.jobposting.service;

import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService;
import com.jobdri.jobdri_api.global.cohere.CohereProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
Expand All @@ -24,14 +25,14 @@ public class MockQuestionCacheVersionProvider {

public MockQuestionCacheVersionProvider(
MockQuestionCacheProperties mockQuestionCacheProperties,
CohereProperties cohereProperties,
@Value("${openai.model.job-posting-extractor:gpt-4o-mini}") String extractionModel,
@Value("${app.corpus.embedding.model:embed-v4.0}") String embeddingModel,
@Value("${app.analysis.retrieval.jd-limit:3}") int jdLimit,
@Value("${app.analysis.retrieval.question-limit:5}") int questionLimit
) {
this.mockQuestionCacheProperties = mockQuestionCacheProperties;
this.extractionModel = extractionModel;
this.embeddingModel = embeddingModel;
this.embeddingModel = cohereProperties.embedding().model();
this.jdLimit = jdLimit;
this.questionLimit = questionLimit;
}
Expand Down
Loading
Loading