Skip to content

Commit b820890

Browse files
committed
add spring ai vector with pgvector
add spring security 6 add assistant suggestion with vector database
1 parent 6431c1e commit b820890

22 files changed

Lines changed: 206 additions & 118 deletions

File tree

compose.yaml

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,6 @@ services:
1414
# db:
1515
# condition: service_healthy
1616

17-
db:
18-
image: mysql:latest
19-
environment:
20-
MYSQL_DATABASE: "spring"
21-
MYSQL_PASSWORD: "password"
22-
MYSQL_ROOT_PASSWORD: "password"
23-
ports:
24-
- "3306:3306"
25-
expose:
26-
- "3306"
27-
volumes:
28-
- db:/var/lib/mysql
29-
healthcheck:
30-
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-ppassword" ]
31-
interval: 30s
32-
timeout: 10s
33-
retries: 5
34-
3517
prometheus:
3618
container_name: prometheus
3719
image: prom/prometheus
@@ -99,6 +81,23 @@ services:
9981
timeout: 10s
10082
retries: 5
10183

84+
postgres:
85+
image: pgvector/pgvector:pg17
86+
container_name: postgres_pgvector
87+
environment:
88+
POSTGRES_USER: root
89+
POSTGRES_PASSWORD: password
90+
POSTGRES_DB: ecommerce
91+
ports:
92+
- "5432:5432"
93+
volumes:
94+
- postgres_data:/var/lib/postgresql/data
95+
healthcheck:
96+
test: [ "CMD-SHELL", "pg_isready -U root -d ecommerce" ]
97+
interval: 10s
98+
timeout: 10s
99+
retries: 3
100+
102101
volumes:
103-
db:
104102
redis_data:
103+
postgres_data:

pom.xml

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,17 @@
1616
<properties>
1717
<java.version>17</java.version>
1818
</properties>
19-
<repositories>
20-
<repository>
21-
<id>spring-milestones</id>
22-
<name>Spring Milestones</name>
23-
<url>https://repo.spring.io/milestone</url>
24-
<snapshots>
25-
<enabled>false</enabled>
26-
</snapshots>
27-
</repository>
28-
<repository>
29-
<id>spring-snapshots</id>
30-
<name>Spring Snapshots</name>
31-
<url>https://repo.spring.io/snapshot</url>
32-
<releases>
33-
<enabled>false</enabled>
34-
</releases>
35-
</repository>
36-
</repositories>
3719
<dependencies>
3820
<dependency>
3921
<groupId>org.springframework.boot</groupId>
4022
<artifactId>spring-boot-starter-data-jpa</artifactId>
4123
</dependency>
42-
<dependency>
43-
<groupId>org.springframework.boot</groupId>
44-
<artifactId>spring-boot-starter-mail</artifactId>
45-
</dependency>
24+
4625
<dependency>
4726
<groupId>org.springframework.boot</groupId>
4827
<artifactId>spring-boot-starter-security</artifactId>
4928
</dependency>
29+
5030
<dependency>
5131
<groupId>org.springframework.boot</groupId>
5232
<artifactId>spring-boot-starter-web</artifactId>
@@ -58,21 +38,19 @@
5838
<scope>runtime</scope>
5939
<optional>true</optional>
6040
</dependency>
61-
<dependency>
62-
<groupId>com.mysql</groupId>
63-
<artifactId>mysql-connector-j</artifactId>
64-
<scope>runtime</scope>
65-
</dependency>
41+
6642
<dependency>
6743
<groupId>org.projectlombok</groupId>
6844
<artifactId>lombok</artifactId>
6945
<optional>true</optional>
7046
</dependency>
47+
7148
<dependency>
7249
<groupId>org.springframework.boot</groupId>
7350
<artifactId>spring-boot-starter-test</artifactId>
7451
<scope>test</scope>
7552
</dependency>
53+
7654
<dependency>
7755
<groupId>org.springframework.security</groupId>
7856
<artifactId>spring-security-test</artifactId>
@@ -84,6 +62,7 @@
8462
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
8563
<version>2.0.2</version>
8664
</dependency>
65+
8766
<dependency>
8867
<groupId>org.springframework.boot</groupId>
8968
<artifactId>spring-boot-starter-validation</artifactId>
@@ -94,65 +73,88 @@
9473
<artifactId>jjwt-api</artifactId>
9574
<version>0.11.5</version>
9675
</dependency>
76+
9777
<dependency>
9878
<groupId>io.jsonwebtoken</groupId>
9979
<artifactId>jjwt-impl</artifactId>
10080
<version>0.11.5</version>
10181
</dependency>
82+
10283
<dependency>
10384
<groupId>io.jsonwebtoken</groupId>
10485
<artifactId>jjwt-jackson</artifactId>
10586
<version>0.11.5</version>
10687
</dependency>
88+
10789
<dependency>
10890
<groupId>org.mapstruct</groupId>
10991
<artifactId>mapstruct</artifactId>
11092
<version>1.6.2</version>
11193
</dependency>
94+
11295
<dependency>
11396
<groupId>org.mapstruct</groupId>
11497
<artifactId>mapstruct-processor</artifactId>
11598
<version>1.6.2</version>
11699
<scope>provided</scope>
117100
</dependency>
101+
118102
<dependency>
119103
<groupId>io.micrometer</groupId>
120104
<artifactId>micrometer-tracing-bridge-brave</artifactId>
121105
</dependency>
106+
122107
<dependency>
123108
<groupId>io.zipkin.reporter2</groupId>
124109
<artifactId>zipkin-reporter-brave</artifactId>
125110
</dependency>
111+
126112
<dependency>
127113
<groupId>io.micrometer</groupId>
128114
<artifactId>micrometer-registry-prometheus</artifactId>
129115
<scope>runtime</scope>
130116
</dependency>
117+
131118
<dependency>
132119
<groupId>com.github.loki4j</groupId>
133120
<artifactId>loki-logback-appender</artifactId>
134121
<version>1.5.2</version>
135122
</dependency>
123+
136124
<dependency>
137125
<groupId>org.springframework.boot</groupId>
138126
<artifactId>spring-boot-starter-actuator</artifactId>
139127
</dependency>
128+
140129
<dependency>
141130
<groupId>org.springframework.boot</groupId>
142131
<artifactId>spring-boot-starter-data-redis</artifactId>
143132
</dependency>
133+
144134
<dependency>
145135
<groupId>org.springframework.ai</groupId>
146136
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
147137
</dependency>
148138

