Nest extraData by section for clearer embedding content

Homebox's location fields were flattened into single-purpose keys
(locationName/locationDescription), which gave EmbeddingService no
way to group related fields when rendering vector-store content.
extraData is now Map<String, Map<String, Object>>, letting
HomeboxItemClient express "location" as its own section; buildExtraData
renders each section as a header with indented fields instead of a flat
"Extradata: key: value" line.
This commit is contained in:
2026-07-21 01:04:19 +02:00
parent 29027d4515
commit 3736cc73ae
5 changed files with 36 additions and 18 deletions
@@ -1,6 +1,6 @@
package com.vaessl.app.homebox; package com.vaessl.app.homebox;
import java.util.HashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -73,12 +73,18 @@ public class HomeboxItemClient {
String description = i.description(); String description = i.description();
HomeboxParent parent = i.parent(); HomeboxParent parent = i.parent();
Map<String, Object> extraData = new HashMap<>(); Map<String, Map<String, Object>> extraData = new LinkedHashMap<>();
Map<String, Object> locationData = new LinkedHashMap<>();
if (parent.name() != null && !parent.name().isBlank()) { if (parent.name() != null && !parent.name().isBlank()) {
extraData.put("locationName", parent.name()); locationData.put("name", parent.name());
} }
if (parent.description() != null && !parent.description().isBlank()) { if (parent.description() != null && !parent.description().isBlank()) {
extraData.put("locationDescription", parent.description()); locationData.put("description", parent.description());
}
if (locationData != null && !locationData.isEmpty()) {
extraData.put("location", locationData);
} }
return new ServiceItem(id, title, description, extraData); return new ServiceItem(id, title, description, extraData);
@@ -4,7 +4,7 @@ import java.util.Map;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
public record ServiceItem(String id, @NotNull String title, String description, public record ServiceItem(String id, @NotNull String title, String description,
Map<String, Object> extraData) { Map<String, Map<String, Object>> extraData) {
public String getExtra(String key) { public String getExtra(String key) {
if (extraData == null) { if (extraData == null) {
@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import org.springframework.ai.document.Document; import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
@@ -54,21 +55,30 @@ public class EmbeddingService {
return new Document(connectionId + ":" + item.id(), content, metadata); return new Document(connectionId + ":" + item.id(), content, metadata);
} }
private String buildExtraData(Map<String, Object> extraData) { private String buildExtraData(Map<String, Map<String, Object>> extraData) {
if (extraData == null) { if (extraData == null) {
return ""; return "";
} }
StringBuilder formattedData = new StringBuilder();
for (Entry<String, Map<String, Object>> entry : extraData.entrySet()) {
StringBuilder data = new StringBuilder(); StringBuilder data = new StringBuilder();
for (Map.Entry<String, Object> entry : extraData.entrySet()) {
if (entry.getValue() == null || entry.getValue().toString().isEmpty()) { if (entry.getValue() == null || entry.getValue().toString().isEmpty()) {
continue; continue;
} }
if (!data.isEmpty()) {
data.append(" \n"); if (entry.getValue().entrySet() != null
&& !entry.getValue().entrySet().toString().isEmpty()) {
for (Entry<String, Object> nestedEntry : entry.getValue().entrySet()) {
data.append(" ").append(nestedEntry.getKey()).append(": ")
.append(nestedEntry.getValue()).append("\n");
} }
data.append(entry.getKey()).append(": ").append(entry.getValue());
} }
return data.toString();
formattedData.append(entry.getKey()).append(": ").append("\n").append(data.toString());
}
return formattedData.toString();
} }
private Map<String, Object> buildMetadata(ServiceItem item, ServiceType serviceType, private Map<String, Object> buildMetadata(ServiceItem item, ServiceType serviceType,
@@ -81,12 +91,13 @@ public class EmbeddingService {
} }
private String buildContent(ServiceItem item, String extraData) { private String buildContent(ServiceItem item, String extraData) {
StringBuilder content = new StringBuilder("Title: ").append(item.title()); StringBuilder content = new StringBuilder("title: ").append(item.title());
if (item.description() != null && !item.description().isEmpty()) { if (item.description() != null && !item.description().isEmpty()) {
content.append("\nDescription: ").append(item.description()); content.append("\ndescription: ").append(item.description());
} }
if (!extraData.isEmpty()) { if (!extraData.isEmpty()) {
content.append("\nExtradata: ").append(extraData);
content.append("\n").append(extraData);
} }
return content.toString(); return content.toString();
} }
@@ -72,7 +72,7 @@ class SearchControllerTest {
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398")) .andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398"))
.andExpect(jsonPath("$.totalElements").value(1)) .andExpect(jsonPath("$.totalElements").value(1))
.andExpect(jsonPath("$.content[0].extraData.locationName") .andExpect(jsonPath("$.content[0].extraData.location.name")
.value("Server Schrank Ikea weiß")); .value("Server Schrank Ikea weiß"));
} }
@@ -18,7 +18,7 @@ class SearchResponseTest {
@Test @Test
void shouldReturnNullWhenExtraDataKeyIsMissing() { void shouldReturnNullWhenExtraDataKeyIsMissing() {
ServiceItem response = new ServiceItem(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value")); ServiceItem response = new ServiceItem(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", Map.of("key", "value")));
assertThat(response.getExtra("missing")).isNull(); assertThat(response.getExtra("missing")).isNull();
} }
@@ -26,7 +26,8 @@ class SearchResponseTest {
@Test @Test
void shouldReturnExtraDataValue() { void shouldReturnExtraDataValue() {
ServiceItem response = new ServiceItem(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value")); ServiceItem response = new ServiceItem(MOCK_ID, MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key",
Map.of("key", "value")));
assertThat(response.id()).isEqualTo(MOCK_ID); assertThat(response.id()).isEqualTo(MOCK_ID);
assertThat(response.getExtra("key")).contains("value"); assertThat(response.getExtra("key")).contains("value");