diff --git a/pom.xml b/pom.xml
index d327df2f7..9a6cc37df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,7 @@
document-readers/tika-reader
embedding-clients/transformers-embedding
vector-stores/spring-ai-pinecone
+ vector-stores/spring-ai-chroma
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java
index 5494ccd9a..dddbf50ba 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParser.java
@@ -191,11 +191,14 @@ public class FilterExpressionTextParser {
@Override
public Filter.Operand visitTextConstant(FiltersParser.TextConstantContext ctx) {
- var twiceQuotedText = ctx.getText();
- String onceQuotedText = twiceQuotedText.substring(1, twiceQuotedText.length() - 1);
+ String onceQuotedText = removeOuterQuotes(ctx.getText());
return new Filter.Value(onceQuotedText);
}
+ private String removeOuterQuotes(String in) {
+ return in.substring(1, in.length() - 1);
+ }
+
@Override
public Filter.Operand visitIntegerConstant(FiltersParser.IntegerConstantContext ctx) {
return new Filter.Value(Integer.valueOf(ctx.getText()));
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/AbstractFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/AbstractFilterExpressionConverter.java
index fbf5246d3..160d02068 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/AbstractFilterExpressionConverter.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/AbstractFilterExpressionConverter.java
@@ -19,6 +19,7 @@ package org.springframework.ai.vectorstore.filter.converter;
import java.util.List;
import org.springframework.ai.vectorstore.filter.Filter;
+import org.springframework.ai.vectorstore.filter.Filter.Expression;
import org.springframework.ai.vectorstore.filter.Filter.ExpressionType;
import org.springframework.ai.vectorstore.filter.Filter.Group;
import org.springframework.ai.vectorstore.filter.Filter.Operand;
@@ -27,16 +28,20 @@ import org.springframework.util.Assert;
/**
* @author Christian Tzolov
*/
-public abstract class AbstractFilterExpressionConverter {
+public abstract class AbstractFilterExpressionConverter implements FilterExpressionConverter {
- public String convert(Operand operand) {
- Assert.notNull(operand, "Operand can't be null");
+ @Override
+ public String convertExpression(Expression expression) {
+ return this.convertOperand(expression);
+ }
+
+ protected String convertOperand(Operand operand) {
var context = new StringBuilder();
- this.convert(operand, context);
+ this.convertOperand(operand, context);
return context.toString();
}
- protected void convert(Operand operand, StringBuilder context) {
+ protected void convertOperand(Operand operand, StringBuilder context) {
if (operand instanceof Filter.Group group) {
this.doGroup(group, context);
@@ -88,7 +93,7 @@ public abstract class AbstractFilterExpressionConverter {
protected void doGroup(Group group, StringBuilder context) {
this.doStartGroup(group, context);
- this.convert(group.content(), context);
+ this.convertOperand(group.content(), context);
this.doEndGroup(group, context);
}
@@ -110,4 +115,14 @@ public abstract class AbstractFilterExpressionConverter {
context.append(",");
}
+ // Utilities
+ protected boolean hasOuterQuotes(String str) {
+ str = str.trim();
+ return (str.startsWith("\"") && str.endsWith("\"")) || (str.startsWith("'") && str.endsWith("'"));
+ }
+
+ protected String removeOuterQuotes(String in) {
+ return in.substring(1, in.length() - 1);
+ }
+
}
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/ChromaFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/ChromaFilterExpressionConverter.java
new file mode 100644
index 000000000..30cc5006d
--- /dev/null
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/ChromaFilterExpressionConverter.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.vectorstore.filter.converter;
+
+import org.springframework.ai.vectorstore.filter.Filter;
+
+/**
+ * Converts {@link Filter.Expression} into Chroma metadata filter expression format.
+ * (https://docs.trychroma.com/usage-guide#using-where-filters)
+ *
+ * @author Christian Tzolov
+ */
+public class ChromaFilterExpressionConverter extends PineconeFilterExpressionConverter {
+
+}
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/FilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/FilterExpressionConverter.java
new file mode 100644
index 000000000..ece17dc0c
--- /dev/null
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/FilterExpressionConverter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.vectorstore.filter.converter;
+
+import org.springframework.ai.vectorstore.filter.Filter;
+
+/**
+ * Converters a generic, portable {@link Filter.Expression} into a
+ * {@link org.springframework.ai.vectorstore.VectorStore} specific expression language
+ * format.
+ *
+ * @author Christian Tzolov
+ */
+public interface FilterExpressionConverter {
+
+ public String convertExpression(Filter.Expression expression);
+
+}
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverter.java
index cbf012548..3a32552a3 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverter.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverter.java
@@ -31,9 +31,9 @@ public class MilvusFilterExpressionConverter extends AbstractFilterExpressionCon
@Override
protected void doExpression(Expression exp, StringBuilder context) {
- this.convert(exp.left(), context);
+ this.convertOperand(exp.left(), context);
context.append(getOperationSymbol(exp));
- this.convert(exp.right(), context);
+ this.convertOperand(exp.right(), context);
}
private String getOperationSymbol(Expression exp) {
@@ -65,12 +65,13 @@ public class MilvusFilterExpressionConverter extends AbstractFilterExpressionCon
@Override
protected void doGroup(Group group, StringBuilder context) {
- this.convert(new Expression(ExpressionType.AND, group.content(), group.content()), context); // trick
+ this.convertOperand(new Expression(ExpressionType.AND, group.content(), group.content()), context); // trick
}
@Override
protected void doKey(Key key, StringBuilder context) {
- context.append("metadata[\"" + key.key() + "\"]");
+ var identifier = (hasOuterQuotes(key.key())) ? removeOuterQuotes(key.key()) : key.key();
+ context.append("metadata[\"" + identifier + "\"]");
}
}
\ No newline at end of file
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverter.java
index 230a71df0..5eeabe96a 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverter.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverter.java
@@ -30,9 +30,9 @@ public class PgVectorFilterExpressionConverter extends AbstractFilterExpressionC
@Override
protected void doExpression(Expression expression, StringBuilder context) {
- this.convert(expression.left(), context);
+ this.convertOperand(expression.left(), context);
context.append(getOperationSymbol(expression));
- this.convert(expression.right(), context);
+ this.convertOperand(expression.right(), context);
}
private String getOperationSymbol(Expression exp) {
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverter.java
index b49f8b062..eb4614c61 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverter.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverter.java
@@ -35,16 +35,16 @@ public class PineconeFilterExpressionConverter extends AbstractFilterExpressionC
if (exp.type() == ExpressionType.AND || exp.type() == ExpressionType.OR) {
context.append(getOperationSymbol(exp));
context.append("[");
- this.convert(exp.left(), context);
+ this.convertOperand(exp.left(), context);
context.append(",");
- this.convert(exp.right(), context);
+ this.convertOperand(exp.right(), context);
context.append("]");
}
else {
- this.convert(exp.left(), context);
+ this.convertOperand(exp.left(), context);
context.append("{");
context.append(getOperationSymbol(exp));
- this.convert(exp.right(), context);
+ this.convertOperand(exp.right(), context);
context.append("}");
}
context.append("}");
@@ -57,7 +57,8 @@ public class PineconeFilterExpressionConverter extends AbstractFilterExpressionC
@Override
protected void doKey(Key key, StringBuilder context) {
- context.append("\"" + key.key() + "\": ");
+ var identifier = (hasOuterQuotes(key.key())) ? removeOuterQuotes(key.key()) : key.key();
+ context.append("\"" + identifier + "\": ");
}
}
\ No newline at end of file
diff --git a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PrintFilterExpressionConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PrintFilterExpressionConverter.java
index 00b4e07c2..6860cc7ff 100644
--- a/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PrintFilterExpressionConverter.java
+++ b/spring-ai-core/src/main/java/org/springframework/ai/vectorstore/filter/converter/PrintFilterExpressionConverter.java
@@ -28,9 +28,9 @@ import org.springframework.ai.vectorstore.filter.Filter.Key;
public class PrintFilterExpressionConverter extends AbstractFilterExpressionConverter {
public void doExpression(Expression expression, StringBuilder context) {
- this.convert(expression.left(), context);
+ this.convertOperand(expression.left(), context);
context.append(" " + expression.type() + " ");
- this.convert(expression.right(), context);
+ this.convertOperand(expression.right(), context);
}
diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionBuilderTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionBuilderTests.java
index 5a2bbf4b4..c05cba98b 100644
--- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionBuilderTests.java
+++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionBuilderTests.java
@@ -18,7 +18,6 @@ package org.springframework.ai.vectorstore.filter;
import java.util.List;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.vectorstore.filter.Filter.Expression;
@@ -44,7 +43,6 @@ public class FilterExpressionBuilderTests {
@Test
public void testEQ() {
- Expression expression = b.eq("country", "BG").build();
// country == "BG"
assertThat(b.eq("country", "BG").build()).isEqualTo(new Expression(EQ, new Key("country"), new Value("BG")));
}
diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java
index 8e82e11ba..057ae86cc 100644
--- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java
+++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/FilterExpressionTextParserTests.java
@@ -123,4 +123,16 @@ public class FilterExpressionTextParserTests {
assertThat(parser.getCache().get("WHERE " + expText)).isEqualTo(exp);
}
+ @Test
+ public void testIdentifiers() {
+ Expression exp = parser.parse("'country.1' == 'BG'");
+ assertThat(exp).isEqualTo(new Expression(EQ, new Key("'country.1'"), new Value("BG")));
+
+ exp = parser.parse("'country_1_2_3' == 'BG'");
+ assertThat(exp).isEqualTo(new Expression(EQ, new Key("'country_1_2_3'"), new Value("BG")));
+
+ exp = parser.parse("\"country 1 2 3\" == 'BG'");
+ assertThat(exp).isEqualTo(new Expression(EQ, new Key("\"country 1 2 3\""), new Value("BG")));
+ }
+
}
diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverterTests.java
index c439316da..ab8d38cfb 100644
--- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverterTests.java
+++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/MilvusFilterExpressionConverterTests.java
@@ -40,12 +40,12 @@ import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR
*/
public class MilvusFilterExpressionConverterTests {
- MilvusFilterExpressionConverter converter = new MilvusFilterExpressionConverter();
+ FilterExpressionConverter converter = new MilvusFilterExpressionConverter();
@Test
public void testEQ() {
// country == "BG"
- String vectorExpr = converter.convert(new Expression(EQ, new Key("country"), new Value("BG")));
+ String vectorExpr = converter.convertExpression(new Expression(EQ, new Key("country"), new Value("BG")));
assertThat(vectorExpr).isEqualTo("metadata[\"country\"] == \"BG\"");
}
@@ -53,7 +53,7 @@ public class MilvusFilterExpressionConverterTests {
public void tesEqAndGte() {
// genre == "drama" AND year >= 2020
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
+ .convertExpression(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
new Expression(GTE, new Key("year"), new Value(2020))));
assertThat(vectorExpr).isEqualTo("metadata[\"genre\"] == \"drama\" && metadata[\"year\"] >= 2020");
}
@@ -61,17 +61,18 @@ public class MilvusFilterExpressionConverterTests {
@Test
public void tesIn() {
// genre in ["comedy", "documentary", "drama"]
- String vectorExpr = converter
- .convert(new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
+ String vectorExpr = converter.convertExpression(
+ new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
assertThat(vectorExpr).isEqualTo("metadata[\"genre\"] in [\"comedy\",\"documentary\",\"drama\"]");
}
@Test
public void testNe() {
// year >= 2020 OR country == "BG" AND city != "Sofia"
- String vectorExpr = converter.convert(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
- new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
- new Expression(NE, new Key("city"), new Value("Sofia")))));
+ String vectorExpr = converter
+ .convertExpression(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
+ new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
+ new Expression(NE, new Key("city"), new Value("Sofia")))));
assertThat(vectorExpr).isEqualTo(
"metadata[\"year\"] >= 2020 || metadata[\"country\"] == \"BG\" && metadata[\"city\"] != \"Sofia\"");
}
@@ -79,7 +80,7 @@ public class MilvusFilterExpressionConverterTests {
@Test
public void testGroup() {
// (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Group(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
new Expression(EQ, new Key("country"), new Value("BG")))),
new Expression(NIN, new Key("city"), new Value(List.of("Sofia", "Plovdiv")))));
@@ -90,7 +91,7 @@ public class MilvusFilterExpressionConverterTests {
@Test
public void tesBoolean() {
// isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Expression(AND, new Expression(EQ, new Key("isOpen"), new Value(true)),
new Expression(GTE, new Key("year"), new Value(2020))),
new Expression(IN, new Key("country"), new Value(List.of("BG", "NL", "US")))));
@@ -103,10 +104,20 @@ public class MilvusFilterExpressionConverterTests {
public void testDecimal() {
// temperature >= -15.6 && temperature <= +20.13
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
+ .convertExpression(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
new Expression(LTE, new Key("temperature"), new Value(20.13))));
assertThat(vectorExpr).isEqualTo("metadata[\"temperature\"] >= -15.6 && metadata[\"temperature\"] <= 20.13");
}
+ @Test
+ public void testComplexIdentifiers() {
+ String vectorExpr = converter
+ .convertExpression(new Expression(EQ, new Key("\"country 1 2 3\""), new Value("BG")));
+ assertThat(vectorExpr).isEqualTo("metadata[\"country 1 2 3\"] == \"BG\"");
+
+ vectorExpr = converter.convertExpression(new Expression(EQ, new Key("'country 1 2 3'"), new Value("BG")));
+ assertThat(vectorExpr).isEqualTo("metadata[\"country 1 2 3\"] == \"BG\"");
+ }
+
}
diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverterTests.java
index 4ad8bb9a1..cac2dd2c5 100644
--- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverterTests.java
+++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PgVectorFilterExpressionConverterTests.java
@@ -40,12 +40,12 @@ import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR
*/
public class PgVectorFilterExpressionConverterTests {
- PgVectorFilterExpressionConverter converter = new PgVectorFilterExpressionConverter();
+ FilterExpressionConverter converter = new PgVectorFilterExpressionConverter();
@Test
public void testEQ() {
// country == "BG"
- String vectorExpr = converter.convert(new Expression(EQ, new Key("country"), new Value("BG")));
+ String vectorExpr = converter.convertExpression(new Expression(EQ, new Key("country"), new Value("BG")));
assertThat(vectorExpr).isEqualTo("$.country == \"BG\"");
}
@@ -53,7 +53,7 @@ public class PgVectorFilterExpressionConverterTests {
public void tesEqAndGte() {
// genre == "drama" AND year >= 2020
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
+ .convertExpression(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
new Expression(GTE, new Key("year"), new Value(2020))));
assertThat(vectorExpr).isEqualTo("$.genre == \"drama\" && $.year >= 2020");
}
@@ -61,24 +61,25 @@ public class PgVectorFilterExpressionConverterTests {
@Test
public void tesIn() {
// genre in ["comedy", "documentary", "drama"]
- String vectorExpr = converter
- .convert(new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
+ String vectorExpr = converter.convertExpression(
+ new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
assertThat(vectorExpr).isEqualTo("$.genre in [\"comedy\",\"documentary\",\"drama\"]");
}
@Test
public void testNe() {
// year >= 2020 OR country == "BG" AND city != "Sofia"
- String vectorExpr = converter.convert(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
- new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
- new Expression(NE, new Key("city"), new Value("Sofia")))));
+ String vectorExpr = converter
+ .convertExpression(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
+ new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
+ new Expression(NE, new Key("city"), new Value("Sofia")))));
assertThat(vectorExpr).isEqualTo("$.year >= 2020 || $.country == \"BG\" && $.city != \"Sofia\"");
}
@Test
public void testGroup() {
// (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Group(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
new Expression(EQ, new Key("country"), new Value("BG")))),
new Expression(NIN, new Key("city"), new Value(List.of("Sofia", "Plovdiv")))));
@@ -89,7 +90,7 @@ public class PgVectorFilterExpressionConverterTests {
@Test
public void tesBoolean() {
// isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Expression(AND, new Expression(EQ, new Key("isOpen"), new Value(true)),
new Expression(GTE, new Key("year"), new Value(2020))),
new Expression(IN, new Key("country"), new Value(List.of("BG", "NL", "US")))));
@@ -101,10 +102,17 @@ public class PgVectorFilterExpressionConverterTests {
public void testDecimal() {
// temperature >= -15.6 && temperature <= +20.13
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
+ .convertExpression(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
new Expression(LTE, new Key("temperature"), new Value(20.13))));
assertThat(vectorExpr).isEqualTo("$.temperature >= -15.6 && $.temperature <= 20.13");
}
+ @Test
+ public void testComplexIdentifiers() {
+ String vectorExpr = converter
+ .convertExpression(new Expression(EQ, new Key("\"country 1 2 3\""), new Value("BG")));
+ assertThat(vectorExpr).isEqualTo("$.\"country 1 2 3\" == \"BG\"");
+ }
+
}
diff --git a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverterTests.java b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverterTests.java
index 3b37fc50e..45a9277b3 100644
--- a/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverterTests.java
+++ b/spring-ai-core/src/test/java/org/springframework/ai/vectorstore/filter/converter/PineconeFilterExpressionConverterTests.java
@@ -40,12 +40,12 @@ import static org.springframework.ai.vectorstore.filter.Filter.ExpressionType.OR
*/
public class PineconeFilterExpressionConverterTests {
- PineconeFilterExpressionConverter converter = new PineconeFilterExpressionConverter();
+ FilterExpressionConverter converter = new PineconeFilterExpressionConverter();
@Test
public void testEQ() {
// country == "BG"
- String vectorExpr = converter.convert(new Expression(EQ, new Key("country"), new Value("BG")));
+ String vectorExpr = converter.convertExpression(new Expression(EQ, new Key("country"), new Value("BG")));
assertThat(vectorExpr).isEqualTo("{\"country\": {\"$eq\": \"BG\"}}");
}
@@ -53,7 +53,7 @@ public class PineconeFilterExpressionConverterTests {
public void tesEqAndGte() {
// genre == "drama" AND year >= 2020
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
+ .convertExpression(new Expression(AND, new Expression(EQ, new Key("genre"), new Value("drama")),
new Expression(GTE, new Key("year"), new Value(2020))));
assertThat(vectorExpr)
.isEqualTo("{\"$and\": [{\"genre\": {\"$eq\": \"drama\"}},{\"year\": {\"$gte\": 2020}}]}");
@@ -62,17 +62,18 @@ public class PineconeFilterExpressionConverterTests {
@Test
public void tesIn() {
// genre in ["comedy", "documentary", "drama"]
- String vectorExpr = converter
- .convert(new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
+ String vectorExpr = converter.convertExpression(
+ new Expression(IN, new Key("genre"), new Value(List.of("comedy", "documentary", "drama"))));
assertThat(vectorExpr).isEqualTo("{\"genre\": {\"$in\": [\"comedy\",\"documentary\",\"drama\"]}}");
}
@Test
public void testNe() {
// year >= 2020 OR country == "BG" AND city != "Sofia"
- String vectorExpr = converter.convert(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
- new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
- new Expression(NE, new Key("city"), new Value("Sofia")))));
+ String vectorExpr = converter
+ .convertExpression(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
+ new Expression(AND, new Expression(EQ, new Key("country"), new Value("BG")),
+ new Expression(NE, new Key("city"), new Value("Sofia")))));
assertThat(vectorExpr).isEqualTo(
"{\"$or\": [{\"year\": {\"$gte\": 2020}},{\"$and\": [{\"country\": {\"$eq\": \"BG\"}},{\"city\": {\"$ne\": \"Sofia\"}}]}]}");
}
@@ -80,7 +81,7 @@ public class PineconeFilterExpressionConverterTests {
@Test
public void testGroup() {
// (year >= 2020 OR country == "BG") AND city NIN ["Sofia", "Plovdiv"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Group(new Expression(OR, new Expression(GTE, new Key("year"), new Value(2020)),
new Expression(EQ, new Key("country"), new Value("BG")))),
new Expression(NIN, new Key("city"), new Value(List.of("Sofia", "Plovdiv")))));
@@ -91,7 +92,7 @@ public class PineconeFilterExpressionConverterTests {
@Test
public void tesBoolean() {
// isOpen == true AND year >= 2020 AND country IN ["BG", "NL", "US"]
- String vectorExpr = converter.convert(new Expression(AND,
+ String vectorExpr = converter.convertExpression(new Expression(AND,
new Expression(AND, new Expression(EQ, new Key("isOpen"), new Value(true)),
new Expression(GTE, new Key("year"), new Value(2020))),
new Expression(IN, new Key("country"), new Value(List.of("BG", "NL", "US")))));
@@ -104,11 +105,21 @@ public class PineconeFilterExpressionConverterTests {
public void testDecimal() {
// temperature >= -15.6 && temperature <= +20.13
String vectorExpr = converter
- .convert(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
+ .convertExpression(new Expression(AND, new Expression(GTE, new Key("temperature"), new Value(-15.6)),
new Expression(LTE, new Key("temperature"), new Value(20.13))));
assertThat(vectorExpr)
.isEqualTo("{\"$and\": [{\"temperature\": {\"$gte\": -15.6}},{\"temperature\": {\"$lte\": 20.13}}]}");
}
+ @Test
+ public void testComplexIdentifiers() {
+ String vectorExpr = converter
+ .convertExpression(new Expression(EQ, new Key("\"country 1 2 3\""), new Value("BG")));
+ assertThat(vectorExpr).isEqualTo("{\"country 1 2 3\": {\"$eq\": \"BG\"}}");
+
+ vectorExpr = converter.convertExpression(new Expression(EQ, new Key("'country 1 2 3'"), new Value("BG")));
+ assertThat(vectorExpr).isEqualTo("{\"country 1 2 3\": {\"$eq\": \"BG\"}}");
+ }
+
}
diff --git a/spring-ai-spring-boot-autoconfigure/pom.xml b/spring-ai-spring-boot-autoconfigure/pom.xml
index 52d128847..98345a519 100644
--- a/spring-ai-spring-boot-autoconfigure/pom.xml
+++ b/spring-ai-spring-boot-autoconfigure/pom.xml
@@ -94,6 +94,14 @@
true
+
+
+ org.springframework.experimental.ai
+ spring-ai-chroma-store
+ ${project.parent.version}
+ true
+
+
org.springframework.boot
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaApiProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaApiProperties.java
new file mode 100644
index 000000000..7fd599995
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaApiProperties.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.autoconfigure.vectorstore.chroma;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author Christian Tzolov
+ */
+@ConfigurationProperties(ChromaApiProperties.CONFIG_PREFIX)
+public class ChromaApiProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vectorstore.chroma.client";
+
+ private String host = "http://localhost";
+
+ private int port = 8000;
+
+ private String keyToken;
+
+ private String username;
+
+ private String password;
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String baseUrl) {
+ this.host = baseUrl;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public String getKeyToken() {
+ return keyToken;
+ }
+
+ public void setKeyToken(String keyToken) {
+ this.keyToken = keyToken;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java
new file mode 100644
index 000000000..096ccc1f3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfiguration.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.autoconfigure.vectorstore.chroma;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.experimental.ai.chroma.ChromaApi;
+import org.springframework.experimental.ai.vectorsore.ChromaVectorStore;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author Christian Tzolov
+ */
+@AutoConfiguration
+@ConditionalOnClass({ EmbeddingClient.class, RestTemplate.class, ChromaVectorStore.class, ObjectMapper.class })
+@EnableConfigurationProperties({ ChromaApiProperties.class, ChromaVectorStoreProperties.class })
+public class ChromaVectorStoreAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ChromaApi chromaApi(ChromaApiProperties apiProperties, RestTemplate restTemplate) {
+
+ String chromaUrl = String.format("%s:%s", apiProperties.getHost(), apiProperties.getPort());
+
+ var chromaApi = new ChromaApi(chromaUrl, restTemplate, new ObjectMapper());
+
+ if (StringUtils.hasText(apiProperties.getKeyToken())) {
+ chromaApi.withKeyToken(apiProperties.getKeyToken());
+ }
+ else if (StringUtils.hasText(apiProperties.getUsername()) && StringUtils.hasText(apiProperties.getPassword())) {
+ chromaApi.withBasicAuthCredentials(apiProperties.getUsername(), apiProperties.getPassword());
+ }
+
+ return chromaApi;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public VectorStore vectorStore(EmbeddingClient embeddingClient, ChromaApi chromaApi,
+ ChromaVectorStoreProperties storeProperties) {
+ return new ChromaVectorStore(embeddingClient, chromaApi, storeProperties.getCollectionName());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreProperties.java
new file mode 100644
index 000000000..1d6b5525f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreProperties.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.autoconfigure.vectorstore.chroma;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.experimental.ai.vectorsore.ChromaVectorStore;
+
+/**
+ * @author Christian Tzolov
+ */
+@ConfigurationProperties(ChromaVectorStoreProperties.CONFIG_PREFIX)
+public class ChromaVectorStoreProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vectorstore.chroma.store";
+
+ private String collectionName = ChromaVectorStore.DEFAULT_COLLECTION_NAME;
+
+ public String getCollectionName() {
+ return collectionName;
+ }
+
+ public void setCollectionName(String collectionName) {
+ this.collectionName = collectionName;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 2c6f7d20a..5d3c956f0 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -5,3 +5,4 @@ org.springframework.ai.autoconfigure.vectorstore.pinecone.PineconeVectorStoreAut
org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.embedding.transformer.TransformersEmbeddingClientAutoConfiguration
org.springframework.ai.autoconfigure.huggingface.HuggingfaceAutoConfiguration
+org.springframework.ai.autoconfigure.vectorstore.chroma.ChromaVectorStoreAutoConfiguration
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfigurationIT.java
new file mode 100644
index 000000000..30bba69cf
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/chroma/ChromaVectorStoreAutoConfigurationIT.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.autoconfigure.vectorstore.chroma;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingClient;
+import org.springframework.ai.embedding.TransformersEmbeddingClient;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+@Testcontainers
+public class ChromaVectorStoreAutoConfigurationIT {
+
+ @Container
+ static GenericContainer> chromaContainer = new GenericContainer<>("ghcr.io/chroma-core/chroma:0.4.15")
+ .withExposedPorts(8000);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(ChromaVectorStoreAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("spring.ai.vectorstore.chroma.client.host=http://localhost",
+ "spring.ai.vectorstore.chroma.client.port=" + chromaContainer.getMappedPort(8000),
+ "spring.ai.vectorstore.chroma.store.collectionName=TestCollection");
+
+ @Test
+ public void addAndSearchWithFilters() {
+
+ contextRunner.run(context -> {
+
+ VectorStore vectorStore = context.getBean(VectorStore.class);
+
+ var bgDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Bulgaria"));
+ var nlDocument = new Document("The World is Big and Salvation Lurks Around the Corner",
+ Map.of("country", "Netherland"));
+
+ vectorStore.add(List.of(bgDocument, nlDocument));
+
+ var request = SearchRequest.query("The World").withTopK(5);
+
+ List results = vectorStore.similaritySearch(request);
+ assertThat(results).hasSize(2);
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Bulgaria'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(bgDocument.getId());
+
+ results = vectorStore
+ .similaritySearch(request.withSimilarityThresholdAll().withFilterExpression("country == 'Netherland'"));
+ assertThat(results).hasSize(1);
+ assertThat(results.get(0).getId()).isEqualTo(nlDocument.getId());
+
+ // Remove all documents from the store
+ vectorStore.delete(List.of(bgDocument, nlDocument).stream().map(doc -> doc.getId()).toList());
+ });
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ public EmbeddingClient embeddingClient() {
+ return new TransformersEmbeddingClient();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java
index 7ef80d715..33bdca50a 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/pgvector/PgVectorStoreAutoConfigurationIT.java
@@ -22,12 +22,10 @@ import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
-import org.springframework.core.io.DefaultResourceLoader;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
-import org.springframework.ai.ResourceUtils;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.TransformersEmbeddingClient;
@@ -39,6 +37,7 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.DefaultResourceLoader;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/vector-stores/spring-ai-chroma/README.md b/vector-stores/spring-ai-chroma/README.md
new file mode 100644
index 000000000..f40c33ff6
--- /dev/null
+++ b/vector-stores/spring-ai-chroma/README.md
@@ -0,0 +1,154 @@
+# Chroma VectorStore
+
+This readme will walk you through setting up the Chroma VectorStore to store document embeddings and perform similarity searches.
+
+
+
+## What is Chroma?
+
+[Chroma](https://docs.trychroma.com/) is the open-source embedding database. It gives you the tools to store document embeddings, content and metadata and to search through those embeddings including metadata filtering.
+
+## Prerequisites
+
+1. OpenAI Account: Create an account at [OpenAI Signup](https://platform.openai.com/signup) and generate the token at [API Keys](https://platform.openai.com/account/api-keys).
+
+2. Access to ChromeDB. The [setup local ChromaDB](#appendix_a) appendix show how to setup a DB locally with a Docker container.
+
+On startup the `ChromaVectorStore` creates the required collection if one is not provisioned already.
+
+## Configuration
+
+To set up ChromaVectorStore, you'll need to provide your OpenAI API Key. Set it as an environment variable like so:
+
+```bash
+export SPRING_AI_OPENAI_API_KEY='Your_OpenAI_API_Key'
+```
+
+## Dependencies
+
+Add these dependencies to your project:
+
+1. OpenAI: Required for calculating embeddings.
+
+ ```xml
+
+ org.springframework.experimental.ai
+ spring-ai-openai-spring-boot-starter
+ 0.7.0-SNAPSHOT
+
+ ```
+
+2. Chroma VectorStore.
+
+ ```xml
+
+ org.springframework.experimental.ai
+ spring-ai-chroma-store
+ 0.7.0-SNAPSHOT
+
+ ```
+
+## Sample Code
+
+Create an `RestTemplate` instance with proper ChromaDB authorization configurations and Use it to create `ChromaApi` instance:
+
+```java
+@Bean
+public RestTemplate restTemplate() {
+ return new RestTemplate();
+}
+
+@Bean
+public ChromaApi chromaApi(RestTemplate restTemplate) {
+ String chromaUrl = "http://localhost:8000";
+ ChromaApi chromaApi = ChromaApi(chromaUrl, restTemplate);
+ return chromaApi;
+}
+```
+> [!NOTE]
+> For ChromaDB secured with [Static API Token Authentication](https://docs.trychroma.com/usage-guide#static-api-token-authentication) use the `ChromaApi#withKeyToken()` method to set your credentials. Check the `ChromaWhereIT` for an example.
+
+> [!NOTE]
+> For ChromaDB secured with [Basic Authentication](https://docs.trychroma.com/usage-guide#basic-authentication) use the `ChromaApi#withBasicAuth(, )` method to set your credentials. Check the `BasicAuthChromaWhereIT` for an example.
+
+
+Integrate with OpenAI's embeddings by adding the Spring Boot OpenAI starter to your project.
+This provides you with an implementation of the Embeddings client:
+
+```java
+@Bean
+public VectorStore chromaVectorStore(EmbeddingClient embeddingClient, ChromaApi chromaApi) {
+ return new ChromaVectorStore(embeddingClient, chromaApi, "TestCollection");
+}
+```
+
+In your main code, create some documents
+
+```java
+List documents = List.of(
+ new Document("Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!! Spring AI rocks!!", Map.of("meta1", "meta1")),
+ new Document("The World is Big and Salvation Lurks Around the Corner"),
+ new Document("You walk forward facing the past and you turn back toward the future.", Map.of("meta2", "meta2")));
+```
+
+Add the documents to your vector store:
+
+```java
+vectorStore.add(List.of(document));
+```
+
+And finally, retrieve documents similar to a query:
+
+```java
+List results = vectorStore.similaritySearch("Spring", 5);
+```
+
+If all goes well, you should retrieve the document containing the text "Spring AI rocks!!".
+
+### Metadata filtering
+
+You can leverage the generic, portable [metadata filters](https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_metadata_filters) with ChromaVector store as well.
+
+For example you can use either the text expression language:
+
+```java
+vectorStore.similaritySearch("The World", TOP_K, SIMILARITY_THRESHOLD,
+ "author in ['john', 'jill'] && article_type == 'blog'");
+```
+
+or programmatically using the `Filter.Expression` DSL:
+
+```java
+FilterExpressionBuilder b = new FilterExpressionBuilder();
+
+vectorStore.similaritySearch("The World", TOP_K, SIMILARITY_THRESHOLD,
+ b.and(
+ b.in(List.of("john", "jill")),
+ b.eq("article_type", "blog")).build());
+```
+
+NOTE: Those (portable) filter expressions get automatically converted into the proprietary Chroma `where` [filter expressions](https://docs.trychroma.com/usage-guide#using-where-filters).
+
+For example this portable filter expression:
+
+```sql
+author in ['john', 'jill'] && article_type == 'blog'
+```
+
+is converted into the proprietary Chroma format:
+
+```json
+{"$and":[
+ {"author": {"$in": ["john", "jill"]}},
+ {"article_type":{"$eq":"blog"}}]
+}"
+```
+
+
+## Appendix A: Run Chroma Locally
+
+```
+docker run -it --rm --name chroma -p 8000:8000 ghcr.io/chroma-core/chroma:0.4.15
+```
+
+starts a chroma store at
diff --git a/vector-stores/spring-ai-chroma/pom.xml b/vector-stores/spring-ai-chroma/pom.xml
new file mode 100644
index 000000000..b6874ab3c
--- /dev/null
+++ b/vector-stores/spring-ai-chroma/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ org.springframework.experimental.ai
+ spring-ai
+ 0.7.1-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-chroma-store
+ jar
+ Spring AI Chroma Vector Store
+ Spring AI Chroma Vector Store
+ https://github.com/spring-projects-experimental/spring-ai
+
+
+ https://github.com/spring-projects-experimental/spring-ai
+ git://github.com/spring-projects-experimental/spring-ai.git
+ git@github.com:spring-projects-experimental/spring-ai.git
+
+
+
+
+ org.springframework.experimental.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+
+ org.springframework.experimental.ai
+ spring-ai-openai
+ ${parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ ${testcontainers.version}
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+
+
+
+
+
+
diff --git a/vector-stores/spring-ai-chroma/src/main/java/org/springframework/experimental/ai/chroma/ChromaApi.java b/vector-stores/spring-ai-chroma/src/main/java/org/springframework/experimental/ai/chroma/ChromaApi.java
new file mode 100644
index 000000000..d0a2aa31c
--- /dev/null
+++ b/vector-stores/spring-ai-chroma/src/main/java/org/springframework/experimental/ai/chroma/ChromaApi.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2023-2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.experimental.ai.chroma;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.experimental.ai.chroma.ChromaApi.QueryRequest.Include;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.support.BasicAuthenticationInterceptor;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Single-class Chroma API implementation based on the (unofficial) Chroma REST API.
+ *
+ * @author Christian Tzolov
+ */
+public class ChromaApi {
+
+ // Regular expression pattern that looks for a message inside the ValueError(...).
+ private static Pattern VALUE_ERROR_PATTERN = Pattern.compile("ValueError\\('([^']*)'\\)");
+
+ private final String baseUrl;
+
+ private final RestTemplate restTemplate;
+
+ private final ObjectMapper objectMapper;
+
+ private String keyToken;
+
+ public ChromaApi(String baseUrl, RestTemplate restTemplate) {
+ this(baseUrl, restTemplate, new ObjectMapper());
+ }
+
+ public ChromaApi(String baseUrl, RestTemplate restTemplate, ObjectMapper objectMapper) {
+ this.baseUrl = baseUrl;
+ this.restTemplate = restTemplate;
+ this.objectMapper = objectMapper;
+ }
+
+ /**
+ * Configure access to ChromaDB secured with static API Token Authentication:
+ * https://docs.trychroma.com/usage-guide#static-api-token-authentication
+ * @param keyToken Chroma static API Token Authentication. (Optional)
+ */
+ public ChromaApi withKeyToken(String keyToken) {
+ this.keyToken = keyToken;
+ return this;
+ }
+
+ /**
+ * Configure access to ChromaDB secured with Basic Authentication:
+ * https://docs.trychroma.com/usage-guide#basic-authentication
+ * @param username Credentials username.
+ * @param password Credentials password.
+ */
+ public ChromaApi withBasicAuthCredentials(String username, String password) {
+ this.restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(username, username));
+ return this;
+ }
+
+ /**
+ * Chroma embedding collection.
+ *
+ * @param id Collection Id.
+ * @param name The name of the collection.
+ * @param metadata Metadata associated with the collection.
+ */
+ public record Collection(String id, String name, Map metadata) {
+ }
+
+ /**
+ * Request to create a new collection with the given name and metadata.
+ *
+ * @param name The name of the collection to create.
+ * @param metadata Optional metadata to associate with the collection.
+ */
+ public record CreateCollectionRequest(String name, Map metadata) {
+ public CreateCollectionRequest(String name) {
+ this(name, new HashMap<>(Map.of("hnsw:space", "cosine")));
+ }
+ }
+
+ /**
+ * Add embeddings to the chroma data store.
+ *
+ * @param ids The ids of the embeddings to add.
+ * @param embeddings The embeddings to add.
+ * @param metadata The metadata to associate with the embeddings. When querying, you
+ * can filter on this metadata.
+ * @param documents The documents contents to associate with the embeddings.
+ */
+ public record AddEmbeddingsRequest(List ids, List embeddings,
+ @JsonProperty("metadatas") List