# Search and recommendations

All three rank the rows the plan chose, so the route's conditions, roles and
ownership still apply. They use Talos's own text embedder (shipped with the
model; French and English) for meaning, next to the words.

## Search

```ts
'GET /api/recipes': { say: 'show the recipes', shape: p => ({ search: p.q }) }        // over `searchable`
'GET /api/articles': { say: 'show the articles', shape: p => ({ find: {
  q: p.q, fields: { title: 3, excerpt: 2, body: 1 }, html: true, limit: 20, suggest: 'title' } }) }
```

`find` weighs fields (BM25), matches prefixes and one-letter typos, ignores
stop words and HTML, and falls back on meaning when no word matches.
`suggest: 'title'` turns it into autocompletion on the query's last word.
`find(rows, options)` is also exported, for rows you already have.

## Similar rows

```ts
'GET /api/recipes/:id/similar': { say: 'show the recipes', pin: false, limit: 4,
  shape: p => ({ similar: { to: { id: Number(p.id) }, text: ['title', 'summary', 'ingredients'] } }) }
```

`to` is a row (`{ id }`, left out of the result) or a text. Words (TF-IDF) and
meaning, mixed 70/30.

## Recommendations

```ts
'GET /api/for-you': { say: 'show the recipes', guard: member, limit: 4, shape: (_, me) => ({ recommend: {
  user: me.id,
  interactions: { table: 'likes', user: 'user_id', item: 'recipe_id' },   // one row per (user, item)
  text: ['title', 'summary'], date: 'created_at', halfLife: 30,
  weights: { content: 1, together: 1, popular: 0.3, recent: 0.3 },
} }) }
```

Each candidate is scored by how close it is to what the reader already liked
(`content`), what readers who liked the same things liked (`together`),
popularity and recency. Items already seen are left out (`keepSeen: true` to
keep them); a visitor (`user: null`) gets popular and recent.

## Meaning, directly

`embedText(text)`, `cosine(a, b)`, `classify(text, { sport: 'match, stade', culture: 'concert' })`,
and the schema's `infer` (a column filled by meaning).
