Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b37c7e5
config: 구성 설정
yongho9064 Oct 2, 2025
57ed72b
config[db]: 다중 DB 설정
yongho9064 Oct 2, 2025
595ab05
remove: 필요없는 클래스 삭제
yongho9064 Oct 2, 2025
46d2c07
feat: qdrant 정확성 수정
yongho9064 Oct 2, 2025
e96b1fd
feat[chat]: 키워드 추출 수정
yongho9064 Oct 2, 2025
e495011
feat[batch]: 스프링 배치 적용 -> 추후 사용
yongho9064 Oct 2, 2025
d282c1b
dependencies: 스프링 배치, 올리마 의존성 추가
yongho9064 Oct 2, 2025
f642f4c
docker: docker-compose 업데이트
yongho9064 Oct 2, 2025
4fb19a3
config[ai]: 구성 설정 변경
yongho9064 Oct 2, 2025
b0810a0
sql: 주석
yongho9064 Oct 2, 2025
664702d
ci/cd: 수정
yongho9064 Oct 2, 2025
9d9855d
ci/cd: 수정
yongho9064 Oct 2, 2025
9ff794e
ci/cd: 수정
yongho9064 Oct 2, 2025
40e4dae
ci/cd: 수정
yongho9064 Oct 2, 2025
5a538d8
ci/cd: 수정
yongho9064 Oct 2, 2025
9f877e9
ci/cd: 수정
yongho9064 Oct 2, 2025
d61bd16
ci/cd: 수정
yongho9064 Oct 2, 2025
94bcf1d
ci/cd: 수정
yongho9064 Oct 2, 2025
9bd81c6
ci/cd: 수정
yongho9064 Oct 2, 2025
5c4c79b
ci/cd: 수정
yongho9064 Oct 2, 2025
45ad00b
ci/cd: 수정
yongho9064 Oct 2, 2025
6d9be9c
ci/cd: 수정
yongho9064 Oct 2, 2025
10ecf91
ci/cd: 수정
yongho9064 Oct 2, 2025
7fa80f3
ci/cd: 수정
yongho9064 Oct 2, 2025
29a4ac1
ci/cd: 수정
yongho9064 Oct 2, 2025
977a166
ci/cd: 수정
yongho9064 Oct 2, 2025
b1292de
ci/cd: 수정
yongho9064 Oct 2, 2025
d7fcf6a
ci/cd: 수정
yongho9064 Oct 2, 2025
358e11e
ci/cd: 수정
yongho9064 Oct 2, 2025
3996bb8
ci/cd: 수정
yongho9064 Oct 2, 2025
5e6bfbe
ci/cd: 수정
yongho9064 Oct 2, 2025
f93d8e2
ci/cd: 수정
yongho9064 Oct 2, 2025
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
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-batch'

// API Documentation (문서화)
implementation 'org.apache.commons:commons-lang3:3.18.0'
Expand Down Expand Up @@ -78,6 +79,8 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'
implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
implementation 'org.springframework.ai:spring-ai-starter-model-huggingface'

// Testing (테스트)
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
20 changes: 20 additions & 0 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ services:
timeout: 5s
retries: 10

ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
entrypoint: [ "/bin/sh", "-c" ]
command: >
"ollama serve &
sleep 5 &&
ollama pull daynice/kure-v1:567m &&
wait"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:11434/api/version" ]
interval: 10s
timeout: 5s
retries: 10

