---
slug: examples/filtered-search
title: Filtered search
kind: howto
surface: vectors
summary: Complete Python and JavaScript examples of Aetherfy filtered vector search using the must, should and must_not vocabulary, including how each SDK spells the exclusion clause.
sources:
  - aetherfy-vectors-python-sdk:aetherfy_vectors/client.py
  - aetherfy-vectors-python-sdk:aetherfy_vectors/models.py
  - aetherfy-vectors-js-sdk:src/client.ts
  - aetherfy-vectors-js-sdk:src/models.ts
---

# Filtered search

A filter restricts a vector search to the points whose payload matches a condition. In Aetherfy the filter is a plain object with up to three clause lists — `must`, `should`, `must_not` — each holding condition objects.

Before you start:

```bash
export AETHERFY_API_KEY="afy_live_your_key_here"
```

## The Aetherfy filter vocabulary

| Clause | Meaning |
| --- | --- |
| `must` | Every listed condition has to hold. Logical AND. |
| `should` | At least one listed condition contributes; used to express an OR-style match. |
| `must_not` | No listed condition may hold. Logical NOT. |

Conditions come in two forms.

| Condition | Shape | Matches |
| --- | --- | --- |
| Exact match | `{"key": "<payload key>", "match": {"value": <value>}}` | Points whose payload key equals the value |
| Range | `{"key": "<payload key>", "range": {"gte": …, "lte": …, "gt": …, "lt": …}}` | Numeric payload values inside the bounds; supply only the bounds you need |

Aetherfy forwards the filter object to the underlying engine verbatim, with **no key translation**. That means the key names above are the literal wire names in both SDKs, and it is the reason for the JavaScript pitfall documented further down.

## Setting up an Aetherfy collection to filter against

Both language sections below assume this collection exists. Vectors are literal floats so the example runs without an embedding model.

### Python setup

```python
from aetherfy_vectors import AetherfyVectorsClient
from aetherfy_vectors.models import VectorConfig, DistanceMetric, Point

client = AetherfyVectorsClient()

client.create_collection(
    "catalogue",
    VectorConfig(size=4, distance=DistanceMetric.COSINE),
)

client.upsert("catalogue", [
    Point(id=1, vector=[0.10, 0.20, 0.30, 0.40],
          payload={"title": "Wireless mouse", "category": "electronics",
                   "price": 24.99, "in_stock": True}),
    Point(id=2, vector=[0.15, 0.18, 0.33, 0.41],
          payload={"title": "Mechanical keyboard", "category": "electronics",
                   "price": 189.00, "in_stock": True}),
    Point(id=3, vector=[0.12, 0.22, 0.29, 0.38],
          payload={"title": "USB-C hub", "category": "electronics",
                   "price": 39.50, "in_stock": False}),
    Point(id=4, vector=[0.90, 0.10, 0.05, 0.02],
          payload={"title": "Ceramic mug", "category": "kitchen",
                   "price": 12.50, "in_stock": True}),
])
```

### JavaScript setup

```typescript
import { AetherfyVectorsClient, DistanceMetric } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient();

await client.createCollection('catalogue', { size: 4, distance: DistanceMetric.COSINE });

await client.upsert('catalogue', [
  { id: 1, vector: [0.10, 0.20, 0.30, 0.40],
    payload: { title: 'Wireless mouse', category: 'electronics', price: 24.99, in_stock: true } },
  { id: 2, vector: [0.15, 0.18, 0.33, 0.41],
    payload: { title: 'Mechanical keyboard', category: 'electronics', price: 189.00, in_stock: true } },
  { id: 3, vector: [0.12, 0.22, 0.29, 0.38],
    payload: { title: 'USB-C hub', category: 'electronics', price: 39.50, in_stock: false } },
  { id: 4, vector: [0.90, 0.10, 0.05, 0.02],
    payload: { title: 'Ceramic mug', category: 'kitchen', price: 12.50, in_stock: true } },
]);
```

## Filtered search in Aetherfy with Python

A filter goes in the `query_filter` keyword. You can pass a plain dictionary or the `Filter` dataclass — the client accepts both.

```python
from aetherfy_vectors import AetherfyVectorsClient

client = AetherfyVectorsClient()

# must: electronics only.
results = client.search(
    "catalogue",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    limit=10,
    query_filter={
        "must": [
            {"key": "category", "match": {"value": "electronics"}},
        ]
    },
)
for r in results:
    print(r.id, r.payload["title"])
# 1 Wireless mouse
# 2 Mechanical keyboard
# 3 USB-C hub

# must + range: electronics under 50.
results = client.search(
    "catalogue",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    query_filter={
        "must": [
            {"key": "category", "match": {"value": "electronics"}},
            {"key": "price", "range": {"lte": 50}},
        ]
    },
)
for r in results:
    print(r.id, r.payload["title"])
# 1 Wireless mouse
# 3 USB-C hub

# must_not: exclude everything out of stock.
results = client.search(
    "catalogue",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    query_filter={
        "must": [{"key": "category", "match": {"value": "electronics"}}],
        "must_not": [{"key": "in_stock", "match": {"value": False}}],
    },
)
for r in results:
    print(r.id, r.payload["title"])
# 1 Wireless mouse
# 2 Mechanical keyboard

# should: either category qualifies.
results = client.search(
    "catalogue",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    query_filter={
        "should": [
            {"key": "category", "match": {"value": "electronics"}},
            {"key": "category", "match": {"value": "kitchen"}},
        ]
    },
)
print(len(results))  # 4

client.close()
```

