Skip to content

Commit

Permalink
HSEARCH-3200 Add minimum should match to match predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
marko-bekhta committed Apr 23, 2024
1 parent ebec41f commit 2b27c27
Show file tree
Hide file tree
Showing 23 changed files with 446 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ SearchException indexManagerUnwrappingWithUnknownType(@FormatWith(ClassFormatter
SearchException cannotGuessFieldType(@FormatWith(ClassFormatter.class) Class<?> inputType, @Param EventContext context);

@Message(id = ID_OFFSET + 53,
value = "Full-text features (analysis, fuzziness) are not supported for fields of this type.")
value = "Full-text features (analysis, fuzziness, minimum should match) are not supported for fields of this type.")
SearchException fullTextFeaturesNotSupportedByFieldType(@Param EventContext context);

@Message(id = ID_OFFSET + 54,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,21 @@

import static org.hibernate.search.backend.elasticsearch.search.predicate.impl.ElasticsearchMatchAllPredicate.MATCH_ALL_ACCESSOR;

import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;

import org.hibernate.search.backend.elasticsearch.gson.impl.GsonUtils;
import org.hibernate.search.backend.elasticsearch.gson.impl.JsonAccessor;
import org.hibernate.search.backend.elasticsearch.logging.impl.Log;
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.BooleanPredicateBuilder;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;

import com.google.gson.JsonObject;

class ElasticsearchBooleanPredicate extends AbstractElasticsearchPredicate {

private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() );

private static final String MUST_PROPERTY_NAME = "must";
private static final String MUST_NOT_PROPERTY_NAME = "must_not";
private static final String SHOULD_PROPERTY_NAME = "should";
Expand All @@ -46,7 +39,7 @@ class ElasticsearchBooleanPredicate extends AbstractElasticsearchPredicate {
// NOTE: below modifiers (minimumShouldMatchConstraints) are used to implement hasNoModifiers() which is based on a
// parent implementation.
// IMPORTANT: Review where current modifiers are used and how the new modifier affects that logic, when adding a new modifier.
private final Map<Integer, ElasticsearchCommonMinimumShouldMatchConstraint> minimumShouldMatchConstraints;
private final ElasticsearchCommonMinimumShouldMatchConstraints minimumShouldMatchConstraints;

private ElasticsearchBooleanPredicate(Builder builder) {
super( builder );
Expand Down Expand Up @@ -91,11 +84,10 @@ protected JsonObject doToJsonQuery(PredicateRequestContext context,
GsonUtils.setOrAppendToArray( innerObject, MUST_PROPERTY_NAME, matchAllClause );
}

if ( minimumShouldMatchConstraints != null ) {
if ( !minimumShouldMatchConstraints.isEmpty() ) {
MINIMUM_SHOULD_MATCH_ACCESSOR.set(
innerObject,
ElasticsearchCommonMinimumShouldMatchConstraint
.formatMinimumShouldMatchConstraints( minimumShouldMatchConstraints )
minimumShouldMatchConstraints.formatMinimumShouldMatchConstraints()
);
}

Expand Down Expand Up @@ -152,7 +144,7 @@ private boolean hasOnlyOneMustNotClause() {

@Override
protected boolean hasNoModifiers() {
return minimumShouldMatchConstraints == null
return minimumShouldMatchConstraints.isEmpty()
&& super.hasNoModifiers();
}

Expand All @@ -165,10 +157,11 @@ static class Builder extends AbstractElasticsearchPredicate.AbstractBuilder impl
// NOTE: below modifiers (minimumShouldMatchConstraints) are used to implement hasNoModifiers() which is based on a
// parent implementation.
// IMPORTANT: Review where current modifiers are used and how the new modifier affects that logic, when adding a new modifier.
private Map<Integer, ElasticsearchCommonMinimumShouldMatchConstraint> minimumShouldMatchConstraints;
private ElasticsearchCommonMinimumShouldMatchConstraints minimumShouldMatchConstraints;

Builder(ElasticsearchSearchIndexScope<?> scope) {
super( scope );
this.minimumShouldMatchConstraints = new ElasticsearchCommonMinimumShouldMatchConstraints();
}

@Override
Expand Down Expand Up @@ -213,37 +206,19 @@ public void filter(SearchPredicate clause) {

@Override
public void minimumShouldMatchNumber(int ignoreConstraintCeiling, int matchingClausesNumber) {
addMinimumShouldMatchConstraint(
ignoreConstraintCeiling,
new ElasticsearchCommonMinimumShouldMatchConstraint( matchingClausesNumber, null )
);
minimumShouldMatchConstraints.minimumShouldMatchNumber( ignoreConstraintCeiling, matchingClausesNumber );
}

@Override
public void minimumShouldMatchPercent(int ignoreConstraintCeiling, int matchingClausesPercent) {
addMinimumShouldMatchConstraint(
ignoreConstraintCeiling,
new ElasticsearchCommonMinimumShouldMatchConstraint( null, matchingClausesPercent )
);
minimumShouldMatchConstraints.minimumShouldMatchPercent( ignoreConstraintCeiling, matchingClausesPercent );
}

@Override
public boolean hasClause() {
return mustClauses != null || shouldClauses != null || mustNotClauses != null || filterClauses != null;
}

private void addMinimumShouldMatchConstraint(int ignoreConstraintCeiling,
ElasticsearchCommonMinimumShouldMatchConstraint constraint) {
if ( minimumShouldMatchConstraints == null ) {
// We'll need to go through the data in ascending order, so use a TreeMap
minimumShouldMatchConstraints = new TreeMap<>();
}
Object previous = minimumShouldMatchConstraints.put( ignoreConstraintCeiling, constraint );
if ( previous != null ) {
throw log.minimumShouldMatchConflictingConstraints( ignoreConstraintCeiling );
}
}

@Override
public SearchPredicate build() {
if ( !hasClause() ) {
Expand Down Expand Up @@ -273,7 +248,7 @@ else if ( hasOnlyOneShouldClause() ) {
}

// Forcing to Lucene's defaults. See HSEARCH-3534
if ( minimumShouldMatchConstraints == null && hasAtLeastOneMustOrFilterPredicate() ) {
if ( minimumShouldMatchConstraints.isEmpty() && hasAtLeastOneMustOrFilterPredicate() ) {
minimumShouldMatchNumber( 0, 0 );
}

Expand Down Expand Up @@ -328,7 +303,7 @@ private boolean hasOnlyOneShouldClause() {

@Override
protected boolean hasNoModifiers() {
return minimumShouldMatchConstraints == null && super.hasNoModifiers();
return minimumShouldMatchConstraints.isEmpty() && super.hasNoModifiers();
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.backend.elasticsearch.search.predicate.impl;

import java.lang.invoke.MethodHandles;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.hibernate.search.backend.elasticsearch.logging.impl.Log;
import org.hibernate.search.engine.search.predicate.spi.MinimumShouldMatchBuilder;
import org.hibernate.search.util.common.logging.impl.LoggerFactory;

public final class ElasticsearchCommonMinimumShouldMatchConstraints implements MinimumShouldMatchBuilder {
private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() );

private Map<Integer, CommonMinimumShouldMatchConstraint> minimumShouldMatchConstraints;

@Override
public void minimumShouldMatchNumber(int ignoreConstraintCeiling, int matchingClausesNumber) {
addMinimumShouldMatchConstraint(
ignoreConstraintCeiling,
new CommonMinimumShouldMatchConstraint( matchingClausesNumber, null )
);
}

@Override
public void minimumShouldMatchPercent(int ignoreConstraintCeiling, int matchingClausesPercent) {
addMinimumShouldMatchConstraint(
ignoreConstraintCeiling,
new CommonMinimumShouldMatchConstraint( null, matchingClausesPercent )
);
}

private void addMinimumShouldMatchConstraint(int ignoreConstraintCeiling,
CommonMinimumShouldMatchConstraint constraint) {
if ( minimumShouldMatchConstraints == null ) {
// We'll need to go through the data in ascending order, so use a TreeMap
minimumShouldMatchConstraints = new TreeMap<>();
}
Object previous = minimumShouldMatchConstraints.put( ignoreConstraintCeiling, constraint );
if ( previous != null ) {
throw log.minimumShouldMatchConflictingConstraints( ignoreConstraintCeiling );
}
}

public String formatMinimumShouldMatchConstraints() {
StringBuilder builder = new StringBuilder();
Iterator<Map.Entry<Integer, CommonMinimumShouldMatchConstraint>> iterator =
minimumShouldMatchConstraints.entrySet().iterator();

// Process the first constraint differently
Map.Entry<Integer, CommonMinimumShouldMatchConstraint> entry = iterator.next();
Integer ignoreConstraintCeiling = entry.getKey();
CommonMinimumShouldMatchConstraint constraint = entry.getValue();
if ( ignoreConstraintCeiling.equals( 0 ) && minimumShouldMatchConstraints.size() == 1 ) {
// Special case: if there's only one constraint and its ignore ceiling is 0, do not mention the ceiling
constraint.appendTo( builder, null );
return builder.toString();
}
else {
entry.getValue().appendTo( builder, ignoreConstraintCeiling );
}

// Process the other constraints normally
while ( iterator.hasNext() ) {
entry = iterator.next();
ignoreConstraintCeiling = entry.getKey();
constraint = entry.getValue();
builder.append( ' ' );
constraint.appendTo( builder, ignoreConstraintCeiling );
}

return builder.toString();
}

public boolean isEmpty() {
return minimumShouldMatchConstraints == null;
}

private static final class CommonMinimumShouldMatchConstraint {
private final Integer matchingClausesNumber;
private final Integer matchingClausesPercent;

CommonMinimumShouldMatchConstraint(Integer matchingClausesNumber, Integer matchingClausesPercent) {
this.matchingClausesNumber = matchingClausesNumber;
this.matchingClausesPercent = matchingClausesPercent;
}

/**
* Format the constraint according to
* <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html">
* the format specified in the Elasticsearch documentation
* </a>.
*
* @param builder The builder to append the formatted value to.
* @param ignoreConstraintCeiling The ceiling above which this constraint is no longer ignored.
*/
void appendTo(StringBuilder builder, Integer ignoreConstraintCeiling) {
if ( ignoreConstraintCeiling != null ) {
builder.append( ignoreConstraintCeiling ).append( '<' );
}
if ( matchingClausesNumber != null ) {
builder.append( matchingClausesNumber );
}
else {
builder.append( matchingClausesPercent ).append( '%' );
}
}
}
}

0 comments on commit 2b27c27

Please sign in to comment.