Skip to content

Commit b915604

Browse files
authored
Merge pull request #222 from prgrms-web-devcourse-final-project/develop
deploy
2 parents 76e5b62 + d785eab commit b915604

File tree

21 files changed

+566
-316
lines changed

21 files changed

+566
-316
lines changed

backend/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies {
4040
implementation 'org.springframework.boot:spring-boot-starter-actuator'
4141
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
4242
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'
43+
implementation 'org.springframework.boot:spring-boot-starter-batch'
4344

4445
// API Documentation (문서화)
4546
implementation 'org.apache.commons:commons-lang3:3.18.0'
@@ -78,6 +79,8 @@ dependencies {
7879
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
7980
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
8081
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'
82+
implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
83+
implementation 'org.springframework.ai:spring-ai-starter-model-huggingface'
8184

8285
// Testing (테스트)
8386
testImplementation 'org.springframework.boot:spring-boot-starter-test'

backend/docker-compose.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ services:
5858
timeout: 5s
5959
retries: 10
6060

61+
ollama:
62+
image: ollama/ollama:latest
63+
container_name: ollama
64+
restart: unless-stopped
65+
ports:
66+
- "11434:11434"
67+
volumes:
68+
- ollama-data:/root/.ollama
69+
entrypoint: [ "/bin/sh", "-c" ]
70+
command: >
71+
"ollama serve &
72+
sleep 5 &&
73+
ollama pull daynice/kure-v1:567m &&
74+
wait"
75+
healthcheck:
76+
test: [ "CMD", "curl", "-f", "http://localhost:11434/api/version" ]
77+
interval: 10s
78+
timeout: 5s
79+
retries: 10
80+
6181
volumes:
6282
mysql-data:
6383
redis-data:

backend/src/main/java/com/ai/lawyer/domain/chatbot/dto/ExtractionDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public static class TitleExtractionDto {
1919
@AllArgsConstructor
2020
@NoArgsConstructor
2121
public static class KeywordExtractionDto {
22-
private List<String> keyword;
22+
private String keyword;
2323
}
2424

2525
}

backend/src/main/java/com/ai/lawyer/domain/chatbot/service/ChatBotService.java

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

60-
if(memberId == null) {
61-
log.error("해당 멤버는 존재하지 않거나, accessToken이 만료되거나 잘못되었습니다.");
62-
}
63-
6460
Member member = memberRepository.findById(memberId)
6561
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
6662
);
6763

6864
// 벡터 검색 (판례, 법령)
69-
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례", 3);
70-
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령", 2);
65+
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례");
66+
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령");
7167

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

170-
for (String keyword : keywordResponse.getKeyword()) {
171-
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keyword);
172-
if (keywordRank == null) {
173-
keywordRank = KeywordRank.builder()
174-
.keyword(keyword)
175-
.score(1L)
176-
.build();
177-
} else {
178-
keywordRank.setScore(keywordRank.getScore() + 1);
179-
}
180-
keywordRankRepository.save(keywordRank);
166+
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());
167+
168+
if (keywordRank == null) {
169+
keywordRank = KeywordRank.builder()
170+
.keyword(keywordResponse.getKeyword())
171+
.score(1L)
172+
.build();
173+
} else {
174+
keywordRank.setScore(keywordRank.getScore() + 1);
181175
}
176+
177+
keywordRankRepository.save(keywordRank);
178+
182179
}
183180

