> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pool.day/llms.txt
> Use this file to discover all available pages before exploring further.

# Indexing

> Pool builds its index on the device, in the background. It answers from the first capture it reads, and understands what photos show once its models are installed.

## What happens to each capture

* Apple's Vision reads text, QR codes and barcodes, labels what the image shows and makes a visual fingerprint.
* Once the engine's models are installed, each image also gets a vector from the same image and text model the Pool app searches with, so a search can match what a photo shows.
* A privacy gate holds back identity documents, payment cards, credentials and health records.
* Fixed rules turn what was read into links, objects and leads such as “a product page with a price”.
* On iOS 26 with Apple Intelligence, the system language model names the product, place, event, recipe, book, music, movie, article or post. Nothing leaves the phone.
* On devices without Apple's model, that naming step can run through [cloud assist](https://docs.pool.day/cloud-assist.md) when you set `namesWithCloudAssist` and the person allows cloud assist. Otherwise those devices index without named things, and search, links and objects still work. Indexing never spends cloud assist units on a device that has Apple's model.

## Progress

```swift theme={null}
for await status in await pool.sources.statusUpdates() {
    progressBar.value = status.progress   // 0 to 1
    label.text = "\(status.indexedAssets) of \(status.knownAssets)"
}
```

Newest captures are indexed first, so recent questions work within seconds. `status.phase` is one of `waitingForGrant`, `waitingForPermission`, `indexing`, `paused` or `idle`. Indexing slows down while the device is hot or in Low Power Mode and keeps going.

### In detail

`status.detail` says where a run is, in words you can show:

```swift theme={null}
let detail = status.detail
switch detail.step {               // starting, loadingModels, readingLibrary, indexing, finishing
case .indexing:
    label.text = "\(detail.indexed) of \(detail.total)"
    if let seconds = detail.secondsRemaining { eta.text = "About \(Int(seconds / 60)) min left" }
default:
    break
}
searchField.isEnabled = detail.searchable   // true once the first slice is indexed
```

| Field | Meaning |
| - | - |
| `step` | `starting`, `loadingModels`, `readingLibrary`, `indexing` or `finishing`. |
| `indexed`, `total`, `fraction` | Captures read, of those the index options and grants cover. `fraction` is 0 to 1. |
| `searchable` | True once search can answer from what is indexed so far. |
| `waitingFor` | What indexing waits on while it carries on with the rest: `places` (an allowed network for place names), `textModel` (Apple Intelligence or cloud assist to name things), `people` (faces still to group), `coolingDown` (the device is warm or in Low Power Mode), `retryingBatch` (a failed pass, tried again shortly) and `events` (events found, waiting for a language model to read them). |
| `skipped` | Held back as sensitive, or outside the index focus. |
| `failed` | Captures PhotoKit gave no image for. |
| `skippedSensitive` | Held back as sensitive, by kind: `personalData`, `health`, `nudity`, `financialInformation` and `authentication`. Counts only, and every kind is always present. There is no on-device nudity detector yet, so that count stays 0. |
| `secondsRemaining` | At the pace of the run that is going. `nil` when no run is going. |
| `searchUpdatedAt` | When search last saw a change. |
| `background` | `continuing` (iOS 26 carries the first index on in the background with progress the person sees), `endedByIOS` (iOS ended the background time; indexing carries on when the app runs again) or `processingTask`. `nil` in the foreground. |

`status.lastRun` says how the last run ended: `reason` is `completed`, `stopped` (your app paused, or a grant changed during the run), `endedByIOS`, `failed`, `interrupted` (the app was closed) or `waiting` (on photo access, a grant or a refused key), with `finishedAt`, `nextAttempt` when PoolKit will try again by itself, and the `indexed` and `failed` counts of that run.

### Show captures as they are indexed

`batches(using:)` streams newly indexed captures in slices of about a hundred, so you can fill a screen while the first index runs.

```swift theme={null}
for await batch in try await pool.sources.batches(using: grant) {
    for item in batch.items {
        grid.append(item.id, mediaType: item.mediaType, date: item.date, labels: item.labels)
    }
}
```

