---
slug: vectors/search-tuning
title: Search tuning
kind: reference
surface: vectors
summary: How to tune query-time search behaviour in the Aetherfy vector database with search_params / searchParams, including hnsw_ef, cache-key behaviour, and how both SDKs reject unknown options before sending a request.
sources:
  - aetherfy-vectors-python-sdk:aetherfy_vectors/client.py
  - aetherfy-vectors-python-sdk:aetherfy_memory/namespace.py
  - aetherfy-vectors-js-sdk:src/client.ts
  - aetherfy-vectors-js-sdk:src/utils/options.ts
  - aetherfy-vectors-js-sdk:src/memory/namespace.ts
  - vectordb:backend/services/cache.js
---

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

```python
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()
```

```typescript
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`:

```bash
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` |

```python
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()
```

```typescript
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](/vectors/filtering); the full `search()` surface is on [the SDK
reference](/vectors/sdk).
