---
slug: vectors/filtering
title: Filtering
kind: reference
surface: vectors
summary: Payload filter syntax for the Aetherfy vector database — must, should and must_not clauses with match and range conditions, forwarded verbatim to Qdrant, including how each SDK spells the exclusion clause.
sources:
  - vectordb:backend/routes/proxy.js
  - aetherfy-vectors-python-sdk:aetherfy_vectors/models.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/client.py
  - aetherfy-vectors-js-sdk:src/models.ts
  - aetherfy-vectors-js-sdk:src/client.ts
---

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

```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,
    query_filter={"must": [{"key": "lang", "match": {"value": "en"}}]},
)
for hit in results:
    print(hit.id, hit.score, hit.payload)
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,
  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:

```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,
       "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.

```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,
    query_filter={
        "must": [
            {"key": "lang", "match": {"value": "en"}},
            {"key": "status", "match": {"value": "published"}},
        ]
    },
)
print([hit.id for hit in results])
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,
  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.

```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,
    query_filter={
        "should": [
            {"key": "lang", "match": {"value": "en"}},
            {"key": "lang", "match": {"value": "de"}},
        ]
    },
)
print([hit.payload for hit in results])
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,
  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.

```python
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`.

```typescript
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: `mustNot` in JavaScript, and a plain dict carrying `mustNot`
  in Python.
- If you copied a `must_not` workaround into JavaScript, remove it. The
  workaround now throws; `mustNot` is 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.

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

```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,
  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.

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

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

```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,
       "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](/vectors/search-tuning). The full method surface
is on [the SDK reference](/vectors/sdk).
