Skip to Content
Vector databaseSearch tuning
Raw

Search tuning in the Aetherfy vector database

If your results are simply wrong — plausibly ranked but not relevant — this is almost certainly the wrong page. Query-time tuning trades recall against cost at the margin; it does not repair results that were never comparable in the first place. Rule these out first, in this order:

SymptomLikely causeWhere it is covered
Results look ranked but unrelated to the queryThe query vector came from a different model, or different preprocessing, than the stored vectorsBring your own embeddings
Ranking is subtly poor across the boardThe collection’s distance metric is not the one your embedding model was trained for, and Aetherfy cannot detect thatBring your own embeddings
The right rows are missing entirelyA filter is excluding them — including a misspelled condition key, which Aetherfy forwards and silently ignoresFiltering
Results are fine but you want a little more recallGenuinely a tuning questionthis page

Only the last row is what follows. Tuning hnsw_ef upward on a collection whose vectors and query come from different models makes the wrong answers arrive more expensively.

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.

SDKOptionTypePosition
Pythonsearch_paramsOptional[Dict[str, Any]]last parameter of search()
JavaScriptsearchParamsRecord<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.

ValueEffect on an Aetherfy search
Larger than the defaultVisits more candidates — better results, slightly higher cost
OmittedUses Aetherfy’s tuned server-side default of hnsw_ef=100
Smaller than the defaultVisits 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.

A Thread.search() also carries the thread’s own payload filter, since every thread in a workspace shares one collection. The tuning option is orthogonal to that: hnsw_ef changes how many candidates the graph walk visits, not which thread they can come from.

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:

SituationResult on Aetherfy
Same query, same paramsMay be served from the cache entry stored under those params
Same query, different paramsCannot hit the entry stored under the other params — it is a different key
Same query, params omitted vs {} presentDifferent 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.

SDKMechanismWhat you get
Pythonsearch() has no **kwargs sinkA native TypeError from the call itself; no request is sent
JavaScriptRuntime check of the options object’s keysA TypeError naming the offending keys and listing the accepted ones; no request is sent

The Aetherfy JavaScript error lists the accepted keys and tells you where the option should have gone:

search: unknown option(s): <names>. Accepted: limit, offset, queryFilter, withPayload, withVectors, scoreThreshold, searchParams. Engine-level search tuning goes in searchParams, e.g. { searchParams: { hnsw_ef: 256 } }.

The check is not limited to search. Every JavaScript method that takes an options object applies it, including the client constructor and create(), retrieve, scroll, scrollIter, count, and the MemoryClient, Namespace and Thread methods. A key is refused even when its value is undefined.

The mistake these guards catch most often is an engine parameter passed as if it were an SDK option. hnsw_ef=256 given straight to Python’s search() raises a TypeError naming hnsw_ef, and hnsw_ef: 256 in the JavaScript search options throws the message above. Neither sends a request. The engine parameter belongs inside the tuning object:

import os from aetherfy_vectors import AetherfyVectorsClient client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"]) # `hnsw_ef` is an engine parameter, so it goes in search_params. 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 }); // `hnsw_ef` is an engine parameter, so it goes in searchParams. 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 passAetherfy sendsCache consequence
NothingNo params key in the bodyThe untuned entry
null or undefinedNo params key in the body — identical to omitting itThe untuned entry
{}"params": {} — the key is sentA 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.

Last updated on