diff --git a/changelog/unreleased/SOLR-18314-empty-phrase-boost-matchalldocs.yml b/changelog/unreleased/SOLR-18314-empty-phrase-boost-matchalldocs.yml new file mode 100644 index 000000000000..e673bfe2d591 --- /dev/null +++ b/changelog/unreleased/SOLR-18314-empty-phrase-boost-matchalldocs.yml @@ -0,0 +1,11 @@ +title: Fix the eDisMax parser adding an empty phrase field (pf) boost clause when no phrase qualifies + for the boost, such as for single-term queries or q=*:*. The empty SHOULD clause did not affect + scoring but defeated the single-clause MatchAllDocsQuery optimization; it is now omitted, so q=*:* + reduces to a MatchAllDocsQuery even when pf is set. +type: fixed +authors: + - name: Ian Springer + nick: ianspringer +links: + - name: SOLR-18314 + url: https://issues.apache.org/jira/browse/SOLR-18314 diff --git a/changelog/unreleased/SOLR-18314-skip-phrase-analysis-single-term.yml b/changelog/unreleased/SOLR-18314-skip-phrase-analysis-single-term.yml new file mode 100644 index 000000000000..9bc522a52ed1 --- /dev/null +++ b/changelog/unreleased/SOLR-18314-skip-phrase-analysis-single-term.yml @@ -0,0 +1,13 @@ +title: The DisMax parser no longer applies the phrase field (pf) boost to single-term queries, matching + the eDisMax parser. A query is single-term when it is one whitespace-free term that analyzes to a + single token in every pf field. A term whose analysis yields multiple tokens (e.g. via CJK, ngram, + WordDelimiterGraph, or multi-word synonyms) keeps its phrase boost, as does a multi-word query on + a keyword or string pf field. This may change relevancy scores for single-term DisMax queries, + including those whose pf fields are not also in qf. +type: changed +authors: + - name: Ian Springer + nick: ianspringer +links: + - name: SOLR-18314 + url: https://issues.apache.org/jira/browse/SOLR-18314 diff --git a/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java b/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java index 91eec6acabd9..f8ca7382c0f9 100644 --- a/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java +++ b/solr/core/src/java/org/apache/solr/search/DisMaxQParser.java @@ -16,13 +16,19 @@ */ package org.apache.solr.search; +import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.BoostQuery; +import org.apache.lucene.search.DisjunctionMaxQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; import org.apache.lucene.search.Query; +import org.apache.lucene.search.SynonymQuery; +import org.apache.lucene.search.TermQuery; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.params.DisMaxParams; import org.apache.solr.common.params.SolrParams; @@ -198,7 +204,12 @@ protected boolean addMainQuery(BooleanQuery.Builder query, SolrParams solrParams query.add(parsedUserQuery, BooleanClause.Occur.MUST); Query phrase = getPhraseQuery(userQuery, pp); - if (null != phrase) { + // A single-term phrase boost imposes no adjacency constraint, so skip it, as eDisMax does. + // Require both a single user term and a single analyzed term: analyzers such as CJK, ngram, + // WordDelimiterGraph, and multi-word synonyms can split one input term into several tokens, + // and a multi-word query can analyze to one token (e.g. an exact-match keyword or string + // field). Either way the phrase boost is meaningful. + if (null != phrase && !(isSingleUserTerm(userQuery) && isEffectivelySingleTerm(phrase))) { query.add(phrase, BooleanClause.Occur.SHOULD); } } @@ -235,6 +246,45 @@ protected Query getPhraseQuery(String userQuery, SolrPluginUtils.DisjunctionMaxQ return phrase; } + /** Returns true if the user query, ignoring quotes, is a single whitespace-free term. */ + private static boolean isSingleUserTerm(String userQuery) { + String term = userQuery.replace("\"", "").strip(); + return !term.isEmpty() && term.codePoints().noneMatch(Character::isWhitespace); + } + + /** + * Returns true if the parsed phrase query boils down to a single analyzed position in every + * field, in which case the phrase boost imposes no adjacency constraint. The parser produces a + * {@link DisjunctionMaxQuery} over per-field {@link BoostQuery}-wrapped queries; we inspect the + * underlying query shape so that analyzers which split a single input term into multiple tokens + * keep their phrase boost. Synonyms at a single position count as a single term when built as a + * {@link SynonymQuery} ({@code synonymQueryStyle=as_same_term}) or {@link DisjunctionMaxQuery} + * ({@code pick_best}); the {@link BooleanQuery} that {@code as_distinct_terms} builds is kept, as + * eDisMax's {@code minClauseSize} gate also keeps it. Unrecognized shapes (and {@code null}) + * conservatively return false (keep the boost). + */ + @VisibleForTesting + static boolean isEffectivelySingleTerm(Query query) { + if (query instanceof BoostQuery boostQuery) { + return isEffectivelySingleTerm(boostQuery.getQuery()); + } + if (query instanceof DisjunctionMaxQuery dmq) { + // Every field's clause is a single term (or synonymQueryStyle=pick_best synonyms). + return !dmq.getDisjuncts().isEmpty() + && dmq.getDisjuncts().stream().allMatch(DisMaxQParser::isEffectivelySingleTerm); + } + if (query instanceof TermQuery || query instanceof SynonymQuery) { + return true; + } + if (query instanceof PhraseQuery phraseQuery) { + return phraseQuery.getTerms().length < 2; + } + if (query instanceof MultiPhraseQuery multiPhraseQuery) { + return multiPhraseQuery.getTermArrays().length < 2; + } + return false; + } + protected Query getUserQuery( String userQuery, SolrPluginUtils.DisjunctionMaxQueryParser up, SolrParams solrParams) throws SyntaxError { diff --git a/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java b/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java index 809c0305be39..955a953570e7 100644 --- a/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java +++ b/solr/core/src/java/org/apache/solr/search/ExtendedDismaxQParser.java @@ -625,8 +625,11 @@ protected void addShingledPhraseQueries( final int slop) throws SyntaxError { - if (null == fields || fields.isEmpty() || null == clauses || clauses.size() < shingleSize) - return; + if (null == fields + || fields.isEmpty() + || null == clauses + || clauses.isEmpty() + || clauses.size() < shingleSize) return; if (0 == shingleSize) shingleSize = clauses.size(); @@ -676,7 +679,10 @@ protected void addShingledPhraseQueries( // TODO: perhaps we shouldn't use synonyms either... Query phrase = pp.parse(userPhraseQuery.toString()); - if (phrase != null) { + // When minClauseSize discards every phrase (e.g. single-term queries or q=*:*), parse returns + // an empty BooleanQuery rather than null; adding it as a SHOULD clause would defeat the + // single-clause MatchAllDocsQuery optimization applied to the main query. + if (!isEmpty(phrase)) { mainQuery.add(phrase, BooleanClause.Occur.SHOULD); } } diff --git a/solr/core/src/test-files/solr/collection1/conf/schema.xml b/solr/core/src/test-files/solr/collection1/conf/schema.xml index e20f31d4bf3b..a2bd7e425da0 100644 --- a/solr/core/src/test-files/solr/collection1/conf/schema.xml +++ b/solr/core/src/test-files/solr/collection1/conf/schema.xml @@ -312,6 +312,17 @@ + + + + + + + + + + @@ -632,7 +643,9 @@ + + diff --git a/solr/core/src/test-files/solr/collection1/conf/synonyms.txt b/solr/core/src/test-files/solr/collection1/conf/synonyms.txt index d7feb34ee647..d017cc127679 100644 --- a/solr/core/src/test-files/solr/collection1/conf/synonyms.txt +++ b/solr/core/src/test-files/solr/collection1/conf/synonyms.txt @@ -32,6 +32,7 @@ pixima => pixma # multiword synonyms wi fi => wifi crow blackbird, grackle +usa => united states of america # Synonyms used in semantic expansion tabby => tabby, cat, feline, animal diff --git a/solr/core/src/test/org/apache/solr/DisMaxRequestHandlerTest.java b/solr/core/src/test/org/apache/solr/DisMaxRequestHandlerTest.java index 673b9cc414af..a6735a73d7f4 100644 --- a/solr/core/src/test/org/apache/solr/DisMaxRequestHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/DisMaxRequestHandlerTest.java @@ -245,4 +245,126 @@ public void testExtraBlankBQ() throws Exception { assertTrue(p.matcher(resp).find()); assertTrue(p_bool.matcher(resp).find()); } + + // SOLR-18314: the pf phrase boost is dropped for single-term queries, i.e. a single user term + // that also analyzes to a single token. A single input term whose analysis yields multiple tokens + // must keep its phrase boost, whether from WordDelimiter splitting "wi-fi" into "wi fi" or a + // multi-word synonym expanding "usa" into "united states of america". So must a multi-word query + // that analyzes to a single token on a string pf field (an exact-match boost). Unlike eDisMax, + // the classic DisMax parser has no minClauseSize gate, so this is enforced by inspecting the + // user query and the parsed phrase query. + // + // CJK is an important example of a query with no spaces that can still parse to multiple terms. + // However, we don't add a dedicated CJK test: CJK analysis of a single input term produces the + // same multi-term PhraseQuery that the "wi-fi" case already exercises, so it would cover no new + // code path while requiring a CJK analyzer to be configured in the test schema. + @Test + public void testSingleTermPhraseBoost() throws Exception { + // Single analyzed token: the pf phrase boost imposes no adjacency, so only the unboosted qf + // clause should appear in the parsed query. + String resp = + h.query( + req( + "q", + "wireless", + "qt", + "/dismax", + "qf", + "subword", + "pf", + "subword^10", + CommonParams.DEBUG_QUERY, + "true")); + assertTrue( + "single-term query should keep its qf clause", + Pattern.compile("\\(subword:wireless\\)(?!\\^)").matcher(resp).find()); + assertFalse( + "single-term query should not produce a phrase boost", + Pattern.compile("subword:wireless\\)\\^10\\.0").matcher(resp).find()); + + // Whitespace-free query that analyzes into multiple tokens: the phrase boost is meaningful and + // must be retained. The qf clause is also the phrase "wi fi" here (this schema's version + // defaults autoGeneratePhraseQueries to true), so match the pf clause by the handler's ps=100 + // slop and the pf boost. + resp = + h.query( + req( + "q", + "wi-fi", + "qt", + "/dismax", + "qf", + "subword", + "pf", + "subword^10", + CommonParams.DEBUG_QUERY, + "true")); + assertTrue( + "multi-token query should retain its phrase boost", + Pattern.compile("subword:\"wi fi\"~100\\)\\^10\\.0").matcher(resp).find()); + + // With autoGeneratePhraseQueries="false" (the default since schema 1.4), the qf clause for + // "wi-fi" is a plain disjunction of its tokens, so the pf boost is the only adjacency signal. + // Gating on the user query's clause count (one, since there's no whitespace) would wrongly drop + // it; this is also the shape of CJK bigram queries. + resp = + h.query( + req( + "q", + "wi-fi", + "qt", + "/dismax", + "qf", + "subword_noautophrase", + "pf", + "subword_noautophrase^10", + CommonParams.DEBUG_QUERY, + "true")); + assertTrue( + "qf clause should not be a phrase without autoGeneratePhraseQueries", + Pattern.compile("subword_noautophrase:wi subword_noautophrase:fi").matcher(resp).find()); + assertTrue( + "multi-token query should retain its phrase boost without autoGeneratePhraseQueries", + Pattern.compile("subword_noautophrase:\"wi fi\"~100\\)\\^10\\.0").matcher(resp).find()); + + // A single input term expanded by a multi-word synonym ("usa" => "united states of america") + // likewise analyzes into multiple tokens, so its phrase boost must be retained. + resp = + h.query( + req( + "q", + "usa", + "qt", + "/dismax", + "qf", + "text_syn", + "pf", + "text_syn^10", + CommonParams.DEBUG_QUERY, + "true")); + assertTrue( + "multi-word synonym query should retain its phrase boost", + Pattern.compile("text_syn:\"united states of america\"~100\\)\\^10\\.0") + .matcher(resp) + .find()); + + // A multi-word query on a string pf field analyzes to a single exact-match term, which is the + // only signal ranking an exact match first, so its phrase boost must be retained. + resp = + h.query( + req( + "q", + "hello world", + "qt", + "/dismax", + "qf", + "subword", + "pf", + "foo_s^10", + CommonParams.DEBUG_QUERY, + "true")); + assertTrue( + "multi-word query should retain its exact-match phrase boost", + Pattern.compile("foo_s:hello world\\)\\^10\\.0").matcher(resp).find()); + } } diff --git a/solr/core/src/test/org/apache/solr/search/TestDisMaxQParser.java b/solr/core/src/test/org/apache/solr/search/TestDisMaxQParser.java new file mode 100644 index 000000000000..a5e03654dd03 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/TestDisMaxQParser.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.search; + +import java.util.List; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BoostQuery; +import org.apache.lucene.search.DisjunctionMaxQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MultiPhraseQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.SynonymQuery; +import org.apache.lucene.search.TermQuery; +import org.apache.solr.SolrTestCase; +import org.junit.Test; + +/** Unit tests for {@link DisMaxQParser} internals that do not need a running core. */ +public class TestDisMaxQParser extends SolrTestCase { + + private static TermQuery term(String field, String text) { + return new TermQuery(new Term(field, text)); + } + + private static BooleanQuery should(Query... queries) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (Query query : queries) { + builder.add(query, BooleanClause.Occur.SHOULD); + } + return builder.build(); + } + + // SOLR-18314: isEffectivelySingleTerm decides whether a parsed pf phrase query collapses to a + // single analyzed position (so the phrase boost imposes no adjacency and should be dropped). It + // must return true only when every field's clause is a single position; any clause spanning two + // or more positions means the phrase boost adds a real adjacency constraint and must be kept. + @Test + public void testIsEffectivelySingleTermIsTrueForSingleTermShapes() { + // A bare single term. + assertTrue(DisMaxQParser.isEffectivelySingleTerm(term("subject", "wireless"))); + + // The parser wraps each field's clause in a BoostQuery; it must be unwrapped. + assertTrue( + DisMaxQParser.isEffectivelySingleTerm(new BoostQuery(term("subject", "wireless"), 10f))); + + // Synonyms at a single position: synonymQueryStyle=as_same_term (the default) builds a + // SynonymQuery and pick_best a DisjunctionMaxQuery. + assertTrue( + DisMaxQParser.isEffectivelySingleTerm( + new SynonymQuery.Builder("subject") + .addTerm(new Term("subject", "tv")) + .addTerm(new Term("subject", "television")) + .build())); + assertTrue( + DisMaxQParser.isEffectivelySingleTerm( + new BoostQuery( + new DisjunctionMaxQuery( + List.of(term("subject", "tv"), term("subject", "television")), 0f), + 10f))); + + // Degenerate one-position phrases (QueryBuilder emits a TermQuery or SynonymQuery instead, but + // these are equally single-term). + assertTrue(DisMaxQParser.isEffectivelySingleTerm(new PhraseQuery("subject", "wireless"))); + MultiPhraseQuery singlePosition = + new MultiPhraseQuery.Builder().add(new Term("subject", "wireless")).build(); + assertTrue(DisMaxQParser.isEffectivelySingleTerm(singlePosition)); + + // A DisjunctionMaxQuery whose every disjunct is single-term is single-term across fields. + DisjunctionMaxQuery allSingle = + new DisjunctionMaxQuery( + List.of( + new BoostQuery(term("subject", "wireless"), 10f), + new BoostQuery(term("features", "wireless"), 5f)), + 0.1f); + assertTrue(DisMaxQParser.isEffectivelySingleTerm(allSingle)); + } + + @Test + public void testIsEffectivelySingleTermIsFalseForMultiTermShapes() { + // A genuine two-term phrase (e.g. WordDelimiter splitting "wi-fi" into "wi fi"). + assertFalse(DisMaxQParser.isEffectivelySingleTerm(new PhraseQuery("subject", "wi", "fi"))); + + // A two-position MultiPhraseQuery (e.g. synonyms within a multi-token phrase). + MultiPhraseQuery twoPositions = + new MultiPhraseQuery.Builder() + .add(new Term("subject", "wi")) + .add(new Term("subject", "fi")) + .build(); + assertFalse(DisMaxQParser.isEffectivelySingleTerm(twoPositions)); + + // A sloppy graph phrase (e.g. a multi-word synonym) is a disjunction of per-path phrases; any + // multi-term path keeps the boost. + assertFalse( + DisMaxQParser.isEffectivelySingleTerm( + should(term("subject", "wifi"), new PhraseQuery(100, "subject", "wi", "fi")))); + + // Any field with a multi-term clause keeps the boost, even if other fields are single-term. + DisjunctionMaxQuery mixed = + new DisjunctionMaxQuery( + List.of( + new BoostQuery(term("subject", "wireless"), 10f), + new BoostQuery(new PhraseQuery("features", "wi", "fi"), 5f)), + 0.1f); + assertFalse(DisMaxQParser.isEffectivelySingleTerm(mixed)); + } + + @Test + public void testIsEffectivelySingleTermIsFalseForUnrecognizedShapes() { + // Unrecognized shapes conservatively keep the boost rather than silently dropping it. + assertFalse(DisMaxQParser.isEffectivelySingleTerm(new MatchAllDocsQuery())); + + // synonymQueryStyle=as_distinct_terms builds a BooleanQuery for single-position synonyms; + // eDisMax's minClauseSize gate keeps it too, so the parsers agree. + assertFalse( + DisMaxQParser.isEffectivelySingleTerm( + should(term("subject", "tv"), term("subject", "television")))); + BooleanQuery required = + new BooleanQuery.Builder() + .add(term("subject", "wireless"), BooleanClause.Occur.MUST) + .build(); + assertFalse(DisMaxQParser.isEffectivelySingleTerm(required)); + + // An empty disjunction is not vacuously single-term. + assertFalse(DisMaxQParser.isEffectivelySingleTerm(new DisjunctionMaxQuery(List.of(), 0f))); + + // null is a valid input (parse() can return it) and must not throw. + assertFalse(DisMaxQParser.isEffectivelySingleTerm(null)); + } +} diff --git a/solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java b/solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java index f1568b257952..be7bd11ad72f 100644 --- a/solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java +++ b/solr/core/src/test/org/apache/solr/search/TestExtendedDismaxParser.java @@ -26,6 +26,7 @@ import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.not; import java.util.Arrays; import java.util.HashSet; @@ -1193,6 +1194,62 @@ public void testOperatorsWithLiteralColons() { "//str[@name='id'][.='146']"); } + // SOLR-18314: the single-term pf boost is dropped, but only for genuinely single-token queries. + // A single input term whose analysis yields multiple tokens (WordDelimiter splitting "wi-fi", or + // a multi-word synonym expanding "usa") must keep its phrase boost. + // + // CJK is an important example of a query with no spaces that can still parse to multiple terms. + // However, we don't add a dedicated CJK test: CJK analysis of a single input term produces the + // same multi-term PhraseQuery that the "wi-fi" and "usa" cases already exercise, so it covers no + // new code path while requiring a CJK analyzer to be configured in the test schema. + public void testSingleTermPhraseBoost() throws Exception { + // Plain single-token query: the pf phrase boost imposes no adjacency, so minClauseSize discards + // it and no boosted (or empty) phrase clause should be present. + String singleTokenParsed = + getParsedQuery( + req( + "q", "wireless", + "qf", "subject", + "pf", "subject^10", + "defType", "edismax", + "debugQuery", "true")); + assertThat(singleTokenParsed, allOf(not(containsString("^10.0")), not(containsString("()")))); + + // Whitespace-free query that analyzes into multiple tokens: the phrase boost is meaningful + // (imposes adjacency that the qf clause does not) and must survive. + String multiTokenParsed = + getParsedQuery( + req( + "q", "wi-fi", + "qf", "subject", + "pf", "subject^10", + "defType", "edismax", + "debugQuery", "true")); + assertThat(multiTokenParsed, containsString("(subject:\"wi fi\")^10.0")); + + // A single raw token that a multi-word synonym expands into several analyzed tokens (here + // "usa" => "united states of america") also warrants a real phrase boost and must be retained. + // This covers the same "no whitespace in, multiple tokens out" case as CJK and ngram analyzers, + // without needing to configure those analyzers in the test schema. + String synonymParsed = + getParsedQuery( + req( + "q", "usa", + "qf", "subject", + "pf", "subject^10", + "defType", "edismax", + "debugQuery", "true")); + // "of" is a stopword (the "?" gap) and Porter stemming yields unit/state/america. + assertThat(synonymParsed, containsString("(subject:\"unit state ? america\")^10.0")); + + // q=*:* must not add an empty phrase clause that would defeat the MatchAllDocsQuery + // optimization. + try (SolrQueryRequest req = req("qf", "subject", "pf", "subject^10")) { + Query matchAll = QParser.getParser("*:*", "edismax", req).getQuery(); + assertThat(matchAll, isA(MatchAllDocsQuery.class)); + } + } + // test phrase fields including pf2 pf3 and phrase slop public void testPfPs() throws Exception { assertU(adoc("id", "s0", "phrase_sw", "foo bar a b c", "boost_d", "1.0")); diff --git a/solr/solr-ref-guide/modules/query-guide/pages/dismax-query-parser.adoc b/solr/solr-ref-guide/modules/query-guide/pages/dismax-query-parser.adoc index 08bb9d08ae70..379d19957b45 100644 --- a/solr/solr-ref-guide/modules/query-guide/pages/dismax-query-parser.adoc +++ b/solr/solr-ref-guide/modules/query-guide/pages/dismax-query-parser.adoc @@ -117,6 +117,9 @@ Once the list of matching documents has been identified using the `fq` and `qf` The format is the same as that used by the `qf` parameter: a list of fields and "boosts" to associate with each of them when making phrase queries out of the entire q parameter. +The `pf` boost is not applied to single-term queries, meaning a single term that analyzes to a single token in every `pf` field, since a one-term phrase imposes no proximity constraint. +Consequently a `pf` field that is not also in `qf` contributes nothing to the score of a single-term query; to boost single-term matches in such a field, add it to `qf` or use the `bq` parameter. + === ps (Phrase Slop) Parameter diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc index ac2de0f81376..f400854d670f 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-11.adoc @@ -25,6 +25,17 @@ This page highlights the most important changes including new features and chang Before starting an upgrade to this version of Solr, please be sure to review all information about changes from the version you are currently on up to this one, to include the minor version number changes as well. For example, if you are currently using Solr 10.1, you should review changes made in all subsequent 10.x releases in addition to the 11.0-specific changes on this page. +== Changes in Default Behavior + +=== DisMax Phrase Field Boost for Single-Term Queries + +The DisMax query parser no longer applies the phrase field (`pf`) boost to single-term queries, bringing it in line with the eDisMax parser. +A query is single-term when it is one whitespace-free term that analyzes to a single token in every `pf` field. +A term that analyzes to several tokens (for example via CJK, ngram, WordDelimiterGraph, or multi-word synonym analysis) keeps its phrase boost, as does a multi-word query on a keyword or string `pf` field. + +This can change the ranking of single-term DisMax queries, most noticeably where a `pf` field is not also in `qf`, since that field no longer contributes to the score at all. +To keep boosting single-term matches in such a field, add it to `qf` or use a `bq` boost query. + == Removed Features === Overseer Roles API