# Bean & Bark: search by taste, not by name

Every chapter so far answered a question about something you could *name* — a customer, a region, a status. The last question in this story has no name in it at all: *"like that one, but brighter."* No `WHERE` clause matches a resemblance. This chapter builds the one kind of index that can — and closes the series where it started: with a customer, a question, and a query.

**You'll learn**

- See why **similarity is not equality** — no attribute filter or GSI can rank by resemblance
- Put a preference into numbers a roaster already uses: a small, **interpretable taste vector**
- Build the door: **`CREATE VECTOR INDEX`** — the on-demand requirement, the double-run confirmation, and the backfill you wait out
- Walk through it: a **`VECTOR_DISTANCE`** top-k search, and the deliberately narrow `WHERE` that keeps it native

> **1:56pm — the buyer at two:** **Nadia:** *"the hotel group is back at 2. they loved Midnight Decaf but want something brighter and less bitter for the lobby bar. can we walk in with the three closest things we roast?"*

## The question no filter can hold

First instinct: filter for it. `RoastLevel < 4`? That's a threshold pretending to be a preference — and on the base table it's a `Scan` anyway. A GSI on `RoastLevel`? A GSI gives a *known attribute* another door; it can order coffees by roast, but it can't rank them by *resemblance to Midnight Decaf*. The buyer didn't give you an attribute value. They gave you a **direction from a reference point** — and that's a geometry problem, not a lookup.

The geometry is already on paper. The roasters score every product on the cupping sheet: **Body, Acidity, Sweetness, Roast**, each 0–1. Written as four numbers, Midnight Decaf is `[0.70, 0.35, 0.55, 0.85]` — full body, low acidity, dark roast. That four-number list is a **vector**, and "tastes similar" becomes "sits nearby". Nobody generated these with a model; the roasting team authored them, the way they always have. DynoStudio validates and searches vectors — it never invents them.

## Build the door

```sql
CREATE VECTOR INDEX "TasteIndex" ON "BeanAndBark" (TasteProfile)
  DIMENSIONS 4
  DISTANCE EUCLIDEAN
  PARTITION BY (Category)
  FILTER BY (InStock)
  PROJECT (Sku, Name, RoastLevel, TastingNotes);
```

- **`DIMENSIONS 4`, `DISTANCE EUCLIDEAN`** — four cupping axes, and straight-line distance between profiles, where intensity matters: a `0.9` roast really is farther from `0.3` than `0.5` is.
- **`PARTITION BY (Category)`** — Bean & Bark sells coffee *and* dog treats. The partition becomes a search-schema field every search must pin with equality, so a tasting flight can never fetch a biscuit.
- **`FILTER BY (InStock)`** — an optional equality filter inside the native search; recommending something you can't ship is worse than no recommendation.
- **`PROJECT (Sku, Name, RoastLevel, TastingNotes)`** — the projection is a query contract: a search returns only projected fields (plus one aliased score). Project what the meeting needs.

Then the lifecycle the studio makes you respect. The table must be **on-demand** — `BeanAndBark` has been since the day you created it. The editor completes every clause from the live `DescribeTable` descriptor, and the create is **double-run confirmed**: the first Run validates and arms the statement, the second unchanged Run dispatches `UpdateTable.VectorIndexUpdates.Create`. And then — you wait. The index **backfills asynchronously**, and every search against it **refuses until it's `ACTIVE`** with backfill complete. Declaring the door doesn't open it; the Background Tasks panel tracks it, and at 1:58pm it flips ready.

## Walk through it

Now translate the buyer. Start from Midnight Decaf `[0.70, 0.35, 0.55, 0.85]`. *Brighter* means acidity up: `0.35 → 0.80`. *Less bitter* means roast down: `0.85 → 0.30`. Body and sweetness they loved — leave them. The query vector **is the conversation**, written in the roasters' own axes:

```sql
SELECT Sku, Name, RoastLevel, TastingNotes,
       VECTOR_DISTANCE(TasteProfile, [0.70, 0.80, 0.55, 0.30]) AS closeness
FROM "BeanAndBark"."TasteIndex"
WHERE Category = 'COFFEE' AND InStock = true
ORDER BY VECTOR_DISTANCE(TasteProfile, [0.70, 0.80, 0.55, 0.30]) ASC
LIMIT 5
```

### The five nearest coffees to what they described

A native top-k search over the taste index — pinned to coffee, filtered to what can ship, nearest profile first.

```sql
SELECT Sku, Name, RoastLevel, TastingNotes,
       VECTOR_DISTANCE(TasteProfile, [0.70, 0.80, 0.55, 0.30]) AS closeness
FROM "BeanAndBark"."TasteIndex"
WHERE Category = 'COFFEE' AND InStock = true
ORDER BY VECTOR_DISTANCE(TasteProfile, [0.70, 0.80, 0.55, 0.30]) ASC
LIMIT 5
```

_Executes as:_ SearchVectors · TasteIndex · TopK 5 · Category HASH = COFFEE · InStock inline filter · eventually consistent · byte-metered

- The score arrives from the service and needs its explicit alias; `EUCLIDEAN` reads nearest-first with `ASC`.
- `LIMIT` is required (1–100) — a similarity search is always "the k nearest", never "everything within earshot".
- The bill is metered in request bytes, not RCUs — a different door with a different meter, disclosed like every other run.

