-
Notifications
You must be signed in to change notification settings - Fork 0
β¨ Feature: indexing #18
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 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,35 +31,35 @@ ext { | |||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| dependencies { | ||||||||||||||
| implementation("com.fasterxml.jackson.module:jackson-module-kotlin") | ||||||||||||||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||||||||||||||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | ||||||||||||||
| implementation 'org.springframework.boot:spring-boot-starter-web' | ||||||||||||||
| implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' | ||||||||||||||
| implementation 'org.jetbrains.kotlin:kotlin-reflect' | ||||||||||||||
| // implementation 'org.springframework.ai:spring-ai-starter-model-openai' | ||||||||||||||
| implementation 'org.springframework.ai:spring-ai-starter-model-openai' | ||||||||||||||
| compileOnly 'org.projectlombok:lombok' | ||||||||||||||
| annotationProcessor 'org.projectlombok:lombok' | ||||||||||||||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||||||||||||||
| testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' | ||||||||||||||
| testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") | ||||||||||||||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||||||||||||||
|
|
||||||||||||||
| // postgresql | ||||||||||||||
| implementation 'org.postgresql:postgresql' | ||||||||||||||
| implementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector' | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+47
to
50
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. π§Ή Nitpick (assertive) Prefer The driver isnβt needed at compile time. Switching to Apply: - implementation 'org.postgresql:postgresql'
+ runtimeOnly 'org.postgresql:postgresql'π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||
| // test | ||||||||||||||
| testImplementation "org.springframework.boot:spring-boot-testcontainers" | ||||||||||||||
| testImplementation "org.testcontainers:postgresql" | ||||||||||||||
| testImplementation "org.testcontainers:junit-jupiter" | ||||||||||||||
| testImplementation "com.pgvector:pgvector:0.1.6" | ||||||||||||||
| testImplementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector' | ||||||||||||||
|
|
||||||||||||||
|
Comment on lines
+49
to
56
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. π§Ή Nitpick (assertive) Unnecessary duplicate dependency in tests spring-ai-starter-vector-store-pgvector is already on the main classpath; duplicating in tests can mask classpath issues and slow resolution. Apply: - testImplementation 'org.springframework.ai:spring-ai-starter-vector-store-pgvector'π€ Prompt for AI Agents |
||||||||||||||
| // swagger | ||||||||||||||
| implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' | ||||||||||||||
|
|
||||||||||||||
| // s3 | ||||||||||||||
| implementation(platform("software.amazon.awssdk:bom:2.25.70")) | ||||||||||||||
| implementation("software.amazon.awssdk:s3") | ||||||||||||||
|
|
||||||||||||||
| // pgvector | ||||||||||||||
| implementation("com.pgvector:pgvector:0.1.6") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| dependencyManagement { | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,18 @@ | ||||||||||||||||||
| package simplerag.ragback.domain.index.dto | ||||||||||||||||||
|
|
||||||||||||||||||
| import jakarta.validation.constraints.NotBlank | ||||||||||||||||||
| import jakarta.validation.constraints.NotEmpty | ||||||||||||||||||
| import jakarta.validation.constraints.Positive | ||||||||||||||||||
| import jakarta.validation.constraints.PositiveOrZero | ||||||||||||||||||
| import org.hibernate.validator.constraints.Length | ||||||||||||||||||
| import simplerag.ragback.domain.index.entity.enums.EmbeddingModel | ||||||||||||||||||
| import simplerag.ragback.domain.index.entity.enums.SimilarityMetric | ||||||||||||||||||
|
|
||||||||||||||||||
| data class IndexCreateRequest( | ||||||||||||||||||
|
|
||||||||||||||||||
| @field:NotEmpty | ||||||||||||||||||
| val dataFileId: List<Long>, | ||||||||||||||||||
|
|
||||||||||||||||||
|
Comment on lines
+13
to
+15
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. π§Ή Nitpick (assertive) Enforce overlapSize β€ chunkingSize in the service (per team decision) Per retrieved learnings, the validation belongs in the service layer, not the entity. Ensure IndexService.createIndex guards this invariant and returns a 400 with a clear message when violated. I can add a guard + unit test (valid/invalid pairs) if you want me to push a patch. π€ Prompt for AI Agentsπ§Ή Nitpick (assertive) Prefer @SiZe(min=1) for lists and align naming to plural ("dataFileIds")
Apply: -import jakarta.validation.constraints.NotEmpty
+import jakarta.validation.constraints.Size
@@
- @field:NotEmpty
- val dataFileId: List<Long>,
+ @field:Size(min = 1, message = "μ΅μ νλ μ΄μμ νμΌ IDκ° νμν©λλ€")
+ val dataFileIds: List<Long>,π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||
| @field:Length(max = 255) | ||||||||||||||||||
| @field:NotBlank | ||||||||||||||||||
| val snapshotName: String, | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package simplerag.ragback.domain.index.embed | ||
|
|
||
| interface Embedder { | ||
| val dim: Int | ||
| fun embed(text: String): FloatArray | ||
| } | ||
|
Comment on lines
+3
to
+6
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. π‘ Verification agent π§© Analysis chainVerify implementor naming (βOpenAIEmbbederβ vs βOpenAIEmbedderβ) Typo in class/file names can confuse DI and readers. π Script executed: #!/bin/bash
# Find embedder implementations and spot typos
rg -nP --type=kotlin -C2 '(class|interface)\s+OpenAIEm[bB]ed{1,2}er|Embedder' | sed -n '1,120p'Length of output: 2492 Rename file to match class name β’ src/main/kotlin/simplerag/ragback/domain/index/embed/OpenAIEmbbeder.kt β rename to β OpenAIEmbedder.kt π€ Prompt for AI Agentsπ§Ή Nitpick (assertive) Add batch embedding to improve throughput; keep single-call default Batching reduces network overhead and cost. Provide a default method to avoid breaking implementors. Apply: interface Embedder {
val dim: Int
fun embed(text: String): FloatArray
+ fun embedAll(texts: List<String>): List<FloatArray> = texts.map(::embed)
}Optionally consider making these suspend and handling upstream backpressure later. π€ Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package simplerag.ragback.domain.index.embed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.ai.openai.OpenAiEmbeddingModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class OpenAIEmbedder( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val openAiEmbeddingModel: OpenAiEmbeddingModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : Embedder { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override val dim: Int = 1536 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override fun embed(text: String): FloatArray = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| openAiEmbeddingModel.embed(text) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+13
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. π οΈ Refactor suggestion Hard-coded dim=1536 will reject 3072-dim models; derive dimension from the configured model. Also, filename typo.
Apply this diff to make dim consistent with configuration (adjust imports if package paths differ): package simplerag.ragback.domain.index.embed
import org.springframework.ai.openai.OpenAiEmbeddingModel
+import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
+import simplerag.ragback.domain.index.entity.enums.EmbeddingModel
@Component
-class OpenAIEmbedder(
- private val openAiEmbeddingModel: OpenAiEmbeddingModel
+class OpenAIEmbedder(
+ private val openAiEmbeddingModel: OpenAiEmbeddingModel,
+ @Value("\${spring.ai.openai.embedding.options.model:text-embedding-3-small}")
+ private val modelId: String
) : Embedder {
- override val dim: Int = 1536
+ override val dim: Int =
+ EmbeddingModel.findByModelId(modelId)?.dim
+ ?: error("Unsupported embedding modelId: $modelId")
override fun embed(text: String): FloatArray =
openAiEmbeddingModel.embed(text)
}If you will support multiple models per index concurrently, consider a factory to return an Embedder bound to req.embeddingModel instead of a single global bean. π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| package simplerag.ragback.domain.index.entity | ||
|
|
||
| import com.pgvector.PGvector | ||
| import jakarta.persistence.* | ||
| import simplerag.ragback.global.entity.BaseEntity | ||
|
|
||
|
|
@@ -9,12 +8,11 @@ import simplerag.ragback.global.entity.BaseEntity | |
| @Table(name = "chunk_embeddings") | ||
| class ChunkEmbedding( | ||
|
|
||
| @Column(name = "content", nullable = false) | ||
| @Lob | ||
| @Column(name = "content", nullable = false, columnDefinition = "text") | ||
| val content: String, | ||
|
|
||
| @Column(name = "embedding", columnDefinition = "vector") | ||
| var embedding: PGvector, | ||
| @Column(name = "embedding", columnDefinition = "vector(1536)", nullable = false) | ||
| var embedding: FloatArray, | ||
|
|
||
|
Comment on lines
+14
to
16
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. Missing JPA converter on FloatArray β pgvector mapping (will persist as binary/bytea or fail).
Apply the converter and import it: @@
- @Column(name = "embedding", columnDefinition = "vector(1536)", nullable = false)
- var embedding: FloatArray,
+ @Column(name = "embedding", columnDefinition = "vector(1536)", nullable = false)
+ @Convert(converter = FloatArrayToPgVectorStringConverter::class)
+ var embedding: FloatArray,Add the missing import at the top of the file: import simplerag.ragback.global.util.FloatArrayToPgVectorStringConverterπ€ Prompt for AI Agents |
||
| @Column(name = "embedding_dim", nullable = false) | ||
| val embeddingDim: Int, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,24 +3,61 @@ package simplerag.ragback.domain.index.service | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.repository.findByIdOrNull | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.document.entity.DataFile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.document.repository.DataFileRepository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.index.dto.* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.index.embed.Embedder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.index.entity.ChunkEmbedding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.index.entity.Index | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.index.repository.IndexRepository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.error.CustomException | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.error.ErrorCode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.error.IndexException | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.loader.ContentLoader | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.TextChunker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class IndexService( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val indexRepository: IndexRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val embedder: Embedder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val dataFileRepository: DataFileRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val contentLoader: ContentLoader, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createIndex(indexCreateRequest: IndexCreateRequest): IndexPreviewResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fun createIndex(req: IndexCreateRequest): IndexPreviewResponse { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateOverlap(req.overlapSize, req.chunkingSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val files: List<DataFile> = dataFileRepository.findAllById(req.dataFileId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (files.size != req.dataFileId.size) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw CustomException(ErrorCode.NOT_FOUND, "Some dataFileIds not found") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+35
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. π§Ή Nitpick (assertive) Error message consistency and clarity for missing dataFileIds. Thrown message mixes English while other messages are localized. Also, returning which IDs are missing helps clients debug. - if (files.size != req.dataFileId.size) {
- throw CustomException(ErrorCode.NOT_FOUND, "Some dataFileIds not found")
- }
+ if (files.size != req.dataFileId.size) {
+ val foundIds = files.mapNotNull { it.id }.toSet()
+ val missing = req.dataFileId.filterNot { it in foundIds }
+ throw CustomException(
+ ErrorCode.NOT_FOUND,
+ "μ‘΄μ¬νμ§ μλ dataFileIds: $missing"
+ )
+ }π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (embedder.dim != req.embeddingModel.dim) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw CustomException(ErrorCode.INVALID_INPUT, "Embedding dim mismatch: model=${req.embeddingModel.dim}, embedder=${embedder.dim}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val index = indexRepository.save(Index.toIndex(req)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+40
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. π‘ Verification agent π§© Analysis chainDim check is good, but it will always fail if OpenAIEmbedder stays at 1536 with a 3072-dim request. Please either (a) make the embedderβs dim reflect the configured OpenAI model (see my OpenAIEmbedder refactor), or (b) restrict allowed embedding models at request validation time to the single supported dimension. I recommend (a) for flexibility; otherwise enforce EmbeddingModel.TEXT_EMBEDDING_3_SMALL at the controller validation layer. π Script executed: #!/bin/bash
# Search for OpenAIEmbedder class definition and inspect its dimension handling
rg -n 'class OpenAIEmbedder' -g '*.kt'
rg -n -A5 -B5 'class OpenAIEmbedder' -g '*.kt'
# Find any declarations or assignments of a 'dim' property in the codebase
rg -n 'val dim' -g '*.kt'
# Locate the EmbeddingModel enum to see supported dimensions
rg -n 'enum class EmbeddingModel' -g '*.kt'
rg -n -A5 -B5 'enum class EmbeddingModel' -g '*.kt'
# Inspect the IndexService snippet around the embedder.dim check for context
rg -n -A5 -B5 'if (embedder\\.dim' src/main/kotlin/simplerag/ragback/domain/index/service/IndexService.ktLength of output: 2875 Ensure Key locations:
Actionable options:
I recommend option (1) for future flexibility; if you choose (2), enforce it in your request DTO validation. π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (file in files) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val url = file.fileUrl | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val content = contentLoader.load(url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| println(content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (content.isBlank()) continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val chunks = TextChunker.chunkByCharsSeq(content, req.chunkingSize, req.overlapSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (chunk in chunks) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val vec = embedder.embed(chunk) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val entity = ChunkEmbedding( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| content = chunk, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embedding = vec, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| embeddingDim = embedder.dim, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index = index | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| index.chunkEmbeddings.add(entity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+57
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. π§Ή Nitpick (assertive) Batch embeddings for throughput; avoid per-chunk remote calls. Current loop does one remote call per chunk. If using Spring AI, the underlying model typically supports batch inputs. Proposed interface and usage (outside this file): // in Embedder.kt
interface Embedder {
val dim: Int
fun embed(text: String): FloatArray
fun embedAll(texts: List<String>): List<FloatArray> = texts.map { embed(it) } // default
}Then update here to batch: - val chunks = TextChunker.chunkByCharsSeq(content, req.chunkingSize, req.overlapSize)
- for (chunk in chunks) {
- val vec = embedder.embed(chunk)
+ val chunks = TextChunker.chunkByCharsSeq(content, req.chunkingSize, req.overlapSize)
+ val vectors = embedder.embedAll(chunks)
+ for ((i, chunk) in chunks.withIndex()) {
+ val vec = vectors[i]
val entity = ChunkEmbedding(
content = chunk,
embedding = vec,
embeddingDim = embedder.dim,
index = index
)
index.chunkEmbeddings.add(entity)
}π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+58
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. π§Ή Nitpick (assertive) π οΈ Refactor suggestion Avoid println of raw content, check null/blank URLs, and handle loader failures without keeping a DB transaction open.
Minimal safety and logging fix: - val url = file.fileUrl
- val content = contentLoader.load(url)
- println(content)
- if (content.isBlank()) continue
+ val url = file.fileUrl ?: continue
+ val content = try {
+ contentLoader.load(url)
+ } catch (ex: Exception) {
+ // TODO: map to domain exception or skip with WARN
+ ""
+ }
+ if (content.isBlank()) continue // consider WARN logLonger-term architecture:
π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validateOverlap(indexCreateRequest.overlapSize, indexCreateRequest.chunkingSize) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val createdIndex = indexRepository.save(Index.toIndex(indexCreateRequest)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return IndexPreviewResponse.toIndexPreviewResponse(createdIndex) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return IndexPreviewResponse.toIndexPreviewResponse(index) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Transactional(readOnly = true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package simplerag.ragback.global.util | ||
|
|
||
| object TextChunker { | ||
| fun chunkByCharsSeq(raw: String, size: Int, overlap: Int): Sequence<String> = sequence { | ||
|
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. π§Ή Nitpick (assertive) Consider an optional word-boundary mode to reduce mid-word splits. Character windows are fine for a first pass. If quality matters, add a whitespace-aware variant (e.g., try to extend to the next whitespace within a small slack). I can add π€ Prompt for AI Agents |
||
| require(size >= 1) { "chunk size must be >= 1" } | ||
| require(overlap in 0 until size) { "overlap must be 0..size-1" } | ||
|
|
||
| val text = normalize(raw) | ||
| if (text.isBlank()) return@sequence | ||
| if (text.length <= size) { yield(text); return@sequence } | ||
|
|
||
| val step = size - overlap | ||
| var start = 0 | ||
| while (start < text.length) { | ||
| val end = (start + size).coerceAtMost(text.length) | ||
| yield(text.substring(start, end)) | ||
| if (end == text.length) break | ||
| start += step | ||
| } | ||
| } | ||
|
|
||
| private fun normalize(s: String): String = | ||
| s.replace("\r\n", "\n").replace("\r", "\n") | ||
| .replace(Regex("[ \t]+"), " ") | ||
| .replace(Regex("\\n{3,}"), "\n\n") | ||
| .trim() | ||
|
Comment on lines
+22
to
+26
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. π§Ή Nitpick (assertive) Avoid recompiling regexes on every call; precompile once. Creating -object TextChunker {
+object TextChunker {
+ private val SPACES = Regex("[ \\t]+")
+ private val MANY_NL = Regex("\\n{3,}")
@@
- private fun normalize(s: String): String =
- s.replace("\r\n", "\n").replace("\r", "\n")
- .replace(Regex("[ \t]+"), " ")
- .replace(Regex("\\n{3,}"), "\n\n")
- .trim()
+ private fun normalize(s: String): String =
+ s.replace("\r\n", "\n").replace("\r", "\n")
+ .replace(SPACES, " ")
+ .replace(MANY_NL, "\n\n")
+ .trim()π€ Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,6 @@ | ||||||||||||||||||||||||||||
| package simplerag.ragback.global.util.loader | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| interface ContentLoader { | ||||||||||||||||||||||||||||
| fun load(url: String): String | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+6
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. π§Ή Nitpick (assertive) Clarify error semantics; avoid empty-string sentinels. Interface returns String, and the HttpContentLoader impl reportedly returns "" on null body. Prefer explicit failure signaling to avoid treating errors as βblank contentβ. Minimal change: add KDoc and commit to throwing a typed exception on failure (or switch to Result if you can ripple changes). @@
-interface ContentLoader {
- fun load(url: String): String
-}
+/**
+ * Loads textual content from a location (e.g., http/https URL).
+ *
+ * Contract:
+ * - Returns the fetched body as-is (UTF-8 normalized by the implementation).
+ * - Throws a domain exception (e.g., ContentLoadException) on failure instead of returning an empty string.
+ */
+interface ContentLoader {
+ fun load(url: String): String
+}I can also switch this to π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||||||
| package simplerag.ragback.global.util.loader | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import org.springframework.http.converter.StringHttpMessageConverter | ||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component | ||||||||||||||||||||||||||||||
| import org.springframework.web.client.RestTemplate | ||||||||||||||||||||||||||||||
| import java.nio.charset.StandardCharsets | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||
| class HttpContentLoader : ContentLoader { | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| private val restTemplate: RestTemplate = RestTemplate().apply { | ||||||||||||||||||||||||||||||
| // κΈ°μ‘΄ String 컨λ²ν° μ κ±° ν UTF-8 컨λ²ν°λ₯Ό 맨 μμ μΆκ° | ||||||||||||||||||||||||||||||
| val replaced = messageConverters.filterNot { it is StringHttpMessageConverter }.toMutableList() | ||||||||||||||||||||||||||||||
| replaced.add(0, StringHttpMessageConverter(StandardCharsets.UTF_8)) | ||||||||||||||||||||||||||||||
| messageConverters = replaced | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+16
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. π οΈ Refactor suggestion Prefer RestTemplateBuilder with explicit timeouts and UTF-8, avoid mutating converters list manually. Manually filtering and replacing converters is brittle. Also, no connect/read timeouts are set, so indexing can hang on slow endpoints. Apply this refactor: - private val restTemplate: RestTemplate = RestTemplate().apply {
- // κΈ°μ‘΄ String 컨λ²ν° μ κ±° ν UTF-8 컨λ²ν°λ₯Ό 맨 μμ μΆκ°
- val replaced = messageConverters.filterNot { it is StringHttpMessageConverter }.toMutableList()
- replaced.add(0, StringHttpMessageConverter(StandardCharsets.UTF_8))
- messageConverters = replaced
- }
+ private val restTemplate: RestTemplate = org.springframework.boot.web.client.RestTemplateBuilder()
+ .setConnectTimeout(java.time.Duration.ofSeconds(5))
+ .setReadTimeout(java.time.Duration.ofSeconds(15))
+ .additionalMessageConverters(StringHttpMessageConverter(StandardCharsets.UTF_8))
+ .build()If you have a central RestTemplateBuilder bean, inject it via constructor for consistency and testability. π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| override fun load(url: String): String { | ||||||||||||||||||||||||||||||
| return restTemplate.getForObject(url, String::class.java) ?: "" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+20
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. π οΈ Refactor suggestion Handle HTTP exceptions and validate URL scheme to avoid SSRF and brittle failures. Load can throw RestClientException; currently it bubbles up. Also, validating allowed schemes (e.g., http/https/s3) reduces SSRF risk when URLs are user-controlled. - override fun load(url: String): String {
- return restTemplate.getForObject(url, String::class.java) ?: ""
- }
+ override fun load(url: String): String {
+ require(url.startsWith("http://") || url.startsWith("https://")) {
+ "Unsupported URL scheme"
+ }
+ return try {
+ restTemplate.getForObject(url, String::class.java) ?: ""
+ } catch (ex: org.springframework.web.client.RestClientException) {
+ // TODO: map to domain exception if desired
+ ""
+ }
+ }If URLs can be s3:// or file:// from your DataFile pipeline, whitelist those explicitly and route to specialized loaders. π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| package simplerag.ragback.global.util | ||
| package simplerag.ragback.global.util.s3 | ||
|
|
||
|
|
||
| enum class S3Type( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,4 @@ | ||||||||||||||||
| package simplerag.ragback.global.util | ||||||||||||||||
| package simplerag.ragback.global.util.s3 | ||||||||||||||||
|
|
||||||||||||||||
| import org.slf4j.LoggerFactory | ||||||||||||||||
| import org.springframework.context.annotation.Profile | ||||||||||||||||
|
|
@@ -58,9 +58,7 @@ class S3UtilImpl( | |||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| override fun urlFromKey(key: String): String = | ||||||||||||||||
| s3.utilities() | ||||||||||||||||
| .getUrl { it.bucket(bucket).key(key) } | ||||||||||||||||
| .toExternalForm() | ||||||||||||||||
| "https://mukit-s3.s3.ap-northeast-2.amazonaws.com/" + key | ||||||||||||||||
|
Comment on lines
60
to
+61
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. Hard-coded S3 URL will break across environments and mishandles URL encoding Using a fixed bucket/region string prevents deploy-time changes and can yield invalid URLs for keys with spaces or special chars. Let the AWS SDK construct the URL (handles region and encoding). Apply: - override fun urlFromKey(key: String): String =
- "https://mukit-s3.s3.ap-northeast-2.amazonaws.com/" + key
+ override fun urlFromKey(key: String): String =
+ s3.utilities().getUrl { it.bucket(bucket).key(key) }.toExternalForm()π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| override fun deleteByUrl(url: String) { | ||||||||||||||||
| val key = keyFromUrl(url) ?: throw S3Exception(ErrorCode.S3_INVALID_URL) | ||||||||||||||||
|
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,6 @@ package simplerag.ragback.domain.document.service | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.AfterEach | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.Assertions.* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.BeforeEach | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.DisplayName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.junit.jupiter.api.Test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.beans.factory.annotation.Autowired | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.support.TransactionTemplate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.web.multipart.MultipartFile | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.containers.PostgreSQLContainer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.junit.jupiter.Container | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.testcontainers.utility.DockerImageName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.document.dto.DataFileBulkCreateRequest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.domain.document.dto.DataFileCreateItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -26,18 +24,18 @@ import simplerag.ragback.global.error.CustomException | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.error.ErrorCode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.error.FileException | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.storage.FakeS3Util | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.S3Type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.sha256Hex | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.s3.S3Type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import simplerag.ragback.global.util.converter.sha256Hex | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.security.MessageDigest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @SpringBootTest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @ActiveProfiles("test") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class DataFileServiceTest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val dataFileService: DataFileService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val dataFileRepository: DataFileRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val tagRepository: TagRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val dataFileTagRepository: DataFileTagRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private val s3Util: FakeS3Util | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired val dataFileService: DataFileService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired val dataFileRepository: DataFileRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired val tagRepository: TagRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired val dataFileTagRepository: DataFileTagRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Autowired val s3Util: FakeS3Util | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
31
to
39
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. π§Ή Nitpick (assertive) Constructor injection in tests: add @Testconstructor(autowireMode = ALL) for clarity and portability. Relying on @Autowired on constructor params alone can be fragile across Boot versions. Align with IndexServiceTest by enabling constructor autowiring explicitly. @SpringBootTest
@ActiveProfiles("test")
+@org.springframework.test.context.TestConstructor(autowireMode = org.springframework.test.context.TestConstructor.AutowireMode.ALL)
class DataFileServiceTest(
- @Autowired val dataFileService: DataFileService,
- @Autowired val dataFileRepository: DataFileRepository,
- @Autowired val tagRepository: TagRepository,
- @Autowired val dataFileTagRepository: DataFileTagRepository,
- @Autowired val s3Util: FakeS3Util
+ @Autowired val dataFileService: DataFileService,
+ @Autowired val dataFileRepository: DataFileRepository,
+ @Autowired val tagRepository: TagRepository,
+ @Autowired val dataFileTagRepository: DataFileTagRepository,
+ @Autowired val s3Util: FakeS3Util
)π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,8 +27,8 @@ import org.testcontainers.utility.DockerImageName | |
| @ActiveProfiles("test") | ||
| @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) | ||
| class IndexServiceTest( | ||
| val indexService: IndexService, | ||
| val indexRepository: IndexRepository, | ||
| @Autowired val indexService: IndexService, | ||
| @Autowired val indexRepository: IndexRepository, | ||
|
Comment on lines
+30
to
+31
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. π§Ή Nitpick (assertive) Drop redundant @Autowired on constructor params (you already enabled constructor autowiring). Since the class is annotated with @Testconstructor(autowireMode = ALL), the @Autowired annotations on the primary constructor parameters are unnecessary noise. Apply this diff: -class IndexServiceTest(
- @Autowired val indexService: IndexService,
- @Autowired val indexRepository: IndexRepository,
+class IndexServiceTest(
+ val indexService: IndexService,
+ val indexRepository: IndexRepository,
)π€ Prompt for AI Agents |
||
| ) { | ||
|
|
||
|
|
||
|
|
||
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.
π‘ Verification agent
π§© Analysis chain
OpenAI starter added β ensure tests donβt require a real API key or make network calls
Adding
spring-ai-starter-model-openaiwill create OpenAI beans at context startup. Make sure yourtestprofile stubs or disables outbound calls so unit tests are hermetic.Run this to verify thereβs a test profile and it isolates OpenAI config:
If you want, I can provide a minimal
application-test.ymlthat sets a dummy key and a@TestConfigurationthat supplies a no-opEmbeddingModelbean for tests.π Script executed:
Length of output: 317
π Script executed:
Length of output: 1699
π Script executed:
Length of output: 235
Configure test profile to stub or disable OpenAI beans
Your
application-test.yml(src/test/resources/application-test.yml) only configures the datasource and JPAβthere are no overrides forspring.ai.openai.*, and no@TestConfigurationto stub out the OpenAI clients. Because your tests use@SpringBootTestwith the βtestβ profile, they will attempt to instantiate the real OpenAI beans at startup, leading to missing-API-key errors or unwanted network calls.β’ application-test.yml (lines 1β200) shows only:
(no
spring.ai.openai.*props)β’ No
@TestConfigurationor@MockBeanwas found to overrideEmbeddingModel,ChatCompletionModel, or the OpenAI client.β’ Tests in src/test/kotlin/** annotated with
@SpringBootTest+@ActiveProfiles("test")will load the real starter beans and fail (or hit the network).To fix, you must:
Add dummy OpenAI props to your test profile:
Stub out the OpenAI beans, for example via a
@TestConfigurationclass insrc/test/javaorsrc/test/kotlin:(Alternate) Use
@MockBeanin your test classes:Let me know if youβd like a complete minimal example of
application-test.ymlplus testβstub config.π€ Prompt for AI Agents