Skip to content

Commit 0abf152

Browse files
committed
Fix for Redis NPE when using FilterExpression #265
- Part of #265
1 parent 6043eda commit 0abf152

File tree

2 files changed

+45
-41
lines changed

2 files changed

+45
-41
lines changed

vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisFilterExpressionConverter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -104,7 +104,7 @@ private void doBinaryOperation(String delimiter, Expression expression, StringBu
104104
private void doField(Expression expression, StringBuilder context) {
105105
Key key = (Key) expression.left();
106106
doKey(key, context);
107-
MetadataField field = metadataFields.getOrDefault(key.key(), MetadataField.tag(key.key()));
107+
MetadataField field = this.metadataFields.getOrDefault(key.key(), MetadataField.tag(key.key()));
108108
Value value = (Value) expression.right();
109109
switch (field.fieldType()) {
110110
case NUMERIC:

vector-stores/spring-ai-redis/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
5757
* offers functionalities like adding, deleting, and performing similarity searches on
5858
* documents.
5959
*
60-
* The store utilizes RedisJSON and RediSearch to handle JSON documents and to index and
60+
* The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
6161
* search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
6262
* efficient similarity searches. Additionally, it allows for custom metadata fields in
6363
* the documents to be stored alongside the vector and content data.
@@ -68,6 +68,7 @@
6868
* them.
6969
*
7070
* @author Julien Ruaux
71+
* @author Christian Tzolov
7172
* @see VectorStore
7273
* @see RedisVectorStoreConfig
7374
* @see EmbeddingClient
@@ -115,6 +116,20 @@ public static final class RedisVectorStoreConfig {
115116

116117
private final List<MetadataField> metadataFields;
117118

119+
private RedisVectorStoreConfig() {
120+
this(builder());
121+
}
122+
123+
private RedisVectorStoreConfig(Builder builder) {
124+
this.uri = builder.uri;
125+
this.indexName = builder.indexName;
126+
this.prefix = builder.prefix;
127+
this.contentFieldName = builder.contentFieldName;
128+
this.embeddingFieldName = builder.embeddingFieldName;
129+
this.vectorAlgorithm = builder.vectorAlgorithm;
130+
this.metadataFields = builder.metadataFields;
131+
}
132+
118133
/**
119134
* Start building a new configuration.
120135
* @return The entry point for creating a new configuration.
@@ -132,16 +147,6 @@ public static RedisVectorStoreConfig defaultConfig() {
132147
return builder().build();
133148
}
134149

135-
private RedisVectorStoreConfig(Builder builder) {
136-
this.uri = builder.uri;
137-
this.indexName = builder.indexName;
138-
this.prefix = builder.prefix;
139-
this.contentFieldName = builder.contentFieldName;
140-
this.embeddingFieldName = builder.embeddingFieldName;
141-
this.vectorAlgorithm = builder.vectorAlgorithm;
142-
this.metadataFields = builder.metadataFields;
143-
}
144-
145150
public static class Builder {
146151

147152
private String uri = DEFAULT_URI;
@@ -290,22 +295,23 @@ public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingClient embedding
290295
this.jedis = new JedisPooled(config.uri);
291296
this.embeddingClient = embeddingClient;
292297
this.config = config;
298+
this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
293299
}
294300

295301
public JedisPooled getJedis() {
296-
return jedis;
302+
return this.jedis;
297303
}
298304

299305
@Override
300306
public void add(List<Document> documents) {
301-
Pipeline pipeline = jedis.pipelined();
307+
Pipeline pipeline = this.jedis.pipelined();
302308
for (Document document : documents) {
303309
var embedding = this.embeddingClient.embed(document);
304310
document.setEmbedding(embedding);
305311

306312
var fields = new HashMap<String, Object>();
307-
fields.put(config.embeddingFieldName, embedding);
308-
fields.put(config.contentFieldName, document.getContent());
313+
fields.put(this.config.embeddingFieldName, embedding);
314+
fields.put(this.config.contentFieldName, document.getContent());
309315
fields.putAll(document.getMetadata());
310316
pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
311317
}
@@ -321,12 +327,12 @@ public void add(List<Document> documents) {
321327
}
322328

323329
private String key(String id) {
324-
return config.prefix + id;
330+
return this.config.prefix + id;
325331
}
326332

327333
@Override
328334
public Optional<Boolean> delete(List<String> idList) {
329-
Pipeline pipeline = jedis.pipelined();
335+
Pipeline pipeline = this.jedis.pipelined();
330336
for (String id : idList) {
331337
pipeline.jsonDel(key(id));
332338
}
@@ -350,21 +356,21 @@ public List<Document> similaritySearch(SearchRequest request) {
350356

351357
String filter = nativeExpressionFilter(request);
352358

353-
String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), config.embeddingFieldName,
359+
String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
354360
EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
355361

356362
List<String> returnFields = new ArrayList<>();
357-
config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
358-
returnFields.add(config.embeddingFieldName);
359-
returnFields.add(config.contentFieldName);
363+
this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
364+
returnFields.add(this.config.embeddingFieldName);
365+
returnFields.add(this.config.contentFieldName);
360366
returnFields.add(DISTANCE_FIELD_NAME);
361367
var embedding = toFloatArray(this.embeddingClient.embed(request.getQuery()));
362368
Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
363369
.returnFields(returnFields.toArray(new String[0]))
364370
.setSortBy(DISTANCE_FIELD_NAME, true)
365371
.dialect(2);
366372

367-
SearchResult result = jedis.ftSearch(config.indexName, query);
373+
SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
368374
return result.getDocuments()
369375
.stream()
370376
.filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
@@ -373,9 +379,10 @@ public List<Document> similaritySearch(SearchRequest request) {
373379
}
374380

375381
private Document toDocument(redis.clients.jedis.search.Document doc) {
376-
var id = doc.getId().substring(config.prefix.length());
377-
var content = doc.hasProperty(config.contentFieldName) ? doc.getString(config.contentFieldName) : null;
378-
Map<String, Object> metadata = config.metadataFields.stream()
382+
var id = doc.getId().substring(this.config.prefix.length());
383+
var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
384+
: null;
385+
Map<String, Object> metadata = this.config.metadataFields.stream()
379386
.map(MetadataField::name)
380387
.filter(doc::hasProperty)
381388
.collect(Collectors.toMap(Function.identity(), doc::getString));
@@ -391,44 +398,41 @@ private String nativeExpressionFilter(SearchRequest request) {
391398
if (request.getFilterExpression() == null) {
392399
return "*";
393400
}
394-
return "(" + filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
401+
return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
395402
}
396403

397404
@Override
398405
public void afterPropertiesSet() {
399406

400407
// If index already exists don't do anything
401-
if (jedis.ftList().contains(config.indexName)) {
408+
if (this.jedis.ftList().contains(this.config.indexName)) {
402409
return;
403410
}
404411

405-
String response = jedis.ftCreate(config.indexName,
406-
FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(config.prefix), schemaFields());
412+
String response = this.jedis.ftCreate(this.config.indexName,
413+
FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
407414
if (!RESPONSE_OK.test(response)) {
408415
String message = MessageFormat.format("Could not create index: {0}", response);
409416
throw new RuntimeException(message);
410417
}
411-
412-
filterExpressionConverter = new RedisFilterExpressionConverter(config.metadataFields);
413-
414418
}
415419

416420
private Iterable<SchemaField> schemaFields() {
417421
Map<String, Object> vectorAttrs = new HashMap<>();
418-
vectorAttrs.put("DIM", embeddingClient.dimensions());
422+
vectorAttrs.put("DIM", this.embeddingClient.dimensions());
419423
vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
420424
vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
421425
List<SchemaField> fields = new ArrayList<>();
422-
fields.add(TextField.of(jsonPath(config.contentFieldName)).as(config.contentFieldName).weight(1.0));
426+
fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
423427
fields.add(VectorField.builder()
424-
.fieldName(jsonPath(config.embeddingFieldName))
428+
.fieldName(jsonPath(this.config.embeddingFieldName))
425429
.algorithm(vectorAlgorithm())
426430
.attributes(vectorAttrs)
427-
.as(config.embeddingFieldName)
431+
.as(this.config.embeddingFieldName)
428432
.build());
429433

430-
if (!CollectionUtils.isEmpty(config.metadataFields)) {
431-
for (MetadataField field : config.metadataFields) {
434+
if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
435+
for (MetadataField field : this.config.metadataFields) {
432436
fields.add(schemaField(field));
433437
}
434438
}

0 commit comments

Comments
 (0)