# pysolr: a thin Python client for Apache Solr

> pysolr is a small Python client library for talking to Solr, and its most interesting recent change is the move to httpx2, which changes how you pass authentication. It is one module, a handful of dependencies and a clear licence, with a release history that says Python 3.11 and Solr 9 are the floor.

**django-haystack/pysolr** — Pysolr — Python Solr client

- Repository: https://github.com/django-haystack/pysolr
- Stars: 702 · Forks: 337
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/django-haystack-pysolr

## One module that maps Python calls onto Solr endpoints

pysolr is a lightweight Python client for Apache Solr. It provides an interface that queries the server and returns results based on the query, and the repository layout shows how little there is to it: a single pysolr.py module at the top level, a tests/ directory, a docker/ directory, a run-tests.py script and a solr-docker-test-env.sh helper. There is no package directory and no module tree, because the whole client is one file.

The features list is short and concrete. Basic operations such as selecting, updating and deleting. Index optimization. More Like This support, if it is set up in Solr. Spelling correction, also if set up in Solr. Timeout support. And SolrCloud awareness.

That last phrase does the heavy lifting in the feature list, because SolrCloud changes where the data lives. A standalone Solr has a core and a URL that points at it, and a SolrCloud deployment has a collection distributed across nodes, with the addresses discovered through ZooKeeper. pysolr handles that discovery, and the optional kazoo dependency, version 2.5 or newer, is what lets it talk to ZooKeeper at all. Since kazoo is an extra rather than a core dependency, a client that only ever talks to a single Solr instance does not pay for it.

It is a transport and convenience layer, and it does not try to be an ORM.

## The httpx2 move changes how you authenticate

The most consequential line in the documentation is the one about the HTTP client. pysolr now uses httpx2 as its HTTP client, and as a result the auth argument accepts any httpx-compatible authentication object, or a plain username and password tuple.

That sounds like a detail and it is not. The previous generation of Python clients was built on requests, and requests has its own authentication plug-in ecosystem, including requests-kerberos for SPNEGO. Moving to httpx means that ecosystem does not carry over unchanged, and the documentation says so directly: for Kerberos or SPNEGO, use httpx-gssapi, the httpx counterpart to requests-kerberos.

The Kerberos example shows the shape of it:

```python
# Setup a Solr instance in a kerborized environment
from httpx_gssapi import HTTPSPNEGOAuth, OPTIONAL

kerberos_auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL)

solr = pysolr.Solr("http://localhost:8983/solr/<core_name>", auth=kerberos_auth)
```

Two things are worth noticing. The client is constructed with a kerborized environment, and the authentication object is created once and passed in, rather than the client configuring authentication itself. That is a better shape for a library, because it means the caller controls the credential lifecycle, and it means an application that already has a working httpx auth stack can hand the same object to Solr.

The migration cost falls on whoever was using a requests-specific auth handler. The behaviour for the common case, a plain tuple, is unchanged, so most installations are unaffected.

## A client, not a query builder

The usage examples make the design stance obvious. You construct a client with a URL and options, ping it, add documents as dictionaries, search, and delete:

```python
import pysolr

# Create a client instance. The timeout and authentication options are not required.
# Solr URL format: http://host:port/solr/<core_name>
solr = pysolr.Solr("http://localhost:8983/solr/<core_name>", always_commit=True, [timeout=10], [auth=<type of authentication>])

# Do a health check.
solr.ping()
```

Documents are plain dicts with an id and fields, and a parent/child relationship is expressed by putting a list of child documents under the special key _doc. The documentation explains why that matters: it is helpful for queries that join together conditions on children and parent documents.

The search side passes Solr parameters straight through. A plain Lucene-style query is enough for the simple case, and for anything more advanced you pass Solr options such as the highlighting flags hl and hl.fragsize as keyword arguments to the search call.

Those keys are Solr's own parameter names, not a Python abstraction. That is the trade the library makes. You get direct access to every Solr feature the server exposes, and you also get to deal with parameter names, string values and Solr's particular response shapes yourself. If you wanted a query builder that turns Python objects into Solr queries, this is not it.

Deletion shows the same compactness, with a single id, a list of ids, or a query:

