282 lines
9.5 KiB
Markdown
282 lines
9.5 KiB
Markdown
# Research: Neurosurgeon RAG Learning Platform
|
|
|
|
**Branch**: `001-neuro-rag-learning`
|
|
**Date**: 2026-03-31
|
|
|
|
---
|
|
|
|
## 1. Spring Boot 4 + Spring AI Versions & BOM
|
|
|
|
**Decision**: Spring Boot **4.0.5** + Spring AI **1.1.4**.
|
|
|
|
**Rationale**: Spring Boot 4.0.5 is GA (released February 2026) — this matches the user's
|
|
original requirement. Spring AI 1.1.4 is the current stable release compatible with
|
|
Spring Boot 4.x. Spring AI 2.0.0-M4 is available in preview but not used (KISS — no
|
|
preview dependencies).
|
|
|
|
**Maven BOM** (in `<dependencyManagement>`):
|
|
```xml
|
|
<dependency>
|
|
<groupId>org.springframework.ai</groupId>
|
|
<artifactId>spring-ai-bom</artifactId>
|
|
<version>1.1.4</version>
|
|
<type>pom</type>
|
|
<scope>import</scope>
|
|
</dependency>
|
|
```
|
|
|
|
**Key starters** (versions managed by BOM):
|
|
```xml
|
|
<!-- pgvector vector store -->
|
|
<dependency>
|
|
<groupId>org.springframework.ai</groupId>
|
|
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
|
|
</dependency>
|
|
|
|
<!-- OpenAI (embedding + chat; swap for any other provider) -->
|
|
<dependency>
|
|
<groupId>org.springframework.ai</groupId>
|
|
<artifactId>spring-ai-starter-openai</artifactId>
|
|
</dependency>
|
|
|
|
<!-- PDF document reader (Spring AI native, Apache PDFBox-based) -->
|
|
<dependency>
|
|
<groupId>org.springframework.ai</groupId>
|
|
<artifactId>spring-ai-pdf-document-reader</artifactId>
|
|
</dependency>
|
|
|
|
<!-- PostgreSQL JDBC driver -->
|
|
<dependency>
|
|
<groupId>org.postgresql</groupId>
|
|
<artifactId>postgresql</artifactId>
|
|
<scope>runtime</scope>
|
|
</dependency>
|
|
```
|
|
|
|
**Alternatives considered**: Spring AI 2.0.0-M4 — rejected (milestone, KISS principle).
|
|
|
|
---
|
|
|
|
## 2. Spring AI RAG Pipeline
|
|
|
|
**Decision**: Use Spring AI's `PagePdfDocumentReader`, `EmbeddingModel`, `PgVectorStore`,
|
|
and `ChatClient` with `QuestionAnswerAdvisor` for RAG.
|
|
|
|
**Key classes**:
|
|
|
|
| Component | Class / Interface | Purpose |
|
|
|-----------|-------------------|---------|
|
|
| Document ingestion | `PagePdfDocumentReader` | Parse PDF pages to `Document` objects |
|
|
| Chunking | `TokenCountBatchingStrategy` | Split docs to respect token limits |
|
|
| Embedding | `EmbeddingModel` | Convert text chunks to vectors |
|
|
| Storage | `VectorStore` / `PgVectorStore` | Persist and search embeddings |
|
|
| RAG query | `QuestionAnswerAdvisor` | Augments prompt with retrieved context |
|
|
| Chat | `ChatClient` | Fluent API for LLM interactions |
|
|
|
|
**RAG pipeline flow**:
|
|
```
|
|
PDF file
|
|
→ PagePdfDocumentReader (extract text per page as Document)
|
|
→ TokenCountBatchingStrategy (chunk to embedding token limit)
|
|
→ EmbeddingModel.embed() (vectorise each chunk)
|
|
→ PgVectorStore.add() (persist chunk + vector + metadata)
|
|
|
|
User query
|
|
→ ChatClient.prompt()
|
|
.advisors(new QuestionAnswerAdvisor(vectorStore))
|
|
.user(question)
|
|
.call()
|
|
→ QuestionAnswerAdvisor runs similaritySearch, injects context
|
|
→ ChatModel generates response grounded in retrieved chunks
|
|
```
|
|
|
|
**application.properties**:
|
|
```properties
|
|
spring.ai.vectorstore.pgvector.dimensions=1536
|
|
spring.ai.vectorstore.pgvector.distance-type=COSINE_DISTANCE
|
|
spring.ai.vectorstore.pgvector.index-type=HNSW
|
|
spring.ai.vectorstore.pgvector.initialize-schema=true
|
|
|
|
spring.ai.openai.api-key=${OPENAI_API_KEY}
|
|
spring.ai.openai.chat.options.model=gpt-4o
|
|
spring.ai.openai.embedding.options.model=text-embedding-3-small
|
|
```
|
|
|
|
**Rationale**: `QuestionAnswerAdvisor` is the Spring AI-idiomatic RAG pattern — zero
|
|
boilerplate. `EmbeddingModel` and `ChatClient` are interfaces; swapping the LLM provider
|
|
is a single property change (Principle II).
|
|
|
|
---
|
|
|
|
## 3. PDF Ingestion & Chunking
|
|
|
|
**Decision**: `PagePdfDocumentReader` (from `spring-ai-pdf-document-reader`) for text
|
|
extraction; default `TokenCountBatchingStrategy` for chunking.
|
|
|
|
**Approach**:
|
|
```java
|
|
PagePdfDocumentReader reader = new PagePdfDocumentReader(
|
|
new FileSystemResource("textbook.pdf"),
|
|
PdfDocumentReaderConfig.builder()
|
|
.withPagesPerDocument(1) // one Document per page
|
|
.build()
|
|
);
|
|
List<Document> pages = reader.get();
|
|
vectorStore.add(pages); // batching + embedding handled internally
|
|
```
|
|
|
|
- Each `Document` carries metadata: source filename, page number.
|
|
- `TokenCountBatchingStrategy` ensures chunks fit the embedding model's context window
|
|
(~8 000 tokens for OpenAI models).
|
|
- Custom metadata (`book_id`, `book_title`, `chunk_type`) is added before calling
|
|
`vectorStore.add()`.
|
|
|
|
**Rationale**: `PagePdfDocumentReader` is the recommended Spring AI PDF reader for
|
|
text-focused RAG — lighter than the Tika reader and purpose-built for PDFs.
|
|
|
|
**Alternatives considered**: `TikaDocumentReader` — provides multi-format support but is
|
|
heavier; rejected for POC (only PDFs are in scope).
|
|
|
|
---
|
|
|
|
## 4. Diagram / Visual Content Handling
|
|
|
|
**Decision**: Extract diagram captions and surrounding text as text chunks tagged
|
|
`chunk_type=diagram`. No pixel-level image embedding for the POC.
|
|
|
|
**Approach**:
|
|
- `PagePdfDocumentReader` extracts all text including figure captions
|
|
(e.g., `"Figure 3.2: Circle of Willis anatomy..."`).
|
|
- A post-processing step identifies lines matching caption patterns
|
|
(`^(Figure|Fig\.|Table|Diagram)\s+[\d.]+`) and tags those `Document` objects with
|
|
`metadata.put("chunk_type", "diagram")`.
|
|
- The caption text plus the surrounding descriptive paragraph are included in the chunk,
|
|
making the diagram content semantically searchable.
|
|
|
|
**Rationale**: This is the simplest approach that satisfies FR-003 within KISS constraints.
|
|
The spec explicitly excludes pixel-level image search from the POC scope.
|
|
|
|
**Future upgrade path**: Use a vision model (GPT-4o vision) to generate text descriptions
|
|
of extracted images and add them as additional `chunk_type=diagram` documents — no
|
|
architectural change needed, just a new processing step.
|
|
|
|
---
|
|
|
|
## 5. Simple Shared-Password Authentication
|
|
|
|
**Decision**: Spring Security HTTP Basic with a single in-memory user.
|
|
|
|
**Spring Security config**:
|
|
```java
|
|
@Configuration
|
|
@EnableWebSecurity
|
|
public class SecurityConfig {
|
|
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
|
|
.httpBasic(Customizer.withDefaults())
|
|
.csrf(AbstractHttpConfigurer::disable); // REST API — no CSRF needed
|
|
return http.build();
|
|
}
|
|
|
|
@Bean
|
|
public UserDetailsService userDetailsService(
|
|
@Value("${app.auth.password}") String password) {
|
|
UserDetails user = User.builder()
|
|
.username("neurosurgeon")
|
|
.password("{noop}" + password) // {noop} = plain text for POC
|
|
.roles("USER")
|
|
.build();
|
|
return new InMemoryUserDetailsManager(user);
|
|
}
|
|
}
|
|
```
|
|
|
|
```properties
|
|
# application.properties
|
|
app.auth.password=${APP_PASSWORD}
|
|
```
|
|
|
|
**Rationale**: Zero database dependency; zero token management. The Vue.js frontend
|
|
sets `Authorization: Basic <base64>` via Axios `auth` config. Fully sufficient for
|
|
< 10 trusted users on a private network (POC constraint).
|
|
|
|
**Alternatives considered**: JWT — rejected (requires token endpoint, more code);
|
|
custom API key filter — rejected (HTTP Basic is simpler and just as secure for this scale).
|
|
|
|
---
|
|
|
|
## 6. Vue.js 3 Project Structure
|
|
|
|
**Decision**: Vite + Vue 3 + TypeScript + Pinia + Vue Router + Axios.
|
|
|
|
**Standard layout** (`npm create vue@latest`):
|
|
```
|
|
frontend/src/
|
|
├── components/ # Reusable UI components (BookCard, ChatMessage, etc.)
|
|
├── views/ # Route-level pages: UploadView, TopicsView, ChatView
|
|
├── stores/ # Pinia: bookStore, topicStore, chatStore
|
|
├── services/ # api.ts — Axios instance with base URL + Basic auth header
|
|
├── router/ # index.ts — Vue Router routes
|
|
└── main.ts
|
|
```
|
|
|
|
**Axios setup** (`services/api.ts`):
|
|
```typescript
|
|
import axios from 'axios'
|
|
|
|
export const api = axios.create({
|
|
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
|
auth: {
|
|
username: 'neurosurgeon',
|
|
password: import.meta.env.VITE_APP_PASSWORD
|
|
}
|
|
})
|
|
```
|
|
|
|
**Rationale**: Axios handles HTTP Basic auth via its `auth` config — no manual
|
|
`btoa()` needed. Pinia is Vue's official state manager (replaced Vuex).
|
|
|
|
---
|
|
|
|
## 7. pgvector Configuration & Schema
|
|
|
|
**Decision**: Spring AI auto-creates the `vector_store` table via `initialize-schema=true`.
|
|
Application tables use Flyway migrations.
|
|
|
|
**Required PostgreSQL extensions** (run once on the provided database):
|
|
```sql
|
|
CREATE EXTENSION IF NOT EXISTS vector;
|
|
CREATE EXTENSION IF NOT EXISTS hstore;
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
```
|
|
|
|
**Spring AI auto-created table**:
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS vector_store (
|
|
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
|
content TEXT NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
embedding VECTOR(1536) NOT NULL
|
|
);
|
|
CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);
|
|
```
|
|
|
|
**Key properties**:
|
|
|
|
| Property | Value | Notes |
|
|
|----------|-------|-------|
|
|
| `dimensions` | `1536` | Matches `text-embedding-3-small`; update if provider changes |
|
|
| `distance-type` | `COSINE_DISTANCE` | Standard for normalised text embeddings |
|
|
| `index-type` | `HNSW` | O(log N) search; best default for POC |
|
|
| `initialize-schema` | `true` | Auto-create table on startup (safe for POC) |
|
|
|
|
**Embedding dimensions note**: if the LLM provider is switched (e.g., to Ollama with a
|
|
768-dim model), update `dimensions` in properties **and** re-embed all books — the
|
|
`vector_store` table must be recreated with the new dimension.
|
|
|
|
**Alternatives considered**: IVFFlat index — more memory-efficient but slower; NONE for very
|
|
small datasets. HNSW is the best default for a POC where correctness matters more than
|
|
storage.
|