diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchMatchNonePredicate.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchMatchNonePredicate.java new file mode 100644 index 00000000000..af69bc74f4d --- /dev/null +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchMatchNonePredicate.java @@ -0,0 +1,49 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.backend.elasticsearch.search.predicate.impl; + +import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor; +import org.hibernate.search.backend.elasticsearch.gson.impl.JsonObjectAccessor; +import org.hibernate.search.backend.elasticsearch.search.common.impl.ElasticsearchSearchIndexScope; +import org.hibernate.search.engine.search.predicate.SearchPredicate; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; + +import com.google.gson.JsonObject; + + +class ElasticsearchMatchNonePredicate extends AbstractElasticsearchPredicate { + + private static final JsonObjectAccessor MATCH_NONE_ACCESSOR = JsonAccessor.root().property( "match_none" ) + .asObject(); + + private ElasticsearchMatchNonePredicate(Builder builder) { + super( builder ); + } + + @Override + public void checkNestableWithin(String expectedParentNestedPath) { + // Nothing to do + } + + @Override + protected JsonObject doToJsonQuery(PredicateRequestContext context, + JsonObject outerObject, JsonObject innerObject) { + MATCH_NONE_ACCESSOR.set( outerObject, innerObject ); + return outerObject; + } + + static class Builder extends AbstractBuilder implements MatchNonePredicateBuilder { + Builder(ElasticsearchSearchIndexScope scope) { + super( scope ); + } + + @Override + public SearchPredicate build() { + return new ElasticsearchMatchNonePredicate( this ); + } + } +} diff --git a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchSearchPredicateBuilderFactory.java b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchSearchPredicateBuilderFactory.java index e25cd9ce34c..50d4cc8db1a 100644 --- a/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchSearchPredicateBuilderFactory.java +++ b/backend/elasticsearch/src/main/java/org/hibernate/search/backend/elasticsearch/search/predicate/impl/ElasticsearchSearchPredicateBuilderFactory.java @@ -10,6 +10,7 @@ import org.hibernate.search.engine.search.predicate.spi.BooleanPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchAllPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchIdPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.SearchPredicateBuilderFactory; import org.hibernate.search.engine.search.predicate.spi.SimpleQueryStringPredicateBuilder; @@ -28,6 +29,11 @@ public MatchAllPredicateBuilder matchAll() { return new ElasticsearchMatchAllPredicate.Builder( scope ); } + @Override + public MatchNonePredicateBuilder matchNone() { + return new ElasticsearchMatchNonePredicate.Builder( scope ); + } + @Override public MatchIdPredicateBuilder id() { return new ElasticsearchMatchIdPredicate.Builder( scope ); diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneMatchNonePredicate.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneMatchNonePredicate.java new file mode 100644 index 00000000000..a1adf2ae893 --- /dev/null +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneMatchNonePredicate.java @@ -0,0 +1,43 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.backend.lucene.search.predicate.impl; + +import org.hibernate.search.backend.lucene.search.common.impl.LuceneSearchIndexScope; +import org.hibernate.search.engine.search.predicate.SearchPredicate; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; + +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; + + +class LuceneMatchNonePredicate extends AbstractLuceneSearchPredicate { + + private LuceneMatchNonePredicate(Builder builder) { + super( builder ); + } + + @Override + public void checkNestableWithin(String expectedParentNestedPath) { + // Nothing to do + } + + @Override + protected Query doToQuery(PredicateRequestContext context) { + return new MatchNoDocsQuery(); + } + + static class Builder extends AbstractBuilder implements MatchNonePredicateBuilder { + Builder(LuceneSearchIndexScope scope) { + super( scope ); + } + + @Override + public SearchPredicate build() { + return new LuceneMatchNonePredicate( this ); + } + } +} diff --git a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneSearchPredicateBuilderFactory.java b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneSearchPredicateBuilderFactory.java index 0e540d09106..fa24db49f25 100644 --- a/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneSearchPredicateBuilderFactory.java +++ b/backend/lucene/src/main/java/org/hibernate/search/backend/lucene/search/predicate/impl/LuceneSearchPredicateBuilderFactory.java @@ -10,6 +10,7 @@ import org.hibernate.search.engine.search.predicate.spi.BooleanPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchAllPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchIdPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.SearchPredicateBuilderFactory; import org.hibernate.search.engine.search.predicate.spi.SimpleQueryStringPredicateBuilder; @@ -29,6 +30,11 @@ public MatchAllPredicateBuilder matchAll() { return new LuceneMatchAllPredicate.Builder( scope ); } + @Override + public MatchNonePredicateBuilder matchNone() { + return new LuceneMatchNonePredicate.Builder( scope ); + } + @Override public MatchIdPredicateBuilder id() { return new LuceneMatchIdPredicate.Builder( scope ); diff --git a/documentation/src/main/asciidoc/reference/search-dsl-predicate.asciidoc b/documentation/src/main/asciidoc/reference/search-dsl-predicate.asciidoc index 2cead9ac7f9..14c6c4786b1 100644 --- a/documentation/src/main/asciidoc/reference/search-dsl-predicate.asciidoc +++ b/documentation/src/main/asciidoc/reference/search-dsl-predicate.asciidoc @@ -67,6 +67,19 @@ include::{sourcedir}/org/hibernate/search/documentation/search/predicate/Predica * The score of a `matchAll` predicate is constant and equal to 1 by default, but can be <>. +[[search-dsl-predicate-match-none]] +== `matchNone`: match no documents + +The `matchNone` predicate is the inverse of `matchAll` and matches no documents. + +.Matching no documents +==== +[source, JAVA, indent=0, subs="+callouts"] +---- +include::{sourcedir}/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java[tags=matchNone] +---- +==== + [[search-dsl-predicate-id]] == `id`: match a document identifier diff --git a/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java b/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java index 5e58d421187..cac7d427cac 100644 --- a/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java +++ b/documentation/src/test/java/org/hibernate/search/documentation/search/predicate/PredicateDslIT.java @@ -115,6 +115,20 @@ public void matchAll() { } ); } + @Test + public void matchNone() { + withinSearchSession( searchSession -> { + // tag::matchNone[] + List hits = searchSession.search( Book.class ) + .where( f -> f.matchNone() ) + .fetchHits( 20 ); + // end::matchNone[] + assertThat( hits ) + .extracting( Book::getId ) + .isEmpty(); + } ); + } + @Test public void id() { // DO NOT USE THE BOOKX_ID CONSTANTS INSIDE TAGS BELOW: diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchNonePredicateFinalStep.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchNonePredicateFinalStep.java new file mode 100644 index 00000000000..ac4f978e070 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/MatchNonePredicateFinalStep.java @@ -0,0 +1,13 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl; + +/** + * The initial and final step in "match none" predicate definition. + */ +public interface MatchNonePredicateFinalStep extends PredicateFinalStep { +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java index 75db83f2562..0615c484981 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/SearchPredicateFactory.java @@ -43,6 +43,14 @@ public interface SearchPredicateFactory { */ MatchAllPredicateOptionsStep matchAll(); + /** + * Match none of the documents. + * + * @return The initial step of a DSL where the "match none" predicate can be defined. + * @see MatchNonePredicateFinalStep + */ + MatchNonePredicateFinalStep matchNone(); + /** * Match documents where the identifier is among the given values. * diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchNonePredicateFinalStepImpl.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchNonePredicateFinalStepImpl.java new file mode 100644 index 00000000000..2399354715c --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/impl/MatchNonePredicateFinalStepImpl.java @@ -0,0 +1,30 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.dsl.impl; + +import org.hibernate.search.engine.search.predicate.SearchPredicate; +import org.hibernate.search.engine.search.predicate.dsl.MatchNonePredicateFinalStep; +import org.hibernate.search.engine.search.predicate.dsl.spi.AbstractPredicateFinalStep; +import org.hibernate.search.engine.search.predicate.dsl.spi.SearchPredicateDslContext; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; + + +public final class MatchNonePredicateFinalStepImpl extends AbstractPredicateFinalStep + implements MatchNonePredicateFinalStep { + + private final MatchNonePredicateBuilder matchNoneBuilder; + + public MatchNonePredicateFinalStepImpl(SearchPredicateDslContext dslContext) { + super( dslContext ); + this.matchNoneBuilder = dslContext.scope().predicateBuilders().matchNone(); + } + + @Override + protected SearchPredicate build() { + return matchNoneBuilder.build(); + } +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/spi/AbstractSearchPredicateFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/spi/AbstractSearchPredicateFactory.java index 16da8f304db..3531bcb93aa 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/spi/AbstractSearchPredicateFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/dsl/spi/AbstractSearchPredicateFactory.java @@ -15,6 +15,7 @@ import org.hibernate.search.engine.search.predicate.dsl.ExtendedSearchPredicateFactory; import org.hibernate.search.engine.search.predicate.dsl.MatchAllPredicateOptionsStep; import org.hibernate.search.engine.search.predicate.dsl.MatchIdPredicateMatchingStep; +import org.hibernate.search.engine.search.predicate.dsl.MatchNonePredicateFinalStep; import org.hibernate.search.engine.search.predicate.dsl.MatchPredicateFieldStep; import org.hibernate.search.engine.search.predicate.dsl.NamedPredicateOptionsStep; import org.hibernate.search.engine.search.predicate.dsl.NestedPredicateFieldStep; @@ -32,6 +33,7 @@ import org.hibernate.search.engine.search.predicate.dsl.impl.ExistsPredicateFieldStepImpl; import org.hibernate.search.engine.search.predicate.dsl.impl.MatchAllPredicateOptionsStepImpl; import org.hibernate.search.engine.search.predicate.dsl.impl.MatchIdPredicateMatchingStepImpl; +import org.hibernate.search.engine.search.predicate.dsl.impl.MatchNonePredicateFinalStepImpl; import org.hibernate.search.engine.search.predicate.dsl.impl.MatchPredicateFieldStepImpl; import org.hibernate.search.engine.search.predicate.dsl.impl.NamedPredicateOptionsStepImpl; import org.hibernate.search.engine.search.predicate.dsl.impl.NestedPredicateFieldStepImpl; @@ -64,6 +66,11 @@ public MatchAllPredicateOptionsStep matchAll() { return new MatchAllPredicateOptionsStepImpl( dslContext, this ); } + @Override + public MatchNonePredicateFinalStep matchNone() { + return new MatchNonePredicateFinalStepImpl( dslContext ); + } + @Override public MatchIdPredicateMatchingStep id() { return new MatchIdPredicateMatchingStepImpl( dslContext ); diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/MatchNonePredicateBuilder.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/MatchNonePredicateBuilder.java new file mode 100644 index 00000000000..99c28743c67 --- /dev/null +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/MatchNonePredicateBuilder.java @@ -0,0 +1,11 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.engine.search.predicate.spi; + +public interface MatchNonePredicateBuilder extends SearchPredicateBuilder { + +} diff --git a/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/SearchPredicateBuilderFactory.java b/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/SearchPredicateBuilderFactory.java index c97e9695318..62befbbf148 100644 --- a/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/SearchPredicateBuilderFactory.java +++ b/engine/src/main/java/org/hibernate/search/engine/search/predicate/spi/SearchPredicateBuilderFactory.java @@ -16,6 +16,8 @@ public interface SearchPredicateBuilderFactory { MatchAllPredicateBuilder matchAll(); + MatchNonePredicateBuilder matchNone(); + MatchIdPredicateBuilder id(); BooleanPredicateBuilder bool(); diff --git a/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/predicate/MatchNonePredicateSpecificsIT.java b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/predicate/MatchNonePredicateSpecificsIT.java new file mode 100644 index 00000000000..203ca1f155f --- /dev/null +++ b/integrationtest/backend/tck/src/main/java/org/hibernate/search/integrationtest/backend/tck/search/predicate/MatchNonePredicateSpecificsIT.java @@ -0,0 +1,87 @@ +/* + * Hibernate Search, full-text search for your domain model + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.search.integrationtest.backend.tck.search.predicate; + +import static org.hibernate.search.util.impl.integrationtest.common.assertion.SearchResultAssert.assertThatQuery; + +import org.hibernate.search.engine.backend.document.IndexFieldReference; +import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaElement; +import org.hibernate.search.engine.backend.types.dsl.IndexFieldTypeFactory; +import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory; +import org.hibernate.search.integrationtest.backend.tck.testsupport.util.rule.SearchSetupHelper; +import org.hibernate.search.util.impl.integrationtest.mapper.stub.SimpleMappedIndex; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +public class MatchNonePredicateSpecificsIT { + + private static final String DOCUMENT_1 = "1"; + private static final String STRING_1 = "aaa"; + + private static final String DOCUMENT_2 = "2"; + private static final String STRING_2 = "bbb"; + + private static final String DOCUMENT_3 = "3"; + private static final String STRING_3 = "ccc"; + + @ClassRule + public static final SearchSetupHelper setupHelper = new SearchSetupHelper(); + + private static final SimpleMappedIndex index = SimpleMappedIndex.of( IndexBinding::new ); + + @BeforeClass + public static void setup() { + setupHelper.start().withIndex( index ).setup(); + + initData(); + } + + @Test + public void matchNone() { + assertThatQuery( index.query() + .where( SearchPredicateFactory::matchNone ) ) + .hasNoHits(); + } + + @Test + public void matchNoneWithinBoolPredicate() { + //check that we will find something with a single match predicate + assertThatQuery( index.query() + .where( + f -> f.bool() + .must( f.match().field( "string" ).matching( STRING_1 ).toPredicate() ) + ) + ).hasTotalHitCount( 1 ); + + // make sure that matchNone will "override" the other matching predicate + assertThatQuery( index.query() + .where( + f -> f.bool() + .must( f.match().field( "string" ).matching( STRING_1 ).toPredicate() ) + .must( f.matchNone() ) + ) + ).hasNoHits(); + } + + private static void initData() { + index.bulkIndexer() + .add( DOCUMENT_1, document -> document.addValue( index.binding().string, STRING_1 ) ) + .add( DOCUMENT_2, document -> document.addValue( index.binding().string, STRING_2 ) ) + .add( DOCUMENT_3, document -> document.addValue( index.binding().string, STRING_3 ) ) + .join(); + } + + private static class IndexBinding { + final IndexFieldReference string; + + IndexBinding(IndexSchemaElement root) { + string = root.field( "string", IndexFieldTypeFactory::asString ).toReference(); + } + } +} diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicate.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicate.java index 08acc9e7bfe..e636f609a45 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicate.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicate.java @@ -19,6 +19,7 @@ import org.hibernate.search.engine.search.predicate.spi.ExistsPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchAllPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchIdPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.NamedPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.NestedPredicateBuilder; @@ -101,6 +102,7 @@ public void flags(Set flags) { } public static class Builder implements MatchAllPredicateBuilder, + MatchNonePredicateBuilder, BooleanPredicateBuilder, MatchIdPredicateBuilder, MatchPredicateBuilder, diff --git a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicateBuilderFactory.java b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicateBuilderFactory.java index 890c9eaf552..02a07253ea6 100644 --- a/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicateBuilderFactory.java +++ b/util/internal/integrationtest/common/src/main/java/org/hibernate/search/util/impl/integrationtest/common/stub/backend/search/predicate/impl/StubSearchPredicateBuilderFactory.java @@ -9,6 +9,7 @@ import org.hibernate.search.engine.search.predicate.spi.BooleanPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchAllPredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.MatchIdPredicateBuilder; +import org.hibernate.search.engine.search.predicate.spi.MatchNonePredicateBuilder; import org.hibernate.search.engine.search.predicate.spi.SearchPredicateBuilderFactory; import org.hibernate.search.engine.search.predicate.spi.SimpleQueryStringPredicateBuilder; @@ -25,6 +26,11 @@ public MatchAllPredicateBuilder matchAll() { return new StubSearchPredicate.Builder(); } + @Override + public MatchNonePredicateBuilder matchNone() { + return new StubSearchPredicate.Builder(); + } + @Override public BooleanPredicateBuilder bool() { return new StubSearchPredicate.Builder();