Hermes Python client
Async Python client for the Hermes gRPC search server.
Installation
pip install hermes-client-python
Python 3.10 or newer is required.
Quick start
import asyncio
from hermes_client_python import HermesClient
async def main():
async with HermesClient("localhost:50051") as client:
await client.create_index(
"articles",
"""
index articles {
field title: text<simple> [indexed, stored]
field body: text<simple> [indexed, stored]
}
""",
)
indexed, error_count, errors = await client.index_documents(
"articles",
[
{"title": "Hello World", "body": "First article"},
{"title": "Hermes Search", "body": "Fast retrieval"},
],
)
if error_count:
raise RuntimeError(errors)
print(f"Indexed {indexed} documents")
await client.commit("articles")
results = await client.search(
"articles",
query={"match": {"field": "title", "text": "hello"}},
fields_to_load=["title", "body"],
)
for hit in results.hits:
print(hit.address, hit.score, hit.fields)
if results.hits:
document = await client.get_document("articles", results.hits[0].address)
print(document.fields if document else "document not found")
await client.delete_index("articles")
asyncio.run(main())
The context manager calls connect() and close() automatically. For manual
lifecycle management:
client = HermesClient("localhost:50051")
await client.connect()
try:
...
finally:
await client.close()
Index management
await client.create_index("articles", schema_sdl)
names = await client.list_indexes()
info = await client.get_index_info("articles")
print(info.num_docs, info.num_segments, info.vector_stats)
await client.force_merge("articles")
await client.reorder("articles")
await client.retrain_vector_index("articles")
await client.delete_index("articles")
commit() is required before newly indexed documents become searchable.
Batch and streaming indexing
indexed, error_count, errors = await client.index_documents(
"articles",
[
{"title": "One", "tags": ["search", "rust"]},
{"title": "Two", "tags": ["python"]},
],
)
async def documents():
for number in range(10_000):
yield {"title": f"Document {number}"}
streamed, stream_errors = await client.index_documents_stream("articles", documents())
Repeated list values become repeated field entries. Flat numeric lists are
dense vectors; lists of (dimension, weight) pairs are sparse vectors.
Searching
Every search takes one query object whose single key matches a Hermes query
variant:
# Exact term
await client.search(
"articles",
query={"term": {"field": "title", "term": "hermes"}},
)
# Tokenized full-text match
await client.search(
"articles",
query={"match": {"field": "body", "text": "fast retrieval"}},
)
# Recursive boolean query
await client.search(
"articles",
query={
"boolean": {
"must": [{"match": {"field": "body", "text": "retrieval"}}],
"must_not": [{"term": {"field": "title", "term": "draft"}}],
}
},
)
# Dense vector query and optional reranking
await client.search(
"articles",
query={
"dense_vector": {
"field": "embedding",
"vector": [0.1, 0.2, 0.3],
"nprobe": 16,
}
},
reranker={"field": "embedding", "vector": [0.1, 0.2, 0.3]},
candidate_limit=20,
limit=10,
fields_to_load=["title"],
)
# Hybrid union fusion
await client.search(
"articles",
query={
"fusion": {
"method": "rrf",
"rrf_k": 60,
"queries": [
{
"query": {
"sparse_vector": {
"field": "sparse_embedding",
"indices": [1, 5],
"values": [0.8, 0.2],
}
},
"weight": 1.0,
},
{
"query": {
"dense_vector": {
"field": "embedding",
"vector": [0.1, 0.2, 0.3],
}
},
"weight": 1.0,
},
],
}
},
)
Other supported variants are phrase, binary_dense_vector, boost, range,
prefix, and all. Search results expose the full DocAddress needed by
get_document():
hit = results.hits[0]
document = await client.get_document("articles", hit.address)
Deadlines and errors
Every RPC accepts an optional timeout in seconds. A per-call value overrides
the client default:
async with HermesClient("localhost:50051", default_timeout=5.0) as client:
results = await client.search(
"articles",
query={"all": {}},
timeout=0.5,
)
await client.force_merge("articles", timeout=3600)
gRPC failures raise grpc.RpcError (normally
grpc.aio.AioRpcError). get_document() is the exception: it returns None
for NOT_FOUND.
import grpc
try:
await client.search("missing", query={"all": {}})
except grpc.RpcError as error:
if error.code() == grpc.StatusCode.NOT_FOUND:
print("index not found")
else:
raise
Development
From hermes-client-python:
uv sync --group dev --group test
uv run ruff check .
uv run ruff format --check .
uv run pytest tests/test_client_unit.py
The remaining tests are integration tests and expect a debug
target/debug/hermes-server binary. Regenerate checked-in protobuf stubs after
changing hermes-proto/hermes.proto:
uv run --group dev python generate_proto.py
License
MIT
Ranking diagnostics and recall traces
Both options default to false and preserve the requested ranking:
response = await client.search(
"articles",
query={
"fusion": {
"queries": [
{
"name": "title",
"query": {"match": {"field": "title", "text": "rust"}},
},
{"name": "body", "query": {"match": {"field": "body", "text": "rust"}}},
]
}
},
include_rrf_scores=True,
tracing=True,
)
for hit in response.hits:
print(hit.score, hit.rrf_score, hit.rrf_contributions)
for shard in response.trace.shards:
for branch in shard.queries:
print(shard.shard_id, branch.query_name, branch.candidates)
rrf_score and per-branch votes use organic nomination ranks merged across all
shards, independently of L1 or reranker scores. Ranks start at 1. ordinal=None
is document context; ordinal 0 is a real passage. Backfilled and score-only
features contribute no votes.
The trace retains every shard's bounded branch nominations and selected results, including candidates absent from the final page, plus query trees and common filters. Candidates contain addresses, raw scores and ordinals; stored fields are loaded only for returned hits. Tracing does not expand retrieval depth or rerun individual Boolean clauses. Oversized diagnostics and unsupported backends fail explicitly. See the scoring and tracing contract.
For a named, scoped L1 query, specify the complete scoring formula:
l1 = {"formula": "0.2 * title + 0.8 * log1p(body) + 3 * rrf"}
formula is the only L1 scoring interface. Coefficients, offsets and RRF
multipliers go in the expression; the former coefficient fields are removed.
Arithmetic, powers, logarithms, sqrt, abs, exp, min/max and trigonometry
are supported. Use {body.bm25} for punctuated branch names. log and ln are
natural logarithms; log2 and log10 select those bases. Missing branch values
use configured missing defaults, otherwise zero. Backfill remains optional.
The formula runs before passage selection and the document combiner. A formula
using rrf makes the broker obtain the complete bounded candidate and passage
union before global inference. Exports that exceed budgets fail explicitly.
Expressions are bounded to 4 KiB, 256 tokens and 32 parenthesis levels. Invalid
variables, invalid syntax and non-finite predictions fail explicitly. Servers
and brokers must support formula_v1 (candidate_scoring_version=3).
Metadata
Release files for hermes-client-python 1.8.133
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hermes_client_python-1.8.133.tar.gz | 23.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hermes_client_python-1.8.133-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 48.0 kB
Release files / hermes_client_python-1.8.133.tar.gz
| Download URL | hermes_client_python-1.8.133.tar.gz |
|---|---|
| Size | 23.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f857f7503a261856913e214575b054367b983219ea6114216add274f5e4f6566
|
|
BLAKE2b-256 checksum How to use checksums |
1fb27a24759ee11d97be40bf85c92c38b0a22b2330dab2566a8831e82f4c8df8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / hermes_client_python-1.8.133-py3-none-any.whl
| Download URL | hermes_client_python-1.8.133-py3-none-any.whl |
|---|---|
| Size | 24.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d9bde4a2486258a052ed7a89feb7428eaeabd0ea584c85f66027bcb6dd613df9
|
|
BLAKE2b-256 checksum How to use checksums |
4517952733274fe1a86001b785e9eb017c8aadec0ce95adbdcffa853c8cf2d51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for hermes-client-python 1.8.133
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.