_Quote this example:_ https://dynostudio.dev/docs/dynostudio-bean-bark-taste-search/#taste-topk

Top of the list: **Sunrise Ridge**, a washed Ethiopian — bright, medium-bodied, roasted light. The tasting notes read like the buyer's sentence run backwards. Nadia walks in at two with three coffees and the story of why each one is *close* — and the lobby-bar account signs that week. The last query of the series didn't match a value. It understood a description.

> **The narrow WHERE is the feature:** You'll be tempted to add `AND RoastLevel < 4` — and the studio will refuse it before the wire. Vector `WHERE` accepts only the partition equality and `FILTER BY` equalities: no ranges, no `OR`, no `contains`. That's not a gap. The roast preference already lives *in the vector* — encoding it twice as a filter would fight the geometry with a threshold. Equality pins the search space; the vector expresses the taste.

**Recap**

- A GSI answers a **known attribute** through another door; a **vector index** answers *"something like this"* — similarity is not equality, and no filter ranks by resemblance.
- A vector doesn't have to be an opaque embedding: **four interpretable cupping axes** made the search explainable — you can *see* why two coffees are close — and the roasters authored them without a model in sight.
- **Creating the index is a lifecycle**, not a statement: on-demand table, live-descriptor validation, double-run confirmation, asynchronous backfill — and searches refuse until it's `ACTIVE`.
- The search itself is **one native `SearchVectors` request**: partition pinned by equality, optional inline filters, required top-k `LIMIT`, service-computed score, byte-metered and disclosed.

**Similarity check**

**1. Why couldn't a GSI on `RoastLevel` answer the buyer?**

- A GSI looks up known attribute values — it can't rank the catalog by resemblance to a reference coffee ✓ — the buyer's request is a distance question across all four taste axes at once. An index on one attribute can sort by that attribute; only a vector index orders by overall closeness.
- GSIs don't work on numeric attributes — they work fine on numbers — that's not the gap. The gap is that "similar to this one" isn't a value any single attribute holds.
- It could, with enough filters — stacking thresholds (roast < 4 AND acidity > 0.7 …) draws a box, not a neighborhood — it can't say which in-box coffee is *closest*, and the box misses near-matches just outside it.

**2. You run the search moments after `CREATE VECTOR INDEX` succeeds. What happens?**

- It refuses — the index is still backfilling, and searches wait for ACTIVE with backfill complete ✓ — the create is accepted immediately but fulfilled asynchronously. Declaring the door doesn't open it; readiness is tracked, and the search path stays closed until the index can answer honestly.
- It searches whatever has been backfilled so far — a partial index would silently miss products — exactly the quiet wrongness the refusal exists to prevent.
- It falls back to a table Scan — nothing reroutes silently in the studio — a vector search either runs natively against a ready index or refuses with the reason.

**Try it yourself**

**1. Translate another buyer**

A café wants something *"like Sunrise Ridge `[0.55, 0.80, 0.60, 0.30]` but sweeter and with more body — their customers found it thin."* Write the search.

_Hint:_ Start from the reference profile and move the axes the sentence names: sweetness up, body up, leave the rest.

_Solution:_ Body 0.55 → 0.80 and sweetness 0.60 → 0.85; acidity and roast stay. The vector is the sentence, translated axis by axis.

```sql
SELECT Sku, Name, RoastLevel, TastingNotes,
       VECTOR_DISTANCE(TasteProfile, [0.80, 0.80, 0.85, 0.30]) AS closeness
FROM "BeanAndBark"."TasteIndex"
WHERE Category = 'COFFEE' AND InStock = true
ORDER BY VECTOR_DISTANCE(TasteProfile, [0.80, 0.80, 0.85, 0.30]) ASC
LIMIT 5
```

**2. Retire a door without touching the room**

A year from now `TasteIndex` is superseded by a v2 with a fifth axis. Write the statement that removes the old read path — and say what happens to the products.

_Hint:_ Dropping an index removes a way of reading, never the data being read.

_Solution:_ Nothing happens to the products — the base table and every item are unchanged. Searches against the dropped index fail as deletion begins; the data keeps living in the table, reachable through every other door.

```sql
DROP VECTOR INDEX "TasteIndex" ON "BeanAndBark";
```

> **The end of the story — and where it leaves you:** When this story began, the whole company fit in one afternoon and one table. It still runs on that table — what grew is the set of *doors*: a GSI for the standing question, a job path past the preview, a search projection for prose, a watermark split for history, and a taste index for resemblance. Every one exists because someone asked something the current model couldn't answer — and every one was disclosed before it cost you anything. The [vector index reference](https://dynostudio.dev/docs/dynostudio-vector-index/) has the full DDL contract, [targeted reads](https://dynostudio.dev/docs/dynostudio-partiql-reads/#vector-similarity-search) covers the search shape, and the [PartiQL dialect series](https://dynostudio.dev/docs/dynostudio-partiql/) goes deeper on everything the studio runs. Thanks for building Bean & Bark with us.

---
_Source: https://dynostudio.dev/docs/dynostudio-bean-bark-taste-search/ · DynoStudio Docs_
