Filtering in the Aetherfy vector database
How filters travel through Aetherfy
A filter narrows a search, count, scroll, or delete to the points whose payload satisfies it. Aetherfy forwards filters to Qdrant verbatim: the proxy never inspects, validates, or rewrites the filter body.
That has one consequence you must design around, and it differs by SDK. Aetherfy itself does not validate filter bodies: a key the engine does not recognise is passed along and quietly does nothing, so a misspelled key inside a condition produces a successful response with unfiltered results rather than a 400.
The JavaScript SDK does check the three top-level CLAUSE names before sending,
throwing on anything that is not must, mustNot or should. That catches the
common misspelling but nothing deeper — a typo in a key or match field is
still forwarded and still silently does nothing. The Python SDK performs no such
check. Either way, the vocabulary below is the whole contract.
Filter vocabulary in the Aetherfy vector database
An Aetherfy filter is an object with up to three clause arrays. Each clause array holds condition objects.
| Clause | Meaning | Logical operator |
|---|---|---|
must | Every condition in the array holds | AND |
should | At least one condition in the array holds | OR |
must_not | No condition in the array holds | NOT |
The wire key for exclusion is must_not, with an underscore. In Python you write
that key directly; in JavaScript you write mustNot and the SDK translates it.
See the SDK spelling section below.
Conditions come in two shapes:
| Condition | Shape | Use |
|---|---|---|
| Match | {"key": "<field>", "match": {"value": <value>}} | Exact equality on a payload field |
| Range | {"key": "<field>", "range": {"gte": ..., "lte": ..., "gt": ..., "lt": ...}} | Numeric bounds; supply any subset of the four bounds |
In the Aetherfy Python SDK you may pass either a plain dictionary to
query_filter= or the Filter dataclass, whose to_dict() emits exactly the
must / must_not / should wire keys. In the Aetherfy JavaScript SDK you pass
a plain object to queryFilter.
Matching one value with Aetherfy
The simplest Aetherfy filter is a single equality condition under must.
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,
query_filter={"must": [{"key": "lang", "match": {"value": "en"}}]},
)
for hit in results:
print(hit.id, hit.score, hit.payload)
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,
queryFilter: { must: [{ key: 'lang', match: { value: 'en' } }] },
});
for (const hit of results) {
console.log(hit.id, hit.score, hit.payload);
}
client.dispose();The same filter over the Aetherfy REST API:
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,
"filter":{"must":[{"key":"lang","match":{"value":"en"}}]}}'Requiring several conditions at once with Aetherfy
Put every condition in the must array. Aetherfy forwards it as a conjunction:
a point is returned only if all of them hold.
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,
query_filter={
"must": [
{"key": "lang", "match": {"value": "en"}},
{"key": "status", "match": {"value": "published"}},
]
},
)
print([hit.id for hit in results])
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,
queryFilter: {
must: [
{ key: 'lang', match: { value: 'en' } },
{ key: 'status', match: { value: 'published' } },
],
},
});
console.log(results.map((hit) => hit.id));
client.dispose();Accepting any of several alternatives with Aetherfy
should is the disjunction. A point qualifies if at least one condition in the
array holds. Aetherfy passes the clause through unchanged, so the engine’s own
should semantics apply.
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,
query_filter={
"should": [
{"key": "lang", "match": {"value": "en"}},
{"key": "lang", "match": {"value": "de"}},
]
},
)
print([hit.payload for hit in results])
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,
queryFilter: {
should: [
{ key: 'lang', match: { value: 'en' } },
{ key: 'lang', match: { value: 'de' } },
],
},
});
console.log(results.map((hit) => hit.payload));
client.dispose();Excluding points with Aetherfy
Exclusion uses must_not — underscore, not camel case. Every condition in the
array must fail for the point to be returned.
In the Aetherfy Python SDK you can write the dataclass or the dictionary; both
produce must_not on the wire.
import os
from aetherfy_vectors import AetherfyVectorsClient
from aetherfy_vectors.models import Filter
client = AetherfyVectorsClient(api_key=os.environ["AETHERFY_API_KEY"])
# Dataclass form — Filter.to_dict() emits the must / must_not / should wire keys.
exclude_french = Filter(must_not=[{"key": "lang", "match": {"value": "fr"}}])
print(exclude_french.to_dict())
results = client.search(
collection_name="articles",
query_vector=[0.1, 0.2, 0.3, 0.4],
limit=10,
query_filter=exclude_french,
)
print([hit.id for hit in results])
# Dictionary form — identical on the wire.
results = client.search(
collection_name="articles",
query_vector=[0.1, 0.2, 0.3, 0.4],
limit=10,
query_filter={"must_not": [{"key": "lang", "match": {"value": "fr"}}]},
)
print([hit.id for hit in results])
client.close()In the Aetherfy JavaScript SDK write mustNot, the camelCase spelling the
Filter interface declares. The SDK translates it to the wire key must_not
for you, the same way it translates withPayload and scoreThreshold.
import { AetherfyVectorsClient } from 'aetherfy-vectors';
import type { Filter } from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const excludeFrench: Filter = {
mustNot: [{ key: 'lang', match: { value: 'fr' } }],
};
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
limit: 10,
queryFilter: excludeFrench,
});
console.log(results.map((hit) => hit.id));
client.dispose();How each Aetherfy SDK spells the exclusion clause
The wire key is must_not. Each SDK reaches it from its own vocabulary, and in
both cases you write the SDK’s spelling, not the wire’s:
| SDK | What you write | What Aetherfy sends |
|---|---|---|
| Python | must_not in a dict, or Filter(must_not=...) | must_not |
| JavaScript / TypeScript | mustNot | must_not — the SDK translates it |
Each Aetherfy SDK rejects the other’s spelling, loudly and by name. Both validate filter clauses before sending and raise on anything outside their own three, with a hint naming the sibling SDK:
| You write | Python | JavaScript |
|---|---|---|
must / should | accepted | accepted |
must_not | accepted | throws — “Write the clause as mustNot; the SDK translates it to the wire key must_not.” |
mustNot | throws — “Write the clause as must_not; mustNot is the JavaScript SDK’s spelling.” | accepted |
| anything else | throws | throws |
Aetherfy itself does not validate the filter body, so an unrecognised clause used to be forwarded and quietly do nothing. Both SDKs now refuse to be the thing that forwards it. A caller who guesses the wrong idiom — and arriving from the other language’s docs is the obvious way to guess wrong — gets an error that names the right spelling instead of results that silently include rows they meant to exclude.
Two consequences worth knowing:
- If you are auditing code written against an older Aetherfy SDK, an exclusion
clause in the wrong idiom was being dropped. Those filters were not filtering,
and their results were wrong rather than merely incomplete. It failed silently
in both languages:
mustNotin JavaScript, and a plain dict carryingmustNotin Python. - If you copied a
must_notworkaround into JavaScript, remove it. The workaround now throws;mustNotis correct.
In Python this applies to plain dicts. A Filter dataclass could never carry the
wrong spelling — its fields are must, must_not, should — so passing a
Filter sidesteps the question entirely. Every filter-taking method accepts
either form, including count(count_filter=...).
Clause order does not matter to you, but Aetherfy fixes it (must, must_not,
should) before sending, so two callers writing the same clauses in different
orders share one server cache entry instead of two.
Filtering by numeric range with Aetherfy
A range condition carries any subset of gte, lte, gt, and lt under the
range key. Aetherfy forwards it unchanged.
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,
query_filter={"must": [{"key": "year", "range": {"gte": 2020, "lte": 2025}}]},
)
print([hit.payload for hit in results])
# Ranges work on count and scroll too.
print(client.count("articles", count_filter={"must": [{"key": "year", "range": {"gt": 2024}}]}, exact=True))
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,
queryFilter: { must: [{ key: 'year', range: { gte: 2020, lte: 2025 } }] },
});
console.log(results.map((hit) => hit.payload));
// Ranges work on count and scroll too.
console.log(
await client.count('articles', {
countFilter: { must: [{ key: 'year', range: { gt: 2024 } }] },
exact: true,
}),
);
client.dispose();Combining all three clauses in an Aetherfy filter
The three clauses compose in a single filter object: everything in must holds,
at least one thing in should holds, and nothing in must_not holds. Aetherfy
sends the whole object as one unit.
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,
query_filter={
"must": [
{"key": "status", "match": {"value": "published"}},
{"key": "year", "range": {"gte": 2020}},
],
"should": [
{"key": "lang", "match": {"value": "en"}},
{"key": "lang", "match": {"value": "de"}},
],
"must_not": [
{"key": "archived", "match": {"value": True}},
],
},
)
for hit in results:
print(hit.id, hit.score, hit.payload)
client.close()import { AetherfyVectorsClient } from 'aetherfy-vectors';
import type { Filter } from 'aetherfy-vectors';
const client = new AetherfyVectorsClient({ apiKey: process.env.AETHERFY_API_KEY });
const combined: Filter = {
must: [
{ key: 'status', match: { value: 'published' } },
{ key: 'year', range: { gte: 2020 } },
],
should: [
{ key: 'lang', match: { value: 'en' } },
{ key: 'lang', match: { value: 'de' } },
],
mustNot: [{ key: 'archived', match: { value: true } }],
};
const results = await client.search('articles', [0.1, 0.2, 0.3, 0.4], {
limit: 10,
queryFilter: combined,
});
for (const hit of results) {
console.log(hit.id, hit.score, hit.payload);
}
client.dispose();The same object over the Aetherfy REST API, where the clause keys are the only spelling that exists:
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,
"filter":{
"must":[{"key":"status","match":{"value":"published"}},
{"key":"year","range":{"gte":2020}}],
"should":[{"key":"lang","match":{"value":"en"}},
{"key":"lang","match":{"value":"de"}}],
"must_not":[{"key":"archived","match":{"value":true}}]
}}'Where filters apply across the Aetherfy vector database
The same filter object is accepted by four Aetherfy operations, under four different parameter names.
| Operation | Python parameter | JavaScript option | REST body key |
|---|---|---|---|
| Search | query_filter | queryFilter | filter |
| Count | count_filter | countFilter | filter |
| Scroll | scroll_filter | scrollFilter | filter |
| Delete points | points_selector (pass the filter object) | pointsSelector (pass the filter object) | filter |
Engine-level query tuning is a separate concern from filtering and travels in its own field — see search tuning. The full method surface is on the SDK reference.