Skip to Content
Raw

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.

ClauseMeaningLogical operator
mustEvery condition in the array holdsAND
shouldAt least one condition in the array holdsOR
must_notNo condition in the array holdsNOT

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:

ConditionShapeUse
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:

SDKWhat you writeWhat Aetherfy sends
Pythonmust_not in a dict, or Filter(must_not=...)must_not
JavaScript / TypeScriptmustNotmust_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 writePythonJavaScript
must / shouldacceptedaccepted
must_notacceptedthrows — “Write the clause as mustNot; the SDK translates it to the wire key must_not.”
mustNotthrows — “Write the clause as must_not; mustNot is the JavaScript SDK’s spelling.”accepted
anything elsethrowsthrows

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.

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.

OperationPython parameterJavaScript optionREST body key
Searchquery_filterqueryFilterfilter
Countcount_filtercountFilterfilter
Scrollscroll_filterscrollFilterfilter
Delete pointspoints_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.

Last updated on