-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] Flyway 마이그레이션 + embedding_models seed #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # #41 벡터 검색 DB 인프라 구축 | ||
|
|
||
| closes #41 | ||
|
|
||
| ## 배경 | ||
|
|
||
| 벡터 검색 기능 구현을 위해 DB 스키마와 Java 엔티티를 pgvector에 맞게 정비한다. | ||
| 기존에 `embeddings.vector`, `search_queries.query_vector` 컬럼이 임시로 `TEXT` 타입으로 매핑되어 있었고, | ||
| Hibernate 스키마 검증(`ddl-auto=validate`)이 실패하는 상태였다. | ||
|
|
||
| ## 작업 내용 | ||
|
|
||
| ### 1. Flyway V32 — vector 컬럼 타입 변환 및 HNSW 인덱스 추가 | ||
|
|
||
| ```sql | ||
| ALTER TABLE embeddings DROP COLUMN vector; | ||
| ALTER TABLE embeddings ADD COLUMN vector vector(1024) NOT NULL; | ||
| ALTER TABLE search_queries ALTER COLUMN query_vector TYPE vector(1024) USING query_vector::vector; | ||
| CREATE INDEX ON embeddings USING hnsw (vector vector_cosine_ops); | ||
| ``` | ||
|
|
||
| - `embeddings.vector`: TEXT → vector(1024) | ||
| - `search_queries.query_vector`: TEXT → vector(1024) | ||
| - HNSW 인덱스: 코사인 거리 기반 ANN 검색 가속 | ||
|
|
||
| ### 2. db/seed 이관 | ||
|
|
||
| 기존 `R__seed_mock_embedding_model.sql`(MOCK 모델)을 제거하고, 실제 BAAI/bge-m3 모델 seed로 교체했다. | ||
|
|
||
| - `R__seed_bge_m3_embedding_model.sql`: HUGGINGFACE/BAAI/bge-m3(1024차원, COSINE) 활성 모델 등록. ON CONFLICT upsert. | ||
| - `R__seed_test_fixtures.sql`: PUBLIC 문서 2개, 청크 4개, 임베딩 4개(개발용 더미 벡터). ON CONFLICT DO NOTHING으로 멱등 처리. | ||
|
|
||
| ### 3. VectorType — 커스텀 Hibernate UserType 구현 | ||
|
|
||
| `global/common/type/VectorType.java` | ||
|
|
||
| - pgvector의 `vector(1024)` SQL 타입(Types#OTHER)을 Java `float[]`로 매핑 | ||
| - PostgreSQL JDBC의 `PGobject`로 write, `getString` + 파싱으로 read | ||
| - Hibernate 스키마 검증 통과: DB의 `Types#OTHER`와 UserType의 `getSqlType() = Types.OTHER`가 일치 | ||
|
|
||
| ### 4. 엔티티 수정 | ||
|
|
||
| | 엔티티 | 변경 전 | 변경 후 | | ||
| |--------|---------|---------| | ||
| | `Embedding.vector` | `String` / `columnDefinition="TEXT"` | `float[]` / `@Type(VectorType.class)` / `columnDefinition="vector(1024)"` | | ||
| | `SearchQuery.queryVector` | `String` / `columnDefinition="TEXT"` | `float[]` / `@Type(VectorType.class)` / `columnDefinition="vector(1024)"` | | ||
|
|
||
| ### 5. build.gradle | ||
|
|
||
| `runtimeOnly 'org.postgresql:postgresql'` → `implementation` | ||
|
|
||
| PGobject를 컴파일 타임에 사용하기 위해 스코프 변경. | ||
|
|
||
| ## 설계 결정 | ||
|
|
||
| - **seed를 db/migration이 아닌 db/seed에 배치**: 데이터 seed는 스키마 변경이 아니므로 분리. `R__`(repeatable) 방식으로 idempotent하게 관리. | ||
| - **VectorType 직접 구현**: 외부 라이브러리(pgvector-java, hypersistence-utils) 없이 JDBC 드라이버만으로 처리. 의존성 최소화. | ||
| - **float[] 선택**: 검색 시 임베딩 서버 응답(`List<Float>`)을 직접 담을 수 있고, pgvector 연산과 자연스럽게 연결됨. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package com.opensource.docgrid.global.common.type; | ||
|
|
||
| import org.hibernate.engine.spi.SharedSessionContractImplementor; | ||
| import org.hibernate.usertype.UserType; | ||
| import org.postgresql.util.PGobject; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.sql.PreparedStatement; | ||
| import java.sql.ResultSet; | ||
| import java.sql.SQLException; | ||
| import java.sql.Types; | ||
| import java.util.Arrays; | ||
|
|
||
| public class VectorType implements UserType<float[]> { | ||
|
|
||
| @Override | ||
| public int getSqlType() { | ||
| return Types.OTHER; | ||
| } | ||
|
|
||
| @Override | ||
| public Class<float[]> returnedClass() { | ||
| return float[].class; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(float[] x, float[] y) { | ||
| return Arrays.equals(x, y); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode(float[] x) { | ||
| return Arrays.hashCode(x); | ||
| } | ||
|
|
||
| @Override | ||
| public float[] nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) | ||
| throws SQLException { | ||
| String value = rs.getString(position); | ||
| if (rs.wasNull() || value == null) { | ||
| return null; | ||
| } | ||
| String[] parts = value.substring(1, value.length() - 1).split(","); | ||
| float[] result = new float[parts.length]; | ||
| for (int i = 0; i < parts.length; i++) { | ||
| result[i] = Float.parseFloat(parts[i].trim()); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| @Override | ||
| public void nullSafeSet(PreparedStatement st, float[] value, int index, SharedSessionContractImplementor session) | ||
| throws SQLException { | ||
| if (value == null) { | ||
| st.setNull(index, Types.OTHER); | ||
| return; | ||
| } | ||
| StringBuilder sb = new StringBuilder("["); | ||
| for (int i = 0; i < value.length; i++) { | ||
| if (i > 0) sb.append(","); | ||
| sb.append(value[i]); | ||
| } | ||
| sb.append("]"); | ||
| PGobject pgObject = new PGobject(); | ||
| pgObject.setType("vector"); | ||
| pgObject.setValue(sb.toString()); | ||
| st.setObject(index, pgObject); | ||
| } | ||
|
|
||
| @Override | ||
| public float[] deepCopy(float[] value) { | ||
| return value == null ? null : Arrays.copyOf(value, value.length); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isMutable() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public Serializable disassemble(float[] value) { | ||
| return deepCopy(value); | ||
| } | ||
|
|
||
| @Override | ||
| public float[] assemble(Serializable cached, Object owner) { | ||
| return deepCopy((float[]) cached); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||||
| -- embeddings.vector: TEXT NOT NULL → vector(1024) NOT NULL | ||||||||
| -- 개발 환경 기준 실 데이터 없음을 전제로 drop/add 방식 사용 | ||||||||
| ALTER TABLE embeddings DROP COLUMN vector; | ||||||||
| ALTER TABLE embeddings ADD COLUMN vector vector(1024) NOT NULL; | ||||||||
|
Comment on lines
+3
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win 실 데이터 유실 위험이 있는 개발 환경에만 적용된다는 가정이 있으나, 해당 Flyway 마이그레이션 스크립트가 운영 및 스테이징 환경에 배포될 경우 As per coding guidelines, do not make assumptions silently; state assumptions, surface uncertainty, and present multiple interpretations when applicable. 🛡️ Proposed fix to preserve data-ALTER TABLE embeddings DROP COLUMN vector;
-ALTER TABLE embeddings ADD COLUMN vector vector(1024) NOT NULL;
+ALTER TABLE embeddings ALTER COLUMN vector TYPE vector(1024) USING vector::vector;📝 Committable suggestion
Suggested change
🧰 Tools🪛 Squawk (2.59.0)[warning] 3-3: Dropping a column may break existing clients. (ban-drop-column) [warning] 4-4: Adding a new column that is (adding-required-field) 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||
|
|
||||||||
| -- search_queries.query_vector: TEXT → vector(1024) (nullable 유지) | ||||||||
| ALTER TABLE search_queries ALTER COLUMN query_vector TYPE vector(1024) USING query_vector::vector; | ||||||||
|
|
||||||||
| -- HNSW 인덱스: 코사인 거리 기반 ANN 검색 | ||||||||
| CREATE INDEX ON embeddings USING hnsw (vector vector_cosine_ops); | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| -- BAAI/bge-m3 임베딩 모델 seed (HUGGINGFACE, 1024차원, COSINE) | ||
| -- 기존 active+searchable 모델을 비활성화한 뒤 bge-m3를 활성 모델로 등록한다. | ||
| -- ON CONFLICT: provider+model_name+model_version unique 제약 기준으로 upsert. | ||
| UPDATE embedding_models | ||
| SET is_active = FALSE, is_searchable = FALSE | ||
| WHERE is_active = TRUE AND is_searchable = TRUE | ||
| AND model_name != 'BAAI/bge-m3'; | ||
|
|
||
| INSERT INTO embedding_models ( | ||
| provider, model_name, model_version, dimension, | ||
| distance_metric, is_active, is_searchable, vector_storage_strategy, | ||
| created_at, updated_at | ||
| ) VALUES ( | ||
| 'HUGGINGFACE', 'BAAI/bge-m3', '1.0', 1024, | ||
| 'COSINE', TRUE, TRUE, 'SINGLE_DIMENSION', | ||
| CURRENT_TIMESTAMP, CURRENT_TIMESTAMP | ||
| ) | ||
| ON CONFLICT (provider, model_name, model_version) | ||
| DO UPDATE SET | ||
| is_active = TRUE, | ||
| is_searchable = TRUE, | ||
| dimension = EXCLUDED.dimension, | ||
| distance_metric = EXCLUDED.distance_metric, | ||
| vector_storage_strategy = EXCLUDED.vector_storage_strategy, | ||
| updated_at = CURRENT_TIMESTAMP; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 429
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 3592
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 1063
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 11505
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 34404
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 3016
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 9978
🏁 Script executed:
Repository: DocGrid/backend
Length of output: 1621
V32__convert_vector_columns_to_pgvector.sql는 drop/add 대신 변환으로 바꾸세요.embeddings.vector를 삭제했다가vector(1024) NOT NULL로 다시 추가하면 기존 행의 벡터가 유실되고, 기본값 없는NOT NULL추가도 실패합니다.ALTER COLUMN ... TYPE vector(1024) USING ...로 변환하거나 새 컬럼 추가 → 백필 →NOT NULL적용 순서로 처리해야 합니다.🤖 Prompt for AI Agents