Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample for reusable repository extensions #690

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ WARNING: If you're done using it, don't forget to shut it down!

== Miscellaneous

* `bom` - Example project how to use the Spring Data release train bom in non-Spring-Boot
scenarios.
* `mongodb/fragment-spi` - Example project how to use Spring Data Fragment SPI to provide reusable custom extensions.
* `bom` - Example project how to use the Spring Data release train bom in non-Spring-Boot scenarios.
* `map` - Example project to show how to use `Map`-backed repositories.
* `multi-store` - Example project to use both Spring Data MongoDB and Spring Data JPA in
one project.
Expand Down
35 changes: 35 additions & 0 deletions mongodb/fragment-spi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Spring Data - Fragment SPI Example

This project contains a sample using `spring.factories` to register implementation details for a repository extension for MongoDB Vector Search that lives outside of the project namespace.

The project is divided into the `atlas-api`, providing the extension, and the `sample` using it.

## atlas-api

The `AtlasRepository` is the base interface containing a `vectorSearch` method that is implemented in `AtlasRepositoryFragment`. The configuration in `src/main/resources/META-INF/spring.factories` makes sure it is picked up by the spring data infrastructure.

The implementation leverages `RepositoryMethodContext` to get hold of method invocation metadata to determine the collection name derived from the repositories domain type `<T>`.
Since providing the metadata needs to be explicitly activated the `AtlasRepositoryFragment` uses the additional marker interface `RepositoryMetadataAccess` enabling the features for repositories extending the `AtlasRepository`.

## sample

The `MovieRepository` extends the `AtlasRepository` from the api project using a `Movie` type targeting the `movies` collection. No further configuration is needed to use the provided `vectorSearch` within the `MovieRepositoryTests`.

The `Movies` class in `src/main/test` takes care of setting up required test data and indexes.

## Running the sample

The is using a local MongoDB Atlas instance bootstrapped by Testcontainers.
Running the `MovieRepositoryTests` the `test/movies` collection will be populated with about 400 entries from the `mflix.embedded_movies.json` file.
Please be patient while data is loaded into the database and the index created afterwards.
Progress information will be printed to the log.
```log
INFO - com.example.data.mongodb.Movies: 73 - Loading movies mflix.embedded_movies.json
INFO - com.example.data.mongodb.Movies: 90 - Created 420 movies in test.movies
INFO - com.example.data.mongodb.Movies: 65 - creating vector index
INFO - com.example.data.mongodb.Movies: 68 - index 'plot_vector_index' created
```
Once data and index are available search result will be printed:
```log
INFO - ...mongodb.MovieRepositoryTests: 183 - Movie{id='66d6ee0937e07b74aa2939cc', ...
```
14 changes: 14 additions & 0 deletions mongodb/fragment-spi/atlas-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-fragment-spi</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>

<artifactId>spring-data-mongodb-fragment-spi-atlas</artifactId>
<name>Spring Data MongoDB - Reusable Fragments - Vector Search Fragment</name>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 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 com.example.spi.mongodb.atlas;

import java.util.List;


/**
* @author Christoph Strobl
*/
public interface AtlasRepository<T> {

List<T> vectorSearch(String index, String path, List<Double> vector);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 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 com.example.spi.mongodb.atlas;

import java.util.List;

import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.Limit;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.RepositoryMethodContext;
import org.springframework.data.repository.core.support.RepositoryMetadataAccess;

class AtlasRepositoryFragment<T> implements AtlasRepository<T>, RepositoryMetadataAccess {

private final MongoOperations mongoOperations;

public AtlasRepositoryFragment(@Autowired MongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}

@Override
@SuppressWarnings("unchecked")
public List<T> vectorSearch(String index, String path, List<Double> vector) {

RepositoryMethodContext methodContext = RepositoryMethodContext.getContext();

Class<?> domainType = resolveDomainType(methodContext.getMetadata());

Document $vectorSearch = createDocument(index, path, vector, Limit.of(10));
Aggregation aggregation = Aggregation.newAggregation(ctx -> $vectorSearch);

return (List<T>) mongoOperations.aggregate(aggregation, mongoOperations.getCollectionName(domainType), domainType).getMappedResults();
}

@SuppressWarnings("unchecked")
private static <T> Class<T> resolveDomainType(RepositoryMetadata metadata) {

// resolve the actual generic type argument of the AtlasRepository<T>.
return (Class<T>) ResolvableType.forClass(metadata.getRepositoryInterface())
.as(AtlasRepository.class)
.getGeneric(0)
.resolve();
}

private static Document createDocument(String indexName, String path, List<Double> vector, Limit limit) {

Document $vectorSearch = new Document();

$vectorSearch.append("index", indexName);
$vectorSearch.append("path", path);
$vectorSearch.append("queryVector", vector);
$vectorSearch.append("limit", limit.max());
$vectorSearch.append("numCandidates", 150);

return new Document("$vectorSearch", $vectorSearch);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.example.spi.mongodb.atlas.AtlasRepository=com.example.spi.mongodb.atlas.AtlasRepositoryFragment
19 changes: 19 additions & 0 deletions mongodb/fragment-spi/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-examples</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>

<artifactId>spring-data-mongodb-fragment-spi</artifactId>
<name>Spring Data MongoDB - Reusable Fragments</name>
<packaging>pom</packaging>

<modules>
<module>atlas-api</module>
<module>sample</module>
</modules>
</project>
26 changes: 26 additions & 0 deletions mongodb/fragment-spi/sample/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-fragment-spi</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>

<artifactId>spring-data-mongodb-fragment-spi-usage</artifactId>
<name>Spring Data MongoDB - Reusable Fragments - Fragment Usage</name>

<dependencies>
<dependency>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-fragment-spi-atlas</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-example-utils</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 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 com.example.data.mongodb;

import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author Christoph Strobl
*/
@SpringBootApplication
public class ApplicationConfiguration {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 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 com.example.data.mongodb;

import org.springframework.data.mongodb.core.mapping.Document;

/**
* @author Christoph Strobl
*/
@Document("movies")
public class Movie {

private String id;
private String title;
private String plot;

public String getPlot() {
return plot;
}

public void setPlot(String plot) {
this.plot = plot;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

@Override
public String toString() {
return "Movie{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", plot='" + plot + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 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 com.example.data.mongodb;

import com.example.spi.mongodb.atlas.AtlasRepository;
import org.springframework.data.repository.CrudRepository;

/**
* @author Christoph Strobl
*/
public interface MovieRepository extends CrudRepository<Movie, String>, AtlasRepository<Movie> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading
Loading