eDisMax is the query parser most production Solr deployments run on. It takes the words a user typed, spreads them across the fields you nominate, decides how many of those words a document has to contain, and rewards documents where the words appear together. This guide covers every parameter that does that work — qf, mm, pf, pf2, pf3, ps, ps2, ps3 — and shows the exact query Solr builds for each one.
The words the user typed stay in q. Everything that shapes ranking lives in separate parameters.
Every parsed query printed on this page is a real capture from a live Opensolr index, taken with debugQuery=true. None of it is illustrative.
01 · What eDisMax actually does
DisMax stands for disjunction maximum: each term is run against several fields at once and the field that scores best is the one that counts. eDisMax is the extended version. It keeps that behaviour and adds full Lucene syntax, shingle-based phrase boosting, function boosting and a set of controls that make relevance tuning a configuration job rather than a string-building job.
The query string stays clean
Field names, weights and boolean logic live in parameters. q holds nothing but what the user typed, which is also what makes query logging, spellcheck and analytics meaningful.
The best field wins, not the sum
A term present in three fields does not score three times. eDisMax takes the highest-scoring field, then adds tie multiplied by the rest. tie defaults to 0.0, which is pure winner-takes-all; 0.1 is the common working value.
User input rarely produces an error
eDisMax first reads the query as Lucene syntax. If that fails it escapes the offending characters and tries again, so a stray quote or a trailing AND becomes text instead of an HTTP 400.
Real operators when someone types them
+, -, AND, OR, NOT, field:value and parentheses all work, which plain DisMax does not support.
Words that travel together are rewarded
Documents holding the query words next to each other, in order, can be lifted above documents that merely contain all of them. That is the entire purpose of pf, pf2 and pf3.
02 · qf — which fields, and how much each one counts
qf is a space-separated list of fields, each with an optional ^ weight. It answers one question: where do the user's words get looked up, and whose opinion matters most.
qf=title^3.0 summary^1.5 text_all^0.5
One term, three fields, three weights. The highest field score is carried forward.
Here is what Solr reports for a four-term query over two fields. Each term becomes its own DisjunctionMaxQuery, and the weights are attached per field, not per term:
q=solr search index tuning defType=edismax qf=title^2 text parsedquery: +(DisjunctionMaxQuery((text:solr | (title:solr)^2.0)) DisjunctionMaxQuery((text:search | (title:search)^2.0)) DisjunctionMaxQuery((text:index | (title:index)^2.0)) DisjunctionMaxQuery((text:tune | (title:tune)^2.0)))
Two things worth reading out of that output. The vertical bar is the disjunction: the field with the best score wins the term. And tuning came back as tune, because the field's analyzer stems it — qf can only reach fields whose analysis chain produces the token the user is looking for.
title^3 summary^1 and title^30 summary^10 rank identically. Change ratios, not magnitudes.03 · mm — how many of the words must be there
mm (minimum should match) is the recall/precision dial. It decides how many of the query terms a document must contain before it is allowed into the result set at all. Set it too high and long queries return nothing; too low and every document containing one common word turns up.
The same setting, read across query lengths: short queries stay strict, long queries relax.
| Form | Example | Meaning |
|---|---|---|
| Positive integer | 3 | At least three clauses must match. |
| Negative integer | -1 | All clauses but one. |
| Percentage | 75% | Three quarters of the clauses, rounded down. |
| Negative percentage | -25% | At most a quarter of the clauses may be missing, rounded down. |
| Conditional | 2<75% | Up to two clauses, all are required. Above two, apply 75%. |
| Chained | 2<-1 5<40% | Conditions are read in order of clause count; the last one that applies wins. |
The number Solr actually enforces shows up at the end of the parsed query as ~N. Real captures:
mm=75% q=alpha beta gamma -> (... alpha ... beta ... gamma)~2 mm=75% q=alpha beta gamma delta -> (... four clauses ...)~3 mm=2<75% q=alpha beta -> (... alpha ... beta)~2 mm=2<75% q=alpha beta gamma -> (... three clauses ...)~2 mm=-25% q=alpha beta gamma delta -> (... four clauses ...)~3
Note the rounding: 75% of three terms is 2.25, and Solr requires two, not three. Percentages always round down. Note the conditional as well — with 2<75% a two-word query needs both words, because the rule only starts applying above two clauses.
mm is not set at all, the effective default depends on q.op: AND behaves as 100%, OR as 0%. Leaving both unset is how a search ends up returning everything that contains any one word.04 · pf, pf2, pf3 — rewarding words that travel together
Phrase fields do not filter anything. They add optional clauses on top of the term matching, so a document that happens to contain the words in sequence scores higher than one where the same words are scattered across a page.
The same four-word query, seen by each phrase parameter.
pf — the whole query as one phrase
One clause, containing every term in order. It only fires when the complete phrase is present, which makes it the strongest and the rarest of the three.
pf2 — every adjacent pair
The query is shingled into bigrams and each pair becomes its own clause. This is what keeps long queries useful: the full phrase almost never matches, but two-word fragments do.
pf3 — every adjacent triple
The same idea with trigrams. Useful when queries are long enough for three-word fragments to carry meaning.
All three, on the same request, with the clauses Solr built for them:
q=alpha beta gamma defType=edismax qf=title^2 text pf=title^5 ps=2 pf2=title^3 ps2=1 pf3=title^2 parsedquery: +(DisjunctionMaxQuery((text:alpha | (title:alpha)^2.0)) DisjunctionMaxQuery((text:beta | (title:beta)^2.0)) DisjunctionMaxQuery(((title:gamma)^2.0 | text:gamma))) DisjunctionMaxQuery(((title:"alpha beta gamma"~2)^5.0)) (DisjunctionMaxQuery(((title:"alpha beta"~1)^3.0)) DisjunctionMaxQuery(((title:"beta gamma"~1)^3.0))) DisjunctionMaxQuery(((title:"alpha beta gamma"~2)^2.0))
pf1, and no ps1. Solr ignores unknown request parameters silently, so a pf1 sitting in a request handler looks harmless and does exactly nothing — the parsed query above gains no clause when it is added. A one-word query has no phrase to boost, and every phrase clause collapses to empty: +DisjunctionMaxQuery((text:alpha | (title:alpha)^2.0)) (). If single-word queries need help, raise the qf weight of the field that matters, or elevate the document.05 · ps, ps2, ps3 — how much drift is still a phrase
Slop is the number of position moves needed to turn the document's wording into the query phrase. One word in between costs one. Swapping two adjacent words costs two. A phrase clause matches when the required moves fit inside the slop you allowed.
Everything to the left of the threshold still earns the phrase boost. Everything to the right earns nothing.
| Parameter | Applies to | When not set |
|---|---|---|
ps | The pf clause | Zero: the phrase must be exact. |
ps2 | The pf2 clauses | Falls back to ps. |
ps3 | The pf3 clauses | Falls back to ps. |
The fallback is visible in the capture above: ps3 was never sent, and the pf3 clause came out as "alpha beta gamma"~2, borrowing the ps=2 set for pf. Raising slop is the cheapest way to make phrase boosting meaningless — at a large enough value every document containing the words counts as a phrase match, and the ranking flattens back to plain term matching.
06 · How the final score is assembled
The parsed query has two parts. The required part is the term matching, filtered by mm — that decides which documents are in the result set at all. Everything after it is optional: the phrase clauses only add score to documents that already qualified.
Two documents matching the same terms. Phrase clauses decide the order between them.
This is why phrase boosting can never rescue a document that mm excluded, and why cranking pf is the wrong answer to “relevant documents are missing”. Missing documents are an mm, an analyzer, or a qf coverage problem. Wrong order is what phrase boosts fix.
07 · Where each parameter lives
Three layers, one direction of precedence.
schema.xml defines what can be searched — the fields, their types, and the analysis chain that decides which tokens end up in the index. A field the analyzer stems, folds and de-duplicates behaves very differently under qf than a string field.
<!-- schema.xml excerpt: one analyzed text type, the fields, and an aggregate catch-all --> <schema name="classicbook" version="1.6"> <fieldType name="text_general" class="solr.TextField" positionIncrementGap="100"> <analyzer type="index"> <charFilter class="solr.HTMLStripCharFilterFactory"/> <tokenizer class="solr.ICUTokenizerFactory"/> <filter class="solr.EnglishPossessiveFilterFactory"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/> <filter class="solr.WordDelimiterGraphFilterFactory" generateWordParts="1" catenateWords="1" preserveOriginal="1"/> <filter class="solr.ASCIIFoldingFilterFactory" preserveOriginal="true"/> <filter class="solr.SnowballPorterFilterFactory" protected="protwords.txt" language="English"/> <filter class="solr.RemoveDuplicatesTokenFilterFactory"/> </analyzer> <analyzer type="query"> <charFilter class="solr.HTMLStripCharFilterFactory"/> <tokenizer class="solr.ICUTokenizerFactory"/> <filter class="solr.EnglishPossessiveFilterFactory"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.SynonymGraphFilterFactory" ignoreCase="true" synonyms="synonyms.txt" expand="true"/> <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt"/> <filter class="solr.WordDelimiterGraphFilterFactory" generateWordParts="1" catenateWords="1" preserveOriginal="1"/> <filter class="solr.ASCIIFoldingFilterFactory" preserveOriginal="true"/> <filter class="solr.SnowballPorterFilterFactory" protected="protwords.txt" language="English"/> <filter class="solr.RemoveDuplicatesTokenFilterFactory"/> </analyzer> </fieldType> <field name="id" type="string" indexed="true" stored="true" required="true"/> <field name="title" type="text_general" indexed="true" stored="true" multiValued="false"/> <field name="author" type="string" indexed="true" stored="true"/> <field name="summary" type="text_general" indexed="true" stored="true" multiValued="false"/> <field name="content" type="text_general" indexed="true" stored="false" multiValued="false"/> <!-- Aggregate catch-all: cheap recall net, kept at a low qf weight --> <field name="text_all" type="text_general" indexed="true" stored="false" multiValued="true"/> <copyField source="title" dest="text_all"/> <copyField source="author" dest="text_all"/> <copyField source="summary" dest="text_all"/> <copyField source="content" dest="text_all"/> <uniqueKey>id</uniqueKey> </schema>
<defaultSearchField> and <solrQueryParser defaultOperator> were deprecated in Solr 3.6 and removed in Solr 7. Use df and q.op in the request handler instead — shown below. Under eDisMax, df only matters for terms a user explicitly field-qualifies; ordinary terms go through qf.solrconfig.xml is where the parser and its defaults belong. Putting them in the handler means every client — your site, your mobile app, an integration you have not written yet — gets the same relevance without repeating itself.
<requestHandler name="/select" class="solr.SearchHandler" default="true"> <lst name="defaults"> <str name="defType">edismax</str> <str name="df">text_all</str> <str name="q.op">OR</str> <!-- Which fields, and how much each one is worth --> <str name="qf">title^3.0 summary^1.5 text_all^0.5</str> <!-- How many of the terms a document must contain --> <str name="mm">2<75% 4<90% 6<100%</str> <!-- Phrase rewards, and how far the words may drift --> <str name="pf">title^5 summary^3</str> <str name="ps">2</str> <str name="pf2">title^4 summary^2</str> <str name="ps2">1</str> <str name="pf3">title^3 summary^1</str> <str name="ps3">1</str> <!-- Best-field scoring with a small contribution from the others --> <str name="tie">0.1</str> <str name="hl">true</str> <str name="hl.fl">title,summary,content</str> </lst> </requestHandler>
08 · Worked examples
A short query. Two words, both required, title matches worth double.
GET /solr/classicbook/select ?q=ancient philosophy &defType=edismax &qf=title^2 summary^1 content^0.2 &mm=100% &pf=title^5&ps=1
Both terms must appear. A document with “ancient” in the title but no “philosophy” anywhere is not in the result set — no phrase boost can bring it back. Among the documents that do qualify, the ones carrying the exact phrase in the title lead.
A long query. Seven words, where demanding all of them returns nothing.
GET /solr/classicbook/select ?q=quantum field theory experiments at low temperatures &defType=edismax &qf=title^2 summary^1 text_all^0.3 &mm=2<75% 6<60% &pf=title^5&ps=2 &pf2=title^4&ps2=1 &pf3=title^3
Seven clauses under 6<60% require four matches, so the result set stays populated. The full phrase almost certainly never occurs, and that is fine: pf2 and pf3 catch “quantum field”, “field theory” and “low temperatures”, and documents holding several of those fragments rise to the top on their own.
A query-time override. Handler defaults are the rule; overrides are for the exception.
GET /solr/classicbook/select ?q=renaissance art paintings &defType=edismax &qf=title^4 summary^2 text_all^0.4 &pf=title^6&ps=3 &rows=20 &sort=score desc, publish_date desc
Anything sent on the request replaces the handler default of the same name for that call only. Sorting by score desc first and a date second keeps relevance in charge while breaking ties in favour of newer material.
&debugQuery=true to any of these and read debug.parsedquery. It is the only way to know what your parameters actually produced, and it is how every capture on this page was taken.09 · Tuning in practice
Put the defaults in the handler
A parameter that lives in solrconfig.xml is a decision made once. A parameter pasted into every client is a decision that drifts.
Move mm before you move boosts
Too many results and too few results are both mm problems. Boosts change order; only mm changes membership.
Start with pf and ps=2, add pf2 when queries get long
Short queries are served by pf alone. Add pf2 once your logs show queries of four words and up.
Resist high slop
Every point of slop makes a phrase match less meaningful. Past four or five the phrase boost stops distinguishing anything.
Match the analyzer to the job
Phrase boosting depends on token positions. A field that strips stopwords holds different positions from one that keeps them, so the same slop behaves differently per field.
Tune against real queries
Pull the actual query log, not the queries you imagine. Zero-result queries point at mm and analysis; low click-through on populated result pages points at boosts.
10 · The same parameters, on Opensolr
Everything above is standard Solr and works the same on any installation. On Opensolr you edit schema.xml and solrconfig.xml straight from the Control Panel, and the parameters in this guide are also exposed as a tuning panel on every index, so relevance changes do not need an XML edit or a reload.
Opensolr builds qf, pf, pf2 and pf3 from one set of field weights, with the phrase multipliers shown.
Those multipliers are not a metaphor for what happens underneath — they are the values Opensolr's own hosted search runs with. Minimum Match is passed through as literal Solr mm syntax; the shipped default is 2<65% 4<50% 8<40%. Every change is read at query time, so the next search already uses it.
Field weights, minimum match, freshness, the balance between keyword and semantic scoring, and results per page — per index, saved automatically, no reindex.
The two files in this guide are editable in the Control Panel. Upload, reload, done — no SSH and no file system access.
When ranking is right for every query except three, pin or exclude documents for those three instead of distorting the boosts for everyone.
Which queries were run, which results were clicked. High impressions with low click-through is the signal that a query needs mm or phrase work.
Seven days of parsed, searchable Solr errors. A malformed query or a missing field shows up here before a user reports it.
The keyword scoring described on this page, fused with vector similarity over multilingual embeddings, in the same index and the same request.
Point it at a URL and it handles crawling, text extraction from HTML, PDF and Office files, enrichment and indexing.
For content a crawler cannot reach — databases, internal systems, product feeds — pushed over REST with embedding, sentiment and language detection applied on the way in.
Run this configuration without running the servers
Managed Solr indexes with master and replica clusters, browser-based config editing, and the tuning panel above on every index.
See plansPlatform guide