## The Aetherfy Filter dataclass in Python

`Filter` lives in `aetherfy_vectors.models`, not in the package root. Its `to_dict()` emits the correct wire keys — `must`, `must_not`, `should` — so the Python path is safe either way.

```python
from aetherfy_vectors import AetherfyVectorsClient
from aetherfy_vectors.models import Filter

client = AetherfyVectorsClient()

f = Filter(
    must=[{"key": "category", "match": {"value": "electronics"}}],
    must_not=[{"key": "in_stock", "match": {"value": False}}],
)

print(f.to_dict())
# {'must': [{'key': 'category', 'match': {'value': 'electronics'}}],
#  'must_not': [{'key': 'in_stock', 'match': {'value': False}}]}

results = client.search(
    "catalogue",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    query_filter=f,
)
for r in results:
    print(r.id, r.payload["title"])
# 1 Wireless mouse
# 2 Mechanical keyboard

client.close()
```

`Filter` has exactly three fields — `must`, `must_not`, `should` — all optional and all defaulting to `None`.

## Filtered search in Aetherfy with JavaScript

The filter goes in the `queryFilter` key of the options object. **Write the clause key as `mustNot`** — the JavaScript SDK translates it to the wire key `must_not` for you.

```typescript
import { AetherfyVectorsClient } from 'aetherfy-vectors';

const client = new AetherfyVectorsClient();

async function main() {
  // must: electronics only.
  let results = await client.search('catalogue', [0.12, 0.19, 0.31, 0.40], {
    limit: 10,
    queryFilter: {
      must: [{ key: 'category', match: { value: 'electronics' } }],
    },
  });
  for (const r of results) console.log(r.id, r.payload.title);
  // 1 Wireless mouse
  // 2 Mechanical keyboard
  // 3 USB-C hub

  // must + range: electronics under 50.
  results = await client.search('catalogue', [0.12, 0.19, 0.31, 0.40], {
    queryFilter: {
      must: [
        { key: 'category', match: { value: 'electronics' } },
        { key: 'price', range: { lte: 50 } },
      ],
    },
  });
  for (const r of results) console.log(r.id, r.payload.title);
  // 1 Wireless mouse
  // 3 USB-C hub

  // mustNot: exclude everything out of stock. The SDK sends it as must_not.
  results = await client.search('catalogue', [0.12, 0.19, 0.31, 0.40], {
    queryFilter: {
      must: [{ key: 'category', match: { value: 'electronics' } }],
      mustNot: [{ key: 'in_stock', match: { value: false } }],
    },
  });
  for (const r of results) console.log(r.id, r.payload.title);
  // 1 Wireless mouse
  // 2 Mechanical keyboard

  // should: either category qualifies.
  results = await client.search('catalogue', [0.12, 0.19, 0.31, 0.40], {
    queryFilter: {
      should: [
        { key: 'category', match: { value: 'electronics' } },
        { key: 'category', match: { value: 'kitchen' } },
      ],
    },
  });
  console.log(results.length); // 4

  client.dispose();
}

main();
```

The filter object is typed, so a `Filter`-annotated variable works without a cast:

```typescript
import type { Filter } from 'aetherfy-vectors';

const queryFilter: Filter = {
  must: [{ key: 'category', match: { value: 'electronics' } }],
  mustNot: [{ key: 'in_stock', match: { value: false } }],
};

const results = await client.search('catalogue', [0.12, 0.19, 0.31, 0.40], { queryFilter });
```

## How each Aetherfy SDK spells the exclusion clause

The wire key is `must_not`. You write your SDK's spelling and it translates:

| SDK | What you write | What Aetherfy sends |
| --- | --- | --- |
| Python | `must_not`, or `Filter(must_not=...)` | `must_not` |
| JavaScript / TypeScript | `mustNot` | `must_not` |

**Each SDK throws on the other's spelling, and names the right one.** Both
validate clauses before sending:

| You write | Python | JavaScript |
| --- | --- | --- |
| `must_not` | accepted | throws — "Write the clause as `mustNot`" |
| `mustNot` | throws — "Write the clause as `must_not`" | accepted |

Aetherfy does not validate the filter body itself, so a clause in the wrong
idiom used to be forwarded and quietly do nothing — no exception, no log line,
just rows you meant to exclude coming back. That failed in **both** languages:
`mustNot` in JavaScript, and a plain dict carrying `mustNot` in Python. If you
have code written against an older SDK, those filters were not filtering; if you
adopted a `must_not` workaround in JavaScript, remove it.

A Python `Filter` dataclass could never carry the wrong spelling — its fields
are `must`, `must_not`, `should` — so passing a `Filter` avoids the question.
Every filter-taking method accepts either a `Filter` or a dict, `count`
included.

## Where else Aetherfy accepts a filter

The same filter object is accepted by four other Aetherfy calls, under different parameter names. The same clause vocabulary applies to every one of them.

| Call | Python parameter | JavaScript option |
| --- | --- | --- |
| `search` | `query_filter` | `queryFilter` |
| `count` | `count_filter` | `countFilter` |
| `scroll` | `scroll_filter` | `scrollFilter` |
| `scroll_iter` / `scrollIter` | `scroll_filter` | `scrollFilter` |
| `delete` | `points_selector` (pass the filter object directly) | `pointsSelector` |

For deleting by filter see [Retrieve, delete, count](/examples/retrieve-delete-count); for paging a filtered set see [Batch upsert & pagination](/examples/batch-and-pagination). The filter vocabulary itself is documented in full at [Filtering](/vectors/filtering).
