Feeds
This page covers feed queries for returning ranked lists of recommended items. For query fundamentals, see Query Basics.
A feed query returns a ranked list of recommended items. Feeds can be chronological, trending, or personalized to a specific user. Common applications include homepages, "For You" carousels, and discovery experiences.
Personalized feeds use a rank pipeline that:
- Retrieves candidate items
- Optionally filters out unwanted items (e.g., already seen)
- Scores candidates using a model
- Reorders results with diversity and exploration
- Returns ranked results
Chronological feeds
Chronological feeds return items ordered by creation time, showing the most recent items first. This is useful for news feeds, social media timelines, or any feed where recency is important.
Prerequisites
- An engine with item data configured
- A timestamp column (e.g.,
created_at) on items
Query example
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
from shaped import RankQueryBuilder, ColumnOrder
# Create a chronological feed query
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['created_at DESC'],
limit=100
)
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Create a chronological feed query
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: 'created_at', ascending: false }],
limit: 100
})
)
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const items = response.items;
SELECT *
FROM column_order(columns='created_at DESC', limit=100)
LIMIT 20
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "created_at", "ascending": false }],
"limit": 100
}
],
"limit": 20
}
}
You can also use the derived chronological rank column if you have an interaction table:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM column_order(columns='_derived_chronological_rank ASC', limit=100)
LIMIT 20
from shaped import RankQueryBuilder, ColumnOrder
# Use derived chronological rank
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['_derived_chronological_rank ASC'],
limit=100
)
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Use derived chronological rank
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: '_derived_chronological_rank', ascending: true }],
limit: 100
})
)
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const items = response.items;
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "_derived_chronological_rank", "ascending": true }],
"limit": 100
}
],
"limit": 20
}
}
Trending feeds
Trending feeds return items ordered by popularity metrics, showing items that are currently popular or gaining traction. This is useful for discovery pages or "trending now" sections.
Prerequisites
- An engine with item data configured
- Popularity metrics (e.g., views, likes, engagement scores)
Query example
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
from shaped import RankQueryBuilder, ColumnOrder
# Create a trending feed query
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['views DESC', 'likes DESC', 'created_at DESC'],
limit=100
)
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# trending_items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Create a trending feed query
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [
{ name: 'views', ascending: false },
{ name: 'likes', ascending: false },
{ name: 'created_at', ascending: false }
],
limit: 100
})
)
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const trendingItems = response.items;
SELECT *
FROM column_order(columns='views DESC, likes DESC, created_at DESC', limit=100)
LIMIT 20
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [
{ "name": "views", "ascending": false },
{ "name": "likes", "ascending": false },
{ "name": "created_at", "ascending": false }
],
"limit": 100
}
],
"limit": 20
}
}
You can also use the derived popular rank column if you have an interaction table:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM column_order(columns='_derived_popular_rank ASC', limit=100)
LIMIT 20
from shaped import RankQueryBuilder, ColumnOrder
# Use derived popular rank
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=100
)
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# trending_items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Use derived popular rank
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 100
})
)
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const trendingItems = response.items;
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 100
}
],
"limit": 20
}
}
Trending with time decay
Use a time decay formula to surface items that are both popular and recent, giving more weight to recent engagement:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM column_order(columns='_derived_popular_rank ASC', limit=1000)
ORDER BY score(expression='(item.score - 1) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)', input_user_id='$user_id', input_interactions_item_ids='$interaction_item_ids')
LIMIT 20
from shaped import RankQueryBuilder, ColumnOrder
# Trending with time decay
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=1000
)
)
.score(
value_model='(item.score - 1) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)',
input_user_id='$user_id',
input_interactions_item_ids='$interaction_item_ids'
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# trending_items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Trending with time decay
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 1000
})
)
.score({
valueModel: '(item.score - 1) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)',
inputUserId: '$user_id',
inputInteractionsItemIds: '$interaction_item_ids'
})
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const trendingItems = response.items;
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 1000
}
],
"score": {
"value_model": "(item.score - 1) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)",
"input_user_id": "$parameters.user_id",
"input_interactions_item_ids": "$parameters.interaction_item_ids"
},
"limit": 20
}
}
Personalized feed with multiple retrievers
A comprehensive personalized feed combines multiple retrieval strategies, scoring models, diversity reordering, and exploration. A good baseline includes:
- A content-based retriever (finds items similar to user's past interactions)
- A collaborative retriever (finds items liked by similar users)
- A trending list (ensures fresh, popular content)
- A click_through_rate scoring model (ranks candidates by predicted engagement)
Prerequisites
- An engine with item data configured
- A trained collaborative embedding (e.g., ALS)
- A trained content embedding (e.g., text embedding)
- A trained scoring model (e.g.,
click_through_rate) - A
user_idto personalize for
Query example
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM similarity(embedding_ref='item_content_embedding',
encoder='interaction_round_robin',
input_user_id='$user_id', limit=50),
similarity(embedding_ref='als_embedding',
encoder='precomputed_user',
input_user_id='$user_id', limit=50),
column_order(columns='_derived_popular_rank ASC', limit=50)
WHERE prebuilt('exclude_seen', input_user_id='$user_id')
ORDER BY score(expression='click_through_rate', input_user_id='$user_id', input_interactions_item_ids='$interaction_item_ids')
REORDER BY diversity(0.3), exploration(0.2)
LIMIT 20
from shaped import RankQueryBuilder, Similarity, ColumnOrder
# Personalized feed with multiple retrievers
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
Similarity(
embedding_ref='item_content_embedding',
encoder={'type': 'interaction_round_robin', 'input_user_id': '$user_id'},
limit=50
),
Similarity(
embedding_ref='als_embedding',
encoder={'type': 'precomputed_user', 'input_user_id': '$user_id'},
limit=50
),
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=50
)
)
.filter(
predicate="prebuilt('exclude_seen', input_user_id='$user_id')"
)
.score(
value_model='click_through_rate',
input_user_id='$user_id',
input_interactions_item_ids='$interaction_item_ids'
)
.reorder(diversity=0.3, exploration=0.2)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(
# query=query,
# parameters={'user_id': 'user123'}
# )
import { RankQueryBuilder } from '@shaped-ai/api';
// Personalized feed with multiple retrievers
const query = new RankQueryBuilder()
.from('item')
.retrieve(
step => step.similarity({
embeddingRef: 'item_content_embedding',
encoder: { type: 'interaction_round_robin', inputUserId: '$user_id' },
limit: 50
}),
step => step.similarity({
embeddingRef: 'als_embedding',
encoder: { type: 'precomputed_user', inputUserId: '$user_id' },
limit: 50
}),
step => step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 50
})
)
.filter({
predicate: "prebuilt('exclude_seen', input_user_id='$user_id')"
})
.score({
valueModel: 'click_through_rate',
inputUserId: '$user_id',
inputInteractionsItemIds: '$interaction_item_ids'
})
.reorder({ diversity: 0.3, exploration: 0.2 })
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({
// query,
// parameters: { user_id: 'user123' }
// });
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "similarity",
"embedding_ref": "item_content_embedding",
"query_encoder": {
"type": "interaction_round_robin",
"input_user_id": "$parameters.user_id"
},
"limit": 50
},
{
"type": "similarity",
"embedding_ref": "als_embedding",
"query_encoder": {
"type": "precomputed_user",
"input_user_id": "$parameters.user_id"
},
"limit": 50
},
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 50
}
],
"filter": {
"predicate": "prebuilt('exclude_seen', input_user_id='$parameters.user_id')"
},
"score": {
"value_model": "click_through_rate",
"input_user_id": "$parameters.user_id",
"input_interactions_item_ids": "$parameters.interaction_item_ids"
},
"reorder": {
"diversity": 0.3,
"exploration": 0.2
},
"limit": 20
},
"parameters": {
"user_id": "user123",
"interaction_item_ids": ["item789", "item012"]
}
}
This query:
- Retrieves candidates from three sources:
- Content-based similarity (items similar to what the user has interacted with)
- Collaborative similarity (items liked by similar users)
- Trending items (popular items for freshness)
- Filters out items the user has already interacted with
- Scores candidates using a click_through_rate model to predict engagement
- Reorders with diversity (30%) to ensure variety and exploration (20%) to surface new items
- Returns the top 20 personalized items
Combining multiple models in personalized feeds
Use an ensemble of models to balance different signals when ranking feed items:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM similarity(embedding_ref='item_content_embedding',
encoder='interaction_round_robin',
input_user_id='$user_id', limit=50),
similarity(embedding_ref='als_embedding',
encoder='precomputed_user',
input_user_id='$user_id', limit=50),
column_order(columns='_derived_popular_rank ASC', limit=50)
WHERE prebuilt('exclude_seen', input_user_id='$user_id')
ORDER BY score(expression='0.6 * click_through_rate + 0.4 * conversion_rate', input_user_id='$user_id', input_interactions_item_ids='$interaction_item_ids')
REORDER BY diversity(0.3), exploration(0.2)
LIMIT 20
from shaped import RankQueryBuilder, Similarity, ColumnOrder
# Combine multiple models in personalized feed
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
Similarity(
embedding_ref='item_content_embedding',
encoder={'type': 'interaction_round_robin', 'input_user_id': '$user_id'},
limit=50
),
Similarity(
embedding_ref='als_embedding',
encoder={'type': 'precomputed_user', 'input_user_id': '$user_id'},
limit=50
),
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=50
)
)
.filter(
predicate="prebuilt('exclude_seen', input_user_id='$user_id')"
)
.score(
value_model='0.6 * click_through_rate + 0.4 * conversion_rate',
input_user_id='$user_id'
)
.reorder(diversity=0.3, exploration=0.2)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(
# query=query,
# parameters={'user_id': 'user123'}
# )
import { RankQueryBuilder } from '@shaped-ai/api';
// Combine multiple models in personalized feed
const query = new RankQueryBuilder()
.from('item')
.retrieve(
step => step.similarity({
embeddingRef: 'item_content_embedding',
encoder: { type: 'interaction_round_robin', inputUserId: '$user_id' },
limit: 50
}),
step => step.similarity({
embeddingRef: 'als_embedding',
encoder: { type: 'precomputed_user', inputUserId: '$user_id' },
limit: 50
}),
step => step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 50
})
)
.filter({
predicate: "prebuilt('exclude_seen', input_user_id='$user_id')"
})
.score({
valueModel: '0.6 * click_through_rate + 0.4 * conversion_rate',
inputUserId: '$user_id'
})
.reorder({ diversity: 0.3, exploration: 0.2 })
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({
// query,
// parameters: { user_id: 'user123' }
// });
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "similarity",
"embedding_ref": "item_content_embedding",
"query_encoder": {
"type": "interaction_round_robin",
"input_user_id": "$parameters.user_id"
},
"limit": 50
},
{
"type": "similarity",
"embedding_ref": "als_embedding",
"query_encoder": {
"type": "precomputed_user",
"input_user_id": "$parameters.user_id"
},
"limit": 50
},
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 50
}
],
"filter": {
"predicate": "prebuilt('exclude_seen', input_user_id='$parameters.user_id')"
},
"score": {
"value_model": "0.6 * click_through_rate + 0.4 * conversion_rate",
"input_user_id": "$parameters.user_id"
},
"reorder": {
"diversity": 0.3,
"exploration": 0.2
},
"limit": 20
},
"parameters": {
"user_id": "user123",
"interaction_item_ids": ["item789", "item012"]
}
}
Personalized trendy feed
Combine popularity, personalization, and time decay to create a feed that surfaces items that are both trending and relevant to the user:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM column_order(columns='_derived_popular_rank ASC', limit=1000)
WHERE prebuilt('exclude_seen', input_user_id='$user_id')
ORDER BY score(expression='((item.score / 1000) + cosine_similarity(text_encoding(item, embedding_ref=''text_embedding''), pooled_text_encoding(user.recent_interactions, embedding_ref=''text_embedding''))) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)', input_user_id='$user_id', input_interactions_item_ids='$interaction_item_ids')
LIMIT 20
from shaped import RankQueryBuilder, ColumnOrder
# Personalized trendy feed with time decay
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=1000
)
)
.filter(
predicate="prebuilt('exclude_seen', input_user_id='$user_id')"
)
.score(
value_model="((item.score / 1000) + cosine_similarity(text_encoding(item, embedding_ref='text_embedding'), pooled_text_encoding(user.recent_interactions, embedding_ref='text_embedding'))) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)",
input_user_id='$user_id',
input_interactions_item_ids='$interaction_item_ids'
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(
# query=query,
# parameters={'user_id': 'user123'}
# )
import { RankQueryBuilder } from '@shaped-ai/api';
// Personalized trendy feed with time decay
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 1000
})
)
.filter({
predicate: "prebuilt('exclude_seen', input_user_id='$user_id')"
})
.score({
valueModel: "((item.score / 1000) + cosine_similarity(text_encoding(item, embedding_ref='text_embedding'), pooled_text_encoding(user.recent_interactions, embedding_ref='text_embedding'))) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)",
inputUserId: '$user_id'
})
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({
// query,
// parameters: { user_id: 'user123' }
// });
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 1000
}
],
"filter": {
"predicate": "prebuilt('exclude_seen', input_user_id='$parameters.user_id')"
},
"score": {
"value_model": "((item.score / 1000) + cosine_similarity(text_encoding(item, embedding_ref='text_embedding'), pooled_text_encoding(user.recent_interactions, embedding_ref='text_embedding'))) / ((((now_seconds() - item.published_at) / 3600) + 2) ** 1.8)",
"input_user_id": "$parameters.user_id"
},
"limit": 20
},
"parameters": {
"user_id": "user123",
"interaction_item_ids": ["item789", "item012"]
}
}
Using user attributes in feed scoring
Incorporate user attributes (age, location, membership tier) into feed scoring to personalize results based on user profile:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM similarity(embedding_ref='als_embedding',
encoder='precomputed_user',
input_user_id='$user_id', limit=50),
column_order(columns='_derived_popular_rank ASC', limit=50)
WHERE prebuilt('exclude_seen', input_user_id='$user_id')
ORDER BY score(expression='click_through_rate + 0.1 * user.age - 0.05 * item.price + 0.2 * item.rating + 0.15 * user.membership_tier', input_user_id='$user_id', input_interactions_item_ids='$interaction_item_ids')
LIMIT 20
from shaped import RankQueryBuilder, Similarity, ColumnOrder
# Use user attributes in feed scoring
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
Similarity(
embedding_ref='als_embedding',
encoder={'type': 'precomputed_user', 'input_user_id': '$user_id'},
limit=50
),
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=50
)
)
.filter(
predicate="prebuilt('exclude_seen', input_user_id='$user_id')"
)
.score(
value_model='click_through_rate + 0.1 * user.age - 0.05 * item.price + 0.2 * item.rating + 0.15 * user.membership_tier',
input_user_id='$user_id',
input_interactions_item_ids='$interaction_item_ids'
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(
# query=query,
# parameters={'user_id': 'user123'}
# )
import { RankQueryBuilder } from '@shaped-ai/api';
// Use user attributes in feed scoring
const query = new RankQueryBuilder()
.from('item')
.retrieve(
step => step.similarity({
embeddingRef: 'als_embedding',
encoder: { type: 'precomputed_user', inputUserId: '$user_id' },
limit: 50
}),
step => step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 50
})
)
.filter({
predicate: "prebuilt('exclude_seen', input_user_id='$user_id')"
})
.score({
valueModel: 'click_through_rate + 0.1 * user.age - 0.05 * item.price + 0.2 * item.rating + 0.15 * user.membership_tier',
inputUserId: '$user_id'
})
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({
// query,
// parameters: { user_id: 'user123' }
// });
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "similarity",
"embedding_ref": "als_embedding",
"query_encoder": {
"type": "precomputed_user",
"input_user_id": "$parameters.user_id"
},
"limit": 50
},
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 50
}
],
"filter": {
"predicate": "prebuilt('exclude_seen', input_user_id='$parameters.user_id')"
},
"score": {
"value_model": "click_through_rate + 0.1 * user.age - 0.05 * item.price + 0.2 * item.rating + 0.15 * user.membership_tier",
"input_user_id": "$parameters.user_id"
},
"limit": 20
},
"parameters": {
"user_id": "user123",
"interaction_item_ids": ["item789", "item012"]
}
}
Anonymous feed
For users without stored history, you can return popular items. This is useful for new users or anonymous visitors:
- ShapedQL
- Python SDK
- TypeScript SDK
- JSON
SELECT *
FROM column_order(columns='_derived_popular_rank ASC', limit=100)
LIMIT 20
from shaped import RankQueryBuilder, ColumnOrder
# Anonymous feed with popular items
query = (
RankQueryBuilder()
.from_entity('item')
.retrieve(
ColumnOrder(
columns=['_derived_popular_rank ASC'],
limit=100
)
)
.limit(20)
.build()
)
# Example usage with client
# response = client.rank(query=query)
# items = response['items']
import { RankQueryBuilder } from '@shaped-ai/api';
// Anonymous feed with popular items
const query = new RankQueryBuilder()
.from('item')
.retrieve(step =>
step.columnOrder({
columns: [{ name: '_derived_popular_rank', ascending: true }],
limit: 100
})
)
.limit(20)
.build();
// Example usage with client
// const response = await client.rank({ query });
// const items = response.items;
{
"query": {
"type": "rank",
"from": "item",
"retrieve": [
{
"type": "column_order",
"columns": [{ "name": "_derived_popular_rank", "ascending": true }],
"limit": 100
}
],
"limit": 20
}
}