Solr's DataImportHandler (DIH) was removed in Solr 9. The modern approach is simple: POST your XML data directly to Solr's /update endpoint via HTTP. No plugins needed, no data-config.xml, no special configuration — just send XML over HTTP.
This guide shows you how to parse XML files and index them into your Opensolr Index using curl, PHP, and Python.
The Solr XML Format
Solr accepts documents in a specific XML format. Every XML file you send must follow this structure:
Each field uses <field name="fieldname">value</field>
Field names must match fields defined in your schema.xml
The id field is required (unique key for each document)
You can include as many <doc> elements as you want in a single <add> block
Example 1: curl — Quick Import from Command Line
The simplest way to import. Post the XML file directly to Solr:
#!/bin/bash# =====================================================================# CONFIGURATION — Replace these with YOUR Opensolr index details# =====================================================================SOLR_HOST="YOUR_HOST"INDEX_NAME="YOUR_INDEX"USERNAME="opensolr"PASSWORD="YOUR_API_KEY"XML_FILE="products.xml"# Import the XML file into Solrecho"Importing $XML_FILE into $INDEX_NAME..."
curl-u"$USERNAME:$PASSWORD"\-H"Content-Type: text/xml"\--data-binary@"$XML_FILE"\"https://$SOLR_HOST/solr/$INDEX_NAME/update?commit=true&wt=json"echo""echo"Done!"
For large files, you can split and send in chunks — see the PHP and Python examples below.
Example 2: PHP — Parse & Index XML with Chunking
This script reads an XML file, parses each <doc> element, and sends them to Solr in configurable batches. Handles large files without running out of memory.
<?php// =====================================================================// CONFIGURATION — Replace these with YOUR Opensolr index details// =====================================================================$solr_host="YOUR_HOST";$index_name="YOUR_INDEX";$username="opensolr";$password="YOUR_API_KEY";$xml_file="products.xml";$batch_size=500;// Documents per batch (adjust based on doc size)$solr_url="https://$solr_host/solr/$index_name/update?commit=true&wt=json";// Load and parse the XML file$xml=simplexml_load_file($xml_file);if($xml===false){die("Error: Could not parse $xml_file");}$docs=$xml->doc;$total=count($docs);echo"Found $total documents in $xml_file";// Send documents in batches$batch=[];$sent=0;foreach($docsas$doc){$batch[]=$doc->asXML();if(count($batch)>=$batch_size){$sent+=send_batch($batch,$solr_url,$username,$password);$batch=[];echo" Indexed $sent / $total";}}// Send remaining documentsif(!empty($batch)){$sent+=send_batch($batch,$solr_url,$username,$password);echo" Indexed $sent / $total";}echo"Done! $sent documents indexed.";functionsend_batch($docs,$url,$user,$pass){$xml_body="<add>".implode("",$docs)."</add>";$ch=curl_init($url);curl_setopt_array($ch,[CURLOPT_USERPWD=>"$user:$pass",CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>$xml_body,CURLOPT_HTTPHEADER=>["Content-Type: text/xml"],CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>120,]);$response=curl_exec($ch);$http_code=curl_getinfo($ch,CURLINFO_HTTP_CODE);curl_close($ch);if($http_code!==200){echo" ERROR (HTTP $http_code): $response";return0;}returncount($docs);}
Example 3: Python — Parse & Index XML with Chunking
Same approach in Python. Uses xml.etree.ElementTree for parsing and requests for HTTP.
# import_xml.py — Parse and index XML documents into Opensolrimportxml.etree.ElementTreeasETimportrequestsimportsys# =====================================================================# CONFIGURATION — Replace these with YOUR Opensolr index details# =====================================================================SOLR_HOST="YOUR_HOST"INDEX_NAME="YOUR_INDEX"USERNAME="opensolr"PASSWORD="YOUR_API_KEY"XML_FILE="products.xml"BATCH_SIZE=500# Documents per batchSOLR_URL=f"https://{SOLR_HOST}/solr/{INDEX_NAME}/update?commit=true&wt=json"defsend_batch(docs,url,auth):"""Send a batch of <doc> elements to Solr."""xml_body="<add>" + "".join(docs) + "</add>"resp=requests.post(url,data=xml_body.encode("utf-8"),headers={"Content-Type":"text/xml"},auth=auth,timeout=120,)ifresp.status_code!=200:print(f" ERROR (HTTP {resp.status_code}): {resp.text}")return0returnlen(docs)defmain():xml_file=sys.argv[1]iflen(sys.argv)>1elseXML_FILEauth=(USERNAME,PASSWORD)# Parse the XML filetree=ET.parse(xml_file)root=tree.getroot()# Collect all <doc> elementsdocs=[]fordocinroot.findall("doc"):docs.append(ET.tostring(doc,encoding="unicode"))total=len(docs)print(f"Found {total} documents in {xml_file}")# Send in batchessent=0foriinrange(0,total,BATCH_SIZE):batch=docs[i:i+BATCH_SIZE]sent+=send_batch(batch,SOLR_URL,auth)print(f" Indexed {sent} / {total}")print(f"Done! {sent} documents indexed.")if__name__=="__main__":main()
Example 4: Python — Convert Non-Solr XML to Solr Format
If your XML is not in Solr's <add><doc> format (e.g., it's a product feed, RSS, or custom XML), you need to convert it first:
# convert_and_index.py — Convert arbitrary XML to Solr format and indeximportxml.etree.ElementTreeasETimportrequestsSOLR_HOST="YOUR_HOST"INDEX_NAME="YOUR_INDEX"USERNAME="opensolr"PASSWORD="YOUR_API_KEY"SOLR_URL=f"https://{SOLR_HOST}/solr/{INDEX_NAME}/update?commit=true&wt=json"# Example: convert a product catalog XML like:# <catalog># <product sku="ABC123"># <name>Widget</name># <price>19.99</price># </product># </catalog>tree=ET.parse("catalog.xml")root=tree.getroot()solr_docs=[]forproductinroot.findall("product"):sku=product.get("sku","")name=product.findtext("name","")price=product.findtext("price","0")category=product.findtext("category","")desc=product.findtext("description","")doc=f"""<doc> <field name="id">{sku}</field> <field name="title">{name}</field> <field name="price">{price}</field> <field name="category">{category}</field> <field name="description">{desc}</field></doc>"""solr_docs.append(doc)print(f"Converted {len(solr_docs)} products to Solr format")# Send to Solr in one batch (or chunk for large datasets)xml_body="<add>" + "".join(solr_docs) + "</add>"resp=requests.post(SOLR_URL,data=xml_body.encode("utf-8"),headers={"Content-Type":"text/xml"},auth=(USERNAME,PASSWORD),timeout=120,)ifresp.status_code==200:print("Indexed successfully!")else:print(f"Error: {resp.status_code} — {resp.text}")
Handling Large XML Files
For files with thousands or millions of documents, keep these in mind:
Concern
Solution
Memory
Use batch processing (500-1000 docs per batch) — shown in examples above
Timeouts
Set commit=false during import, then send a final commit: curl -u user:pass "https://HOST/solr/INDEX/update?commit=true"
Speed
Use commitWithin=10000 instead of commit=true to let Solr batch commits every 10 seconds
Errors
Send small batches so you can identify which batch has bad data
Encoding
Always use UTF-8. Escape XML special characters (&<>")
Quick Reference
Placeholder
Where to Find It
YOUR_HOST
Your Index Control Panel → "Hostname"
YOUR_INDEX
Your Index name
YOUR_API_KEY
Control Panel → Dashboard → "Secret API Key"
Endpoint
Method
Content-Type
/solr/INDEX/update
POST
text/xml
/solr/INDEX/update?commit=true
POST
text/xml (auto-commit after import)
/solr/INDEX/update?commitWithin=10000
POST
text/xml (commit within 10 seconds)
You can also use the Opensolr Data Ingestion API to push documents in JSON format without needing to deal with XML at all.
Solr’s RAM appetite is legendary. Don’t worry, you’re not alone. Let’s help you keep your heap happy, your queries snappy, and your boss off your back.
Why Does Solr Use So Much Memory?
Search results: Returns tons of docs? RAM feast.
Caches: Four flavors, all with big appetites.
Big fields, bad configs, massive requests: Boom—there goes your heap.
Solr: “Give me RAM, and I shall give you… maybe some results.”
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.
Weights are relative, not absolute. 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:
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.
If 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:
There is no 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 --><schemaname="classicbook"version="1.6"><fieldTypename="text_general"class="solr.TextField"positionIncrementGap="100"><analyzertype="index"><charFilterclass="solr.HTMLStripCharFilterFactory"/><tokenizerclass="solr.ICUTokenizerFactory"/><filterclass="solr.EnglishPossessiveFilterFactory"/><filterclass="solr.LowerCaseFilterFactory"/><filterclass="solr.StopFilterFactory"ignoreCase="true"words="stopwords.txt"/><filterclass="solr.WordDelimiterGraphFilterFactory"generateWordParts="1"catenateWords="1"preserveOriginal="1"/><filterclass="solr.ASCIIFoldingFilterFactory"preserveOriginal="true"/><filterclass="solr.SnowballPorterFilterFactory"protected="protwords.txt"language="English"/><filterclass="solr.RemoveDuplicatesTokenFilterFactory"/></analyzer><analyzertype="query"><charFilterclass="solr.HTMLStripCharFilterFactory"/><tokenizerclass="solr.ICUTokenizerFactory"/><filterclass="solr.EnglishPossessiveFilterFactory"/><filterclass="solr.LowerCaseFilterFactory"/><filterclass="solr.SynonymGraphFilterFactory"ignoreCase="true"synonyms="synonyms.txt"expand="true"/><filterclass="solr.StopFilterFactory"ignoreCase="true"words="stopwords.txt"/><filterclass="solr.WordDelimiterGraphFilterFactory"generateWordParts="1"catenateWords="1"preserveOriginal="1"/><filterclass="solr.ASCIIFoldingFilterFactory"preserveOriginal="true"/><filterclass="solr.SnowballPorterFilterFactory"protected="protwords.txt"language="English"/><filterclass="solr.RemoveDuplicatesTokenFilterFactory"/></analyzer></fieldType><fieldname="id"type="string"indexed="true"stored="true"required="true"/><fieldname="title"type="text_general"indexed="true"stored="true"multiValued="false"/><fieldname="author"type="string"indexed="true"stored="true"/><fieldname="summary"type="text_general"indexed="true"stored="true"multiValued="false"/><fieldname="content"type="text_general"indexed="true"stored="false"multiValued="false"/><!-- Aggregate catch-all: cheap recall net, kept at a low qf weight --><fieldname="text_all"type="text_general"indexed="true"stored="false"multiValued="true"/><copyFieldsource="title"dest="text_all"/><copyFieldsource="author"dest="text_all"/><copyFieldsource="summary"dest="text_all"/><copyFieldsource="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.
<requestHandlername="/select"class="solr.SearchHandler"default="true"><lstname="defaults"><strname="defType">edismax</str><strname="df">text_all</str><strname="q.op">OR</str><!-- Which fields, and how much each one is worth --><strname="qf">title^3.0summary^1.5text_all^0.5</str><!-- How many of the terms a document must contain --><strname="mm">2<75%4<90%6<100%</str><!-- Phrase rewards, and how far the words may drift --><strname="pf">title^5summary^3</str><strname="ps">2</str><strname="pf2">title^4summary^2</str><strname="ps2">1</str><strname="pf3">title^3summary^1</str><strname="ps3">1</str><!-- Best-field scoring with a small contribution from the others --><strname="tie">0.1</str><strname="hl">true</str><strname="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.
Add &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.
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.