```python
solr.delete(id='doc_1')

solr.delete(id=['doc_1', 'doc_2'])

solr.delete(q='*:*')
```

The last of those wipes the index, and the fact that it is one line in the documentation is a fair signal of how little ceremony this client applies.

## Commit policy and the cursor for large scans

Two behaviours in the documentation deserve more attention than they usually get, because getting them wrong is the difference between a search index that behaves and one that does not.

The first is the commit policy. The documentation states that auto_commit defaults to False for performance, and gives three alternatives: set auto_commit=True so that commands always update the index immediately, make an update call with commit=True, or use Solr's own autoCommit and commitWithin so the data is committed following a particular policy. The default of False is a deliberate performance choice, and it means an indexer that never commits will leave documents visible to a Solr searcher that has not yet read the transaction log.

The second is the cursor. The documentation shows traversing a cursor using its iterator:

```python
for doc in solr.search('*:*',fl='id',sort='id ASC',cursorMark='*'):
    print(doc['id'])
```

That is how you scan a whole index without hitting the default result limit. The search returns a Results object that stores the total number of results found, by default the top ten most relevant results, and any additional data such as facets, highlighting or spelling. The cursor iterates instead, which is the only practical way to walk every document.

Custom request handlers are configured on the constructor, and there is a rule worth memorising. If use_qt_param is True, the handler name must be exactly what is configured in solrconfig.xml, including the leading slash if there is one. If use_qt_param is False, which is the default, the leading and trailing slashes can be omitted. The search handler defaults to /select, and the individual methods, such as more_like_this and suggest_terms, accept a handler kwarg to override the value set in solrconfig.xml, with an explicit handler in search overriding the search_handler setting.

## SolrCloud needs ZooKeeper before it needs a URL

For a SolrCloud deployment the client is constructed differently, from a ZooKeeper connection string and a collection name rather than a URL:

```python
zookeeper = pysolr.ZooKeeper("zkhost1:2181,zkhost2:2181,zkhost3:2181")
solr = pysolr.SolrCloud(zookeeper, "collection1", auth=<type of authentication>)
```

The three host entries in that string are the ZooKeeper ensemble. What the client does with it is discover where the collection's shards live, which is the step that makes SolrCloud different from a standalone core, where the URL is simply fixed.

For a multicore index the instruction is the opposite: point the URL at the index core, and the timeout is optional.

```python
# Setup a Solr instance. The timeout is optional.
solr = pysolr.Solr("http://localhost:8983/solr/<core_name>", timeout=10)
```

The practical consequence of the two modes is operational. A standalone Solr or a multicore deployment is configured in one place, in the application settings. A SolrCloud deployment introduces a ZooKeeper dependency and, with it, a set of failure modes that have nothing to do with your code: the ensemble can be unreachable, the collection can be rebalancing, and the client can be talking to a shard that is about to move. The client handles discovery, but it cannot decide what your application should do when discovery is slow.

That is also why kazoo is an optional extra rather than a hard dependency. A project that will never talk to SolrCloud should not have to install a ZooKeeper client.

## Packaging, versions and the boundaries of the project

The packaging is modern and minimal. The build backend is hatchling, the project metadata declares the version 3.11.0, the licence is BSD, and the runtime dependency list is a single entry, httpx2 at 2.13 or newer, with the SolrCloud extra pulling in kazoo at 2.5 or newer. Python 3.11 or newer is required, and the classifiers cover 3.11 through 3.15, including the free-threaded builds, with the project marked as production stable and operating system independent.

The requirements are stated in the README in the same terms: Python 3.11+, httpx2 2.13+, and kazoo optionally for SolrCloud mode. The badge in the README also names Solr 9+ as the supported server version, which is a useful constraint because Solr has removed endpoints across major versions and a client that has not been updated for the current server will fail in ways that look like network problems.

The development tooling is worth a mention because it explains how the project stays small. The pyproject configures ruff with a broad lint selection, including flake8-builtins, flake8-bugbear, flake8-async, eradicate and McCabe complexity checks, and the repository carries a pre-commit configuration, a uv.lock file, a yamlfmt config and an .editorconfig. Tests are run with pytest and coverage, and the test environment is provided through Docker, with solr-docker-test-env.sh and a docker/ directory, which is the practical way to test a client against a real Solr rather than a mock.

