Skip to content

Commit 7c6bbef

Browse files
meistermeiertzolov
authored andcommitted
Allow more customization for Neo4j store (id and constraint).
Unrelated to this change, the Neo4j test version increased to be current.
1 parent 152420f commit 7c6bbef

File tree

4 files changed

+93
-26
lines changed

4 files changed

+93
-26
lines changed

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/neo4j/Neo4jVectorStoreAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public Neo4jVectorStore vectorStore(Driver driver, EmbeddingClient embeddingClie
4545
.withLabel(properties.getLabel())
4646
.withEmbeddingProperty(properties.getEmbeddingProperty())
4747
.withIndexName(properties.getIndexName())
48+
.withIdProperty(properties.getIdProperty())
49+
.withConstraintName(properties.getConstraintName())
4850
.build();
4951

5052
return new Neo4jVectorStore(driver, embeddingClient, config);

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/neo4j/Neo4jVectorStoreProperties.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public class Neo4jVectorStoreProperties {
3838

3939
private String indexName = Neo4jVectorStore.DEFAULT_INDEX_NAME;
4040

41+
private String idProperty = Neo4jVectorStore.DEFAULT_ID_PROPERTY;
42+
43+
private String constraintName = Neo4jVectorStore.DEFAULT_CONSTRAINT_NAME;
44+
4145
public String getDatabaseName() {
4246
return this.databaseName;
4347
}
@@ -86,4 +90,20 @@ public void setIndexName(String indexName) {
8690
this.indexName = indexName;
8791
}
8892

93+
public String getIdProperty() {
94+
return this.idProperty;
95+
}
96+
97+
public void setIdProperty(String idProperty) {
98+
this.idProperty = idProperty;
99+
}
100+
101+
public String getConstraintName() {
102+
return this.constraintName;
103+
}
104+
105+
public void setConstraintName(String constraintName) {
106+
this.constraintName = constraintName;
107+
}
108+
89109
}

vector-stores/spring-ai-neo4j-store/src/main/java/org/springframework/ai/vectorstore/Neo4jVectorStore.java

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,19 @@ public static final class Neo4jVectorStoreConfig {
6363

6464
private final Neo4jDistanceType distanceType;
6565

66-
private final String label;
67-
6866
private final String embeddingProperty;
6967

70-
private final String quotedLabel;
68+
private final String label;
7169

7270
private final String indexName;
7371

72+
// needed for similarity search call
73+
private final String indexNameNotSanitized;
74+
75+
private final String idProperty;
76+
77+
private final String constraintName;
78+
7479
/**
7580
* Start building a new configuration.
7681
* @return The entry point for creating a new configuration.
@@ -96,10 +101,12 @@ private Neo4jVectorStoreConfig(Builder builder) {
96101
.orElseGet(SessionConfig::defaultConfig);
97102
this.embeddingDimension = builder.embeddingDimension;
98103
this.distanceType = builder.distanceType;
99-
this.label = builder.label;
100-
this.embeddingProperty = builder.embeddingProperty;
101-
this.quotedLabel = SchemaNames.sanitize(this.label).orElseThrow();
102-
this.indexName = builder.indexName;
104+
this.embeddingProperty = SchemaNames.sanitize(builder.embeddingProperty).orElseThrow();
105+
this.label = SchemaNames.sanitize(builder.label).orElseThrow();
106+
this.indexNameNotSanitized = builder.indexName;
107+
this.indexName = SchemaNames.sanitize(builder.indexName, true).orElseThrow();
108+
this.constraintName = SchemaNames.sanitize(builder.constraintName).orElseThrow();
109+
this.idProperty = SchemaNames.sanitize(builder.idProperty).orElseThrow();
103110
}
104111

105112
public static class Builder {
@@ -116,6 +123,10 @@ public static class Builder {
116123

117124
private String indexName = DEFAULT_INDEX_NAME;
118125

126+
private String idProperty = DEFAULT_ID_PROPERTY;
127+
128+
private String constraintName = DEFAULT_CONSTRAINT_NAME;
129+
119130
private Builder() {
120131
}
121132

@@ -202,6 +213,35 @@ public Builder withIndexName(String newIndexName) {
202213
return this;
203214
}
204215

216+
/**
217+
* Configures the id property to be used. Defaults to {@literal id}.
218+
* @param newIdProperty The name of the id property of the {@link Document}
219+
* entity
220+
* @return this builder
221+
*/
222+
public Builder withIdProperty(String newIdProperty) {
223+
224+
Assert.hasText(newIdProperty, "Id property may not be null or blank");
225+
226+
this.idProperty = newIdProperty;
227+
return this;
228+
}
229+
230+
/**
231+
* Configures the constraint name to be used. Defaults to
232+
* {@literal Document_unique_idx}.
233+
* @param newConstraintName The name of the unique constraint for the id
234+
* property.
235+
* @return this builder
236+
*/
237+
public Builder withConstraintName(String newConstraintName) {
238+
239+
Assert.hasText(newConstraintName, "Constraint name may not be null or blank");
240+
241+
this.constraintName = newConstraintName;
242+
return this;
243+
}
244+
205245
/**
206246
* {@return the immutable configuration}
207247
*/
@@ -222,6 +262,10 @@ public Neo4jVectorStoreConfig build() {
222262

223263
public static final String DEFAULT_EMBEDDING_PROPERTY = "embedding";
224264

265+
public static final String DEFAULT_ID_PROPERTY = "id";
266+
267+
public static final String DEFAULT_CONSTRAINT_NAME = DEFAULT_LABEL + "_unique_idx";
268+
225269
private final Neo4jVectorFilterExpressionConverter filterExpressionConverter = new Neo4jVectorFilterExpressionConverter();
226270

227271
private final Driver driver;
@@ -249,16 +293,16 @@ public void add(List<Document> documents) {
249293
try (var session = this.driver.session()) {
250294
var statement = """
251295
UNWIND $rows AS row
252-
MERGE (u:%s {id: row.id})
296+
MERGE (u:%s {%2$s: row.id})
253297
ON CREATE
254298
SET u += row.properties
255299
ON MATCH
256300
SET u = {}
257-
SET u.id = row.id,
301+
SET u.%2$s = row.id,
258302
u += row.properties
259303
WITH row, u
260304
CALL db.create.setNodeVectorProperty(u, $embeddingProperty, row.embedding)
261-
""".formatted(this.config.quotedLabel);
305+
""".formatted(this.config.label, this.config.idProperty);
262306
session.run(statement, Map.of("rows", rows, "embeddingProperty", this.config.embeddingProperty)).consume();
263307
}
264308
}
@@ -268,10 +312,12 @@ public Optional<Boolean> delete(List<String> idList) {
268312

269313
try (var session = this.driver.session(this.config.sessionConfig)) {
270314

271-
var summary = session.run("""
272-
MATCH (n:%s) WHERE n.id IN $ids
273-
CALL { WITH n DETACH DELETE n } IN TRANSACTIONS OF $transactionSize ROWS
274-
""".formatted(this.config.quotedLabel), Map.of("ids", idList, "transactionSize", 10_000))
315+
var summary = session
316+
.run("""
317+
MATCH (n:%s) WHERE n.%s IN $ids
318+
CALL { WITH n DETACH DELETE n } IN TRANSACTIONS OF $transactionSize ROWS
319+
""".formatted(this.config.label, this.config.idProperty),
320+
Map.of("ids", idList, "transactionSize", 10_000))
275321
.consume();
276322
return Optional.of(idList.size() == summary.counters().nodesDeleted());
277323
}
@@ -297,10 +343,9 @@ public List<Document> similaritySearch(SearchRequest request) {
297343
RETURN node, score""".formatted(condition);
298344

299345
return session
300-
.run(query,
301-
Map.of("indexName", this.config.indexName, "numberOfNearestNeighbours", request.getTopK(),
302-
"embeddingValue", embedding, "threshold", request.getSimilarityThreshold()))
303-
.list(Neo4jVectorStore::recordToDocument);
346+
.run(query, Map.of("indexName", this.config.indexNameNotSanitized, "numberOfNearestNeighbours",
347+
request.getTopK(), "embeddingValue", embedding, "threshold", request.getSimilarityThreshold()))
348+
.list(this::recordToDocument);
304349
}
305350
}
306351

@@ -310,8 +355,8 @@ public void afterPropertiesSet() {
310355
try (var session = this.driver.session(this.config.sessionConfig)) {
311356

312357
session
313-
.run("CREATE CONSTRAINT %s IF NOT EXISTS FOR (n:%s) REQUIRE n.id IS UNIQUE".formatted(
314-
SchemaNames.sanitize(this.config.label + "_unique_idx").orElseThrow(), this.config.quotedLabel))
358+
.run("CREATE CONSTRAINT %s IF NOT EXISTS FOR (n:%s) REQUIRE n.%s IS UNIQUE"
359+
.formatted(this.config.constraintName, this.config.label, this.config.idProperty))
315360
.consume();
316361

317362
var statement = """
@@ -320,9 +365,8 @@ public void afterPropertiesSet() {
320365
`vector.dimensions`: %d,
321366
`vector.similarity_function`: '%s'
322367
}}
323-
""".formatted(SchemaNames.sanitize(this.config.indexName, true).orElseThrow(),
324-
this.config.quotedLabel, this.config.embeddingProperty, this.config.embeddingDimension,
325-
this.config.distanceType.name);
368+
""".formatted(this.config.indexName, this.config.label, this.config.embeddingProperty,
369+
this.config.embeddingDimension, this.config.distanceType.name);
326370
session.run(statement).consume();
327371
session.run("CALL db.awaitIndexes()").consume();
328372
}
@@ -355,7 +399,7 @@ private static float[] toFloatArray(List<Double> embeddingDouble) {
355399
return embeddingFloat;
356400
}
357401

358-
private static Document recordToDocument(org.neo4j.driver.Record neoRecord) {
402+
private Document recordToDocument(org.neo4j.driver.Record neoRecord) {
359403
var node = neoRecord.get("node").asNode();
360404
var score = neoRecord.get("score").asFloat();
361405
var metaData = new HashMap<String, Object>();
@@ -366,7 +410,8 @@ private static Document recordToDocument(org.neo4j.driver.Record neoRecord) {
366410
}
367411
});
368412

369-
return new Document(node.get("id").asString(), node.get("text").asString(), Map.copyOf(metaData));
413+
return new Document(node.get(this.config.idProperty).asString(), node.get("text").asString(),
414+
Map.copyOf(metaData));
370415
}
371416

372417
}

vector-stores/spring-ai-neo4j-store/src/test/java/org/springframework/ai/vectorstore/Neo4jVectorStoreIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class Neo4jVectorStoreIT {
5858
// creation
5959
// function.
6060
@Container
61-
static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5.15"))
61+
static Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>(DockerImageName.parse("neo4j:5.18"))
6262
.withRandomPassword();
6363

6464
List<Document> documents = List.of(

0 commit comments

Comments
 (0)