volumes:
mysql-data:
redis-data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class TitleExtractionDto {
@AllArgsConstructor
@NoArgsConstructor
public static class KeywordExtractionDto {
private List<String> keyword;
private String keyword;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,13 @@ public class ChatBotService {
// 멤버 조회 -> 벡터 검색 (판례, 법령) -> 프롬프트 생성 (시스템, 유저) -> 채팅 클라이언트 호출 (스트림) -> 응답 저장, 제목/키워드 추출
public Flux<ChatResponse> sendMessage(Long memberId, ChatRequest chatChatRequestDto, Long roomId) {

if(memberId == null) {
log.error("해당 멤버는 존재하지 않거나, accessToken이 만료되거나 잘못되었습니다.");
}

Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
);

// 벡터 검색 (판례, 법령)
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례", 3);
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령", 2);
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례");
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령");

// 판례와 법령 정보를 구분 있게 포맷팅
String caseContext = formatting(similarCaseDocuments);
Expand Down Expand Up @@ -167,18 +163,19 @@ private void handlerTasks(ChatRequest chatDto, History history, String fullRespo
private void extractAndUpdateKeywordRanks(String message) {
KeywordExtractionDto keywordResponse = keywordExtract(message, keywordExtraction, KeywordExtractionDto.class);

for (String keyword : keywordResponse.getKeyword()) {
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keyword);
if (keywordRank == null) {
keywordRank = KeywordRank.builder()
.keyword(keyword)
.score(1L)
.build();
} else {
keywordRank.setScore(keywordRank.getScore() + 1);
}
keywordRankRepository.save(keywordRank);
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());

if (keywordRank == null) {
keywordRank = KeywordRank.builder()
.keyword(keywordResponse.getKeyword())
.score(1L)
.build();
} else {
keywordRank.setScore(keywordRank.getScore() + 1);
}

keywordRankRepository.save(keywordRank);

}

private void setHistoryTitle(ChatRequest chatDto, History history, String fullResponse) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ai.lawyer.global.batch;

/*@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class BatchScheduler {

private final JobLauncher jobLauncher;
private final Job dataVectorizationJob;

@Scheduled(cron = "#{${batch.scheduler.run-every-minute} ? '* * * * * *' : '* * 2 * * *'}")
public void runVectorizationJob() {
log.info("전체 데이터(판례, 법령) 벡터화 스케줄러 실행...");
try {
JobParameters jobParameters = new JobParametersBuilder()
.addString("requestDate", LocalDateTime.now().toString())
.toJobParameters();

jobLauncher.run(dataVectorizationJob, jobParameters); // Job 실행
} catch (Exception e) {
log.error("전체 데이터 벡터화 배치 작업 실행 중 오류 발생", e);
}
}
}*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package com.ai.lawyer.global.batch;

/*@Slf4j
@Configuration
@RequiredArgsConstructor
public class DataVectorizationJobConfig {

private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final EntityManagerFactory entityManagerFactory;
private final VectorStore vectorStore;

private final JangRepository jangRepository;
private final JoRepository joRepository;
private final HangRepository hangRepository;
private final HoRepository hoRepository;

private final TokenTextSplitter tokenSplitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(0)
.withMinChunkLengthToEmbed(5)
.withMaxNumChunks(10000)
.withKeepSeparator(true)
.build();

private static final int CHUNK_SIZE = 10; // 배치 처리 시 한 번에 읽어올 데이터 수

@Value("${batch.page.size.precedent}")
private int precedentPageSize; // 하루에 처리할 판례 수

@Value("${batch.page.size.law}")
private int lawPageSize; // 하루에 처리할 법령 수

@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-thread-");
executor.initialize();
return executor;
}

// -------------- 전체 데이터 벡터화 정의 --------------
@Bean
public Job dataVectorizationJob() {
return new JobBuilder("dataVectorizationJob", jobRepository)
.start(precedentVectorizationStep()) // 판례 벡터화 Step 실행
.next(lawVectorizationStep()) // 법령 벡터화 Step 실행
.build();
}

// -------------- 판례 벡터화 ---------------
@Bean
public Step precedentVectorizationStep() {
log.info(">>>>>> 판례 벡터화 시작");
return new StepBuilder("precedentVectorizationStep", jobRepository)
.<Precedent, List<Document>>chunk(CHUNK_SIZE, transactionManager)
.reader(precedentItemReader())
.processor(precedentItemProcessor())
.writer(documentItemWriter())
.taskExecutor(taskExecutor())
.build();
}

@Bean
public JpaPagingItemReader<Precedent> precedentItemReader() {
return new JpaPagingItemReaderBuilder<Precedent>()
.name("precedentItemReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(CHUNK_SIZE)
.maxItemCount(precedentPageSize)
.queryString("SELECT p FROM Precedent p ORDER BY p.id ASC")
.build();
}

@Bean
public ItemProcessor<Precedent, List<Document>> precedentItemProcessor() {

return precedent -> {
String content = precedent.getPrecedentContent();
if (content == null || content.isBlank()) return null;

Document originalDoc = new Document(content, Map.of(
"type", "판례",
"caseNumber", precedent.getCaseNumber(),
"court", precedent.getCourtName(),
"caseName", precedent.getCaseName()
));

List<Document> chunkDocs = tokenSplitter.split(originalDoc);
List<Document> finalChunks = new ArrayList<>();

// 청크별로 메타데이터에 인덱스 추가 -> 구분 용도
for (int i = 0; i < chunkDocs.size(); i++) {
Document chunk = chunkDocs.get(i);
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
newMetadata.put("chunkIndex", i);
finalChunks.add(new Document(chunk.getText(), newMetadata));
}
return finalChunks;
};
}

// -------------- 법령 백터화 ---------------
@Bean
public Step lawVectorizationStep() {
log.info(">>>>>> 법령 벡터화 시작");
return new StepBuilder("lawVectorizationStep", jobRepository)
.<Law, List<Document>>chunk(CHUNK_SIZE, transactionManager) // 법령은 한 번에 10개씩 처리
.reader(lawItemReader())
.processor(lawItemProcessor())
.writer(documentItemWriter())
.taskExecutor(taskExecutor())
.build();
}

@Bean
public JpaPagingItemReader<Law> lawItemReader() {
return new JpaPagingItemReaderBuilder<Law>()
.name("lawItemReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(CHUNK_SIZE)
.maxItemCount(lawPageSize)
.queryString("SELECT l FROM Law l ORDER BY l.id ASC")
.build();
}

@Bean
public ItemProcessor<Law, List<Document>> lawItemProcessor() {
return law -> {
List<Document> finalChunks = new ArrayList<>();

List<Jang> jangs = jangRepository.findByLaw(law);

for (Jang jang : jangs) {

StringBuilder contentBuilder = new StringBuilder();

contentBuilder.append(law.getLawName()).append("\n");

if (jang.getContent() != null && !jang.getContent().isBlank()) {
contentBuilder.append(jang.getContent()).append("\n");
}

List<Jo> jos = joRepository.findByJang(jang);
for (Jo jo : jos) {

if (jo.getContent() != null && !jo.getContent().isBlank()) {
contentBuilder.append(jo.getContent()).append("\n");
}

List<Hang> hangs = hangRepository.findByJo(jo);
for (Hang hang : hangs) {
if (hang.getContent() != null && !hang.getContent().isBlank()) {
contentBuilder.append(hang.getContent()).append("\n");
}

List<Ho> hos = hoRepository.findByHang(hang);
for (Ho ho : hos) {
if (ho.getContent() != null && !ho.getContent().isBlank()) {
contentBuilder.append(ho.getContent()).append("\n");
}
}
}
}

// === Jang 단위로 문서화 ===
String finalContent = contentBuilder.toString();

if (!finalContent.isBlank()) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("type", "법령");
metadata.put("lawName", law.getLawName());
metadata.put("jangId", jang.getId());

Document originalDoc = new Document(finalContent, metadata);

List<Document> chunkDocs = tokenSplitter.split(originalDoc);

for (int i = 0; i < chunkDocs.size(); i++) {
Document chunk = chunkDocs.get(i);
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
newMetadata.put("chunkIndex", i);
finalChunks.add(new Document(chunk.getText(), newMetadata));
}
}
}

return finalChunks.isEmpty() ? null : finalChunks;
};
}

@Bean
public ItemWriter<List<Document>> documentItemWriter() {
return chunk -> {
List<Document> totalDocuments = chunk.getItems().stream()
.flatMap(List::stream)
.collect(Collectors.toList());

if (!totalDocuments.isEmpty()) {
vectorStore.add(totalDocuments);
log.info(">>>>>> {}개의 Document 청크를 벡터 저장소에 저장했습니다.", totalDocuments.size());
}
};
}
}*/

16 changes: 10 additions & 6 deletions backend/src/main/java/com/ai/lawyer/global/config/AIConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class AIConfig {

@Bean
@Primary
public EmbeddingModel primaryOllamaEmbeddingModel(OllamaEmbeddingModel ollamaEmbeddingModel) {
return ollamaEmbeddingModel;
}

@Bean
public ChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
return JdbcChatMemoryRepository.builder()
Expand All @@ -26,9 +34,5 @@ public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}

@Bean
public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter(500, 150, 5, 10000, true);
}

}

Loading