diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/pom.xml b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/pom.xml
new file mode 100644
index 00000000000..3ee702e55b5
--- /dev/null
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/pom.xml
@@ -0,0 +1,130 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 2.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-autoconfigure-vector-store-infinispan
+ jar
+ Spring AI Auto Configuration for Infinispan vector store
+ Spring AI Auto Configuration for Infinispan vector store
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.infinispan
+ infinispan-bom
+ ${infinispan.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-infinispan-store
+ ${project.parent.version}
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.infinispan
+ infinispan-spring-boot4-starter-remote
+ true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure-processor
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+ org.infinispan
+ testcontainers-infinispan
+ test
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.springframework.ai
+ spring-ai-transformers
+ ${project.parent.version}
+ test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfiguration.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfiguration.java
new file mode 100644
index 00000000000..381bae1ed08
--- /dev/null
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfiguration.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023-2025 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.infinispan.autoconfigure;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.SpringAIVectorStoreTypes;
+import org.springframework.ai.vectorstore.infinispan.InfinispanVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Infinispan Vector Store.
+ *
+ * @author Katia Aresti
+ */
+@AutoConfiguration
+@ConditionalOnClass({ InfinispanVectorStore.class, EmbeddingModel.class })
+@EnableConfigurationProperties(InfinispanVectorStoreProperties.class)
+@ConditionalOnProperty(name = SpringAIVectorStoreTypes.TYPE, havingValue = SpringAIVectorStoreTypes.GEMFIRE,
+ matchIfMissing = true)
+public class InfinispanVectorStoreAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public InfinispanVectorStore infinispanVectorStore(EmbeddingModel embeddingModel,
+ InfinispanVectorStoreProperties properties, RemoteCacheManager infinispanClient,
+ ObjectProvider observationRegistry,
+ ObjectProvider customObservationConvention) {
+
+ return InfinispanVectorStore.builder(infinispanClient, embeddingModel)
+ .createStore(properties.isCreateStore())
+ .registerSchema(properties.isRegisterSchema())
+ .schemaFileName(properties.getSchemaFileName())
+ .storeName(properties.getStoreName())
+ .storeConfig(properties.getStoreConfig())
+ .observationRegistry(observationRegistry.getIfAvailable())
+ .customObservationConvention(customObservationConvention.getIfAvailable())
+ .distance(properties.getDistance())
+ .similarity(properties.getSimilarity())
+ .packageName(properties.getPackageName())
+ .springAiItemName(properties.getItemName())
+ .metadataItemName(properties.getMetadataItemName())
+ .build();
+ }
+
+}
diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreProperties.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreProperties.java
new file mode 100644
index 00000000000..6103856f043
--- /dev/null
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreProperties.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023-2025 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.infinispan.autoconfigure;
+
+import org.springframework.ai.vectorstore.properties.CommonVectorStoreProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import static org.springframework.ai.vectorstore.infinispan.autoconfigure.InfinispanVectorStoreProperties.CONFIG_PREFIX;
+
+/**
+ * Configuration properties for Infinispan Vector Store.
+ */
+@ConfigurationProperties(prefix = CONFIG_PREFIX)
+public class InfinispanVectorStoreProperties extends CommonVectorStoreProperties {
+
+ /**
+ * Configuration prefix for Spring AI VectorStore Infinispan.
+ */
+ public static final String CONFIG_PREFIX = "spring.ai.vectorstore.infinispan";
+
+ private Boolean registerSchema;
+
+ private Boolean createStore;
+
+ private String storeName;
+
+ private String storeConfig;
+
+ private Integer distance;
+
+ private String similarity;
+
+ private String schemaFileName;
+
+ private String packageName;
+
+ private String itemName;
+
+ private String metadataItemName;
+
+ public String getStoreName() {
+ return storeName;
+ }
+
+ public void setStoreName(String storeName) {
+ this.storeName = storeName;
+ }
+
+ public String getStoreConfig() {
+ return storeConfig;
+ }
+
+ public void setStoreConfig(String storeConfig) {
+ this.storeConfig = storeConfig;
+ }
+
+ public Integer getDistance() {
+ return distance;
+ }
+
+ public void setDistance(Integer distance) {
+ this.distance = distance;
+ }
+
+ public String getSimilarity() {
+ return similarity;
+ }
+
+ public void setSimilarity(String similarity) {
+ this.similarity = similarity;
+ }
+
+ public String getSchemaFileName() {
+ return schemaFileName;
+ }
+
+ public void setSchemaFileName(String schemaFileName) {
+ this.schemaFileName = schemaFileName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+
+ public void setPackageName(String packageName) {
+ this.packageName = packageName;
+ }
+
+ public String getItemName() {
+ return itemName;
+ }
+
+ public void setItemName(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public String getMetadataItemName() {
+ return metadataItemName;
+ }
+
+ public void setMetadataItemName(String metadataItemName) {
+ this.metadataItemName = metadataItemName;
+ }
+
+ public Boolean isRegisterSchema() {
+ return registerSchema;
+ }
+
+ public void setRegisterSchema(Boolean registerSchema) {
+ this.registerSchema = registerSchema;
+ }
+
+ public Boolean isCreateStore() {
+ return createStore;
+ }
+
+ public void setCreateStore(boolean createStore) {
+ this.createStore = createStore;
+ }
+
+}
diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..0d9ec659b4d
--- /dev/null
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-2025 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.
+#
+org.springframework.ai.vectorstore.infinispan.autoconfigure.InfinispanVectorStoreAutoConfiguration
diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/test/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/test/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfigurationIT.java
new file mode 100644
index 00000000000..b76d1acf0ab
--- /dev/null
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan/src/test/java/org/springframework/ai/vectorstore/infinispan/autoconfigure/InfinispanVectorStoreAutoConfigurationIT.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2023-2024 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.infinispan.autoconfigure;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import io.micrometer.observation.tck.TestObservationRegistry;
+import org.awaitility.Awaitility;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.commons.marshall.ProtoStreamMarshaller;
+import org.infinispan.commons.util.Version;
+import org.infinispan.protostream.schema.Schema;
+import org.infinispan.spring.starter.remote.InfinispanRemoteAutoConfiguration;
+import org.infinispan.testcontainers.InfinispanContainer;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.test.vectorstore.ObservationTestUtil;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.ai.util.ResourceUtils;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.ai.vectorstore.infinispan.InfinispanVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+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 org.springframework.core.io.DefaultResourceLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+
+/**
+ * @author Katia Aresti
+ */
+@Testcontainers
+class InfinispanVectorStoreAutoConfigurationIT {
+
+ @Container
+ private static final InfinispanContainer infinispanContainer = new InfinispanContainer(
+ InfinispanContainer.IMAGE_BASENAME + ":" + Version.getVersion());
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(InfinispanRemoteAutoConfiguration.class,
+ InfinispanVectorStoreAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("spring.ai.vectorstore.infinispan.distance=" + 10,
+ "infinispan.remote.server-list=" + serverList(),
+ "infinispan.remote.auth-username=" + InfinispanContainer.DEFAULT_USERNAME,
+ // Needs the marshalling property until fix
+ // https://github.com/infinispan/infinispan/issues/16440
+ "infinispan.remote.marshaller=" + ProtoStreamMarshaller.class.getName(),
+ "infinispan.remote.auth-password=" + InfinispanContainer.DEFAULT_PASSWORD);
+
+ List documents = List.of(
+ new Document(ResourceUtils.getText("classpath:/test/data/spring.ai.txt"), Map.of("spring", "great")),
+ new Document(ResourceUtils.getText("classpath:/test/data/time.shelter.txt")), new Document(
+ ResourceUtils.getText("classpath:/test/data/great.depression.txt"), Map.of("depression", "bad")));
+
+ @Test
+ public void addAndSearchTest() {
+ this.contextRunner.run(context -> {
+ InfinispanVectorStore vectorStore = context.getBean(InfinispanVectorStore.class);
+ TestObservationRegistry observationRegistry = context.getBean(TestObservationRegistry.class);
+
+ vectorStore.add(this.documents);
+
+ ObservationTestUtil.assertObservationRegistry(observationRegistry, VectorStoreProvider.INFINISPAN,
+ VectorStoreObservationContext.Operation.ADD);
+ observationRegistry.clear();
+
+ Awaitility.await()
+ .until(() -> vectorStore.similaritySearch(
+ SearchRequest.builder().query("Great Depression").topK(1).similarityThreshold(0).build()),
+ hasSize(1));
+
+ observationRegistry.clear();
+
+ List results = vectorStore.similaritySearch(
+ SearchRequest.builder().query("Great Depression").topK(1).similarityThreshold(0).build());
+
+ assertThat(results).hasSize(1);
+ Document resultDoc = results.get(0);
+ assertThat(resultDoc.getId()).isEqualTo(this.documents.get(2).getId());
+ assertThat(resultDoc.getText()).contains("The Great Depression (1929–1939) was an economic shock");
+ assertThat(resultDoc.getMetadata()).hasSize(1);
+
+ ObservationTestUtil.assertObservationRegistry(observationRegistry, VectorStoreProvider.INFINISPAN,
+ VectorStoreObservationContext.Operation.QUERY);
+ observationRegistry.clear();
+
+ // Remove all documents from the store
+ vectorStore.delete(this.documents.stream().map(Document::getId).toList());
+
+ ObservationTestUtil.assertObservationRegistry(observationRegistry, VectorStoreProvider.INFINISPAN,
+ VectorStoreObservationContext.Operation.DELETE);
+ observationRegistry.clear();
+
+ Awaitility.await()
+ .until(() -> vectorStore.similaritySearch(
+ SearchRequest.builder().query("Great Depression").topK(1).similarityThreshold(0).build()),
+ hasSize(0));
+ });
+ }
+
+ @Test
+ public void propertiesTest() {
+ new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(InfinispanVectorStoreAutoConfiguration.class,
+ InfinispanRemoteAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .withPropertyValues("infinispan.remote.server-list=" + serverList(),
+ "infinispan.remote.auth-username=" + InfinispanContainer.DEFAULT_USERNAME,
+ "infinispan.remote.auth-password=" + InfinispanContainer.DEFAULT_PASSWORD,
+ "spring.ai.vectorstore.infinispan.distance=20",
+ "spring.ai.vectorstore.infinispan.item-name=ItemExample",
+ "spring.ai.vectorstore.infinispan.metadata-item-name=MetadataExample",
+ "spring.ai.vectorstore.infinispan.package-name=exam.pac",
+ "spring.ai.vectorstore.infinispan.store-name=mycoolstore",
+ "spring.ai.vectorstore.infinispan.schema-file-name=schemaName.proto",
+ "spring.ai.vectorstore.infinispan.similarity=cosine")
+ .run(context -> {
+ var properties = context.getBean(InfinispanVectorStoreProperties.class);
+ assertThat(properties).isNotNull();
+ assertThat(properties.getDistance()).isEqualTo(20);
+ assertThat(properties.getItemName()).isEqualTo("ItemExample");
+ assertThat(properties.getMetadataItemName()).isEqualTo("MetadataExample");
+ assertThat(properties.getSimilarity()).isEqualTo("cosine");
+
+ InfinispanVectorStore infinispanVectorStore = context.getBean(InfinispanVectorStore.class);
+ assertThat(infinispanVectorStore).isNotNull();
+
+ RemoteCacheManager cacheManager = context.getBean(RemoteCacheManager.class);
+ RemoteCache cache = cacheManager.getCache("mycoolstore");
+ assertThat(cache).isNotNull();
+ Optional schema = cacheManager.administration().schemas().get("schemaName.proto");
+ assertThat(schema).isNotEmpty();
+ String schemaContent = schema.get().getContent();
+ assertThat(schemaContent).contains("ItemExample");
+ assertThat(schemaContent).contains("MetadataExample");
+ assertThat(schemaContent).contains("package exam.pac");
+ });
+ }
+
+ @Test
+ public void autoConfigurationDisabledWhenTypeIsNone() {
+ this.contextRunner.withPropertyValues("spring.ai.vectorstore.type=none").run(context -> {
+ assertThat(context.getBeansOfType(InfinispanVectorStoreProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(InfinispanVectorStore.class)).isEmpty();
+ assertThat(context.getBeansOfType(VectorStore.class)).isEmpty();
+ });
+ }
+
+ @Test
+ public void autoConfigurationEnabledByDefault() {
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(InfinispanVectorStoreProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VectorStore.class)).isNotEmpty();
+ assertThat(context.getBean(VectorStore.class)).isInstanceOf(InfinispanVectorStore.class);
+ });
+ }
+
+ @Test
+ public void autoConfigurationEnabledWhenTypeIsInfinispan() {
+ this.contextRunner.withPropertyValues("spring.ai.vectorstore.type=infinispan").run(context -> {
+ assertThat(context.getBeansOfType(InfinispanVectorStoreProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VectorStore.class)).isNotEmpty();
+ assertThat(context.getBean(VectorStore.class)).isInstanceOf(InfinispanVectorStore.class);
+ });
+ }
+
+ private static @NotNull String serverList() {
+ return infinispanContainer.getHost() + ":"
+ + infinispanContainer.getMappedPort(InfinispanContainer.DEFAULT_HOTROD_PORT);
+ }
+
+ private String getText(String uri) {
+ var resource = new DefaultResourceLoader().getResource(uri);
+ try {
+ return resource.getContentAsString(StandardCharsets.UTF_8);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ return new TransformersEmbeddingModel();
+ }
+
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 190f086a801..2d1dc175faa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@
vector-stores/spring-ai-elasticsearch-store
vector-stores/spring-ai-gemfire-store
vector-stores/spring-ai-hanadb-store
+ vector-stores/spring-ai-infinispan-store
vector-stores/spring-ai-mariadb-store
vector-stores/spring-ai-milvus-store
vector-stores/spring-ai-mongodb-atlas-store
@@ -137,6 +138,7 @@
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-couchbase
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-elasticsearch
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-gemfire
+ auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-infinispan
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mariadb
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-milvus
auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mongodb-atlas
@@ -333,6 +335,7 @@
3.5.3
9.2.0
0.22.0
+ 16.0.5
3.9.1
2024.5.1
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index dde9366f78e..9cb308e1bb6 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -479,6 +479,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-infinispan-store
+ ${project.version}
+
+
@@ -764,6 +770,12 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-autoconfigure-vector-store-infinispan
+ ${project.version}
+
+
org.springframework.ai
spring-ai-autoconfigure-vector-store-mariadb
diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java b/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java
index a9c3e3f2b12..3666c4ba5c8 100644
--- a/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java
+++ b/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/VectorStoreProvider.java
@@ -70,6 +70,11 @@ public enum VectorStoreProvider {
*/
HANA("hana"),
+ /**
+ * Vector store provided by Infinispan.
+ */
+ INFINISPAN("infinispan"),
+
/**
* Vector store provided by MariaDB.
*/
diff --git a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SpringAIVectorStoreTypes.java b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SpringAIVectorStoreTypes.java
index 4e8f8913b7a..1403f8e6854 100644
--- a/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SpringAIVectorStoreTypes.java
+++ b/spring-ai-vector-store/src/main/java/org/springframework/ai/vectorstore/SpringAIVectorStoreTypes.java
@@ -40,6 +40,8 @@ private SpringAIVectorStoreTypes() {
public static final String HANADB = "hanadb";
+ public static final String INFINISPAN = "infinispan";
+
public static final String MARIADB = "mariadb";
public static final String MILVUS = "milvus";
diff --git a/vector-stores/spring-ai-infinispan-store/pom.xml b/vector-stores/spring-ai-infinispan-store/pom.xml
new file mode 100644
index 00000000000..4df72a86281
--- /dev/null
+++ b/vector-stores/spring-ai-infinispan-store/pom.xml
@@ -0,0 +1,127 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 2.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-infinispan-store
+ jar
+ Spring AI Vector Store - Infinispan
+ Spring AI Infinispan Vector Store
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+ 17
+ 17
+
+
+
+
+
+ org.infinispan
+ infinispan-bom
+ ${infinispan.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-vector-store
+ ${project.parent.version}
+
+
+
+ org.infinispan
+ infinispan-client-hotrod
+ ${infinispan.version}
+
+
+
+
+ org.springframework.ai
+ spring-ai-transformers
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.infinispan
+ testcontainers-infinispan
+ ${infinispan.version}
+ test
+
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ test
+
+
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
+
+
+ org.apache.commons
+ commons-lang3
+ test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanFilterExpressionConverter.java b/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanFilterExpressionConverter.java
new file mode 100644
index 00000000000..7af2df8488c
--- /dev/null
+++ b/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanFilterExpressionConverter.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2023-2025 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.infinispan;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.springframework.ai.vectorstore.filter.Filter;
+import org.springframework.ai.vectorstore.filter.converter.AbstractFilterExpressionConverter;
+
+class InfinispanFilterExpressionConverter extends AbstractFilterExpressionConverter {
+
+ private static final Pattern DATE_FORMAT_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");
+
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
+ .withZone(ZoneOffset.UTC);
+
+ private int i = -1;
+
+ public String doJoin() {
+ StringBuilder sb = new StringBuilder();
+ for (int j = 0; j <= this.i; j++) {
+ sb.append(" join i.metadata m").append(j);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected void doExpression(Filter.Expression expression, StringBuilder context) {
+ switch (expression.type()) {
+ case AND:
+ context.append("((");
+ doExpression((Filter.Expression) expression.left(), context);
+ context.append(") AND (");
+ doExpression((Filter.Expression) expression.right(), context);
+ context.append("))");
+ break;
+ case OR:
+ context.append("((");
+ doExpression((Filter.Expression) expression.left(), context);
+ context.append(") OR (");
+ doExpression((Filter.Expression) expression.right(), context);
+ context.append("))");
+ break;
+ default:
+ doField(expression, context);
+ break;
+ }
+ }
+
+ @Override
+ protected void doKey(Filter.Key filterKey, StringBuilder context) {
+
+ }
+
+ @Override
+ public void doStartGroup(Filter.Group group, StringBuilder context) {
+ context.append("(");
+ }
+
+ @Override
+ public void doEndGroup(Filter.Group group, StringBuilder context) {
+ context.append(")");
+ }
+
+ private void doField(Filter.Expression expression, StringBuilder context) {
+ Filter.Key key = (Filter.Key) expression.left();
+ Filter.Value value = (Filter.Value) expression.right();
+ String result = switch (expression.type()) {
+ case EQ -> mapEqual(key, value, true);
+ case NE -> mapEqual(key, value, false);
+ case GT -> mapGreaterThan(key, value);
+ case GTE -> mapGreaterThanEqual(key, value);
+ case LT -> mapLessThan(key, value);
+ case LTE -> mapLessThanEqual(key, value);
+ case IN -> mapIn(key, value, true);
+ case NIN -> mapIn(key, value, false);
+ case ISNULL -> mapIsNull(key);
+ case ISNOTNULL -> mapIsNotNull(key);
+ default -> throw new UnsupportedOperationException("Unsupported value: " + expression.type());
+ };
+ context.append(result);
+ }
+
+ private String mapIsNull(Filter.Key key) {
+ incrementJoin();
+ String m = "m" + this.i + ".";
+ return "(" + metadataKey(key) + String.format(
+ "%svalue IS NULL and %svalue_int IS NULL and %svalue_date IS NULL and %svalue_float IS NULL and %svalue_bool IS NULL)",
+ m, m, m, m, m) + " OR (" + m + "name" + " NOT IN('" + key.key() + "'))";
+ }
+
+ private String mapIsNotNull(Filter.Key key) {
+ incrementJoin();
+ String m = "m" + this.i + ".";
+ return metadataKey(key) + String.format(
+ "(%svalue IS NOT NULL or %svalue_int IS NOT NULL or %svalue_date IS NOT NULL or %svalue_float IS NOT NULL or %svalue_bool IS NOT NULL)",
+ m, m, m, m, m);
+ }
+
+ private String mapEqual(Filter.Key key, Filter.Value value, boolean equals) {
+ incrementJoin();
+ String filter = metadataKey(key) + computeValue(equals ? "=" : "!=", value.value());
+ if (equals) {
+ return filter;
+ }
+ return filter + " " + addMetadataNullCheck();
+ }
+
+ private String mapGreaterThan(Filter.Key key, Filter.Value value) {
+ incrementJoin();
+ return metadataKey(key) + computeValue(">", value.value());
+ }
+
+ private String mapGreaterThanEqual(Filter.Key key, Filter.Value value) {
+ incrementJoin();
+ return metadataKey(key) + computeValue(">=", value.value());
+ }
+
+ private String mapLessThan(Filter.Key key, Filter.Value value) {
+ incrementJoin();
+ return metadataKey(key) + computeValue("<", value.value());
+ }
+
+ private String mapLessThanEqual(Filter.Key key, Filter.Value value) {
+ incrementJoin();
+ return metadataKey(key) + computeValue("<=", value.value());
+ }
+
+ private String mapIn(Filter.Key key, Filter.Value value, boolean in) {
+ incrementJoin();
+ String inStatement;
+ Object first;
+ if (value.value() instanceof List> values) {
+ if (values.isEmpty()) {
+ throw new UnsupportedOperationException("Infinispan metadata filter IN must contain values");
+ }
+ first = values.get(0);
+ inStatement = formattedComparisonValues(values);
+ }
+ else {
+ // single value
+ first = value.value();
+ inStatement = first instanceof String ? "'" + value.value() + "'" : value.value().toString();
+ }
+
+ String m = "m" + this.i + ".";
+ String inFilter = m + "value IN (" + inStatement + ")";
+ if (first instanceof Integer || first instanceof Long) {
+ inFilter = m + "value_int IN (" + inStatement + ")";
+ }
+ else if (first instanceof Float || first instanceof Double) {
+ inFilter = m + "value_float IN (" + inStatement + ")";
+ }
+ else if (first instanceof Boolean) {
+ inFilter = m + "value_bool IN (" + inStatement + ")";
+ }
+ else if (first instanceof Date) {
+ inFilter = m + "value_date IN (" + inStatement + ")";
+ }
+
+ if (in) {
+ return metadataKey(key) + inFilter;
+ }
+
+ String notInFilter = m + "value NOT IN (" + inStatement + ")";
+ if (first instanceof Integer || first instanceof Long) {
+ notInFilter = m + "value_int NOT IN (" + inStatement + ")";
+ }
+ else if (first instanceof Float || first instanceof Double) {
+ notInFilter = m + "value_float NOT IN (" + inStatement + ")";
+ }
+
+ return "(" + notInFilter + metadataKeyLast(key) + ") " + "OR (" + inFilter + " and " + m + "name!='" + key.key()
+ + "')" + " " + addMetadataNullCheck();
+ }
+
+ private String metadataKey(Filter.Key key) {
+ return "m" + this.i + ".name='" + key.key() + "' and ";
+ }
+
+ private String metadataKeyLast(Filter.Key key) {
+ return " and m" + this.i + ".name='" + key.key() + "' ";
+ }
+
+ private String computeValue(String operator, Object value) {
+ String m = "m" + this.i + ".";
+ String filterQuery = "";
+ if (value instanceof String text && DATE_FORMAT_PATTERN.matcher(text).matches()) {
+ try {
+ filterQuery = m + "value_date" + operator
+ + Instant.from(DATE_TIME_FORMATTER.parse(text)).toEpochMilli();
+ }
+ catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Invalid date type:" + text, e);
+ }
+ }
+ else if (value instanceof Integer || value instanceof Long) {
+ Long longValue = getLongValue(value);
+ filterQuery = m + "value_int" + operator + longValue;
+ }
+ else if (value instanceof Float || value instanceof Double) {
+ Double doubleValue = getDoubleValue(value);
+ filterQuery = m + "value_float" + operator + doubleValue;
+ }
+ else if (value instanceof Date || value instanceof Instant) {
+ filterQuery = m + "value_date" + operator + getDate(value);
+ }
+ else if (value instanceof Boolean bool) {
+ filterQuery = m + "value_bool" + operator + bool.booleanValue();
+ }
+ else {
+ // Any other case
+ filterQuery = m + "value" + operator + "'" + value + "'";
+ }
+ return filterQuery;
+ }
+
+ private long getDate(Object value) {
+ if (value instanceof Date date) {
+ return date.toInstant().toEpochMilli();
+ }
+ if (value instanceof Instant instant) {
+ return instant.toEpochMilli();
+ }
+ return 0L;
+ }
+
+ private Long getLongValue(Object value) {
+ return value instanceof Integer ? ((Integer) value).longValue() : (Long) value;
+ }
+
+ private Double getDoubleValue(Object value) {
+ return value instanceof Float ? ((Float) value).doubleValue() : (Double) value;
+ }
+
+ private String formattedComparisonValues(Collection> comparisonValues) {
+ String inStatement = comparisonValues.stream()
+ .map(s -> s instanceof String || s instanceof Date ? "'" + s + "'" : s.toString())
+ .collect(Collectors.joining(", "));
+ return inStatement;
+ }
+
+ private String addMetadataNullCheck() {
+ return "OR (i.metadata is null)";
+ }
+
+ private void incrementJoin() {
+ this.i++;
+ }
+
+}
diff --git a/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanVectorStore.java b/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanVectorStore.java
new file mode 100644
index 00000000000..38e4e198c11
--- /dev/null
+++ b/vector-stores/spring-ai-infinispan-store/src/main/java/org/springframework/ai/vectorstore/infinispan/InfinispanVectorStore.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2023-2025 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.infinispan;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.commons.api.query.Query;
+import org.infinispan.commons.configuration.StringConfiguration;
+import org.infinispan.commons.marshall.ProtoStreamMarshaller;
+import org.infinispan.protostream.schema.Field;
+import org.infinispan.protostream.schema.Schema;
+import org.infinispan.protostream.schema.Type;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.embedding.EmbeddingOptions;
+import org.springframework.ai.observation.conventions.VectorStoreProvider;
+import org.springframework.ai.observation.conventions.VectorStoreSimilarityMetric;
+import org.springframework.ai.vectorstore.AbstractVectorStoreBuilder;
+import org.springframework.ai.vectorstore.SearchRequest;
+import org.springframework.ai.vectorstore.filter.Filter;
+import org.springframework.ai.vectorstore.observation.AbstractObservationVectorStore;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+public class InfinispanVectorStore extends AbstractObservationVectorStore implements InitializingBean {
+
+ /**
+ * Default Store Name
+ */
+ public static final String DEFAULT_STORE_NAME = "defaultStore";
+
+ /**
+ * Default Cache Config
+ */
+ public static final String DEFAULT_CACHE_CONFIG = "\n"
+ + "\n" + "\n"
+ + "SPRING_AI_ITEM\n" + "\n" + "\n"
+ + "";
+
+ /**
+ * Default package of the schema
+ */
+ public static final String DEFAULT_PACKAGE = "dev.spring_ai";
+
+ /**
+ * Default name of the protobuf springAi item. Size will be added
+ */
+ public static final String DEFAULT_ITEM_NAME = "SpringAiItem";
+
+ /**
+ * Default name of the protobuf metadata item. Size will be added
+ */
+ public static final String DEFAULT_METADATA_ITEM = "SpringAiMetadata";
+
+ /**
+ * The default distance to for the search
+ */
+ public static final int DEFAULT_DISTANCE = 3;
+
+ /**
+ * Default vector similarity
+ */
+ public static final String DEFAULT_SIMILARITY = VectorStoreSimilarityMetric.COSINE.value();
+
+ private final RemoteCacheManager infinispanClient;
+
+ private final String storeName;
+
+ private final String storeConfig;
+
+ private final int distance;
+
+ private final int dimension;
+
+ private final String similarity;
+
+ private final String schemaFileName;
+
+ private final String packageName;
+
+ private final String itemName;
+
+ private final String metadataItemName;
+
+ private final boolean registerSchema;
+
+ private final boolean createStore;
+
+ private final String itemFullName;
+
+ private final String metadataFullName;
+
+ private RemoteCache remoteCache;
+
+ protected InfinispanVectorStore(Builder builder) {
+ super(builder);
+ Assert.notNull(builder.infinispanClient, "infinispanClientBuilder must not be null");
+ Assert.notNull(builder.dimension, "dimension must not be null");
+ Assert.isTrue(builder.distance == null || (builder.distance != null && builder.distance > 0),
+ "provided distance must be greater than 0");
+ this.infinispanClient = builder.infinispanClient;
+ this.dimension = builder.dimension;
+ this.storeConfig = builder.storeConfig;
+ this.createStore = builder.createStore == null ? true : builder.createStore;
+ this.storeName = builder.storeName == null ? DEFAULT_STORE_NAME : builder.storeName;
+ this.distance = builder.distance == null ? DEFAULT_DISTANCE : builder.distance;
+ this.similarity = builder.similarity == null ? DEFAULT_SIMILARITY : builder.similarity;
+ this.packageName = builder.packageName == null ? DEFAULT_PACKAGE : builder.packageName;
+ this.itemName = builder.itemName == null ? DEFAULT_ITEM_NAME : builder.itemName;
+ this.metadataItemName = builder.metadataItemName == null ? DEFAULT_METADATA_ITEM : builder.metadataItemName;
+ this.registerSchema = builder.registerSchema == null ? true : builder.registerSchema;
+ this.schemaFileName = getSchemaFileName(builder);
+ this.itemFullName = computeProtoFullName(this.itemName);
+ this.metadataFullName = computeProtoFullName(this.metadataItemName);
+ }
+
+ private String getSchemaFileName(Builder builder) {
+ if (builder.schemaFileName != null) {
+ return builder.schemaFileName;
+ }
+ return builder.packageName + "." + "dimension." + builder.dimension + ".proto";
+ }
+
+ private String computeProtoFullName(String name) {
+ return this.packageName + "." + name;
+ }
+
+ @Override
+ public void doAdd(List documents) {
+ Map elements = new HashMap<>(documents.size());
+ List embeddings = this.embeddingModel.embed(documents, EmbeddingOptions.builder().build(),
+ this.batchingStrategy);
+
+ for (int i = 0; i < embeddings.size(); i++) {
+ Document document = documents.get(i);
+ float[] vector = embeddings.get(i);
+ Set metadataSet = document.getMetadata()
+ .entrySet()
+ .stream()
+ .map(e -> new SpringAiMetadata(e.getKey(), e.getValue()))
+ .collect(Collectors.toSet());
+ elements.put(document.getId(), new SpringAiInfinispanItem(document.getId(), document.getText(), metadataSet,
+ vector, document.getMetadata()));
+ }
+
+ this.remoteCache.putAll(elements);
+ }
+
+ @Override
+ public void doDelete(List idList) {
+ if (idList == null || idList.isEmpty()) {
+ throw new IllegalArgumentException("ids cannot be null or empty");
+ }
+
+ for (String id : idList) {
+ this.remoteCache.remove(id);
+ }
+ }
+
+ @Override
+ public void doDelete(Filter.Expression filterExpression) {
+ InfinispanFilterExpressionConverter filterExpressionConverter = new InfinispanFilterExpressionConverter();
+ String filteringPart = filterExpressionConverter.convertExpression(filterExpression);
+ String joinPart = filterExpressionConverter.doJoin();
+ String deleteQuery = "DELETE FROM " + this.itemFullName + " i " + joinPart + " where " + filteringPart;
+ Query query = this.remoteCache.query(deleteQuery);
+ query.execute();
+ }
+
+ @Override
+ public List doSimilaritySearch(SearchRequest searchRequest) {
+ String joinPart = "";
+ String filteringPart = "";
+
+ if (searchRequest.hasFilterExpression()) {
+ InfinispanFilterExpressionConverter filterExpressionConverter = new InfinispanFilterExpressionConverter();
+ filteringPart = "filtering("
+ + filterExpressionConverter.convertExpression(searchRequest.getFilterExpression()) + ")";
+ joinPart = filterExpressionConverter.doJoin();
+ }
+
+ var embedding = this.embeddingModel.embed(searchRequest.getQuery());
+ String vectorQuery = "select i, score(i) from " + this.itemFullName + " i " + joinPart
+ + " where i.embedding <-> " + Arrays.toString(embedding) + "~" + this.distance + " " + filteringPart;
+
+ Query