It needs the `observe` operation and covers only the sources in the grant. Each batch has an id (`bat_…`) and items with the `SourceAsset` id, the media type, the date and the labels. Labels are filled only when the grant also has `evidence`. Sensitive and out-of-focus captures are left out, and shots of one moment (same source, 10 seconds or less apart, near-identical images) appear once. The stream ends when the grant stops covering anything, for example when it is revoked.

### Why it paused

`paused` means there is work left and Pool is not doing it. `status.pauseReason` is `failed` when listing, reading or storing went wrong, with a short `failureDescription` for your logs, or `interrupted` when a background task ran out of time. Pool starts again by itself in both cases. `byApp` means your app called `pause()`.

## The engine's models

The models are about 375 MB. They are not in the package. PoolKit asks Pool's service for the list with your app key, downloads the files after the first indexing pass has listed the library, and checks each one against its SHA-256 before using it. Until then search, queries, facets and recurring things work from text, labels and things.

```swift theme={null}
var configuration = PoolConfiguration(appKey: "pk_live_…")
configuration.modelDownloads = .unmeteredOnly   // the default: Wi-Fi and other unmetered networks
// .anyNetwork also allows cellular. .never keeps PoolKit on Apple's frameworks alone.

for await state in await pool.sources.modelStateUpdates() {
    switch state {
    case .downloading(let fraction): progress.value = fraction
    case .ready: break                // search now matches what photos show
    case .notInstalled, .disabled, .unavailable: break
    }
}
```

A failed download is tried again later and never stops indexing. `status.visuallyIndexedAssets` counts the photos that already have a vector, and `status.models` carries the same state as the stream.

## Choose what is indexed

`IndexOptions` says what the index covers and how it runs. Set it when you open the client, and change it at any time.

```swift theme={null}
var options = IndexOptions()
options.window = .growing(from: .months(6))   // six months back, then everything new
options.mediaTypes = [.screenshots, .photos]  // leave videos out
options.focus = .food                          // only food photos and recipe or restaurant screenshots
options.maxPhotos = 20_000                     // the newest 20,000 at most
options.sendsRunReports = true                 // see Keys, apps and run reports

let pool = try await PoolClient.open(
    configuration: PoolConfiguration(appKey: "pk_live_…", indexOptions: options)
)

// Later, from a settings screen:
options.places = false
try await pool.sources.setIndexOptions(options)
```

