diff --git a/.gemini/commands/core/javadocs.toml b/.gemini/commands/core/javadocs.toml new file mode 100644 index 0000000..16944f2 --- /dev/null +++ b/.gemini/commands/core/javadocs.toml @@ -0,0 +1,6 @@ +description="Update javadocs in the core module" +prompt=""" +Analyze the local main git branch and current one that is currently checkout and check what change in code were added. +Based on your analyzis update javadocs to public methods in production that were changed. +Or add javadocs to new public methods and types in production code. +""" \ No newline at end of file diff --git a/.gemini/commands/core/readme.toml b/.gemini/commands/core/readme.toml new file mode 100644 index 0000000..221ede7 --- /dev/null +++ b/.gemini/commands/core/readme.toml @@ -0,0 +1,5 @@ +description="Update README.md file in the core module" +prompt=""" +Analyze the local main git branch and current one that is currently checkout and check what change in code were added. +Based on your analyzis update README.md file in the core module and mention what was changed or added. +""" \ No newline at end of file diff --git a/.gemini/commands/root/changelog.toml b/.gemini/commands/root/changelog.toml new file mode 100644 index 0000000..c71e0a7 --- /dev/null +++ b/.gemini/commands/root/changelog.toml @@ -0,0 +1,5 @@ +description="Update CHANGELOG.md file in the root module" +prompt=""" +Analyze the local main git branch and current one that is currently checkout and check what changes were done. +Based on your analyzis update GHANGELOG.md file in the root module. +""" \ No newline at end of file diff --git a/.gemini/commands/root/readme.toml b/.gemini/commands/root/readme.toml new file mode 100644 index 0000000..79ac1ab --- /dev/null +++ b/.gemini/commands/root/readme.toml @@ -0,0 +1,5 @@ +description="Update README.md file in the root module" +prompt=""" +Analyze the local main git branch and current one that is currently checkout and check what changes were done. +Based on your analyzis update README.md file in the root module and mention what was changed or added. +""" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b8647dd..87dcec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * [Unreleased](#unreleased) +* [0.8.0](#080---2026-03-16) * [0.7.0](#070---2026-03-06) * [0.6.0](#060---2026-03-04) * [0.5.1](#051---2026-03-03) @@ -18,6 +19,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.0] - 2026-03-16 + +### Added + +#### Core Module +* Added support for `$search` operator translation to MongoDB Atlas Search aggregation stages. ([#37](https://github.com/starnowski/jamolingo/issues/37)) +* `com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchParser` class for parsing OData search options. ([#37](https://github.com/starnowski/jamolingo/issues/37)) +* `com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchOptions` and `DefaultODataSearchToMongoAtlasSearchOptions` for search configuration. ([#37](https://github.com/starnowski/jamolingo/issues/37)) +* Support for search score filtering using `$set` and `$match` stages with configurable field names. ([#37](https://github.com/starnowski/jamolingo/issues/37)) +* `com.github.starnowski.jamolingo.core.operators.search.SearchOperatorResult` and `SearchOperatorResultForAtlasSearch` interfaces. ([#37](https://github.com/starnowski/jamolingo/issues/37)) + +### Changed + ## [0.7.0] - 2026-03-06 ### Added diff --git a/README.md b/README.md index 30f1d12..ce2fa2f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A Java library for translating OData queries and concepts into MongoDB aggregati ### Prerequisites * **Java 8** or higher. * **MongoDB 4.4** or higher (supporting aggregation pipelines and explain). +* **MongoDB Atlas** or **MongoDB Atlas Local** (required for `$search` operator support). ### Installation (Maven) Add the following dependencies to your `pom.xml`: @@ -33,13 +34,13 @@ Add the following dependencies to your `pom.xml`: com.github.starnowski.jamolingo core - 0.7.0 + 0.8.0-SNAPSHOT com.github.starnowski.jamolingo perf - 0.7.0 + 0.8.0-SNAPSHOT ``` @@ -84,6 +85,9 @@ The `core` module contains the primary logic for translating OData concepts and * Comparison (`eq`, `ne`, `in`, etc.) and Logical (`and`, `or`, `not`) operators. * String, Math, and Date/Time functions. * Collection operators (`any`, `all`) and `/$count`. +* Translates `$search` to MongoDB Atlas Search stages (`$search`, `$set`, `$match`) with support for: + * Full-text search with logical operators (`AND`, `OR`, `NOT`). + * Search score filtering and custom score field names. * Translates `$select` to MongoDB `$project` stages. * Translates `$orderby`, `$top`, `$skip`, and `$count` to corresponding MongoDB stages (`$sort`, `$limit`, `$skip`, `$count`). * Handles OData-to-MongoDB mapping configuration and supports customizing mappings via overrides. diff --git a/common/json/pom.xml b/common/json/pom.xml index fbadfc2..822aafa 100644 --- a/common/json/pom.xml +++ b/common/json/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo common - 0.7.0 + 0.8.0-SNAPSHOT json diff --git a/common/pom.xml b/common/pom.xml index c26557a..a5c66f6 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT pom diff --git a/compat-driver-5.x/pom.xml b/compat-driver-5.x/pom.xml index 3781125..6791668 100644 --- a/compat-driver-5.x/pom.xml +++ b/compat-driver-5.x/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT compat-driver-5.x @@ -72,6 +72,11 @@ 1.5.3 test + + org.testcontainers + testcontainers + test + com.github.starnowski.jamolingo perf diff --git a/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/MongoAtlasResource.java b/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/MongoAtlasResource.java new file mode 100644 index 0000000..dc94ee3 --- /dev/null +++ b/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/MongoAtlasResource.java @@ -0,0 +1,48 @@ +package com.github.starnowski.jamolingo; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class MongoAtlasResource implements QuarkusTestResourceLifecycleManager { + + private GenericContainer mongoAtlasContainer; + + @Override + public Map start() { + mongoAtlasContainer = + new GenericContainer<>("mongodb/mongodb-atlas-local:7.0.11") + .withPrivilegedMode(true) + .withCreateContainerCmdModifier( + cmd -> { + cmd.getHostConfig().withMemory(4 * 1024 * 1024 * 1024L); + cmd.getHostConfig().withShmSize(2 * 1024 * 1024 * 1024L); + }) + .withExposedPorts(27017, 27027) + .withEnv("MONGOT_LOG_FILE", "/dev/stdout") + .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5))); + + mongoAtlasContainer.start(); + Wait.forLogMessage(".*Starting TCP server.*", 2) + .withStartupTimeout(Duration.ofSeconds(30)) + .waitUntilReady(mongoAtlasContainer); + Wait.forLogMessage(".*Starting message server.*", 2) + .withStartupTimeout(Duration.ofSeconds(30)) + .waitUntilReady(mongoAtlasContainer); + String connectionString = + String.format( + "mongodb://%s:%d/?directConnection=true", + mongoAtlasContainer.getHost(), mongoAtlasContainer.getMappedPort(27017)); + return Collections.singletonMap("quarkus.mongodb.connection-string", connectionString); + } + + @Override + public void stop() { + if (mongoAtlasContainer != null) { + mongoAtlasContainer.stop(); + } + } +} diff --git a/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/compat/driver/operators/search/SearchOperatorTest.java b/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/compat/driver/operators/search/SearchOperatorTest.java new file mode 100644 index 0000000..41d06e5 --- /dev/null +++ b/compat-driver-5.x/src/test/java/com/github/starnowski/jamolingo/compat/driver/operators/search/SearchOperatorTest.java @@ -0,0 +1,202 @@ +package com.github.starnowski.jamolingo.compat.driver.operators.search; + +import com.github.starnowski.jamolingo.AbstractItTest; +import com.github.starnowski.jamolingo.MongoAtlasResource; +import com.github.starnowski.jamolingo.core.operators.search.DefaultODataSearchToMongoAtlasSearchOptions; +import com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchOptions; +import com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchParser; +import com.github.starnowski.jamolingo.core.operators.search.SearchDocumentForQueryStringFactory; +import com.github.starnowski.jamolingo.core.operators.search.SearchDocumentForQueryStringFactory.QueryStringParsingResult; +import com.github.starnowski.jamolingo.core.operators.search.SearchOperatorResult; +import com.github.starnowski.jamolingo.junit5.MongoDocument; +import com.github.starnowski.jamolingo.junit5.MongoSetup; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import java.util.*; +import java.util.stream.Collectors; +import javax.xml.stream.XMLStreamException; +import org.apache.olingo.commons.api.edm.Edm; +import org.apache.olingo.server.api.OData; +import org.apache.olingo.server.api.ODataApplicationException; +import org.apache.olingo.server.api.uri.UriInfo; +import org.apache.olingo.server.api.uri.queryoption.expression.ExpressionVisitException; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.core.uri.parser.Parser; +import org.apache.olingo.server.core.uri.parser.UriParserException; +import org.apache.olingo.server.core.uri.validator.UriValidationException; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@QuarkusTest +@QuarkusTestResource(MongoAtlasResource.class) +public class SearchOperatorTest extends AbstractItTest { + + @Inject protected MongoClient mongoClient; + + @ParameterizedTest + @MethodSource("provideSearchTests") + @MongoSetup( + mongoDocuments = { + @MongoDocument( + database = "testdb", + collection = "Items", + bsonFilePath = "bson/search/search1.json"), + @MongoDocument( + database = "testdb", + collection = "Items", + bsonFilePath = "bson/search/search2.json") + }) + public void shouldReturnExpectedDocumentsBasedOnSearchOperator( + String search, Set expectedPlainStrings) + throws UriValidationException, + UriParserException, + XMLStreamException, + ExpressionVisitException, + ODataApplicationException, + InterruptedException { + // GIVEN + MongoDatabase database = mongoClient.getDatabase("testdb"); + MongoCollection collection = database.getCollection("Items"); + ensureSearchIndex(collection); + + Edm edm = loadEmdProvider("edm/edm6_filter_main.xml"); + UriInfo uriInfo = + new Parser(edm, OData.newInstance()).parseUri("examples2", "$search=" + search, null, null); + ODataSearchToMongoAtlasSearchParser tested = + new ODataSearchToMongoAtlasSearchParser( + new SearchDocumentForQueryStringFactory() { + @Override + public Bson build( + SearchExpression searchExpression, + QueryStringParsingResult queryStringParsingResult, + ODataSearchToMongoAtlasSearchOptions options) { + return new Document("index", "atlas_search_index") + .append( + "queryString", + new Document("query", queryStringParsingResult.getQuery()) + .append("defaultPath", "plainString")); + } + }); + + // WHEN + SearchOperatorResult result = tested.parse(uriInfo.getSearchOption()); + List pipeline = new ArrayList<>(result.getStageObjects()); + System.out.println(new Document("pipeline", pipeline).toJson()); + + // THEN + List results = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + results.clear(); + collection.aggregate(pipeline).into(results); + if (results.size() == expectedPlainStrings.size()) { + break; + } + Thread.sleep(500); + } + + Assertions.assertEquals(expectedPlainStrings.size(), results.size()); + Set actual = + results.stream() + .map(d -> d.get("plainString")) + .filter(Objects::nonNull) + .map(s -> (String) s) + .collect(Collectors.toSet()); + Assertions.assertEquals(expectedPlainStrings, actual); + } + + @Test + @MongoSetup( + mongoDocuments = { + @MongoDocument( + database = "testdb", + collection = "Items", + bsonFilePath = "bson/search/search1.json"), + @MongoDocument( + database = "testdb", + collection = "Items", + bsonFilePath = "bson/search/search2.json") + }) + public void shouldReturnExpectedDocumentsBasedOnSearchOperatorWithDefaultScore() + throws UriValidationException, + UriParserException, + XMLStreamException, + ExpressionVisitException, + ODataApplicationException, + InterruptedException { + // GIVEN + MongoDatabase database = mongoClient.getDatabase("testdb"); + MongoCollection collection = database.getCollection("Items"); + ensureSearchIndex(collection); + + Edm edm = loadEmdProvider("edm/edm6_filter_main.xml"); + UriInfo uriInfo = + new Parser(edm, OData.newInstance()).parseUri("examples2", "$search=search", null, null); + ODataSearchToMongoAtlasSearchParser tested = + new ODataSearchToMongoAtlasSearchParser( + new SearchDocumentForQueryStringFactory() { + @Override + public Bson build( + SearchExpression searchExpression, + QueryStringParsingResult queryStringParsingResult, + ODataSearchToMongoAtlasSearchOptions options) { + return new Document("index", "atlas_search_index") + .append( + "queryString", + new Document("query", queryStringParsingResult.getQuery()) + .append("defaultPath", "plainString")); + } + }); + ODataSearchToMongoAtlasSearchOptions options = + new DefaultODataSearchToMongoAtlasSearchOptions(); + options.setDefaultTextScore(0.01d); + + // WHEN + SearchOperatorResult result = tested.parse(uriInfo.getSearchOption(), options); + List pipeline = new ArrayList<>(result.getStageObjects()); + System.out.println(new Document("pipeline", pipeline).toJson()); + + // THEN + List results = new ArrayList<>(); + collection.aggregate(pipeline).into(results); + Assertions.assertFalse(results.isEmpty()); + } + + private void ensureSearchIndex(MongoCollection collection) { + try { + collection.createSearchIndex( + "atlas_search_index", new Document("mappings", new Document("dynamic", true))); + // Wait for index to be ready + while (true) { + boolean ready = false; + for (Document index : collection.listSearchIndexes()) { + if ("atlas_search_index".equals(index.getString("name")) + && "READY".equals(index.getString("status"))) { + ready = true; + break; + } + } + if (ready) break; + Thread.sleep(500); + } + } catch (Exception e) { + // Index might already exist + } + } + + private static java.util.stream.Stream provideSearchTests() { + return java.util.stream.Stream.of( + Arguments.of("database", Set.of("database search")), + Arguments.of("search", Set.of("database search", "only search")), + Arguments.of("database AND search", Set.of("database search")), + Arguments.of("database OR \"only search\"", Set.of("database search", "only search"))); + } +} diff --git a/compat-driver-5.x/src/test/resources/application.properties b/compat-driver-5.x/src/test/resources/application.properties index 40b5083..d31ec51 100644 --- a/compat-driver-5.x/src/test/resources/application.properties +++ b/compat-driver-5.x/src/test/resources/application.properties @@ -1 +1,2 @@ -quarkus.mongodb.uuid-representation=STANDARD \ No newline at end of file +quarkus.mongodb.uuid-representation=STANDARD +quarkus.mongodb.devservices.enabled=false \ No newline at end of file diff --git a/compat-driver-5.x/src/test/resources/bson/search/search1.json b/compat-driver-5.x/src/test/resources/bson/search/search1.json new file mode 100644 index 0000000..a9a4b60 --- /dev/null +++ b/compat-driver-5.x/src/test/resources/bson/search/search1.json @@ -0,0 +1,8 @@ +{ + "plainString": "database search", + "tags": [ + "mongo", + "atlas" + ], + "active": true +} diff --git a/compat-driver-5.x/src/test/resources/bson/search/search2.json b/compat-driver-5.x/src/test/resources/bson/search/search2.json new file mode 100644 index 0000000..acaf264 --- /dev/null +++ b/compat-driver-5.x/src/test/resources/bson/search/search2.json @@ -0,0 +1,8 @@ +{ + "plainString": "only search", + "tags": [ + "olingo", + "search" + ], + "active": true +} diff --git a/core/README.md b/core/README.md index 908444f..8e6f22d 100644 --- a/core/README.md +++ b/core/README.md @@ -90,6 +90,52 @@ List stages = result.getStageObjects(); // e.g. collection.aggregate(stages); ``` +#### $search + +The `$search` operator allows clients to perform full-text search. The `core` module translates this into MongoDB Atlas Search aggregation stages. + +**Translation Details:** +- Translates to a `$search` aggregation stage. +- Optionally adds `$set` and `$match` stages to filter by search score. +- Supports logical operators (`AND`, `OR`, `NOT`) within the search expression. +- Allows specifying a minimum score and a custom field name for the score value. + +**Usage:** + +The `ODataSearchToMongoAtlasSearchParser` class is responsible for this translation. + +```java +import com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchParser; +import com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchOptions; +import com.github.starnowski.jamolingo.core.operators.search.DefaultODataSearchToMongoAtlasSearchOptions; +import com.github.starnowski.jamolingo.core.operators.search.SearchOperatorResult; +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +// ... other imports + +// 1. Initialize the parser with a SearchDocumentFactory +// SearchDocumentFactory factory = ...; +ODataSearchToMongoAtlasSearchParser parser = new ODataSearchToMongoAtlasSearchParser(factory); + +// 2. Obtain the SearchOption from the Olingo UriInfo +SearchOption searchOption = uriInfo.getSearchOption(); + +// 3. (Optional) Provide search options (e.g., minimum score) +ODataSearchToMongoAtlasSearchOptions options = DefaultODataSearchToMongoAtlasSearchOptions.builder() + .withDefaultTextScore(0.05) + .withScoreFieldName("search_score") + .build(); + +// 4. Parse the option +SearchOperatorResult result = parser.parse(searchOption, options); + +// 5. Use the result in your MongoDB aggregation pipeline +List stages = result.getStageObjects(); +// Or get specific stages +List searchStages = result.getSearchStages(); +List scoreFilterStages = result.getScoreFilterStages(); +// e.g. collection.aggregate(stages); +``` + #### $orderby The `$orderby` operator specifies the sort order of the returned items. The `core` module translates this into a MongoDB aggregation stage. diff --git a/core/pom.xml b/core/pom.xml index 8e32954..f773372 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT core diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/DefaultODataSearchToMongoAtlasSearchOptions.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/DefaultODataSearchToMongoAtlasSearchOptions.java new file mode 100644 index 0000000..5cfa2b4 --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/DefaultODataSearchToMongoAtlasSearchOptions.java @@ -0,0 +1,108 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import java.util.Objects; + +/** Default implementation of ODataSearchToMongoAtlasSearchOptions. */ +public class DefaultODataSearchToMongoAtlasSearchOptions + implements ODataSearchToMongoAtlasSearchOptions { + + private Double defaultTextScore; + private String scoreFieldName; + + @Override + public void setDefaultTextScore(Double defaultTextScore, String scoreFieldName) { + this.defaultTextScore = defaultTextScore; + this.scoreFieldName = scoreFieldName; + } + + @Override + public Double getDefaultTextScore() { + return defaultTextScore; + } + + @Override + public String getScoreFieldName() { + return scoreFieldName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultODataSearchToMongoAtlasSearchOptions that = + (DefaultODataSearchToMongoAtlasSearchOptions) o; + return Objects.equals(defaultTextScore, that.defaultTextScore) + && Objects.equals(scoreFieldName, that.scoreFieldName); + } + + @Override + public int hashCode() { + return Objects.hash(defaultTextScore, scoreFieldName); + } + + @Override + public String toString() { + return "DefaultODataSearchToMongoAtlasSearchOptions{" + + "defaultTextScore=" + + defaultTextScore + + ", scoreFieldName='" + + scoreFieldName + + '\'' + + '}'; + } + + /** + * Returns a new builder for DefaultODataSearchToMongoAtlasSearchOptions. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for DefaultODataSearchToMongoAtlasSearchOptions. */ + public static class Builder { + + private Double defaultTextScore; + private String scoreFieldName = + ODataSearchToMongoAtlasSearchParser.SEARCH_SCORE_DEFAULT_VARIABLE; + + /** + * Sets the minimum text score. + * + * @param defaultTextScore the default text score + * @return the builder instance + */ + public Builder withDefaultTextScore(Double defaultTextScore) { + this.defaultTextScore = defaultTextScore; + return this; + } + + /** + * Sets the score field name. + * + * @param scoreFieldName the name of the field to store the score + * @return the builder instance + */ + public Builder withScoreFieldName(String scoreFieldName) { + this.scoreFieldName = scoreFieldName; + return this; + } + + /** + * Builds a new DefaultODataSearchToMongoAtlasSearchOptions instance. + * + * @return a new instance + */ + public DefaultODataSearchToMongoAtlasSearchOptions build() { + DefaultODataSearchToMongoAtlasSearchOptions options = + new DefaultODataSearchToMongoAtlasSearchOptions(); + options.setDefaultTextScore(defaultTextScore, scoreFieldName); + return options; + } + } +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchOptions.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchOptions.java new file mode 100644 index 0000000..c583bdc --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchOptions.java @@ -0,0 +1,7 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +/** + * Interface representing options specific to MongoDB Atlas Search translation. It extends {@link + * ODataSearchToMongoTextSearchOptions} to include general text search options. + */ +public interface ODataSearchToMongoAtlasSearchOptions extends ODataSearchToMongoTextSearchOptions {} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParser.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParser.java new file mode 100644 index 0000000..fdd3df9 --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParser.java @@ -0,0 +1,125 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.apache.olingo.server.api.uri.queryoption.SearchOption; +import org.bson.Document; +import org.bson.conversions.Bson; + +/** + * Parser responsible for converting OData search options into MongoDB Atlas Search aggregation + * pipeline stages. + */ +public class ODataSearchToMongoAtlasSearchParser + implements ODataSearchToMongoTextSearchParser< + ODataSearchToMongoAtlasSearchOptions, SearchOperatorResultForAtlasSearch> { + + /** Default variable name used to store the search score in the pipeline. */ + public static final String SEARCH_SCORE_DEFAULT_VARIABLE = "jamolingo_search_score"; + + private final SearchDocumentFactory searchDocumentFactory; + + /** + * Constructs a new ODataSearchToMongoAtlasSearchParser. + * + * @param searchDocumentFactory the factory to build search documents + */ + public ODataSearchToMongoAtlasSearchParser(SearchDocumentFactory searchDocumentFactory) { + this.searchDocumentFactory = searchDocumentFactory; + } + + @Override + public SearchOperatorResultForAtlasSearch parse(SearchOption searchOption) { + return parse(searchOption, null); + } + + @Override + public SearchOperatorResultForAtlasSearch parse( + SearchOption searchOption, ODataSearchToMongoAtlasSearchOptions options) { + List searchStages = new ArrayList<>(); + List scoreFilterStages = new ArrayList<>(); + Document searchStage = + new Document( + "$search", searchDocumentFactory.build(searchOption.getSearchExpression(), options)); + searchStages.add(searchStage); + if (options != null && options.getDefaultTextScore() != null) { + searchStage + .get("$search", Document.class) + .append("scoreDetails", true); // Optional, but can be useful + + String scoreFieldName = options.getScoreFieldName(); + scoreFilterStages.add( + new Document("$set", new Document(scoreFieldName, new Document("$meta", "searchScore")))); + scoreFilterStages.add( + new Document( + "$match", + new Document(scoreFieldName, new Document("$gte", options.getDefaultTextScore())))); + } + List allStages = new ArrayList<>(searchStages); + allStages.addAll(scoreFilterStages); + return new DefaultSearchOperatorResult(allStages, searchStages, scoreFilterStages, options); + } + + private static class DefaultSearchOperatorResult implements SearchOperatorResultForAtlasSearch { + + private final List stageObjects; + private final List searchStages; + private final List scoreFilterStages; + private final ODataSearchToMongoAtlasSearchOptions options; + + private DefaultSearchOperatorResult( + List stages, + List searchStages, + List scoreFilterStages, + ODataSearchToMongoAtlasSearchOptions options) { + this.stageObjects = Collections.unmodifiableList(stages); + this.searchStages = Collections.unmodifiableList(searchStages); + this.scoreFilterStages = Collections.unmodifiableList(scoreFilterStages); + this.options = options; + } + + @Override + public List getStageObjects() { + return stageObjects; + } + + @Override + public List getSearchStages() { + return searchStages; + } + + @Override + public List getScoreFilterStages() { + return scoreFilterStages; + } + + @Override + public List getUsedMongoDocumentProperties() { + return List.of(); + } + + @Override + public List getWrittenMongoDocumentProperties() { + return List.of(); + } + + @Override + public List getAddedMongoDocumentProperties() { + if (options != null && options.getDefaultTextScore() != null) { + return List.of(options.getScoreFieldName()); + } + return List.of(); + } + + @Override + public List getRemovedMongoDocumentProperties() { + return List.of(); + } + + @Override + public boolean isDocumentShapeRedefined() { + return false; + } + } +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchOptions.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchOptions.java new file mode 100644 index 0000000..54ece8e --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchOptions.java @@ -0,0 +1,37 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import static com.github.starnowski.jamolingo.core.operators.search.ODataSearchToMongoAtlasSearchParser.SEARCH_SCORE_DEFAULT_VARIABLE; + +public interface ODataSearchToMongoTextSearchOptions { + + /** + * Sets the minimum text score required for a document to be returned. + * + * @param defaultTextScore the minimum text score + */ + default void setDefaultTextScore(Double defaultTextScore) { + setDefaultTextScore(defaultTextScore, SEARCH_SCORE_DEFAULT_VARIABLE); + } + + /** + * Sets the minimum text score and the field name for the score value. + * + * @param defaultTextScore the minimum text score + * @param scoreFieldName the name of the field that will store the score value + */ + void setDefaultTextScore(Double defaultTextScore, String scoreFieldName); + + /** + * Returns the minimum text score required for a document to be returned. + * + * @return the minimum text score, or null if not set + */ + Double getDefaultTextScore(); + + /** + * Returns the name of the field that will store the text search score value. + * + * @return the name of the field + */ + String getScoreFieldName(); +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchParser.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchParser.java new file mode 100644 index 0000000..92c3512 --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoTextSearchParser.java @@ -0,0 +1,31 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import org.apache.olingo.server.api.uri.queryoption.SearchOption; + +/** + * Interface for parsing OData search options into MongoDB text search aggregation pipeline stages. + * + * @param the type of search options + * @param the type of search operator result + */ +public interface ODataSearchToMongoTextSearchParser< + T extends ODataSearchToMongoTextSearchOptions, R extends SearchOperatorResult> { + + /** + * Parses the given OData search option into a search operator result. + * + * @param searchOption the OData search option to parse + * @return the resulting search operator result + */ + R parse(SearchOption searchOption); + + /** + * Parses the given OData search option into a search operator result, applying the provided + * options. + * + * @param searchOption the OData search option to parse + * @param options the search options to apply + * @return the resulting search operator result + */ + R parse(SearchOption searchOption, T options); +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentFactory.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentFactory.java new file mode 100644 index 0000000..b7e3560 --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentFactory.java @@ -0,0 +1,18 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.bson.conversions.Bson; + +/** Factory interface for building MongoDB Bson documents from OData search expressions. */ +public interface SearchDocumentFactory { + + /** + * Builds a Bson document representing the search stage based on the provided search expression + * and options. + * + * @param searchExpression the OData search expression + * @param options the search options + * @return the Bson document representing the search operation + */ + Bson build(SearchExpression searchExpression, ODataSearchToMongoAtlasSearchOptions options); +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentForQueryStringFactory.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentForQueryStringFactory.java new file mode 100644 index 0000000..89de883 --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchDocumentForQueryStringFactory.java @@ -0,0 +1,139 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchBinaryOperatorKind; +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression; +import org.apache.olingo.server.api.uri.queryoption.search.SearchTerm; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnary; +import org.apache.olingo.server.api.uri.queryoption.search.SearchUnaryOperatorKind; +import org.bson.conversions.Bson; + +/** + * Abstract factory class for building search documents based on query strings. It parses the OData + * search expression into a query string result, which is then used by concrete implementations. + */ +public abstract class SearchDocumentForQueryStringFactory implements SearchDocumentFactory { + @Override + public Bson build( + SearchExpression searchExpression, ODataSearchToMongoAtlasSearchOptions options) { + return build(searchExpression, pares(searchExpression), options); + } + + private QueryStringParsingResult pares(SearchExpression searchExpression) { + try { + return new QueryStringParsingResult( + parseSearchExpressionToString(searchExpression), true, null); + } catch (Exception ex) { + return new QueryStringParsingResult(null, false, ex); + } + } + + /** + * Parses the search expression and converts it to a formatted string. + * + * @param searchExpression the OData search expression + * @return the formatted string representing the query + */ + protected String parseSearchExpressionToString(SearchExpression searchExpression) { + if (searchExpression instanceof SearchTerm) { + return formatTerm(((SearchTerm) searchExpression).getSearchTerm()); + } else if (searchExpression instanceof SearchBinary) { + SearchBinary binary = (SearchBinary) searchExpression; + String left = parseSearchExpressionToString(binary.getLeftOperand()); + String right = parseSearchExpressionToString(binary.getRightOperand()); + + if (binary.getLeftOperand() instanceof SearchBinary) { + left = "(" + left + ")"; + } + + if (binary.getRightOperand() instanceof SearchBinary) { + right = "(" + right + ")"; + } + + if (binary.getOperator() == SearchBinaryOperatorKind.AND + && binary.getRightOperand() instanceof SearchUnary) { + if (((SearchUnary) binary.getRightOperand()).getOperator() == SearchUnaryOperatorKind.NOT) { + return left + " " + right; + } + } + + return left + " " + binary.getOperator().toString() + " " + right; + } else if (searchExpression instanceof SearchUnary) { + SearchUnary unary = (SearchUnary) searchExpression; + String operand = parseSearchExpressionToString(unary.getOperand()); + if (unary.getOperand() instanceof SearchBinary) { + operand = "(" + operand + ")"; + } + return "NOT " + operand; + } + return ""; + } + + private String formatTerm(String term) { + if (term.contains(" ") || term.equals("AND") || term.equals("OR") || term.equals("NOT")) { + return "\"" + term.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + return term; + } + + /** + * Builds the MongoDB Bson document using the parsed query string result and options. + * + * @param searchExpression the original search expression + * @param queryStringParsingResult the result of parsing the query string + * @param options the search options + * @return the Bson document representing the search stage + */ + public abstract Bson build( + SearchExpression searchExpression, + QueryStringParsingResult queryStringParsingResult, + ODataSearchToMongoAtlasSearchOptions options); + + /** Result of parsing the query string, including the query itself and status. */ + public static class QueryStringParsingResult { + + private final String query; + private final boolean success; + private final Exception cause; + + /** + * Gets the parsed query string. + * + * @return the query string + */ + public String getQuery() { + return query; + } + + /** + * Checks if the parsing was successful. + * + * @return true if successful, false otherwise + */ + public boolean isSuccess() { + return success; + } + + /** + * Gets the exception that caused the parsing to fail, if any. + * + * @return the cause exception + */ + public Exception getCause() { + return cause; + } + + /** + * Constructs a new QueryStringParsingResult. + * + * @param query the parsed query string + * @param success the success status + * @param cause the exception cause + */ + public QueryStringParsingResult(String query, boolean success, Exception cause) { + this.query = query; + this.success = success; + this.cause = cause; + } + } +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResult.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResult.java new file mode 100644 index 0000000..5c098ad --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResult.java @@ -0,0 +1,22 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +import com.github.starnowski.jamolingo.core.operators.OlingoOperatorResult; +import java.util.List; +import org.bson.conversions.Bson; + +public interface SearchOperatorResult extends OlingoOperatorResult { + + /** + * MongoDB aggregation pipeline stages related to search operations. + * + * @return list of Bson objects representing the stages + */ + List getSearchStages(); + + /** + * MongoDB aggregation pipeline stages that filter documents based on the score value. + * + * @return list of Bson objects representing the stages + */ + List getScoreFilterStages(); +} diff --git a/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResultForAtlasSearch.java b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResultForAtlasSearch.java new file mode 100644 index 0000000..110eb6c --- /dev/null +++ b/core/src/main/java/com/github/starnowski/jamolingo/core/operators/search/SearchOperatorResultForAtlasSearch.java @@ -0,0 +1,6 @@ +package com.github.starnowski.jamolingo.core.operators.search; + +/** + * Represents the result of parsing an OData search option specifically for MongoDB Atlas Search. + */ +public interface SearchOperatorResultForAtlasSearch extends SearchOperatorResult {} diff --git a/core/src/test/groovy/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParserTest.groovy b/core/src/test/groovy/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParserTest.groovy new file mode 100644 index 0000000..368901a --- /dev/null +++ b/core/src/test/groovy/com/github/starnowski/jamolingo/core/operators/search/ODataSearchToMongoAtlasSearchParserTest.groovy @@ -0,0 +1,165 @@ +package com.github.starnowski.jamolingo.core.operators.search + +import com.github.starnowski.jamolingo.core.AbstractSpecification +import com.mongodb.MongoClientSettings +import org.apache.olingo.commons.api.edm.Edm +import org.apache.olingo.server.api.OData +import org.apache.olingo.server.api.uri.UriInfo +import org.apache.olingo.server.api.uri.queryoption.search.SearchExpression +import org.apache.olingo.server.core.uri.parser.Parser +import org.bson.Document +import org.bson.UuidRepresentation +import org.bson.codecs.DocumentCodec +import org.bson.codecs.UuidCodecProvider +import org.bson.codecs.configuration.CodecRegistries +import org.bson.codecs.configuration.CodecRegistry +import org.bson.conversions.Bson +import org.bson.json.JsonWriterSettings +import spock.lang.Unroll + +class ODataSearchToMongoAtlasSearchParserTest extends AbstractSpecification { + + + def "should return separate search and score filter stages"(){ + given: + Edm edm = loadEmdProvider("edm/edm6_filter_main.xml") + JsonWriterSettings settings = JsonWriterSettings.builder().build() + CodecRegistry registry = CodecRegistries.fromRegistries( + CodecRegistries.fromProviders(new UuidCodecProvider(UuidRepresentation.STANDARD)), + MongoClientSettings.getDefaultCodecRegistry() + ) + DocumentCodec codec = new DocumentCodec(registry) + + UriInfo uriInfo = new Parser(edm, OData.newInstance()) + .parseUri("examples2", + "\$search=database" + , null, null) + ODataSearchToMongoAtlasSearchParser tested = new ODataSearchToMongoAtlasSearchParser(new SearchDocumentForQueryStringFactory() { + @Override + Bson build(SearchExpression searchExpression, SearchDocumentForQueryStringFactory.QueryStringParsingResult queryStringParsingResult, ODataSearchToMongoAtlasSearchOptions options) { + return new Document().append("index", "default") + .append("queryString", new Document() + .append("query", queryStringParsingResult.getQuery()) + .append("path", Arrays.asList("name","description")) + ) + } + }) + ODataSearchToMongoAtlasSearchOptions options = DefaultODataSearchToMongoAtlasSearchOptions.builder().withDefaultTextScore(0.5d).build() + + when: + def result = tested.parse(uriInfo.getSearchOption(), options) + + then: + result.getSearchStages().size() == 1 + ((Document)result.getSearchStages().get(0)).get("\$search") != null + ((Document)result.getSearchStages().get(0)).get("\$search", Document.class).get("scoreDetails") == true + result.getScoreFilterStages().size() == 2 + ((Document)result.getScoreFilterStages().get(0)).get("\$set") != null + ((Document)result.getScoreFilterStages().get(0)).get("\$set", Document.class).get("jamolingo_search_score", Document.class).get("\$meta") == "searchScore" + ((Document)result.getScoreFilterStages().get(1)).get("\$match") != null + ((Document)result.getScoreFilterStages().get(1)).get("\$match", Document.class).get("jamolingo_search_score", Document.class).get("\$gte") == 0.5d + result.getStageObjects().size() == 3 + result.getStageObjects().get(0) == result.getSearchStages().get(0) + result.getStageObjects().get(1) == result.getScoreFilterStages().get(0) + result.getStageObjects().get(2) == result.getScoreFilterStages().get(1) + result.getAddedMongoDocumentProperties() == ["jamolingo_search_score"] + } + + def "should return separate search and score filter stages with custom score field name"(){ + given: + Edm edm = loadEmdProvider("edm/edm6_filter_main.xml") + String customScoreField = "my_custom_score" + + UriInfo uriInfo = new Parser(edm, OData.newInstance()) + .parseUri("examples2", + "\$search=database" + , null, null) + ODataSearchToMongoAtlasSearchParser tested = new ODataSearchToMongoAtlasSearchParser(new SearchDocumentForQueryStringFactory() { + @Override + Bson build(SearchExpression searchExpression, SearchDocumentForQueryStringFactory.QueryStringParsingResult queryStringParsingResult, ODataSearchToMongoAtlasSearchOptions options) { + return new Document().append("index", "default") + .append("queryString", new Document() + .append("query", queryStringParsingResult.getQuery()) + .append("path", Arrays.asList("name","description")) + ) + } + }) + ODataSearchToMongoAtlasSearchOptions options = DefaultODataSearchToMongoAtlasSearchOptions.builder() + .withDefaultTextScore(0.5d) + .withScoreFieldName(customScoreField) + .build() + + when: + def result = tested.parse(uriInfo.getSearchOption(), options) + + then: + result.getSearchStages().size() == 1 + result.getScoreFilterStages().size() == 2 + ((Document)result.getScoreFilterStages().get(0)).get("\$set", Document.class).get(customScoreField) != null + ((Document)result.getScoreFilterStages().get(1)).get("\$match", Document.class).get(customScoreField) != null + result.getAddedMongoDocumentProperties() == [customScoreField] + } + + /** + * Verifies that the generated MongoDB $match stage matches the expected BSON document. + */ + @Unroll + def "should return expected stage bson objects"(){ + given: + System.out.println("Testing search: " + searchValue) + Bson expectedBson = Document.parse(expectedBsonJson) + Edm edm = loadEmdProvider("edm/edm6_filter_main.xml") + JsonWriterSettings settings = JsonWriterSettings.builder().build() + CodecRegistry registry = CodecRegistries.fromRegistries( + CodecRegistries.fromProviders(new UuidCodecProvider(UuidRepresentation.STANDARD)), + MongoClientSettings.getDefaultCodecRegistry() + ) + DocumentCodec codec = new DocumentCodec(registry) + + UriInfo uriInfo = new Parser(edm, OData.newInstance()) + .parseUri("examples2", + "\$search=" +searchValue + , null, null) + ODataSearchToMongoAtlasSearchParser tested = new ODataSearchToMongoAtlasSearchParser(new SearchDocumentForQueryStringFactory() { + @Override + Bson build(SearchExpression searchExpression, SearchDocumentForQueryStringFactory.QueryStringParsingResult queryStringParsingResult, ODataSearchToMongoAtlasSearchOptions options) { + return new Document().append("index", "default") + .append("queryString", new Document() + .append("query", queryStringParsingResult.getQuery()) + .append("path", Arrays.asList("name","description")) + ) + } + }) + + when: + def result = tested.parse(uriInfo.getSearchOption()) + + then: + [((Document)result.getStageObjects().get(0)).toJson(settings, codec)] == [expectedBson.toJson(settings, codec)] + + where: + searchValue || expectedBsonJson + """database AND search""" || """{ "\$search": { "index": "default", "queryString": { "query": "database AND search", "path": ["name","description"] }}}""" + """database OR search""" || """{ "\$search": { "index": "default", "queryString": { "query": "database OR search", "path": ["name","description"] }}}""" + """database NOT legacy""" || """{ "\$search": { "index": "default", "queryString": { "query": "database NOT legacy", "path": ["name","description"] }}}""" + """\"AND\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"AND\\"", "path": ["name","description"] }}}""" + """\"OR\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"OR\\"", "path": ["name","description"] }}}""" + """\"NOT\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"NOT\\"", "path": ["name","description"] }}}""" + """\"AND operator\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"AND operator\\"", "path": ["name","description"] }}}""" + """\"rock AND roll\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"rock AND roll\\"", "path": ["name","description"] }}}""" + """"OR condition" AND database""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"OR condition\\" AND database", "path": ["name","description"] }}}""" + """"NOT operator" OR logic""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"NOT operator\\" OR logic", "path": ["name","description"] }}}""" + """(database OR search) AND index""" || """{ "\$search": { "index": "default", "queryString": { "query": "(database OR search) AND index", "path": ["name","description"] }}}""" + """("AND" OR "OR") AND logic""" || """{ "\$search": { "index": "default", "queryString": { "query": "(\\"AND\\" OR \\"OR\\") AND logic", "path": ["name","description"] }}}""" + """(database OR "AND") AND system""" || """{ "\$search": { "index": "default", "queryString": { "query": "(database OR \\"AND\\") AND system", "path": ["name","description"] }}}""" + """\"database OR search\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"database OR search\\"", "path": ["name","description"] }}}""" + """\"AND OR NOT\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"AND OR NOT\\"", "path": ["name","description"] }}}""" + """database AND ("OR" OR "NOT")""" || """{ "\$search": { "index": "default", "queryString": { "query": "database AND (\\"OR\\" OR \\"NOT\\")", "path": ["name","description"] }}}""" + """\"logical AND operator\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"logical AND operator\\"", "path": ["name","description"] }}}""" + """("database search" OR "full text") AND engine""" || """{ "\$search": { "index": "default", "queryString": { "query": "(\\"database search\\" OR \\"full text\\") AND engine", "path": ["name","description"] }}}""" + """\"\\"AND\\" keyword\"""" || """{ "\$search": { "index": "default", "queryString": { "query": "\\"\\\\\\"AND\\\\\\" keyword\\"", "path": ["name","description"] }}}""" + """(database AND search) OR ("AND operator" AND logic)""" || """{ "\$search": { "index": "default", "queryString": { "query": "(database AND search) OR (\\"AND operator\\" AND logic)", "path": ["name","description"] }}}""" + } + + +} diff --git a/demos/pom.xml b/demos/pom.xml index 44970f8..c15084a 100644 --- a/demos/pom.xml +++ b/demos/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT demos diff --git a/demos/quarkus-webapp/pom.xml b/demos/quarkus-webapp/pom.xml index fd6f699..44f23b9 100644 --- a/demos/quarkus-webapp/pom.xml +++ b/demos/quarkus-webapp/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo demos - 0.7.0 + 0.8.0-SNAPSHOT quarkus-webapp diff --git a/demos/spring-boot-webapp/pom.xml b/demos/spring-boot-webapp/pom.xml index d038314..4e51e29 100644 --- a/demos/spring-boot-webapp/pom.xml +++ b/demos/spring-boot-webapp/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo demos - 0.7.0 + 0.8.0-SNAPSHOT spring-boot-webapp diff --git a/junit5-mongo-extension-parent/junit5-mongo-extension-quarkus/pom.xml b/junit5-mongo-extension-parent/junit5-mongo-extension-quarkus/pom.xml index 148d071..f6915c8 100644 --- a/junit5-mongo-extension-parent/junit5-mongo-extension-quarkus/pom.xml +++ b/junit5-mongo-extension-parent/junit5-mongo-extension-quarkus/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo junit5-mongo-extension-parent - 0.7.0 + 0.8.0-SNAPSHOT junit5-mongo-extension-quarkus diff --git a/junit5-mongo-extension-parent/junit5-mongo-extension-spring/pom.xml b/junit5-mongo-extension-parent/junit5-mongo-extension-spring/pom.xml index 04b89ed..c51eb21 100644 --- a/junit5-mongo-extension-parent/junit5-mongo-extension-spring/pom.xml +++ b/junit5-mongo-extension-parent/junit5-mongo-extension-spring/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo junit5-mongo-extension-parent - 0.7.0 + 0.8.0-SNAPSHOT junit5-mongo-extension-spring diff --git a/junit5-mongo-extension-parent/junit5-mongo-extension/pom.xml b/junit5-mongo-extension-parent/junit5-mongo-extension/pom.xml index f13ca63..4330470 100644 --- a/junit5-mongo-extension-parent/junit5-mongo-extension/pom.xml +++ b/junit5-mongo-extension-parent/junit5-mongo-extension/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo junit5-mongo-extension-parent - 0.7.0 + 0.8.0-SNAPSHOT junit5-mongo-extension diff --git a/junit5-mongo-extension-parent/pom.xml b/junit5-mongo-extension-parent/pom.xml index 7d55f50..be24f4d 100644 --- a/junit5-mongo-extension-parent/pom.xml +++ b/junit5-mongo-extension-parent/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT pom diff --git a/perf/pom.xml b/perf/pom.xml index 3e2fb15..229b96b 100644 --- a/perf/pom.xml +++ b/perf/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT perf diff --git a/pom.xml b/pom.xml index b8a247f..f38ccaf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.github.starnowski.jamolingo parent - 0.7.0 + 0.8.0-SNAPSHOT pom ${project.groupId}:${project.artifactId}