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

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.132
File Size Uploaded Provenance
hermes_client_python-1.8.132.tar.gz 23.2 kB Without provenance Details

Built distribution (wheel)

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

Total release size: 48.0 kB

Release files / hermes_client_python-1.8.132.tar.gz

Download URL hermes_client_python-1.8.132.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ef72f4573458551f3e310bfc3d2e0bd857bde093424bd6fc508ab9d5bb19b8c4
BLAKE2b-256 checksum
How to use checksums
8b643f15bbf17f44ed7e739bf9a655759b97655619dddfcdbc4aae8609dfa090
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.132-py3-none-any.whl

Download URL hermes_client_python-1.8.132-py3-none-any.whl
Size 24.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cde7878c86e0c9b99472429b6a59ddde84b8e47f5dd400c636fe9cc7f18952c2
BLAKE2b-256 checksum
How to use checksums
1a1d05efd59d04e77dd6576062d8dbece0ef7f1b677641ce93051c0ec111f5e4
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.132

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