| Option | Default | Meaning |
| - | - | - |
| `window` | `.growing(from: .months(3))` | `.everything` for the whole library; `.growing(from:)` starts that far back from the first index and keeps everything new; `.sliding(_:)` always covers the last span, so older captures leave the index. Spans are `.days`, `.weeks`, `.months` or `.years`. |
| `mediaTypes` | photos, screenshots, videos | Kinds of capture to index, within `PoolConfiguration.sources` and the grants. `sources` leaves videos out by default, so add `.videos` there too. Videos are read from a still frame. Screen recordings are never indexed. |
| `savedVideos` | `true` | Whether videos saved from other apps are indexed. Photos does not say where a video came from, so `false` leaves out every video without a location. |
| `focus` | `.everything` | `.food` reads only photos Vision labels as food or drink and screenshots of recipes or restaurants. The rest is listed but not read and never returned. |
| `maxPhotos` | no limit | The most captures to index, newest first. |
| `allowsCellular` | `false` | Whether place lookups may use cellular data. Model downloads follow `PoolConfiguration.modelDownloads`. |
| `updatesAutomatically` | `true` | When `false`, the index changes only when you call `indexNow()` or a grant changes. |
| `sendsRunReports` | `false` | Send a short report after each run: counts, timings and how it ended, nothing read from a photo. |
| `backgroundProgress` | "Preparing your photos" | The title and subtitle iOS 26 shows while a first index runs in the background. A `nil` subtitle shows the progress. |
| `places` | `true` | Whether places are indexed when a grant includes `.placesVisited` or `.locations`. See [Places and visits](https://docs.pool.day/places.md). |
| `people` | `false` | Whether faces are grouped when a grant includes `.people`. |
| `namesWithCloudAssist` | `false` | Whether indexing may name things with cloud assist on a device where Apple's on-device model cannot run. Needs `allowsCloudAssist` and a grant that allows it. |

Changes apply in place, and the next pass starts at once:

* Turning `places` off deletes every coordinate, city and visit straight away.
* A new `focus` hides or reads again what it changes. Switching back to `.everything` reads the rest.
* A new `window` is anchored from now.

The default window changed in 0.2.0. Before, the whole library was indexed; pass `.everything` to keep that. `PoolConfiguration.indexWindow` and `maximumAssets` still work and set the same options.

```swift theme={null}
var configuration = PoolConfiguration(appKey: "pk_live_…")
configuration.sources = [.screenshots]
configuration.allowsNetworkDownloads = false  // skip photos that are only in iCloud
```

## Pause and resume

```swift theme={null}
await pool.sources.pause()    // stops indexing and model downloads
await pool.sources.resume()
let paused = await pool.sources.isPaused
```

A pause survives relaunches. Queries keep answering from what is already indexed.

## While the app is closed

Register the background tasks once, before the app finishes launching:

```swift theme={null}
// In application(_:didFinishLaunchingWithOptions:) or your App's init.
PoolSources.registerBackgroundTasks(identifier: "com.example.app.pool.index")
```

Then list these under `BGTaskSchedulerPermittedIdentifiers` in Info.plist, and add `processing` to `UIBackgroundModes`. PoolKit uses only the identifiers that are listed.

| Identifier | What it does |
| - | - |
| The one you pass, such as `com.example.app.pool.index` | A processing task that carries on the first index while the app is closed. |
| `<bundle ID>.pool.update` | Keeps an indexed library current: new captures, deletions and changed grants. |
| `<bundle ID>.pool.continue.*` | iOS 26: lets the first index carry on right after the person leaves the app, with progress iOS shows them. |

```xml theme={null}
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.app.pool.index</string>
    <string>com.example.app.pool.update</string>
    <string>com.example.app.pool.continue.*</string>
</array>
<key>UIBackgroundModes</key>
<array>
    <string>processing</string>
</array>
```

With a `nil` identifier, `registerBackgroundTasks()` reads it from the `PoolKitBackgroundTaskIdentifier` Info.plist key. It returns `false` when iOS refuses the registration, for example when an identifier is not listed. Registering does not need a client: when a task starts, it waits up to 20 seconds for your app to open one, and otherwise asks again in 10 minutes. So open the client early in launch. `pool.sources.registerBackgroundIndexing(identifier:)` on an open client still works and does the same.

How it runs:

* **Leaving the app during the first index.** On iOS 26, PoolKit asks iOS to continue the work and iOS shows `IndexOptions.backgroundProgress`: your title, and "n of m" or your own subtitle. Before iOS 26, or when iOS says no, PoolKit asks for the extra time iOS grants to finish the slice it is reading. `status.detail.background` is `continuing` in both cases.
* **First index unfinished.** The next processing run is requested a minute out, and `detail.background` is `processingTask` while it runs.
* **Once indexed.** Update runs are requested every 15 minutes. iOS decides when they actually run.
* **Ended by iOS.** `detail.background` and `lastRun.reason` are `endedByIOS`, and the next run carries on where this one stopped.
* **Failed.** The next background attempt waits 10 minutes.

To schedule the work yourself instead, call `pool.sources.indexNow()` from your own task.

## Storage

The index is a SQLite file in your app's Application Support folder, protected by iOS file protection and excluded from backups. It holds derived text, labels, fingerprints and vectors, never images. The models are stored next to it. `pool.sources.rebuildIndex()` deletes and rebuilds the index and keeps the grants.

```swift theme={null}
await pool.sources.pause()           // keep the index empty afterwards
try await pool.sources.deleteIndex()
```

`deleteIndex()` deletes the index and everything derived from the captures, and keeps the grants, the index options and the downloaded models. While a grant is active, indexing starts again from the sources; call `pause()` first to keep it empty.

## Erase everything

```swift theme={null}
try await pool.reset()
```

Deletes the index, every grant, the downloaded models and the install identifier. Call it when the person signs out or asks you to delete their data. The client stays usable and starts empty.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.