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
98 changes: 98 additions & 0 deletions docs/gimin-#10-embedding-model-basic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Issue #10 기본 임베딩 모델 등록 및 조회

## 1. 목적

문서 업로드 시 생성되는 `EmbeddingJob`이 사용할 기본 임베딩 모델 메타데이터를 등록하고 조회한다.

이번 범위는 실제 모델 호출이 아니라 다음 계약을 제공하는 것이다.

```text
local/test 환경의 기본 모델 Seed
→ 내부 서비스가 활성 모델 Entity 조회
→ 업로드 시 EmbeddingJob.embedding_model_id 고정
→ 외부 API는 공개 가능한 모델 정보만 DTO로 반환
```

## 2. 기본 모델 정책

기본 모델은 다음 조건을 동시에 만족하는 모델이다.

```text
is_active = true
is_searchable = true
```

두 필드의 역할은 다르다.

- `is_active`: 새 EmbeddingJob에 사용할 수 있는가
- `is_searchable`: 해당 모델로 생성한 기존 Vector를 현재 검색에 사용할 수 있는가

모델을 교체할 때 기존 모델을 `false + true`로 유지하면 신규 작업에는 사용하지 않으면서 기존 Vector 검색은 계속 허용할 수 있다.

## 3. DB 제약

모델 차원은 양수여야 하며, 활성·검색 가능 모델은 최대 하나만 존재해야 한다.

```sql
ALTER TABLE embedding_models
ADD CONSTRAINT ck_embedding_models_dimension_positive
CHECK (dimension > 0);

CREATE UNIQUE INDEX uk_embedding_models_one_active_searchable
ON embedding_models ((1))
WHERE is_active = TRUE
AND is_searchable = TRUE;
```

일반 `UNIQUE(is_active, is_searchable)`는 모든 Boolean 조합을 한 건으로 제한하므로 사용하지 않았다. Partial Unique Index는 `true + true`인 행만 같은 Key로 인덱싱한다.

## 4. Mock Seed

local/test Profile에서는 다음 모델을 멱등 Seed로 등록한다.

| 항목 | 값 |
|---|---|
| Provider | `MOCK` |
| Model name | `mock-bge-m3` |
| Version | `v1` |
| Dimension | `1024` |
| Distance metric | `COSINE` |
| Active | `true` |
| Searchable | `true` |

Seed는 `db/seed` Flyway Location을 사용하는 local/test에서만 적용되며 운영 Profile에는 적용하지 않는다.

## 5. 조회 구조

내부 도메인 로직과 외부 API의 반환 타입을 분리했다.

```text
EmbeddingModelQueryService.getActiveModel()
→ EmbeddingJob 생성에 필요한 Entity 반환

GET /api/embedding-models/active
→ 외부 공개용 EmbeddingModelResponse 반환
```

업로드 시점에 `EmbeddingJob`이 모델 ID를 저장하므로, 이후 기본 모델이 바뀌어도 이미 접수된 작업이 다른 모델로 실행되지 않는다.

외부 응답에는 Provider, 모델명, 버전, 차원, 거리 계산 방식 등 필요한 메타데이터만 포함하고 내부 설정과 감사 필드는 노출하지 않는다.

## 6. 예외 처리

- 기본 모델이 없으면 설정 오류로 처리
- 데이터가 비정상적으로 여러 건이면 중복 설정 오류로 처리
- 내부 설정 문제와 정상적인 사용자 요청 오류를 구분

DB는 최대 한 개를 보장하고, 서비스는 0개인 상태도 명시적으로 처리한다.

## 7. 검증

- Entity의 필수값과 차원 검증
- Repository의 활성·검색 가능 모델 조회
- Service의 정상·미설정·중복 결과 처리
- Controller의 응답 DTO와 오류 응답
- V27 Migration과 반복 가능한 Seed 적용
- 애플리케이션 재기동 후 Mock 모델이 한 건으로 유지되는지 확인