184181
private void setHistoryTitle(ChatRequest chatDto, History history, String fullResponse) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.ai.lawyer.global.batch;
2+
3+
/*@Slf4j
4+
@Component
5+
@EnableScheduling
6+
@RequiredArgsConstructor
7+
public class BatchScheduler {
8+
9+
private final JobLauncher jobLauncher;
10+
private final Job dataVectorizationJob;
11+
12+
@Scheduled(cron = "#{${batch.scheduler.run-every-minute} ? '* * * * * *' : '* * 2 * * *'}")
13+
public void runVectorizationJob() {
14+
log.info("전체 데이터(판례, 법령) 벡터화 스케줄러 실행...");
15+
try {
16+
JobParameters jobParameters = new JobParametersBuilder()
17+
.addString("requestDate", LocalDateTime.now().toString())
18+
.toJobParameters();
19+
20+
jobLauncher.run(dataVectorizationJob, jobParameters); // Job 실행
21+
} catch (Exception e) {
22+
log.error("전체 데이터 벡터화 배치 작업 실행 중 오류 발생", e);
23+
}
24+
}
25+
}*/
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package com.ai.lawyer.global.batch;
2+
3+
/*@Slf4j
4+
@Configuration
5+
@RequiredArgsConstructor
6+
public class DataVectorizationJobConfig {
7+
8+
private final JobRepository jobRepository;
9+
private final PlatformTransactionManager transactionManager;
10+
private final EntityManagerFactory entityManagerFactory;
11+
private final VectorStore vectorStore;
12+
13+
private final JangRepository jangRepository;
14+
private final JoRepository joRepository;
15+
private final HangRepository hangRepository;
16+
private final HoRepository hoRepository;
17+
18+
private final TokenTextSplitter tokenSplitter = TokenTextSplitter.builder()
19+
.withChunkSize(800)
20+
.withMinChunkSizeChars(0)
21+
.withMinChunkLengthToEmbed(5)
22+
.withMaxNumChunks(10000)
23+
.withKeepSeparator(true)
24+
.build();
25+
26+
private static final int CHUNK_SIZE = 10; // 배치 처리 시 한 번에 읽어올 데이터 수
27+
28+
@Value("${batch.page.size.precedent}")
29+
private int precedentPageSize; // 하루에 처리할 판례 수
30+
31+
@Value("${batch.page.size.law}")
32+
private int lawPageSize; // 하루에 처리할 법령 수
33+
34+
@Bean
35+
public TaskExecutor taskExecutor() {
36+
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
37+
executor.setCorePoolSize(10);
38+
executor.setMaxPoolSize(20);
39+
executor.setQueueCapacity(100);
40+
executor.setThreadNamePrefix("async-thread-");
41+
executor.initialize();
42+
return executor;
43+
}
44+
45+
// -------------- 전체 데이터 벡터화 정의 --------------
46+
@Bean
47+
public Job dataVectorizationJob() {
48+
return new JobBuilder("dataVectorizationJob", jobRepository)
49+
.start(precedentVectorizationStep()) // 판례 벡터화 Step 실행
50+
.next(lawVectorizationStep()) // 법령 벡터화 Step 실행
51+
.build();
52+
}
53+
54+
// -------------- 판례 벡터화 ---------------
55+
@Bean
56+
public Step precedentVectorizationStep() {
57+
log.info(">>>>>> 판례 벡터화 시작");
58+
return new StepBuilder("precedentVectorizationStep", jobRepository)
59+
.<Precedent, List<Document>>chunk(CHUNK_SIZE, transactionManager)
60+
.reader(precedentItemReader())
61+
.processor(precedentItemProcessor())
62+
.writer(documentItemWriter())
63+
.taskExecutor(taskExecutor())
64+
.build();
65+
}
66+
67+
@Bean
68+
public JpaPagingItemReader<Precedent> precedentItemReader() {
69+
return new JpaPagingItemReaderBuilder<Precedent>()
70+
.name("precedentItemReader")
71+
.entityManagerFactory(entityManagerFactory)
72+
.pageSize(CHUNK_SIZE)
73+
.maxItemCount(precedentPageSize)
74+
.queryString("SELECT p FROM Precedent p ORDER BY p.id ASC")
75+
.build();
76+
}
77+
78+
@Bean
79+
public ItemProcessor<Precedent, List<Document>> precedentItemProcessor() {
80+
81+
return precedent -> {
82+
String content = precedent.getPrecedentContent();
83+
if (content == null || content.isBlank()) return null;
84+
85+
Document originalDoc = new Document(content, Map.of(
86+
"type", "판례",
87+
"caseNumber", precedent.getCaseNumber(),
88+
"court", precedent.getCourtName(),
89+
"caseName", precedent.getCaseName()
90+
));
91+
92+
List<Document> chunkDocs = tokenSplitter.split(originalDoc);
93+
List<Document> finalChunks = new ArrayList<>();
94+
95+
// 청크별로 메타데이터에 인덱스 추가 -> 구분 용도
96+
for (int i = 0; i < chunkDocs.size(); i++) {
97+
Document chunk = chunkDocs.get(i);
98+
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
99+
newMetadata.put("chunkIndex", i);
100+
finalChunks.add(new Document(chunk.getText(), newMetadata));
101+
}
102+
return finalChunks;
103+
};
104+
}
105+
106+
// -------------- 법령 백터화 ---------------
107+
@Bean
108+
public Step lawVectorizationStep() {
109+
log.info(">>>>>> 법령 벡터화 시작");
110+
return new StepBuilder("lawVectorizationStep", jobRepository)
111+
.<Law, List<Document>>chunk(CHUNK_SIZE, transactionManager) // 법령은 한 번에 10개씩 처리
112+
.reader(lawItemReader())
113+
.processor(lawItemProcessor())
114+
.writer(documentItemWriter())
115+
.taskExecutor(taskExecutor())
116+
.build();
117+
}
118+
119+
@Bean
120+
public JpaPagingItemReader<Law> lawItemReader() {
121+
return new JpaPagingItemReaderBuilder<Law>()
122+
.name("lawItemReader")
123+
.entityManagerFactory(entityManagerFactory)
124+
.pageSize(CHUNK_SIZE)
125+
.maxItemCount(lawPageSize)
126+
.queryString("SELECT l FROM Law l ORDER BY l.id ASC")
127+
.build();
128+
}
129+
130+
@Bean
131+
public ItemProcessor<Law, List<Document>> lawItemProcessor() {
132+
return law -> {
133+
List<Document> finalChunks = new ArrayList<>();
134+
135+
List<Jang> jangs = jangRepository.findByLaw(law);
136+
137+
for (Jang jang : jangs) {
138+
139+
StringBuilder contentBuilder = new StringBuilder();
140+
141+
contentBuilder.append(law.getLawName()).append("\n");
142+
143+
if (jang.getContent() != null && !jang.getContent().isBlank()) {
144+
contentBuilder.append(jang.getContent()).append("\n");
145+
}
146+
147+
List<Jo> jos = joRepository.findByJang(jang);
148+
for (Jo jo : jos) {
149+
150+
if (jo.getContent() != null && !jo.getContent().isBlank()) {
151+
contentBuilder.append(jo.getContent()).append("\n");
152+
}
153+
154+
List<Hang> hangs = hangRepository.findByJo(jo);
155+
for (Hang hang : hangs) {
156+
if (hang.getContent() != null && !hang.getContent().isBlank()) {
157+
contentBuilder.append(hang.getContent()).append("\n");
158+
}
159+
160+
List<Ho> hos = hoRepository.findByHang(hang);
161+
for (Ho ho : hos) {
162+
if (ho.getContent() != null && !ho.getContent().isBlank()) {
163+
contentBuilder.append(ho.getContent()).append("\n");
164+
}
165+
}
166+
}
167+
}
168+
169+
// === Jang 단위로 문서화 ===
170+
String finalContent = contentBuilder.toString();
171+
172+
if (!finalContent.isBlank()) {
173+
Map<String, Object> metadata = new HashMap<>();
174+
metadata.put("type", "법령");
175+
metadata.put("lawName", law.getLawName());
176+
metadata.put("jangId", jang.getId());
177+
178+
Document originalDoc = new Document(finalContent, metadata);
179+
180+
List<Document> chunkDocs = tokenSplitter.split(originalDoc);
181+
182+
for (int i = 0; i < chunkDocs.size(); i++) {
183+
Document chunk = chunkDocs.get(i);
184+
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
185+
newMetadata.put("chunkIndex", i);
186+
finalChunks.add(new Document(chunk.getText(), newMetadata));
187+
}
188+
}
189+
}
190+
191+
return finalChunks.isEmpty() ? null : finalChunks;
192+
};
193+
}
194+
195+
@Bean
196+
public ItemWriter<List<Document>> documentItemWriter() {
197+
return chunk -> {
198+
List<Document> totalDocuments = chunk.getItems().stream()
199+
.flatMap(List::stream)
200+
.collect(Collectors.toList());
201+
202+
if (!totalDocuments.isEmpty()) {
203+
vectorStore.add(totalDocuments);
204+
log.info(">>>>>> {}개의 Document 청크를 벡터 저장소에 저장했습니다.", totalDocuments.size());
205+
}
206+
};
207+
}
208+
}*/
209+

