# Collections

> filter, some, every, rank, find and compare over lists. Which cost one request and which cost N.

Section: Concepts · HTML: https://jevascript.org/docs/collections · Markdown: https://jevascript.org/docs/collections.md

Batching sends many questions about **one** state. Collections are the other axis: one question about **many** items. Because a request carries a single state, most collection operations cost one request per item. Two of them do not.

## Cost at a glance

| Operation | Requests | Use when |
|---|---|---|
| `find(items, condition)` | **1** | You want the single best match, or none. |
| `compare(a, b, { by })` | **1** | You want to know which of two is better. |
| `filter(items, condition)` | N | You need every match. |
| `some` / `every` | N | Built on `filter`. |
| `rank(items, { by })` | N | You need the whole order. |

N-request operations run up to `concurrency` items at a time, 8 by default.

## `find()` — the best item, in one request

```ts
const dupe = await semantic.find(openBugs, "This report describes the same bug as the new one.", {
  label: (bug) => `${bug.title}\n${bug.body}`,
})
// the matching bug, or undefined
```

The items become the options of a single choice. Alongside it travels an existence check: *does any candidate actually satisfy the condition?* That second question is not optional. Choice probabilities always sum to 1, so something always wins, including when nothing fits. Without the existence check, an empty match returns confident nonsense. `find` returns `undefined` when the existence probability is at or below `threshold`.

If there are more items than the provider's `maxChoiceOptions` (255 for Jev), `find` falls back to `rank` and returns the top item if it clears the threshold.

## `compare()` — two values, one criterion

```ts
const better = await semantic.compare(draftA, draftB, {
  by: "Answers the customer's actual question directly.",
})
// "left" | "right" | "equal"
```

Both values are sent in one state under `left` and `right`. Use `label` to control how each is rendered.

## `filter()`, `some()`, `every()`

```ts
const complaints = await semantic.filter(messages, "This message is a complaint.", {
  falseWhen: "A question, a feature request, or praise.",
  concurrency: 4,
})

await semantic.some(messages, "This message mentions a competitor.")   // boolean
await semantic.every(messages, "This message is in English.")           // boolean
```

Each item is judged in its own request against the condition and kept when its probability exceeds `threshold`.

## `rank()` — order by fit

```ts
const ordered = await semantic.rank(candidates, {
  by: "This candidate fits the query.",
  context: query,
})
// [{ item, score }, ...] sorted by score, descending
```

`context` is shared state every candidate is judged against, sent with each one as `{ context, candidate }`. The same operation is available on a context, with the wrapped value as the context:

```ts
const ordered = await semantic(query).rank(candidates, { by: "This candidate fits the query." })
```

## Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `(item, index) => string` | — | How to render an item as state. Strings pass through; other values default to JSON. |
| `concurrency` | `number` | `8` | Items evaluated at the same time for N-request operations. |
| `threshold` | `number` | `0.5` | Probability above which an item counts as matching. |
| `trueWhen` | `string` | — | What clearly counts (`filter`, `rank`). |
| `falseWhen` | `string` | — | What clearly does not (`filter`, `rank`). |
| `cache` | `string \| number` | — | TTL per item answer. |
| `timeoutMs` | `number` | — | Per-request timeout. |
| `samples` | `number` | — | Rounds per item. Multiplies the request count. |
| `provider` | `SemanticProvider` | — | Override the provider for this call. |

## When items are small: pack them yourself

If each item is short and there are not too many, putting them all into one state and asking one question per item is a single request instead of N. The [RAG gate](https://jevascript.org/docs/scenarios/rag-gate) scenario does exactly this with twelve passages:

```ts
const context = semantic({ question, passages: { p0: "...", p1: "...", p2: "..." } })

const verdicts = await context.batch({
  p0: is("Passage p0 contains information that helps answer the question."),
  p1: is("Passage p1 contains information that helps answer the question."),
  p2: is("Passage p2 contains information that helps answer the question."),
})
```

The trade-off is context size: every passage is in every question's state. It pays off while the whole set fits comfortably in `maxStateTokens`.

## Where they live

Collections are available three ways, all with the same signatures:

```ts
import { filter, find } from "jevascript"       // bound to the module-level instance
semantic.filter(items, "...")                    // on a createSemantic() instance
semantic(query).rank(items, { by: "..." })       // rank, with the wrapped value as context
```
