---
slug: examples/first-collection
title: First collection - upsert and search
kind: howto
surface: vectors
summary: Complete Python and JavaScript examples that create an Aetherfy collection, upsert points with payloads, and run a vector search, including the point-id rules and the returned SearchResult shape.
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
---

# First collection: upsert and search

The smallest useful Aetherfy program: create a collection, write points into it, and search it. Both versions below are complete files. The vectors are written out as literal floats so the example runs with no embedding model installed; in production you would produce them with an embedding model — see [Bring your own embeddings](/examples/embeddings).

Before you start, install the SDK and export a key from [https://app.aetherfy.com/dashboard/settings/api-keys](https://app.aetherfy.com/dashboard/settings/api-keys):

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

## Creating an Aetherfy collection in Python

Install with `pip install aetherfy-vectors` (Python >= 3.9).

`VectorConfig` and `DistanceMetric` are not re-exported from the package root, so import them from `aetherfy_vectors.models`.

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

# Reads AETHERFY_API_KEY from the environment. timeout is in seconds.
client = AetherfyVectorsClient(timeout=30.0)

collection = client.create_collection(
    "products",
    VectorConfig(size=4, distance=DistanceMetric.COSINE),
    description="Product catalogue for the storefront",
)

print(collection.name)             # products
print(collection.config.size)      # 4
print(collection.config.distance)  # DistanceMetric.COSINE
```

`size` is the dimension of every vector you will store in the collection and cannot be changed later. It must match the output dimension of whatever produces your vectors.

Re-running this file is safe. On Aetherfy, re-creating a collection with the **same** configuration is idempotent and returns 200, so the script calls create unconditionally rather than guarding it with `collection_exists`. Only a create that asks for a *different* configuration under a name already in use fails, with 409 `COLLECTION_NAME_TAKEN`.

## Upserting points into an Aetherfy collection in Python

```python
points = [
    Point(id=1, vector=[0.10, 0.20, 0.30, 0.40],
          payload={"title": "Wireless mouse", "category": "electronics", "price": 24.99}),
    Point(id=2, vector=[0.15, 0.18, 0.33, 0.41],
          payload={"title": "Mechanical keyboard", "category": "electronics", "price": 89.00}),
    Point(id=3, vector=[0.90, 0.10, 0.05, 0.02],
          payload={"title": "Ceramic mug", "category": "kitchen", "price": 12.50}),
]

ok = client.upsert("products", points)
print(ok)  # True
```

`upsert` returns a plain `bool` in the Aetherfy Python SDK — it does not return the written points. Writing a point with an id that already exists replaces it.

## Searching an Aetherfy collection in Python

```python
results = client.search(
    "products",
    query_vector=[0.12, 0.19, 0.31, 0.40],
    limit=2,
    with_payload=True,
)

for r in results:
    print(r.id, r.payload)

# 1 {'title': 'Wireless mouse', 'category': 'electronics', 'price': 24.99}
# 2 {'title': 'Mechanical keyboard', 'category': 'electronics', 'price': 89.0}

client.close()
```

Each element is a `SearchResult` dataclass with the fields listed in the shape table below. `score` is a float produced by the distance metric you chose for the collection; its magnitude depends entirely on your vectors, so do not hard-code a threshold without measuring against your own data.

## Creating an Aetherfy collection in JavaScript

Install with `npm install aetherfy-vectors` (Node >= 20). Import everything from the package root; there is no `aetherfy-vectors/…` subpath export.

Save this as `main.mjs`, or set `"type": "module"` in your `package.json`.

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

// Reads AETHERFY_API_KEY from the environment. timeout is in milliseconds.
const client = new AetherfyVectorsClient({ timeout: 30000 });

async function main() {
  const collection = await client.createCollection(
    'products',
    { size: 4, distance: DistanceMetric.COSINE },
    'Product catalogue for the storefront',
  );

  console.log(collection.name);            // products
  console.log(collection.config.size);     // 4
  console.log(collection.config.distance); // Cosine
}

main();
```

Note the argument order difference: the Aetherfy JS `createCollection(collectionName, vectorsConfig, description?, regions?)` takes the description third and has **no** separate `distance` parameter — the distance lives inside the vectors config. The Python `create_collection(collection_name, vectors_config, distance=None, description=None, regions=None)` accepts an optional standalone `distance`.

## Upserting points into an Aetherfy collection in JavaScript

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

const client = new AetherfyVectorsClient();

async function main() {
  const ok = await client.upsert('products', [
    { id: 1, vector: [0.10, 0.20, 0.30, 0.40],
      payload: { title: 'Wireless mouse', category: 'electronics', price: 24.99 } },
    { id: 2, vector: [0.15, 0.18, 0.33, 0.41],
      payload: { title: 'Mechanical keyboard', category: 'electronics', price: 89.00 } },
    { id: 3, vector: [0.90, 0.10, 0.05, 0.02],
      payload: { title: 'Ceramic mug', category: 'kitchen', price: 12.50 } },
  ]);

  console.log(ok); // true
}

main();
```

`upsert` resolves to a `boolean` in the Aetherfy JS SDK, matching the Python behaviour.

## Searching an Aetherfy collection in JavaScript

Everything after the query vector goes in a single options object, unlike the positional/keyword mix in Python.

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

const client = new AetherfyVectorsClient();

async function main() {
  const results = await client.search('products', [0.12, 0.19, 0.31, 0.40], {
    limit: 2,
    withPayload: true,
  });

  for (const r of results) {
    console.log(r.id, r.payload);
  }
  // 1 { title: 'Wireless mouse', category: 'electronics', price: 24.99 }
  // 2 { title: 'Mechanical keyboard', category: 'electronics', price: 89 }

  client.dispose();
}

main();
```

## The full Aetherfy search options

| Python keyword | JavaScript option key | Default | Meaning |
| --- | --- | --- | --- |
| `limit` | `limit` | `10` | Maximum number of results |
| `offset` | `offset` | `0` | Skip this many results |
| `query_filter` | `queryFilter` | `None` / undefined | Payload filter, see [Filtered search](/examples/filtered-search) |
| `with_payload` | `withPayload` | `True` / `true` | Include the stored payload |
| `with_vectors` | `withVectors` | `False` / `false` | Include the stored vector |
| `score_threshold` | `scoreThreshold` | `None` / undefined | Drop results below this score |
| `search_params` | `searchParams` | `None` / undefined | Engine-level search tuning, see [Search tuning](/vectors/search-tuning) |

## The Aetherfy response shapes

These are the model definitions, not a sample. Python uses snake_case dataclasses; JavaScript uses camelCase interfaces.

`Point` — what you send to `upsert`:

| Field | Python type | JavaScript type | Required |
| --- | --- | --- | --- |
| `id` | `Union[str, int]` | `string \| number` | yes |
| `vector` | `List[float]` | `number[]` | yes |
| `payload` | `Optional[Dict]` | `Record<string, unknown>` (optional) | no |

`SearchResult` — what `search` returns:

| Field | Python type | JavaScript type | Present when |
| --- | --- | --- | --- |
| `id` | `Union[str, int]` | `string \| number` | always |
| `score` | `float` | `number` | always |
| `payload` | `Optional[Dict]` | optional | `with_payload` / `withPayload` is true |
| `vector` | `Optional[List[float]]` | optional | `with_vectors` / `withVectors` is true |

`Collection` — what `create_collection` and `get_collection` return. **The point-count field is named differently in each SDK.**

| Python field | JavaScript field | Type |
| --- | --- | --- |
| `name` | `name` | string |
| `config` | `config` | `VectorConfig` — `size` (int) and `distance` |
| `description` | `description` | string, optional |
| `points_count` | `pointsCount` | number, optional; `None`/undefined when the server does not report it |
| `status` | `status` | optional |
| `regions` | `regions` | optional; only populated on a plan with more than one region |

## The Aetherfy distance metrics

`DistanceMetric` is an enum in both SDKs. The wire values are capitalised, and one of them is not what Aetherfy stores.

| Enum member | Wire value sent | Value Aetherfy stores and returns |
| --- | --- | --- |
| `DistanceMetric.COSINE` | `Cosine` | `Cosine` |
| `DistanceMetric.EUCLIDEAN` | `Euclidean` | **`Euclid`** |
| `DistanceMetric.DOT` | `Dot` | `Dot` |
| `DistanceMetric.MANHATTAN` | `Manhattan` | `Manhattan` |

`EUCLIDEAN` does **not** round-trip. Aetherfy normalises distance names case-insensitively and stores both `euclid` and `euclidean` as `Euclid`, so a collection created with `DistanceMetric.EUCLIDEAN` reads back `config.distance == "Euclid"` — comparing it to `'Euclidean'` silently fails. `Cosine`, `Dot` and `Manhattan` come back unchanged.

Pick the metric your embedding model was trained for. Cosine is the usual choice for normalised text embeddings.

## Point id rules in Aetherfy

Both Aetherfy SDKs validate ids client-side, before any request goes out. Only two forms are accepted:

| Form | Accepted | Example |
| --- | --- | --- |
| Unsigned integer, `0 <= id <= 2**53 - 1` | yes | `1`, `9007199254740991` |
| Canonical UUID string | yes | `"3f1c2a7e-9b1e-4f2a-8c5d-1a2b3c4d5e6f"` |
| UUID without hyphens (32 hex) | yes | `"3f1c2a7e9b1e4f2a8c5d1a2b3c4d5e6f"` |
| Braced UUID | yes | `"{3f1c2a7e-9b1e-4f2a-8c5d-1a2b3c4d5e6f}"` |
| URN UUID | yes | `"urn:uuid:3f1c2a7e-9b1e-4f2a-8c5d-1a2b3c4d5e6f"` |
| Arbitrary slug | **no** | `"memory-001"` raises `ValidationError` |
| Negative integer | **no** | `-1` raises `ValidationError` |
| Boolean (Python) | **no** | `True` is rejected, it is not treated as `1` |

There is no coercion in either direction: an id you sent as an integer comes back as an integer, and an id you sent as a UUID string comes back as a string. If your domain keys are slugs such as `order-12345`, store the slug in the payload and use a UUID or an integer as the point id.

```python
# Storing a domain key that is not a legal point id.
import uuid

client.upsert("products", [
    Point(
        id=str(uuid.uuid4()),
        vector=[0.10, 0.20, 0.30, 0.40],
        payload={"sku": "memory-001", "title": "Wireless mouse"},
    )
])
```

```typescript
// The JavaScript equivalent, using the built-in crypto.randomUUID().
await client.upsert('products', [
  {
    id: crypto.randomUUID(),
    vector: [0.10, 0.20, 0.30, 0.40],
    payload: { sku: 'memory-001', title: 'Wireless mouse' },
  },
]);
```

## Where Aetherfy puts this collection

Collection placement depends on your plan. On the Free and Starter plans a collection lives in a single region. Replication across more than one region begins at the plan named Performance, and only then does the `regions` argument to `create_collection` / `createCollection` and the `regions` field on `Collection` carry more than one entry. See [Regions & replication](/platform/regions).

## Next steps with Aetherfy

- Narrow results by payload: [Filtered search](/examples/filtered-search)
- Write more than a handful of points at a time: [Batch upsert & pagination](/examples/batch-and-pagination)
- Remove or count what you wrote: [Retrieve, delete, count](/examples/retrieve-delete-count)