backend/src/main/java/com/ai/lawyer/global/config/AIConfig.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@
33
import org.springframework.ai.chat.client.ChatClient;
44
import org.springframework.ai.chat.memory.ChatMemoryRepository;
55
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
6+
import org.springframework.ai.embedding.EmbeddingModel;
7+
import org.springframework.ai.ollama.OllamaEmbeddingModel;
68
import org.springframework.ai.openai.OpenAiChatModel;
7-
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
89
import org.springframework.context.annotation.Bean;
910
import org.springframework.context.annotation.Configuration;
11+
import org.springframework.context.annotation.Primary;
1012
import org.springframework.jdbc.core.JdbcTemplate;
1113
import org.springframework.transaction.PlatformTransactionManager;
1214

1315
@Configuration
1416
public class AIConfig {
1517

18+
@Bean
19+
@Primary
20+
public EmbeddingModel primaryOllamaEmbeddingModel(OllamaEmbeddingModel ollamaEmbeddingModel) {
21+
return ollamaEmbeddingModel;
22+
}
23+
1624
@Bean
1725
public ChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
1826
return JdbcChatMemoryRepository.builder()
@@ -26,9 +34,5 @@ public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
2634
return ChatClient.create(openAiChatModel);
2735
}
2836

29-
@Bean
30-
public TokenTextSplitter tokenTextSplitter() {
31-
return new TokenTextSplitter(500, 150, 5, 10000, true);
32-
}
33-
3437
}
38+

0 commit comments

Comments
 (0)