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:
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
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
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.
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.
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.
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:
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; for paging a filtered set see Batch upsert & pagination. The filter vocabulary itself is documented in full at Filtering.