Skip to main content

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.128

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hermes-client-python 1.8.128
File Size Uploaded Provenance
hermes_client_python-1.8.128.tar.gz 23.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for hermes-client-python 1.8.128
File Interpreter ABI Platform
hermes_client_python-1.8.128-py3-none-any.whl Python 3 none any Details

Total release size: 48.0 kB

Release files / hermes_client_python-1.8.128.tar.gz

Download URL hermes_client_python-1.8.128.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
f8b0f4600488507e8f9a5379d767e74ef77ca332aa214731c80e1d19d353e42f
BLAKE2b-256 checksum
How to use checksums
a15e780a997634f95e35acdd625516dbf6f0d9ddccad76e59c001202624b638a
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.128-py3-none-any.whl

Download URL hermes_client_python-1.8.128-py3-none-any.whl
Size 24.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
67cbd7c8682f4be319b072afd200b0f3741c5be65a0389e56b53f1687414b015
BLAKE2b-256 checksum
How to use checksums
db84a51388d5911d856ed3f7c630ea2381f4828e00d8bbf32f5b7cc7412f8003
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.128

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.

Release history Release notifications | RSS feed

This release

1.8.128 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page