The release history is the honest part of the picture. v3.11.0 was released on 2025-11-18, v3.10.0 on 2024-09-18, and there is a v3.10.0b2 from 2023-10-13 described as final testing before the 3.10 release. The last push to the repository was on 2026-10-19, and the repository is not archived, so the code is moving even when the tags are not.

## Where a Solr client stops being enough

The first limitation is the one the design makes explicit. pysolr will hand you a Solr query and a Solr response. It will not build a query from a Python object, it will not map a Django model to a Solr schema, it will not manage your solrconfig.xml, and it will not keep your index in step with your database. If the gap between your models and the index is the hard part of your search problem, this client is not the answer, and the gap has to be closed by the index management layer, which in the Django world is Haystack.

The second limitation is Solr's own. A client cannot make Solr 9 behave like Solr 4. The parameter names, the response formats, the handler paths and the removed endpoints all come from the server, and the client can only be as compatible as the server version it targets. The README's own Solr 9+ badge says this plainly, and it is the reason a server upgrade can break a working integration even though the client did not change.

The third is the migration to httpx2. It also changes the authentication contract and breaks requests-based auth plugins unless you move to the httpx equivalents. An application with a working Kerberos setup built on requests-kerberos has work to do.

The real alternative is not another Python client but a different integration strategy. If your application already has a document model layer with its own indexing and querying, that layer's search backend may be a better fit than driving Solr directly. If you need Solr's full query language and its specialised handlers, pysolr is the shortest path and its small surface is the advantage: you can read the whole client in one sitting and see exactly what goes over the wire. If you need Solr's full query language and a managed index, the honest comparison is with Solr's own clients in Java, which are the reference implementation and are maintained alongside the server.

## Conclusion

Adopt pysolr if you are integrating Python with Solr and want a client with a small surface, cursor support and SolrCloud awareness, and if you are prepared to write the Solr query language yourself rather than have an ORM generate it. Do not adopt it if you want a document object mapper, automatic schema handling or a query builder, because pysolr deliberately does none of those things. Before you start, check that your Solr is version 9 or newer and your Python is 3.11 or newer, and note that the httpx2 move means your existing requests-style auth tuple or requests-kerberos setup has to be revisited before the upgrade lands in your code.

## FAQ

### What is pysolr?

pysolr is a lightweight Python client for Apache Solr, provided as a single module at the repository root. It supports selecting, updating and deleting documents, index optimization, More Like This, spelling correction, timeouts, cursors and SolrCloud awareness, and it is BSD licensed.

### How do I install pysolr?

pysolr is on PyPI, so pip install pysolr is the normal route, and pip install . installs it directly from a repository checkout. It requires Python 3.11 or newer and httpx2 2.13 or newer, and the kazoo extra is needed only for SolrCloud mode.

### How do I use pysolr with SolrCloud?

Construct a ZooKeeper object with the ensemble hosts and pass it to SolrCloud along with the collection name, for example pysolr.SolrCloud(zookeeper, "collection1", auth=...). The client uses the ZooKeeper connection to discover where the collection lives, and kazoo is the optional dependency that provides it.

### Why does my search index show nothing after indexing?

The most common cause is the commit policy. The documentation states that auto_commit defaults to False for performance, so you either set auto_commit=True, make the update call with commit=True, or rely on Solr's own autoCommit and commitWithin settings to commit on a schedule.

### Which Solr version does pysolr support?

The README badge names Solr 9 and newer as the supported server version, and the project metadata requires Python 3.11 or newer with classifiers for 3.11 through 3.15. The most recent release is v3.11.0 from 2025-11-18, and the last push to the repository was on 2026-10-19.

## Sources

- [django-haystack/pysolr on GitHub](https://github.com/django-haystack/pysolr)
- [Issues](https://github.com/django-haystack/pysolr/issues)
- [License: BSD-3-Clause](https://github.com/django-haystack/pysolr/blob/master/LICENSE)
- [README](https://github.com/django-haystack/pysolr/blob/master/README.md)
- [Releases](https://github.com/django-haystack/pysolr/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/django-haystack-pysolr
