Search tuning in the Aetherfy vector database
What search tuning is in the Aetherfy vector database
Collection-level configuration in the Aetherfy vector database is fixed server-side: you cannot set index or optimizer parameters when you create a collection. What you can control is per-query behaviour, one search at a time, through a single pass-through option.
| SDK | Option | Type | Position |
|---|---|---|---|
| Python | search_params | Optional[Dict[str, Any]] | last parameter of search() |
| JavaScript | searchParams | Record<string, unknown> | field on SearchOptions |
Aetherfy sends the object verbatim as the request body’s params field. It is
added last, and only when present — omitting it produces a body with no params
key at all, which is not the same request as one carrying an empty object.
This works against every deployed Aetherfy backend. There is no version gate and no capability negotiation.
Passing search parameters through the Aetherfy SDKs
The option is available on the Aetherfy client’s search() in both languages.
import os
from aetherfy_vectors import AetherfyVectorsClient
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
results = client.search(
collection_name="articles",
query_vector=[0.1, 0.2, 0.3, 0.4],
limit=10,
search_params={"hnsw_ef": 256},
)
for hit in results:
print(hit.id, hit.score)
client.close()import { AetherfyVectorsClient } from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
limit: 10,
searchParams: { hnsw_ef: 256 },
});
for (const hit of results) {
console.log(hit.id, hit.score);
}
client.dispose();The equivalent request against the Aetherfy REST API puts the same object under
params:
curl -s -X POST https://vectors.aetherfy.com/api/v1/collections/articles/points/search \
-H "Authorization: Bearer $AETHERFY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vector":[0.1,0.2,0.3,0.4],"limit":10,"params":{"hnsw_ef":256}}'Tuning the graph walk with hnsw_ef on Aetherfy
The headline use of the option on Aetherfy is hnsw_ef.
A larger ef makes the HNSW graph walk visit more candidates: better results, at
slightly higher cost per query. A smaller ef does the reverse — it visits
fewer candidates and finishes sooner, at the cost of result quality. There is no
universally correct value; it is a dial between quality and cost for your
workload.
| Value | Effect on an Aetherfy search |
|---|---|
| Larger than the default | Visits more candidates — better results, slightly higher cost |
| Omitted | Uses Aetherfy’s tuned server-side default of hnsw_ef=100 |
| Smaller than the default | Visits fewer candidates — cheaper, lower result quality |
Omit the option entirely unless you have measured a reason not to. The server-side default is tuned, and a hand-picked value that has not been compared against it is a guess.
Tuning memory-layer searches on Aetherfy
The same option is accepted by the Aetherfy memory layer, not just the raw vector
client. Namespace and Thread both expose a .search() that takes
search_params in Python and searchParams in JavaScript, with identical
pass-through semantics: the object goes to the request body’s params field
untouched.
The guidance the Aetherfy SDKs give for this layer is worth repeating verbatim: recall matters here — retrieving the right memory usually beats saving a millisecond. If you are going to move the dial for memory retrieval, move it towards visiting more candidates, not fewer.
How Aetherfy’s response cache treats search parameters
Aetherfy derives its server-side cache key from the request body bytes. Because
the tuning object is part of the body, the same query issued at a different ef
is a separate cache entry.
Two consequences follow, and both are safe:
| Situation | Result on Aetherfy |
|---|---|
| Same query, same params | May be served from the cache entry stored under those params |
| Same query, different params | Cannot hit the entry stored under the other params — it is a different key |
Same query, params omitted vs {} present | Different bodies, therefore different keys |
A params-varying call can never be answered by an entry stored under different
params. There is no scenario in which Aetherfy returns you a result computed at
an ef you did not ask for. The cost of varying the dial is cache-entry
multiplication, not correctness.
What Aetherfy does not validate in the tuning object
Neither Aetherfy SDK inspects, validates, or translates the contents of the
tuning object. Key names, value types, and value ranges are owned by the Aetherfy
vector API and by Qdrant beneath it — the SDK’s only responsibility is to
serialise the object into params.
This is deliberate: enumerating engine parameter names in the client would make every SDK release a compatibility treadmill behind the engine’s own schema. The cost is that a key the engine does not recognise is not rejected by the SDK. What is rejected — loudly — is a misspelling of the SDK’s own option name, which is the next section.
How unknown options fail in the Aetherfy SDKs
Both Aetherfy SDKs refuse an unrecognised option before any HTTP request is made. Neither one silently drops it. This is the distinction that makes the pass-through design safe: engine parameters are opaque, but SDK option names are closed.
| SDK | Mechanism | What you get |
|---|---|---|
| Python | search() has no **kwargs sink | A native TypeError from the call itself; no request is sent |
| JavaScript | Explicit runtime guard on the options object | A thrown error naming the offending keys; no request is sent |
The Aetherfy JavaScript guard’s message tells you exactly where the option should have gone:
search: unknown option(s): <names>. Engine-level search tuning goes in searchParams, e.g. { searchParams: { hnsw_ef: 256 } }.The guard is not limited to search. These Aetherfy JavaScript methods are
guarded:
| Guarded method |
|---|
client.search |
client.scrollIter |
Scope.search |
Namespace.iter |
Thread.iterHistory |
import os
from aetherfy_vectors import AetherfyVectorsClient
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
try:
# `hnsw_ef` is an engine parameter, not an SDK keyword argument.
client.search("articles", query_vector=[0.1, 0.2, 0.3, 0.4], hnsw_ef=256)
except TypeError as err:
print("rejected before any request:", err)
# The correct form.
results = client.search(
"articles",
query_vector=[0.1, 0.2, 0.3, 0.4],
search_params={"hnsw_ef": 256},
)
print(len(results))
client.close()import { AetherfyVectorsClient } from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
try {
// `hnsw_ef` is an engine parameter, not a SearchOptions field.
await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
hnsw_ef: 256,
} as never);
} catch (err) {
console.log('rejected before any request:', err);
}
// The correct form.
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
searchParams: { hnsw_ef: 256 },
});
console.log(results.length);
client.dispose();Absent, null, and empty tuning objects on Aetherfy
The three states are not equivalent, and the difference is observable in Aetherfy’s cache-key behaviour.
| You pass | Aetherfy sends | Cache consequence |
|---|---|---|
| Nothing | No params key in the body | The untuned entry |
null or undefined | No params key in the body — identical to omitting it | The untuned entry |
{} | "params": {} — the key is sent | A distinct cache entry from the untuned one |
If you are building the tuning object conditionally, prefer leaving it undefined
over defaulting it to {}: the empty object costs you a second Aetherfy cache
entry for what is otherwise the same query.
Filter syntax, which travels in a different body field, is on
filtering; the full search() surface is on the SDK
reference.