139+
<dependency>
140+
<groupId>org.springframework.ai</groupId>
141+
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
142+
<version>1.0.0-M6</version>
143+
</dependency>
144+
145+
<dependency>
146+
<groupId>org.postgresql</groupId>
147+
<artifactId>postgresql</artifactId>
148+
<scope>runtime</scope>
149+
</dependency>
150+
149151
</dependencies>
150152
<dependencyManagement>
151153
<dependencies>
152154
<dependency>
153155
<groupId>org.springframework.ai</groupId>
154156
<artifactId>spring-ai-bom</artifactId>
155-
<version>1.0.0-SNAPSHOT</version>
157+
<version>1.0.0-M6</version>
156158
<type>pom</type>
157159
<scope>import</scope>
158160
</dependency>

src/main/java/com/david/ecommerceapi/assistant/application/AssistantServiceImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.stereotype.Service;
88

9+
import java.util.Optional;
10+
911
@Service
1012
@RequiredArgsConstructor
1113
public class AssistantServiceImpl implements AssistantService {
1214

1315
private final AssistantSuggestion assistantSuggestion;
1416

15-
public Product getProductSuggestion(String message) {
17+
public Optional<Product> getProductSuggestion(String message) {
1618
return assistantSuggestion.getProductSuggestion(message);
1719
}
1820
}

src/main/java/com/david/ecommerceapi/assistant/domain/AssistantService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.david.ecommerceapi.product.domain.Product;
44

5+
import java.util.Optional;
6+
57
public interface AssistantService {
68

7-
Product getProductSuggestion(String message);
9+
Optional<Product> getProductSuggestion(String message);
810
}

src/main/java/com/david/ecommerceapi/assistant/domain/AssistantSuggestion.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import com.david.ecommerceapi.product.domain.Product;
44

5+
import java.util.Optional;
6+
57
public interface AssistantSuggestion {
68

7-
Product getProductSuggestion(String message);
9+
Optional<Product> getProductSuggestion(String message);
810

911
}

src/main/java/com/david/ecommerceapi/assistant/infraestructure/AssistantControllerImpl.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.springframework.web.bind.annotation.RequestParam;
1313
import org.springframework.web.bind.annotation.RestController;
1414

15+
import java.util.Optional;
16+
1517
@RestController
1618
@RequestMapping("api/assistant")
1719
@SecurityRequirement(name = "Bearer Authentication")
@@ -23,8 +25,14 @@ public class AssistantControllerImpl implements AssistantController {
2325

2426
@GetMapping("/products")
2527
public ResponseEntity<ProductSuggestionDTO> getProductSuggestions(@RequestParam String message) {
26-
Product productSuggestion = assistantService.getProductSuggestion(message);
27-
ProductSuggestionDTO productSuggestionDTO = suggestionMapper.productToProductSuggestionDTO(productSuggestion);
28+
Optional<Product> productSuggestion = assistantService.getProductSuggestion(message);
29+
30+
if (productSuggestion.isEmpty()) {
31+
return ResponseEntity.notFound().build();
32+
}
33+
34+
ProductSuggestionDTO productSuggestionDTO = suggestionMapper.productToProductSuggestionDTO(productSuggestion.get());
35+
2836
return ResponseEntity.ok(productSuggestionDTO);
2937
}
3038

src/main/java/com/david/ecommerceapi/assistant/infraestructure/dto/ProductSuggestionDTO.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class ProductSuggestionDTO {
1919
@JsonProperty(required = true)
2020
private String description;
2121
@JsonProperty(required = true)
22+
private double price;
23+
@JsonProperty(required = true)
2224
private String imageUrl;
2325

2426
}

src/main/java/com/david/ecommerceapi/assistant/infraestructure/rest/OpenAiAssistantImpl.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,26 @@
66
import com.david.ecommerceapi.assistant.infraestructure.mapper.SuggestionMapper;
77
import com.david.ecommerceapi.assistant.infraestructure.utility.PromptLoader;
88
import com.david.ecommerceapi.product.domain.Product;
9+
import com.david.ecommerceapi.product.infrastructure.repository.implementation.PostgresProductRepositoryImpl;
910
import lombok.RequiredArgsConstructor;
1011
import lombok.extern.slf4j.Slf4j;
1112
import org.springframework.ai.chat.messages.Message;
1213
import org.springframework.ai.chat.model.ChatResponse;
1314
import org.springframework.ai.chat.prompt.Prompt;
1415
import org.springframework.ai.chat.prompt.PromptTemplate;
1516
import org.springframework.ai.converter.BeanOutputConverter;
17+
import org.springframework.ai.document.Document;
1618
import org.springframework.ai.openai.OpenAiChatModel;
1719
import org.springframework.ai.openai.OpenAiChatOptions;
1820
import org.springframework.ai.openai.api.ResponseFormat;
21+
import org.springframework.ai.vectorstore.SearchRequest;
22+
import org.springframework.ai.vectorstore.VectorStore;
1923
import org.springframework.core.io.Resource;
2024
import org.springframework.stereotype.Component;
2125

26+
import java.util.List;
27+
import java.util.Optional;
28+
2229
@Slf4j
2330
@RequiredArgsConstructor
2431
@Component
@@ -27,21 +34,43 @@ public class OpenAiAssistantImpl implements AssistantSuggestion {
2734
private final OpenAiChatModel openAiChatModel;
2835
private final PromptLoader promptLoader;
2936
private final SuggestionMapper suggestionMapper;
37+
private final VectorStore vectorStore;
38+
private final PostgresProductRepositoryImpl productRepository;
3039

3140
@Override
32-
public Product getProductSuggestion(String request) {
41+
public Optional<Product> getProductSuggestion(String request) {
3342

34-
BeanOutputConverter<ProductSuggestionDTO> outputConverter = new BeanOutputConverter<>(ProductSuggestionDTO.class);
43+
SearchRequest searchRequest = SearchRequest.builder()
44+
.query(request)
45+
.topK(3)
46+
.build();
3547

36-
String jsonSchema = outputConverter.getJsonSchema();
48+
List<Document> documents = vectorStore.similaritySearch(searchRequest);
49+
50+
if (documents == null || documents.isEmpty()) {
51+
return Optional.empty();
52+
}
53+
54+
List<Long> productIds = documents.stream().map(document -> Long.valueOf(document.getMetadata().get("productId").toString())).toList();
55+
56+
List<Product> products = productRepository.findAllByIdIn(productIds);
57+
58+
if (products.isEmpty()) {
59+
return Optional.empty();
60+
}
3761

3862
Resource promptResource = promptLoader.getPromptResource(PromptType.PRODUCT_SUGGESTION);
3963

4064
PromptTemplate promptTemplate = new PromptTemplate(promptResource);
4165
promptTemplate.add("request", request);
66+
promptTemplate.add("products", products);
4267

4368
Message message = promptTemplate.createMessage();
4469

70+
BeanOutputConverter<ProductSuggestionDTO> outputConverter = new BeanOutputConverter<>(ProductSuggestionDTO.class);
71+
72+
String jsonSchema = outputConverter.getJsonSchema();
73+
4574
Prompt prompt = new Prompt(message, OpenAiChatOptions.builder()
4675
.responseFormat(new ResponseFormat(ResponseFormat.Type.JSON_SCHEMA, jsonSchema))
4776
.build()
@@ -50,8 +79,9 @@ public Product getProductSuggestion(String request) {
5079
ChatResponse response = openAiChatModel.call(prompt);
5180

5281
String content = response.getResult().getOutput().getText();
82+
5383
ProductSuggestionDTO productSuggestionDTO = outputConverter.convert(content);
5484

55-
return suggestionMapper.productSuggestionDTOToProduct(productSuggestionDTO);
85+
return Optional.ofNullable(suggestionMapper.productSuggestionDTOToProduct(productSuggestionDTO));
5686
}
5787
}

0 commit comments

Comments
 (0)