Partial Unique Index의 실제 동시 트랜잭션 검증과 Benchmark 결과는 [Issue #12 테스트 결과](./test-results/gimin-#12-embedding-model-concurrency-verification.md)에 별도로 정리했다.
144 changes: 144 additions & 0 deletions docs/gimin-#15-document-upload-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Issue #15 문서 업로드 접수 API 설계 및 구현

## 1. 목적

사용자가 TXT 또는 Markdown 파일을 업로드하면 원본을 MinIO에 저장하고 인덱싱 작업을 접수한다.

```http
POST /api/documents
Content-Type: multipart/form-data
Authorization: Bearer {token}
```

요청 한 번으로 다음 데이터가 생성된다.

```text
FileObject
Document
DocumentVersion 1
PENDING EmbeddingJob
```

파싱, 청킹, 임베딩 실행과 Vector 저장은 포함하지 않는다.

## 2. 요청과 인증

| 필드 | 설명 |
|---|---|
| `file` | 업로드할 TXT 또는 Markdown 파일 |
| `title` | 논리 문서 제목 |
| `description` | 문서 설명 |
| `visibility` | `PUBLIC` 또는 `PRIVATE` |

사용자 ID는 요청값으로 받지 않고 인증된 `@CurrentUser`에서 가져온다.

## 3. 파일 검증

MinIO에 저장하기 전에 다음 항목을 검증한다.

- 빈 파일 여부
- 최대 파일 크기
- 허용 확장자 `.txt`, `.md`
- 확장자와 Content-Type 조합
- 빈 파일명과 최대 길이
- Path Traversal이 포함된 파일명

검증을 통과한 파일은 전체 바이트로 SHA-256을 계산한다. 파일 크기만 같다고 동일 파일로 판단하지 않는다.

## 4. 논리 문서와 실제 파일 분리

```text
Document
→ 사용자가 관리하는 논리 문서

DocumentVersion
→ 특정 시점의 문서 내용

FileObject
→ MinIO Object의 위치와 해시·크기를 보관하는 메타데이터
```

동일한 파일을 여러 번 새 문서로 올릴 수 있으므로 Document는 요청마다 생성하지만, 실제 파일은 `file_hash + file_size`가 같으면 기존 `FileObject`를 재사용한다.

## 5. 업로드 처리 흐름

```text
파일 검증과 SHA-256 계산
→ 기존 FileObject 사전 조회
→ 있으면 MinIO 저장 없이 재사용
→ 없으면 UUID 기반 Object Key로 MinIO 후보 저장
→ DB 트랜잭션에서 FileObject 결정
→ Document와 Version 1 생성
→ 기본 EmbeddingModel을 연결한 PENDING Job 생성
→ 첫 Version을 current_version_id로 설정
```

Object Key는 원본 파일명을 직접 사용하지 않고 UUID를 조합한다.

```text
documents/{directoryUuid}/{objectUuid}.{extension}
```

## 6. 동일 파일 동시 업로드

사전 조회는 불필요한 저장을 줄이는 최적화일 뿐 동시성 제어가 아니다. 두 요청이 동시에 조회하면 둘 다 FileObject가 없다고 판단할 수 있다.

DB에는 다음 제약을 둔다.

```sql
UNIQUE (file_hash, file_size)
```

그리고 `INSERT ... ON CONFLICT DO NOTHING`의 영향 행 수로 현재 후보가 채택됐는지 판단한다.

```text
inserted = 1
→ 현재 후보 채택

inserted = 0
→ 다른 요청이 만든 FileObject 재조회·재사용
→ 현재 요청의 미사용 MinIO 후보 삭제
```

따라서 서로 다른 논리 Document 요청은 모두 성공하면서 실제 파일은 하나만 공유할 수 있다.

## 7. 트랜잭션과 보상 처리

MinIO는 PostgreSQL 트랜잭션에 참여하지 않는다. MinIO 저장 후 DB 작업이 실패하면 DB만 Rollback되고 Object는 남을 수 있다.

Facade는 외부 저장소 경계를 관리하고, Command Service는 DB 원자성을 관리한다.

```text
Facade
→ 파일 검증, 해시, MinIO 후보 저장, 실패·미채택 후보 삭제

DocumentUploadService
→ FileObject, Document, DocumentVersion, EmbeddingJob을 하나의 DB 트랜잭션으로 저장
```

DB 실패 시에는 현재 요청이 새로 저장한 후보만 삭제한다. 이미 다른 Version이 공유하는 기존 Object는 삭제하지 않는다.

## 8. 생성 상태

```text
Document.status = UPLOADED
DocumentVersion.status = UPLOADED
DocumentVersion.version_no = 1
Document.current_version_id = Version 1
EmbeddingJob.status = PENDING
```

EmbeddingJob에는 접수 시점의 활성 EmbeddingModel ID를 저장한다.

## 9. 검증 범위

- 정상 TXT·Markdown 업로드
- 잘못된 파일 요청 거절
- 같은 파일 순차 업로드 시 FileObject 재사용
- 같은 파일 동시 업로드 시 FileObject 한 건 유지
- MinIO 실패 시 DB 작업 미실행
- DB 실패 시 신규 후보 Object 보상 삭제
- 활성 EmbeddingModel이 없을 때 DB 전체 Rollback
- Controller, 검증, 해시, Facade, Command Service 단위·통합 테스트

실제 MinIO에서 경합 패자의 Object가 삭제되는지는 [Issue #27 테스트 결과](./test-results/gimin-#27-minio-concurrency-integration-test.md)에서 추가로 검증한다.
36 changes: 18 additions & 18 deletions docs/gimin-#25-document-version-upload.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PR 2.1 문서 새 버전 업로드 설계
# Issue #25 문서 새 버전 업로드 설계

## 1. 목적

Expand All @@ -15,7 +15,7 @@ current_version_id는 새 버전이 INDEXED될 때까지 기존 버전 유지
이 API에서는 파싱, 청킹, 임베딩을 수행하지 않는다. 새 버전의 원본 파일과 인덱싱 Job만 접수한다.

```text
브랜치명: feature/25
GitHub Issue: #25
```

---
Expand Down Expand Up @@ -160,14 +160,14 @@ public record DocumentVersionUploadRequest(
}
```

PR 2.1은 파일 내용의 버전만 관리한다. 제목과 설명은 Document 공통 메타데이터이며 별도 메타데이터 수정 API에서 변경한다.
이번 구현은 파일 내용의 버전만 관리한다. 제목과 설명은 Document 공통 메타데이터이며 별도 메타데이터 수정 API에서 변경한다.

```text
새 Version.title_snapshot
→ Version 접수 시점의 Document.title 복사

Version 처리 중 별도 API로 Document.title 변경
PR 9 완료 처리에서 title_snapshot으로 덮어쓰지 않음
후속 인덱싱 완료 처리에서 title_snapshot으로 덮어쓰지 않음
```

`title_snapshot`은 해당 Version이 접수될 당시의 제목을 기록하는 감사·출처용 스냅샷이다. 현재 문서 제목을 교체하기 위한 후보 값으로 사용하지 않는다. 추후 제목이나 설명 자체를 버전 자산으로 관리해야 한다면 메타데이터 revision과 `description_snapshot`을 포함한 별도 설계를 추가한다.
Expand Down Expand Up @@ -225,7 +225,7 @@ document_versions
- created_by
```

PR 2.1 마이그레이션에서 다음 컬럼을 추가한다.
이슈 #25 마이그레이션에서 다음 컬럼을 추가한다.

```sql
ALTER TABLE document_versions
Expand All @@ -245,7 +245,7 @@ URL / API / MCP Version

기존 데이터는 `file_object_id`가 있는 행만 연결된 `file_objects` 값으로 백필한다. `file_objects.original_filename`과 `content_type`은 기존 호환성을 위해 이번 PR에서는 제거하지 않는다.

PR 2.1 이후에는 두 업로드 경로가 모두 Version 메타데이터를 채워야 한다.
이슈 #25 적용 이후에는 두 업로드 경로가 모두 Version 메타데이터를 채워야 한다.

```text
DocumentUploadService
Expand Down Expand Up @@ -282,7 +282,7 @@ DOCUMENT_VERSION_TYPE_MISMATCH

## 8. 권한 및 보안 정책

PR 2.1 MVP에서는 문서 소유자만 새 버전을 생성한다.
현재 MVP에서는 문서 소유자만 새 버전을 생성한다.

```text
document.owner_user_id = 현재 인증 사용자
Expand Down Expand Up @@ -364,7 +364,7 @@ documents.source_type = UPLOAD
→ 기존 FAILED current_version_id는 새 Version 완료 전까지 유지
```

최신 FAILED Version과 같은 파일을 다시 전송하면 새 Version을 만들지 않고 PR 17 수동 재처리를 안내한다. FAILED Version 뒤에 더 최신 Version이 생성되면 과거 FAILED Version은 수동 재처리할 수 없다.
최신 FAILED Version과 같은 파일을 다시 전송하면 새 Version을 만들지 않고 별도의 수동 재처리 기능을 사용한다. FAILED Version 뒤에 더 최신 Version이 생성되면 과거 FAILED Version은 수동 재처리할 수 없다.

---

Expand Down Expand Up @@ -450,7 +450,7 @@ embedding_jobs.document_version_id = 21
embedding_jobs.status = PENDING
```

Version 2가 INDEXED되면 PR 9에서:
Version 2가 INDEXED되면 후속 인덱싱 완료 처리에서:

```text
documents.current_version_id = 21
Expand Down Expand Up @@ -497,7 +497,7 @@ CREATE UNIQUE INDEX uk_document_versions_one_in_progress

### current_version 단조 증가

PR 9 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다.
후속 인덱싱 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다.

```text
완료 Version.version_no > currentVersion.version_no
Expand All @@ -510,7 +510,7 @@ PR 9 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다

stale 완료 오류는 `INDEXING_STALE_VERSION_COMPLETION`을 사용한다. Claim Token 검증과 별개로 오래된 완료 요청의 상태 쓰기를 최종 차단한다.

PR 17 수동 재처리에서는 FAILED Version보다 최신 Version이 이미 존재하면 재처리를 거부한다.
후속 수동 재처리 기능에서는 FAILED Version보다 최신 Version이 이미 존재하면 재처리를 거부한다.

---

Expand Down Expand Up @@ -603,9 +603,9 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로

---

## 16. 후속 PR 계약
## 16. 후속 기능 계약

### PR 3 문서 상태 조회
### 문서 상태 조회

현재 검색 가능한 버전과 처리 중 버전을 함께 반환한다.

Expand All @@ -623,24 +623,24 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로
}
```

### PR 9 인덱싱 완료
### 인덱싱 완료

- 완료 Version이 현재 Version보다 최신인지 검증한다.
- 최신 Version이면 `current_version_id`를 교체한다.
- `documents.title`은 별도 메타데이터이므로 완료 처리에서 덮어쓰지 않는다.
- 오래된 Version 완료가 현재 Version을 덮어쓰지 못하게 한다.
- stale 완료 요청은 Version과 Job 상태를 포함해 아무것도 변경하지 않고 409로 거절한다.

### PR 10 실패 및 자동 재시도
### 실패 및 자동 재시도

- 새 Version 실패 시 기존 current Version을 유지한다.
- 재시도 중에도 기존 INDEXED Version을 검색 가능하게 유지한다.

### PR 17 수동 재처리
### 수동 재처리

- FAILED Version보다 최신 Version이 존재하면 오래된 Version 재처리를 거부한다.

### PR 18 E2E
### E2E 검증

- Version 2 처리 중 Version 1 검색 가능 여부
- Version 2 성공 후 current Version 교체
Expand Down Expand Up @@ -702,4 +702,4 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로
- 새 Version마다 별도의 PENDING Job이 생성된다.
- DB 실패 또는 동시성 패배 시 이번 요청의 미사용 MinIO 후보만 삭제된다.
- FileObject 직접 접근 없이 문서 권한을 거쳐 파일에 접근한다.
- PR 3, 9, 10, 17, 18과의 계약이 문서화된다.
- 문서 상태 조회, 인덱싱 완료, 실패·재시도, 수동 재처리, E2E 검증과의 계약이 문서화된다.
2 changes: 1 addition & 1 deletion docs/gimin-#4-local-db.md → docs/gimin-#3-local-db.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Local DB
# Issue #3 로컬 DB 개발 환경 구성

이 문서는 로컬 개발 환경에서 OpenSQL-PG 기반 DB 컨테이너를 띄우고 Spring Boot `local` profile로 Flyway migration 실행을 확인하는 최소 가이드입니다.

Expand Down
Loading