hermes-client-python 1.8.145__tar.gz → 1.8.147__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hermes_client_python-1.8.147/PKG-INFO +191 -0
- hermes_client_python-1.8.147/README.md +167 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/pyproject.toml +1 -1
- hermes_client_python-1.8.145/PKG-INFO +0 -380
- hermes_client_python-1.8.145/README.md +0 -356
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/.gitignore +0 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/__init__.py +0 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/client.py +0 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/hermes_pb2.py +0 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/hermes_pb2_grpc.py +0 -0
- {hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/types.py +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hermes-client-python
|
|
3
|
+
Version: 1.8.147
|
|
4
|
+
Summary: Async Python client for Hermes search server
|
|
5
|
+
Project-URL: Homepage, https://github.com/SpaceFrontiers/hermes
|
|
6
|
+
Project-URL: Repository, https://github.com/SpaceFrontiers/hermes
|
|
7
|
+
Author: izihawa
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: async,full-text-search,grpc,search
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Database :: Database Engines/Servers
|
|
19
|
+
Classifier: Topic :: Text Processing :: Indexing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: grpcio>=1.76.0
|
|
22
|
+
Requires-Dist: protobuf>=6.33.4
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Hermes Python client
|
|
26
|
+
|
|
27
|
+
Async Python client for the
|
|
28
|
+
[Hermes](https://github.com/SpaceFrontiers/hermes) gRPC search server.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install hermes-client-python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Python 3.10 or newer is required.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
|
|
43
|
+
from hermes_client_python import HermesClient
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with HermesClient("localhost:50051") as client:
|
|
48
|
+
await client.create_index(
|
|
49
|
+
"articles",
|
|
50
|
+
"""
|
|
51
|
+
index articles {
|
|
52
|
+
field id: text<raw> [primary, stored]
|
|
53
|
+
field title: text<simple> [indexed, stored]
|
|
54
|
+
field body: text<simple> [indexed, stored]
|
|
55
|
+
}
|
|
56
|
+
""",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
indexed, error_count, errors = await client.index_documents(
|
|
60
|
+
"articles",
|
|
61
|
+
[
|
|
62
|
+
{"id": "1", "title": "Hello World", "body": "First article"},
|
|
63
|
+
{"id": "2", "title": "Hermes Search", "body": "Fast retrieval"},
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
if error_count:
|
|
67
|
+
raise RuntimeError(errors)
|
|
68
|
+
print(f"Indexed {indexed} documents")
|
|
69
|
+
|
|
70
|
+
await client.commit("articles")
|
|
71
|
+
|
|
72
|
+
results = await client.search(
|
|
73
|
+
"articles",
|
|
74
|
+
query={"match": {"field": "title", "text": "hello"}},
|
|
75
|
+
fields_to_load=["title", "body"],
|
|
76
|
+
)
|
|
77
|
+
for hit in results.hits:
|
|
78
|
+
print(hit.address, hit.score, hit.fields)
|
|
79
|
+
|
|
80
|
+
if results.hits:
|
|
81
|
+
document = await client.get_document("articles", results.hits[0].address)
|
|
82
|
+
print(document.fields if document else "document not found")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The context manager connects and closes the channel. Manual callers use
|
|
89
|
+
`await client.connect()` / `await client.close()`.
|
|
90
|
+
|
|
91
|
+
## Index management
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
await client.list_indexes()
|
|
95
|
+
info = await client.get_index_info("articles")
|
|
96
|
+
await client.reorder("articles")
|
|
97
|
+
await client.retrain_vector_index("articles")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`index_documents` returns `(indexed_count, error_count, errors)`;
|
|
101
|
+
`index_documents_stream` takes an async iterable and returns counts. Inspect
|
|
102
|
+
errors, then `commit()` to publish accepted work. Repeated values are lists;
|
|
103
|
+
flat numeric lists are dense vectors, `(dimension, weight)` pairs are sparse
|
|
104
|
+
vectors. See [client types](src/hermes_client_python/types.py).
|
|
105
|
+
|
|
106
|
+
### Delete and upsert documents
|
|
107
|
+
|
|
108
|
+
With a text primary key, delete by exact key or supply a complete replacement:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
await client.delete_document("articles", "2")
|
|
112
|
+
await client.upsert_document("articles", {"id": "1", "title": "Updated title"})
|
|
113
|
+
await client.commit("articles")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`delete_documents` / `upsert_documents` return `DocumentMutationResult` with
|
|
117
|
+
`accepted_count` and `errors: [{index, error}]`. Single-item helpers raise on
|
|
118
|
+
rejection. Missing deletes succeed; upserts insert missing keys. Staged rows can
|
|
119
|
+
be replaced/deleted again before commit; the latest accepted version wins.
|
|
120
|
+
Optional [content hashes](../docs/content-deduplication.md) skip unchanged writes.
|
|
121
|
+
|
|
122
|
+
Limits: 100,000 deletion keys / 8 MiB key bytes; 1,000 replacements / 32 MiB
|
|
123
|
+
encoded protobuf, or one replacement / 200 MiB including the request envelope.
|
|
124
|
+
Broker commits are atomic per partition. See [mutation semantics](../docs/row-deletion.md).
|
|
125
|
+
|
|
126
|
+
### Compact deleted rows
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
await client.force_merge("articles") # Retain tombstones.
|
|
130
|
+
await client.force_merge("articles", compact=True) # Physically remove deleted rows.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Compaction handles singleton segments and may change addresses and BM25 scores.
|
|
134
|
+
Index info exposes `num_docs`, `physical_num_docs`, `num_deleted_docs`, and
|
|
135
|
+
`deleted_ratio`. Use primary keys for durable identity.
|
|
136
|
+
|
|
137
|
+
## Searching
|
|
138
|
+
|
|
139
|
+
`search(index_name, query=..., limit=10, fields_to_load=[...])` accepts one query
|
|
140
|
+
variant: `term`, `match`, `phrase`, `boolean`, `sparse_vector`, `dense_vector`,
|
|
141
|
+
`binary_dense_vector`, `boost`, `range`, `prefix`, `all`, or `fusion`.
|
|
142
|
+
See [query types](src/hermes_client_python/types.py) and the
|
|
143
|
+
[wire contract](../hermes-proto/hermes.proto) for options.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
results = await client.search(
|
|
147
|
+
"articles",
|
|
148
|
+
query={"boolean": {
|
|
149
|
+
"must": [{"match": {"field": "title", "text": "search"}}],
|
|
150
|
+
"must_not": [{"term": {"field": "title", "term": "draft"}}],
|
|
151
|
+
}},
|
|
152
|
+
fields_to_load=["title"],
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`get_document(index_name, hit.address)` uses the full segment/document address
|
|
157
|
+
and returns `None` on `NOT_FOUND`.
|
|
158
|
+
|
|
159
|
+
## Ranking diagnostics and recall traces
|
|
160
|
+
|
|
161
|
+
Search options `include_rrf_scores=True` and `tracing=True` default to false.
|
|
162
|
+
RRF diagnostics describe organic branch nominations; traces retain bounded
|
|
163
|
+
candidates and query trees, including hits outside the final page. Neither
|
|
164
|
+
changes retrieval depth or ranking. Oversized exports fail explicitly.
|
|
165
|
+
|
|
166
|
+
For named branches, use `l1={"formula": "0.2 * title + 0.8 * body + 3 * rrf"}`.
|
|
167
|
+
The formula is the only L1 scoring interface; old coefficient fields are removed.
|
|
168
|
+
See [candidate scoring](../docs/candidate-rescoring.md) for backfill, passage
|
|
169
|
+
selection, expression limits, capability versions, and distributed behavior.
|
|
170
|
+
|
|
171
|
+
## Deadlines and errors
|
|
172
|
+
|
|
173
|
+
RPCs accept `timeout` in seconds, overriding the constructor's `default_timeout`.
|
|
174
|
+
gRPC failures raise `grpc.aio.AioRpcError`, except document `NOT_FOUND` as above.
|
|
175
|
+
An expired mutation may already be staged, and an accepted commit continues
|
|
176
|
+
after disconnection. Resolve the outcome before retrying replacements.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
From this directory:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv sync --group dev --group test
|
|
184
|
+
uv run ruff check .
|
|
185
|
+
uv run ruff format --check .
|
|
186
|
+
uv run pytest tests/test_client_unit.py
|
|
187
|
+
uv run --group dev python generate_proto.py
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Integration tests require `target/debug/hermes-server`. Regenerate bindings after
|
|
191
|
+
[protocol changes](../hermes-proto/README.md#regeneration-and-validation).
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Hermes Python client
|
|
2
|
+
|
|
3
|
+
Async Python client for the
|
|
4
|
+
[Hermes](https://github.com/SpaceFrontiers/hermes) gRPC search server.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install hermes-client-python
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Python 3.10 or newer is required.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
|
|
19
|
+
from hermes_client_python import HermesClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
async with HermesClient("localhost:50051") as client:
|
|
24
|
+
await client.create_index(
|
|
25
|
+
"articles",
|
|
26
|
+
"""
|
|
27
|
+
index articles {
|
|
28
|
+
field id: text<raw> [primary, stored]
|
|
29
|
+
field title: text<simple> [indexed, stored]
|
|
30
|
+
field body: text<simple> [indexed, stored]
|
|
31
|
+
}
|
|
32
|
+
""",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
indexed, error_count, errors = await client.index_documents(
|
|
36
|
+
"articles",
|
|
37
|
+
[
|
|
38
|
+
{"id": "1", "title": "Hello World", "body": "First article"},
|
|
39
|
+
{"id": "2", "title": "Hermes Search", "body": "Fast retrieval"},
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
if error_count:
|
|
43
|
+
raise RuntimeError(errors)
|
|
44
|
+
print(f"Indexed {indexed} documents")
|
|
45
|
+
|
|
46
|
+
await client.commit("articles")
|
|
47
|
+
|
|
48
|
+
results = await client.search(
|
|
49
|
+
"articles",
|
|
50
|
+
query={"match": {"field": "title", "text": "hello"}},
|
|
51
|
+
fields_to_load=["title", "body"],
|
|
52
|
+
)
|
|
53
|
+
for hit in results.hits:
|
|
54
|
+
print(hit.address, hit.score, hit.fields)
|
|
55
|
+
|
|
56
|
+
if results.hits:
|
|
57
|
+
document = await client.get_document("articles", results.hits[0].address)
|
|
58
|
+
print(document.fields if document else "document not found")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The context manager connects and closes the channel. Manual callers use
|
|
65
|
+
`await client.connect()` / `await client.close()`.
|
|
66
|
+
|
|
67
|
+
## Index management
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
await client.list_indexes()
|
|
71
|
+
info = await client.get_index_info("articles")
|
|
72
|
+
await client.reorder("articles")
|
|
73
|
+
await client.retrain_vector_index("articles")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`index_documents` returns `(indexed_count, error_count, errors)`;
|
|
77
|
+
`index_documents_stream` takes an async iterable and returns counts. Inspect
|
|
78
|
+
errors, then `commit()` to publish accepted work. Repeated values are lists;
|
|
79
|
+
flat numeric lists are dense vectors, `(dimension, weight)` pairs are sparse
|
|
80
|
+
vectors. See [client types](src/hermes_client_python/types.py).
|
|
81
|
+
|
|
82
|
+
### Delete and upsert documents
|
|
83
|
+
|
|
84
|
+
With a text primary key, delete by exact key or supply a complete replacement:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
await client.delete_document("articles", "2")
|
|
88
|
+
await client.upsert_document("articles", {"id": "1", "title": "Updated title"})
|
|
89
|
+
await client.commit("articles")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`delete_documents` / `upsert_documents` return `DocumentMutationResult` with
|
|
93
|
+
`accepted_count` and `errors: [{index, error}]`. Single-item helpers raise on
|
|
94
|
+
rejection. Missing deletes succeed; upserts insert missing keys. Staged rows can
|
|
95
|
+
be replaced/deleted again before commit; the latest accepted version wins.
|
|
96
|
+
Optional [content hashes](../docs/content-deduplication.md) skip unchanged writes.
|
|
97
|
+
|
|
98
|
+
Limits: 100,000 deletion keys / 8 MiB key bytes; 1,000 replacements / 32 MiB
|
|
99
|
+
encoded protobuf, or one replacement / 200 MiB including the request envelope.
|
|
100
|
+
Broker commits are atomic per partition. See [mutation semantics](../docs/row-deletion.md).
|
|
101
|
+
|
|
102
|
+
### Compact deleted rows
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
await client.force_merge("articles") # Retain tombstones.
|
|
106
|
+
await client.force_merge("articles", compact=True) # Physically remove deleted rows.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Compaction handles singleton segments and may change addresses and BM25 scores.
|
|
110
|
+
Index info exposes `num_docs`, `physical_num_docs`, `num_deleted_docs`, and
|
|
111
|
+
`deleted_ratio`. Use primary keys for durable identity.
|
|
112
|
+
|
|
113
|
+
## Searching
|
|
114
|
+
|
|
115
|
+
`search(index_name, query=..., limit=10, fields_to_load=[...])` accepts one query
|
|
116
|
+
variant: `term`, `match`, `phrase`, `boolean`, `sparse_vector`, `dense_vector`,
|
|
117
|
+
`binary_dense_vector`, `boost`, `range`, `prefix`, `all`, or `fusion`.
|
|
118
|
+
See [query types](src/hermes_client_python/types.py) and the
|
|
119
|
+
[wire contract](../hermes-proto/hermes.proto) for options.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
results = await client.search(
|
|
123
|
+
"articles",
|
|
124
|
+
query={"boolean": {
|
|
125
|
+
"must": [{"match": {"field": "title", "text": "search"}}],
|
|
126
|
+
"must_not": [{"term": {"field": "title", "term": "draft"}}],
|
|
127
|
+
}},
|
|
128
|
+
fields_to_load=["title"],
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`get_document(index_name, hit.address)` uses the full segment/document address
|
|
133
|
+
and returns `None` on `NOT_FOUND`.
|
|
134
|
+
|
|
135
|
+
## Ranking diagnostics and recall traces
|
|
136
|
+
|
|
137
|
+
Search options `include_rrf_scores=True` and `tracing=True` default to false.
|
|
138
|
+
RRF diagnostics describe organic branch nominations; traces retain bounded
|
|
139
|
+
candidates and query trees, including hits outside the final page. Neither
|
|
140
|
+
changes retrieval depth or ranking. Oversized exports fail explicitly.
|
|
141
|
+
|
|
142
|
+
For named branches, use `l1={"formula": "0.2 * title + 0.8 * body + 3 * rrf"}`.
|
|
143
|
+
The formula is the only L1 scoring interface; old coefficient fields are removed.
|
|
144
|
+
See [candidate scoring](../docs/candidate-rescoring.md) for backfill, passage
|
|
145
|
+
selection, expression limits, capability versions, and distributed behavior.
|
|
146
|
+
|
|
147
|
+
## Deadlines and errors
|
|
148
|
+
|
|
149
|
+
RPCs accept `timeout` in seconds, overriding the constructor's `default_timeout`.
|
|
150
|
+
gRPC failures raise `grpc.aio.AioRpcError`, except document `NOT_FOUND` as above.
|
|
151
|
+
An expired mutation may already be staged, and an accepted commit continues
|
|
152
|
+
after disconnection. Resolve the outcome before retrying replacements.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
From this directory:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv sync --group dev --group test
|
|
160
|
+
uv run ruff check .
|
|
161
|
+
uv run ruff format --check .
|
|
162
|
+
uv run pytest tests/test_client_unit.py
|
|
163
|
+
uv run --group dev python generate_proto.py
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Integration tests require `target/debug/hermes-server`. Regenerate bindings after
|
|
167
|
+
[protocol changes](../hermes-proto/README.md#regeneration-and-validation).
|
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: hermes-client-python
|
|
3
|
-
Version: 1.8.145
|
|
4
|
-
Summary: Async Python client for Hermes search server
|
|
5
|
-
Project-URL: Homepage, https://github.com/SpaceFrontiers/hermes
|
|
6
|
-
Project-URL: Repository, https://github.com/SpaceFrontiers/hermes
|
|
7
|
-
Author: izihawa
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
Keywords: async,full-text-search,grpc,search
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
-
Classifier: Topic :: Database :: Database Engines/Servers
|
|
19
|
-
Classifier: Topic :: Text Processing :: Indexing
|
|
20
|
-
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist: grpcio>=1.76.0
|
|
22
|
-
Requires-Dist: protobuf>=6.33.4
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
|
-
# Hermes Python client
|
|
26
|
-
|
|
27
|
-
Async Python client for the
|
|
28
|
-
[Hermes](https://github.com/SpaceFrontiers/hermes) gRPC search server.
|
|
29
|
-
|
|
30
|
-
## Installation
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
pip install hermes-client-python
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Python 3.10 or newer is required.
|
|
37
|
-
|
|
38
|
-
## Quick start
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
import asyncio
|
|
42
|
-
|
|
43
|
-
from hermes_client_python import HermesClient
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
async def main():
|
|
47
|
-
async with HermesClient("localhost:50051") as client:
|
|
48
|
-
await client.create_index(
|
|
49
|
-
"articles",
|
|
50
|
-
"""
|
|
51
|
-
index articles {
|
|
52
|
-
field title: text<simple> [indexed, stored]
|
|
53
|
-
field body: text<simple> [indexed, stored]
|
|
54
|
-
}
|
|
55
|
-
""",
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
indexed, error_count, errors = await client.index_documents(
|
|
59
|
-
"articles",
|
|
60
|
-
[
|
|
61
|
-
{"title": "Hello World", "body": "First article"},
|
|
62
|
-
{"title": "Hermes Search", "body": "Fast retrieval"},
|
|
63
|
-
],
|
|
64
|
-
)
|
|
65
|
-
if error_count:
|
|
66
|
-
raise RuntimeError(errors)
|
|
67
|
-
print(f"Indexed {indexed} documents")
|
|
68
|
-
|
|
69
|
-
await client.commit("articles")
|
|
70
|
-
|
|
71
|
-
results = await client.search(
|
|
72
|
-
"articles",
|
|
73
|
-
query={"match": {"field": "title", "text": "hello"}},
|
|
74
|
-
fields_to_load=["title", "body"],
|
|
75
|
-
)
|
|
76
|
-
for hit in results.hits:
|
|
77
|
-
print(hit.address, hit.score, hit.fields)
|
|
78
|
-
|
|
79
|
-
if results.hits:
|
|
80
|
-
document = await client.get_document("articles", results.hits[0].address)
|
|
81
|
-
print(document.fields if document else "document not found")
|
|
82
|
-
|
|
83
|
-
await client.delete_index("articles")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
asyncio.run(main())
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
The context manager calls `connect()` and `close()` automatically. For manual
|
|
90
|
-
lifecycle management:
|
|
91
|
-
|
|
92
|
-
```python
|
|
93
|
-
client = HermesClient("localhost:50051")
|
|
94
|
-
await client.connect()
|
|
95
|
-
try:
|
|
96
|
-
...
|
|
97
|
-
finally:
|
|
98
|
-
await client.close()
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Index management
|
|
102
|
-
|
|
103
|
-
```python
|
|
104
|
-
await client.create_index("articles", schema_sdl)
|
|
105
|
-
names = await client.list_indexes()
|
|
106
|
-
info = await client.get_index_info("articles")
|
|
107
|
-
print(info.num_docs, info.num_segments, info.vector_stats)
|
|
108
|
-
|
|
109
|
-
await client.force_merge("articles")
|
|
110
|
-
await client.reorder("articles")
|
|
111
|
-
await client.retrain_vector_index("articles")
|
|
112
|
-
await client.delete_index("articles")
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`commit()` is required before newly indexed documents become searchable.
|
|
116
|
-
|
|
117
|
-
### Batch and streaming indexing
|
|
118
|
-
|
|
119
|
-
```python
|
|
120
|
-
indexed, error_count, errors = await client.index_documents(
|
|
121
|
-
"articles",
|
|
122
|
-
[
|
|
123
|
-
{"title": "One", "tags": ["search", "rust"]},
|
|
124
|
-
{"title": "Two", "tags": ["python"]},
|
|
125
|
-
],
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
async def documents():
|
|
130
|
-
for number in range(10_000):
|
|
131
|
-
yield {"title": f"Document {number}"}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
streamed, stream_errors = await client.index_documents_stream("articles", documents())
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Repeated list values become repeated field entries. Flat numeric lists are
|
|
138
|
-
dense vectors; lists of `(dimension, weight)` pairs are sparse vectors.
|
|
139
|
-
|
|
140
|
-
## Searching
|
|
141
|
-
|
|
142
|
-
Every search takes one `query` object whose single key matches a Hermes query
|
|
143
|
-
variant:
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
# Exact term
|
|
147
|
-
await client.search(
|
|
148
|
-
"articles",
|
|
149
|
-
query={"term": {"field": "title", "term": "hermes"}},
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
# Tokenized full-text match
|
|
153
|
-
await client.search(
|
|
154
|
-
"articles",
|
|
155
|
-
query={"match": {"field": "body", "text": "fast retrieval"}},
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
# Recursive boolean query
|
|
159
|
-
await client.search(
|
|
160
|
-
"articles",
|
|
161
|
-
query={
|
|
162
|
-
"boolean": {
|
|
163
|
-
"must": [{"match": {"field": "body", "text": "retrieval"}}],
|
|
164
|
-
"must_not": [{"term": {"field": "title", "term": "draft"}}],
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# Dense vector query and optional reranking
|
|
170
|
-
await client.search(
|
|
171
|
-
"articles",
|
|
172
|
-
query={
|
|
173
|
-
"dense_vector": {
|
|
174
|
-
"field": "embedding",
|
|
175
|
-
"vector": [0.1, 0.2, 0.3],
|
|
176
|
-
"nprobe": 16,
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
reranker={"field": "embedding", "vector": [0.1, 0.2, 0.3]},
|
|
180
|
-
candidate_limit=20,
|
|
181
|
-
limit=10,
|
|
182
|
-
fields_to_load=["title"],
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
# Hybrid union fusion
|
|
186
|
-
await client.search(
|
|
187
|
-
"articles",
|
|
188
|
-
query={
|
|
189
|
-
"fusion": {
|
|
190
|
-
"method": "rrf",
|
|
191
|
-
"rrf_k": 60,
|
|
192
|
-
"queries": [
|
|
193
|
-
{
|
|
194
|
-
"query": {
|
|
195
|
-
"sparse_vector": {
|
|
196
|
-
"field": "sparse_embedding",
|
|
197
|
-
"indices": [1, 5],
|
|
198
|
-
"values": [0.8, 0.2],
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
"weight": 1.0,
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
"query": {
|
|
205
|
-
"dense_vector": {
|
|
206
|
-
"field": "embedding",
|
|
207
|
-
"vector": [0.1, 0.2, 0.3],
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
"weight": 1.0,
|
|
211
|
-
},
|
|
212
|
-
],
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Other supported variants are `phrase`, `binary_dense_vector`, `boost`, `range`,
|
|
219
|
-
`prefix`, and `all`. Search results expose the full `DocAddress` needed by
|
|
220
|
-
`get_document()`:
|
|
221
|
-
|
|
222
|
-
```python
|
|
223
|
-
hit = results.hits[0]
|
|
224
|
-
document = await client.get_document("articles", hit.address)
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Deadlines and errors
|
|
228
|
-
|
|
229
|
-
Every RPC accepts an optional `timeout` in seconds. A per-call value overrides
|
|
230
|
-
the client default:
|
|
231
|
-
|
|
232
|
-
```python
|
|
233
|
-
async with HermesClient("localhost:50051", default_timeout=5.0) as client:
|
|
234
|
-
results = await client.search(
|
|
235
|
-
"articles",
|
|
236
|
-
query={"all": {}},
|
|
237
|
-
timeout=0.5,
|
|
238
|
-
)
|
|
239
|
-
await client.force_merge("articles", timeout=3600)
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
gRPC failures raise `grpc.RpcError` (normally
|
|
243
|
-
`grpc.aio.AioRpcError`). `get_document()` is the exception: it returns `None`
|
|
244
|
-
for `NOT_FOUND`.
|
|
245
|
-
|
|
246
|
-
```python
|
|
247
|
-
import grpc
|
|
248
|
-
|
|
249
|
-
try:
|
|
250
|
-
await client.search("missing", query={"all": {}})
|
|
251
|
-
except grpc.RpcError as error:
|
|
252
|
-
if error.code() == grpc.StatusCode.NOT_FOUND:
|
|
253
|
-
print("index not found")
|
|
254
|
-
else:
|
|
255
|
-
raise
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
## Development
|
|
259
|
-
|
|
260
|
-
From `hermes-client-python`:
|
|
261
|
-
|
|
262
|
-
```bash
|
|
263
|
-
uv sync --group dev --group test
|
|
264
|
-
uv run ruff check .
|
|
265
|
-
uv run ruff format --check .
|
|
266
|
-
uv run pytest tests/test_client_unit.py
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
The remaining tests are integration tests and expect a debug
|
|
270
|
-
`target/debug/hermes-server` binary. Regenerate checked-in protobuf stubs after
|
|
271
|
-
changing `hermes-proto/hermes.proto`:
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
uv run --group dev python generate_proto.py
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
## License
|
|
278
|
-
|
|
279
|
-
MIT
|
|
280
|
-
|
|
281
|
-
## Ranking diagnostics and recall traces
|
|
282
|
-
|
|
283
|
-
Both options default to false and preserve the requested ranking:
|
|
284
|
-
|
|
285
|
-
```python
|
|
286
|
-
response = await client.search(
|
|
287
|
-
"articles",
|
|
288
|
-
query={
|
|
289
|
-
"fusion": {
|
|
290
|
-
"queries": [
|
|
291
|
-
{
|
|
292
|
-
"name": "title",
|
|
293
|
-
"query": {"match": {"field": "title", "text": "rust"}},
|
|
294
|
-
},
|
|
295
|
-
{"name": "body", "query": {"match": {"field": "body", "text": "rust"}}},
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
include_rrf_scores=True,
|
|
300
|
-
tracing=True,
|
|
301
|
-
)
|
|
302
|
-
for hit in response.hits:
|
|
303
|
-
print(hit.score, hit.rrf_score, hit.rrf_contributions)
|
|
304
|
-
for shard in response.trace.shards:
|
|
305
|
-
for branch in shard.queries:
|
|
306
|
-
print(shard.shard_id, branch.query_name, branch.candidates)
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
`rrf_score` and per-branch votes use organic nomination ranks merged across all
|
|
310
|
-
shards, independently of L1 or reranker scores. Ranks start at 1. `ordinal=None`
|
|
311
|
-
is document context; ordinal 0 is a real passage. Backfilled and score-only
|
|
312
|
-
features contribute no votes.
|
|
313
|
-
|
|
314
|
-
The trace retains every shard's bounded branch nominations and selected results,
|
|
315
|
-
including candidates absent from the final page, plus query trees and common
|
|
316
|
-
filters. Candidates contain addresses, raw scores and ordinals; stored fields
|
|
317
|
-
are loaded only for returned hits. Tracing does not expand retrieval depth or
|
|
318
|
-
rerun individual Boolean clauses. Oversized diagnostics and unsupported backends
|
|
319
|
-
fail explicitly. See the [scoring and tracing contract](../docs/candidate-rescoring.md).
|
|
320
|
-
|
|
321
|
-
For a named, scoped L1 query, specify the complete scoring formula:
|
|
322
|
-
|
|
323
|
-
```python
|
|
324
|
-
l1 = {"formula": "0.2 * title + 0.8 * log1p(body) + 3 * rrf"}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
`formula` is the only L1 scoring interface. Coefficients, offsets and RRF
|
|
328
|
-
multipliers go in the expression; the former coefficient fields are removed.
|
|
329
|
-
Arithmetic, powers, logarithms, `sqrt`, `abs`, `exp`, `min`/`max` and trigonometry
|
|
330
|
-
are supported. Use `{body.bm25}` for punctuated branch names. `log` and `ln` are
|
|
331
|
-
natural logarithms; `log2` and `log10` select those bases. Missing branch values
|
|
332
|
-
use configured missing defaults, otherwise zero. Backfill remains optional.
|
|
333
|
-
|
|
334
|
-
The formula runs before passage selection and the document combiner. A formula
|
|
335
|
-
using `rrf` makes the broker obtain the complete bounded candidate and passage
|
|
336
|
-
union before global inference. Exports that exceed budgets fail explicitly.
|
|
337
|
-
Expressions are bounded to 4 KiB, 256 tokens and 32 parenthesis levels. Invalid
|
|
338
|
-
variables, invalid syntax and non-finite predictions fail explicitly. Servers
|
|
339
|
-
and brokers must support `formula_v1` (`candidate_scoring_version=3`).
|
|
340
|
-
|
|
341
|
-
### Compact deleted rows
|
|
342
|
-
|
|
343
|
-
```python
|
|
344
|
-
await client.force_merge("articles") # Copy encoded data and retain tombstones.
|
|
345
|
-
await client.force_merge("articles", compact=True) # Remove deleted rows physically.
|
|
346
|
-
info = await client.get_index_info("articles")
|
|
347
|
-
print(info.num_deleted_docs, info.physical_num_docs, info.deleted_ratio)
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Compaction also works when there is only one segment. It preserves surviving
|
|
351
|
-
values and may change document addresses and BM25 statistics.
|
|
352
|
-
|
|
353
|
-
### Delete and upsert documents
|
|
354
|
-
|
|
355
|
-
Declare one text field `[primary]` in the schema. Deletion removes the document
|
|
356
|
-
and all of its chunks; upserts replace the entire document, including indexed-only
|
|
357
|
-
fields, and insert when the key is absent.
|
|
358
|
-
|
|
359
|
-
```python
|
|
360
|
-
await client.delete_document("articles", "obsolete-key")
|
|
361
|
-
await client.upsert_document(
|
|
362
|
-
"articles",
|
|
363
|
-
{"id": "article-42", "body": ["replacement chunk one", "replacement chunk two"]},
|
|
364
|
-
)
|
|
365
|
-
await client.commit("articles")
|
|
366
|
-
|
|
367
|
-
result = await client.delete_documents("articles", ["old-a", "old-b"])
|
|
368
|
-
print(result.accepted_count, result.errors) # errors: [{"index": 0, "error": "..."}]
|
|
369
|
-
await client.commit("articles") # publishes accepted operations
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
`upsert_documents` takes a list of complete replacement documents and returns the
|
|
373
|
-
same `DocumentMutationResult`. Single-document helpers raise on rejection. Missing
|
|
374
|
-
deletion keys are accepted. Commit before deleting/upserting a key with a pending
|
|
375
|
-
insertion or replacement. Limits are 100,000 deletion keys / 8 MiB key bytes and
|
|
376
|
-
1,000 replacement documents / 32 MiB encoded bytes, or one replacement / 200 MiB
|
|
377
|
-
including the request envelope. Mutations use the usual timeout
|
|
378
|
-
argument; an expired RPC may have staged work, so do not blindly retry replacements.
|
|
379
|
-
Broker commits are atomic within each partition. Physical cleanup remains
|
|
380
|
-
`await client.force_merge("articles", compact=True)`.
|
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
# Hermes Python client
|
|
2
|
-
|
|
3
|
-
Async Python client for the
|
|
4
|
-
[Hermes](https://github.com/SpaceFrontiers/hermes) gRPC search server.
|
|
5
|
-
|
|
6
|
-
## Installation
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
pip install hermes-client-python
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Python 3.10 or newer is required.
|
|
13
|
-
|
|
14
|
-
## Quick start
|
|
15
|
-
|
|
16
|
-
```python
|
|
17
|
-
import asyncio
|
|
18
|
-
|
|
19
|
-
from hermes_client_python import HermesClient
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
async def main():
|
|
23
|
-
async with HermesClient("localhost:50051") as client:
|
|
24
|
-
await client.create_index(
|
|
25
|
-
"articles",
|
|
26
|
-
"""
|
|
27
|
-
index articles {
|
|
28
|
-
field title: text<simple> [indexed, stored]
|
|
29
|
-
field body: text<simple> [indexed, stored]
|
|
30
|
-
}
|
|
31
|
-
""",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
indexed, error_count, errors = await client.index_documents(
|
|
35
|
-
"articles",
|
|
36
|
-
[
|
|
37
|
-
{"title": "Hello World", "body": "First article"},
|
|
38
|
-
{"title": "Hermes Search", "body": "Fast retrieval"},
|
|
39
|
-
],
|
|
40
|
-
)
|
|
41
|
-
if error_count:
|
|
42
|
-
raise RuntimeError(errors)
|
|
43
|
-
print(f"Indexed {indexed} documents")
|
|
44
|
-
|
|
45
|
-
await client.commit("articles")
|
|
46
|
-
|
|
47
|
-
results = await client.search(
|
|
48
|
-
"articles",
|
|
49
|
-
query={"match": {"field": "title", "text": "hello"}},
|
|
50
|
-
fields_to_load=["title", "body"],
|
|
51
|
-
)
|
|
52
|
-
for hit in results.hits:
|
|
53
|
-
print(hit.address, hit.score, hit.fields)
|
|
54
|
-
|
|
55
|
-
if results.hits:
|
|
56
|
-
document = await client.get_document("articles", results.hits[0].address)
|
|
57
|
-
print(document.fields if document else "document not found")
|
|
58
|
-
|
|
59
|
-
await client.delete_index("articles")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
asyncio.run(main())
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The context manager calls `connect()` and `close()` automatically. For manual
|
|
66
|
-
lifecycle management:
|
|
67
|
-
|
|
68
|
-
```python
|
|
69
|
-
client = HermesClient("localhost:50051")
|
|
70
|
-
await client.connect()
|
|
71
|
-
try:
|
|
72
|
-
...
|
|
73
|
-
finally:
|
|
74
|
-
await client.close()
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Index management
|
|
78
|
-
|
|
79
|
-
```python
|
|
80
|
-
await client.create_index("articles", schema_sdl)
|
|
81
|
-
names = await client.list_indexes()
|
|
82
|
-
info = await client.get_index_info("articles")
|
|
83
|
-
print(info.num_docs, info.num_segments, info.vector_stats)
|
|
84
|
-
|
|
85
|
-
await client.force_merge("articles")
|
|
86
|
-
await client.reorder("articles")
|
|
87
|
-
await client.retrain_vector_index("articles")
|
|
88
|
-
await client.delete_index("articles")
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
`commit()` is required before newly indexed documents become searchable.
|
|
92
|
-
|
|
93
|
-
### Batch and streaming indexing
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
indexed, error_count, errors = await client.index_documents(
|
|
97
|
-
"articles",
|
|
98
|
-
[
|
|
99
|
-
{"title": "One", "tags": ["search", "rust"]},
|
|
100
|
-
{"title": "Two", "tags": ["python"]},
|
|
101
|
-
],
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
async def documents():
|
|
106
|
-
for number in range(10_000):
|
|
107
|
-
yield {"title": f"Document {number}"}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
streamed, stream_errors = await client.index_documents_stream("articles", documents())
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Repeated list values become repeated field entries. Flat numeric lists are
|
|
114
|
-
dense vectors; lists of `(dimension, weight)` pairs are sparse vectors.
|
|
115
|
-
|
|
116
|
-
## Searching
|
|
117
|
-
|
|
118
|
-
Every search takes one `query` object whose single key matches a Hermes query
|
|
119
|
-
variant:
|
|
120
|
-
|
|
121
|
-
```python
|
|
122
|
-
# Exact term
|
|
123
|
-
await client.search(
|
|
124
|
-
"articles",
|
|
125
|
-
query={"term": {"field": "title", "term": "hermes"}},
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
# Tokenized full-text match
|
|
129
|
-
await client.search(
|
|
130
|
-
"articles",
|
|
131
|
-
query={"match": {"field": "body", "text": "fast retrieval"}},
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
# Recursive boolean query
|
|
135
|
-
await client.search(
|
|
136
|
-
"articles",
|
|
137
|
-
query={
|
|
138
|
-
"boolean": {
|
|
139
|
-
"must": [{"match": {"field": "body", "text": "retrieval"}}],
|
|
140
|
-
"must_not": [{"term": {"field": "title", "term": "draft"}}],
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
# Dense vector query and optional reranking
|
|
146
|
-
await client.search(
|
|
147
|
-
"articles",
|
|
148
|
-
query={
|
|
149
|
-
"dense_vector": {
|
|
150
|
-
"field": "embedding",
|
|
151
|
-
"vector": [0.1, 0.2, 0.3],
|
|
152
|
-
"nprobe": 16,
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
reranker={"field": "embedding", "vector": [0.1, 0.2, 0.3]},
|
|
156
|
-
candidate_limit=20,
|
|
157
|
-
limit=10,
|
|
158
|
-
fields_to_load=["title"],
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
# Hybrid union fusion
|
|
162
|
-
await client.search(
|
|
163
|
-
"articles",
|
|
164
|
-
query={
|
|
165
|
-
"fusion": {
|
|
166
|
-
"method": "rrf",
|
|
167
|
-
"rrf_k": 60,
|
|
168
|
-
"queries": [
|
|
169
|
-
{
|
|
170
|
-
"query": {
|
|
171
|
-
"sparse_vector": {
|
|
172
|
-
"field": "sparse_embedding",
|
|
173
|
-
"indices": [1, 5],
|
|
174
|
-
"values": [0.8, 0.2],
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
"weight": 1.0,
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
"query": {
|
|
181
|
-
"dense_vector": {
|
|
182
|
-
"field": "embedding",
|
|
183
|
-
"vector": [0.1, 0.2, 0.3],
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
"weight": 1.0,
|
|
187
|
-
},
|
|
188
|
-
],
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
)
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Other supported variants are `phrase`, `binary_dense_vector`, `boost`, `range`,
|
|
195
|
-
`prefix`, and `all`. Search results expose the full `DocAddress` needed by
|
|
196
|
-
`get_document()`:
|
|
197
|
-
|
|
198
|
-
```python
|
|
199
|
-
hit = results.hits[0]
|
|
200
|
-
document = await client.get_document("articles", hit.address)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Deadlines and errors
|
|
204
|
-
|
|
205
|
-
Every RPC accepts an optional `timeout` in seconds. A per-call value overrides
|
|
206
|
-
the client default:
|
|
207
|
-
|
|
208
|
-
```python
|
|
209
|
-
async with HermesClient("localhost:50051", default_timeout=5.0) as client:
|
|
210
|
-
results = await client.search(
|
|
211
|
-
"articles",
|
|
212
|
-
query={"all": {}},
|
|
213
|
-
timeout=0.5,
|
|
214
|
-
)
|
|
215
|
-
await client.force_merge("articles", timeout=3600)
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
gRPC failures raise `grpc.RpcError` (normally
|
|
219
|
-
`grpc.aio.AioRpcError`). `get_document()` is the exception: it returns `None`
|
|
220
|
-
for `NOT_FOUND`.
|
|
221
|
-
|
|
222
|
-
```python
|
|
223
|
-
import grpc
|
|
224
|
-
|
|
225
|
-
try:
|
|
226
|
-
await client.search("missing", query={"all": {}})
|
|
227
|
-
except grpc.RpcError as error:
|
|
228
|
-
if error.code() == grpc.StatusCode.NOT_FOUND:
|
|
229
|
-
print("index not found")
|
|
230
|
-
else:
|
|
231
|
-
raise
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Development
|
|
235
|
-
|
|
236
|
-
From `hermes-client-python`:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
uv sync --group dev --group test
|
|
240
|
-
uv run ruff check .
|
|
241
|
-
uv run ruff format --check .
|
|
242
|
-
uv run pytest tests/test_client_unit.py
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The remaining tests are integration tests and expect a debug
|
|
246
|
-
`target/debug/hermes-server` binary. Regenerate checked-in protobuf stubs after
|
|
247
|
-
changing `hermes-proto/hermes.proto`:
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
uv run --group dev python generate_proto.py
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
## License
|
|
254
|
-
|
|
255
|
-
MIT
|
|
256
|
-
|
|
257
|
-
## Ranking diagnostics and recall traces
|
|
258
|
-
|
|
259
|
-
Both options default to false and preserve the requested ranking:
|
|
260
|
-
|
|
261
|
-
```python
|
|
262
|
-
response = await client.search(
|
|
263
|
-
"articles",
|
|
264
|
-
query={
|
|
265
|
-
"fusion": {
|
|
266
|
-
"queries": [
|
|
267
|
-
{
|
|
268
|
-
"name": "title",
|
|
269
|
-
"query": {"match": {"field": "title", "text": "rust"}},
|
|
270
|
-
},
|
|
271
|
-
{"name": "body", "query": {"match": {"field": "body", "text": "rust"}}},
|
|
272
|
-
]
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
include_rrf_scores=True,
|
|
276
|
-
tracing=True,
|
|
277
|
-
)
|
|
278
|
-
for hit in response.hits:
|
|
279
|
-
print(hit.score, hit.rrf_score, hit.rrf_contributions)
|
|
280
|
-
for shard in response.trace.shards:
|
|
281
|
-
for branch in shard.queries:
|
|
282
|
-
print(shard.shard_id, branch.query_name, branch.candidates)
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
`rrf_score` and per-branch votes use organic nomination ranks merged across all
|
|
286
|
-
shards, independently of L1 or reranker scores. Ranks start at 1. `ordinal=None`
|
|
287
|
-
is document context; ordinal 0 is a real passage. Backfilled and score-only
|
|
288
|
-
features contribute no votes.
|
|
289
|
-
|
|
290
|
-
The trace retains every shard's bounded branch nominations and selected results,
|
|
291
|
-
including candidates absent from the final page, plus query trees and common
|
|
292
|
-
filters. Candidates contain addresses, raw scores and ordinals; stored fields
|
|
293
|
-
are loaded only for returned hits. Tracing does not expand retrieval depth or
|
|
294
|
-
rerun individual Boolean clauses. Oversized diagnostics and unsupported backends
|
|
295
|
-
fail explicitly. See the [scoring and tracing contract](../docs/candidate-rescoring.md).
|
|
296
|
-
|
|
297
|
-
For a named, scoped L1 query, specify the complete scoring formula:
|
|
298
|
-
|
|
299
|
-
```python
|
|
300
|
-
l1 = {"formula": "0.2 * title + 0.8 * log1p(body) + 3 * rrf"}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
`formula` is the only L1 scoring interface. Coefficients, offsets and RRF
|
|
304
|
-
multipliers go in the expression; the former coefficient fields are removed.
|
|
305
|
-
Arithmetic, powers, logarithms, `sqrt`, `abs`, `exp`, `min`/`max` and trigonometry
|
|
306
|
-
are supported. Use `{body.bm25}` for punctuated branch names. `log` and `ln` are
|
|
307
|
-
natural logarithms; `log2` and `log10` select those bases. Missing branch values
|
|
308
|
-
use configured missing defaults, otherwise zero. Backfill remains optional.
|
|
309
|
-
|
|
310
|
-
The formula runs before passage selection and the document combiner. A formula
|
|
311
|
-
using `rrf` makes the broker obtain the complete bounded candidate and passage
|
|
312
|
-
union before global inference. Exports that exceed budgets fail explicitly.
|
|
313
|
-
Expressions are bounded to 4 KiB, 256 tokens and 32 parenthesis levels. Invalid
|
|
314
|
-
variables, invalid syntax and non-finite predictions fail explicitly. Servers
|
|
315
|
-
and brokers must support `formula_v1` (`candidate_scoring_version=3`).
|
|
316
|
-
|
|
317
|
-
### Compact deleted rows
|
|
318
|
-
|
|
319
|
-
```python
|
|
320
|
-
await client.force_merge("articles") # Copy encoded data and retain tombstones.
|
|
321
|
-
await client.force_merge("articles", compact=True) # Remove deleted rows physically.
|
|
322
|
-
info = await client.get_index_info("articles")
|
|
323
|
-
print(info.num_deleted_docs, info.physical_num_docs, info.deleted_ratio)
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
Compaction also works when there is only one segment. It preserves surviving
|
|
327
|
-
values and may change document addresses and BM25 statistics.
|
|
328
|
-
|
|
329
|
-
### Delete and upsert documents
|
|
330
|
-
|
|
331
|
-
Declare one text field `[primary]` in the schema. Deletion removes the document
|
|
332
|
-
and all of its chunks; upserts replace the entire document, including indexed-only
|
|
333
|
-
fields, and insert when the key is absent.
|
|
334
|
-
|
|
335
|
-
```python
|
|
336
|
-
await client.delete_document("articles", "obsolete-key")
|
|
337
|
-
await client.upsert_document(
|
|
338
|
-
"articles",
|
|
339
|
-
{"id": "article-42", "body": ["replacement chunk one", "replacement chunk two"]},
|
|
340
|
-
)
|
|
341
|
-
await client.commit("articles")
|
|
342
|
-
|
|
343
|
-
result = await client.delete_documents("articles", ["old-a", "old-b"])
|
|
344
|
-
print(result.accepted_count, result.errors) # errors: [{"index": 0, "error": "..."}]
|
|
345
|
-
await client.commit("articles") # publishes accepted operations
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
`upsert_documents` takes a list of complete replacement documents and returns the
|
|
349
|
-
same `DocumentMutationResult`. Single-document helpers raise on rejection. Missing
|
|
350
|
-
deletion keys are accepted. Commit before deleting/upserting a key with a pending
|
|
351
|
-
insertion or replacement. Limits are 100,000 deletion keys / 8 MiB key bytes and
|
|
352
|
-
1,000 replacement documents / 32 MiB encoded bytes, or one replacement / 200 MiB
|
|
353
|
-
including the request envelope. Mutations use the usual timeout
|
|
354
|
-
argument; an expired RPC may have staged work, so do not blindly retry replacements.
|
|
355
|
-
Broker commits are atomic within each partition. Physical cleanup remains
|
|
356
|
-
`await client.force_merge("articles", compact=True)`.
|
|
File without changes
|
{hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/__init__.py
RENAMED
|
File without changes
|
{hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/client.py
RENAMED
|
File without changes
|
{hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/hermes_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_client_python-1.8.145 → hermes_client_python-1.8.147}/src/hermes_client_python/types.py
RENAMED
|
File without changes
|