Feeds — Related items
The "more like this" pattern. Given a clicked record, return the N nearest neighbors ranked however you want — similarity + recency, similarity + engagement, or pure similarity with diversify.
The trick: use mode: 'recordVector' and the seed's existing stored
embedding as the target. Zero embedding-API call at request time.
Why this is cheap
Semantic "more like this" is usually expensive: you take the current document's text, embed it, then do a vector search. That's one embedding API call per click.
With mode: 'recordVector', the server skips the embed call entirely:
- Client sends
context: { seedRecordId: 'r_104' }. - Server reads the stored vector for
r_104(indexed read). - Server uses that vector as the similarity target directly.
Zero embedding API calls at request time. The seed's vector already exists — the request is an indexed read plus an ANN search, nothing more. This is what makes "more like this" cheap enough to ship on every detail page.
React hook
Because seedRecordId is part of the hook's dependencies, navigating to a
new recipe re-runs the feed with the new seed. No manual cache busting.
Blending with popularity
Pure similarity often surfaces obscure near-duplicates. Adding a small
engagement weight pulls "popular related items" forward:
Rule of thumb: similarity 0.7–0.8, engagement 0.15–0.25, everything else pure icing.
excludeIds — don't return the seed
Without pagination.excludeIds, the highest-similarity result to r_104 is
usually r_104 itself (distance zero). Always set:
The value is a context path that resolves to a row id (or an array of ids). The server filters those out after ranking.
Limits
- Only records with stored embeddings work as seeds. If the row hasn't been ingested yet (or has
searchable: falseon every field), there's no vector to use. The server returns400 seed_record_not_found. - One seed per request. Pass more than one id in
seedRecordIdand you'll get a validation error. For multi-seed blends, run N requests client-side and union. Or pre-compute an average vector on your side and pass it as a plainnumber[]. - The seed's own engagement isn't contextualized. If you want "related items by users similar to me," you'll need a user-context component on top — the engagement scorer alone aggregates over the whole engagement lens, not per-user.
Next: Live evolution — push updates when new matching content arrives.