Visitar URL original
feat(cli): Add recommendation template for feast init by doc-johnson · Pull Request #6078 · feast-dev/feast · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(cli): Add recommendation template for feast init (#5478)
Add a new "recommendation" template demonstrating product recommendations
using vector similarity search with pre-computed embeddings (all-MiniLM-L6-v2)
and SQLite online store with vector_enabled.

Signed-off-by: doc-johnson <hustler@mail.ru>
  • Loading branch information
doc-johnson committed Jun 26, 2026
commit 7478002b8e60d358d96a65c0a346f2ce12297373
1 change: 1 addition & 0 deletions sdk/python/feast/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ def materialize_incremental_command(
"ray",
"ray_rag",
"pytorch_nlp",
"recommendation",
],
case_sensitive=False,
),
Expand Down
Empty file.
159 changes: 159 additions & 0 deletions sdk/python/feast/templates/recommendation/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from feast.file_utils import replace_str_in_file


def bootstrap():
# Called automatically by init_repo() during `feast init`

import pathlib
from datetime import datetime, timedelta

import numpy as np
import pandas as pd

try:
from sentence_transformers import SentenceTransformer
except ImportError:
raise SystemExit(
"sentence-transformers is required for this template: "
"pip install sentence-transformers"
)

repo_path = pathlib.Path(__file__).parent.absolute() / "feature_repo"
project_name = pathlib.Path(__file__).parent.absolute().name
data_path = repo_path / "data"
data_path.mkdir(exist_ok=True)

products = [
(
"P001",
"Wireless Noise-Cancelling Headphones",
"Premium over-ear headphones with active noise cancellation and 30-hour battery life.",
"Electronics",
299.99,
4.7,
),
(
"P002",
"Bluetooth Portable Speaker",
"Waterproof portable speaker with deep bass and 12-hour playtime.",
"Electronics",
79.99,
4.5,
),
(
"P003",
"Mechanical Gaming Keyboard",
"RGB mechanical keyboard with Cherry MX switches and programmable keys.",
"Electronics",
149.99,
4.6,
),
(
"P004",
"Ergonomic Wireless Mouse",
"Vertical ergonomic mouse designed to reduce wrist strain.",
"Electronics",
49.99,
4.3,
),
(
"P005",
"Python Machine Learning Cookbook",
"Practical recipes for building ML models with scikit-learn and TensorFlow.",
"Books",
39.99,
4.4,
),
(
"P006",
"Data Engineering Fundamentals",
"Comprehensive guide to building modern data pipelines and architectures.",
"Books",
44.99,
4.6,
),
(
"P007",
"Introduction to Deep Learning",
"Beginner-friendly deep learning textbook with hands-on PyTorch examples.",
"Books",
54.99,
4.7,
),
(
"P008",
"Trail Running Shoes",
"Lightweight trail running shoes with superior grip and cushioning.",
"Sports",
129.99,
4.6,
),
(
"P009",
"Premium Yoga Mat",
"Non-slip extra-thick yoga mat with carrying strap.",
"Sports",
34.99,
4.4,
),
(
"P010",
"Resistance Bands Set",
"Set of 5 resistance bands with varying tension levels for home workouts.",
"Sports",
24.99,
4.3,
),
(
"P011",
"Robot Vacuum Cleaner",
"Self-navigating robot vacuum with app control and auto-charging.",
"Home",
349.99,
4.5,
),
(
"P012",
"Air Purifier with HEPA Filter",
"Room air purifier with true HEPA filter and air quality sensor.",
"Home",
199.99,
4.6,
),
]

model = SentenceTransformer("all-MiniLM-L6-v2")
descriptions = [f"{name}. {desc}" for _, name, desc, _, _, _ in products]
embeddings = model.encode(descriptions, normalize_embeddings=True)

end_date = datetime.now().replace(microsecond=0, second=0, minute=0)
start_date = end_date - timedelta(days=15)
timestamps = [start_date + timedelta(hours=i) for i in range(len(products))]

columns = [
"product_id",
"product_name",
"description",
"category",
"price",
"rating",
]
df = pd.DataFrame(products, columns=columns)
df["price"] = df["price"].astype(np.float32)
df["rating"] = df["rating"].astype(np.float32)
df["embedding"] = [emb.tolist() for emb in embeddings]
df["event_timestamp"] = timestamps
df["created"] = end_date

products_path = data_path / "products.parquet"
df.to_parquet(path=str(products_path), allow_truncated_timestamps=True)

example_py_file = repo_path / "feature_definitions.py"
replace_str_in_file(example_py_file, "%PROJECT_NAME%", str(project_name))
replace_str_in_file(
example_py_file, "%PARQUET_PATH%", str(products_path.relative_to(repo_path))
)


if __name__ == "__main__":
bootstrap()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import timedelta

from feast import Entity, FeatureService, FeatureView, Field, FileSource, Project
from feast.types import Array, Float32, String

project = Project(
name="%PROJECT_NAME%",
description="A project for product recommendations using vector similarity search",
)

product = Entity(name="product", join_keys=["product_id"])

# Parquet source with pre-computed embeddings. Replace with BigQuery,
# Snowflake, etc. for production use cases.
products_source = FileSource(
name="products_source",
path="%PARQUET_PATH%",
timestamp_field="event_timestamp",
created_timestamp_column="created",
)

# Embedding generated by all-MiniLM-L6-v2 (384 dimensions)
product_embeddings = FeatureView(
name="product_embeddings",
entities=[product],
ttl=timedelta(days=365),
schema=[
Field(
name="embedding", dtype=Array(Float32), vector_index=True, vector_length=384
),
Field(name="product_name", dtype=String),
Field(name="description", dtype=String),
Field(name="category", dtype=String),
Field(name="price", dtype=Float32),
Field(name="rating", dtype=Float32),
],
online=True,
source=products_source,
tags={"team": "recommendations"},
)

recommendation_service = FeatureService(
name="recommendation_service",
features=[product_embeddings],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
project: my_project
# By default, the registry is a file (but can be turned into a more scalable SQL-backed registry)
registry: data/registry.db
# The provider primarily specifies default offline / online stores & storing the registry in a given cloud
provider: local
online_store:
type: sqlite
path: data/online_store.db
vector_enabled: true
entity_key_serialization_version: 3
# By default, no_auth for authentication and authorization, other possible values kubernetes and oidc. Refer the documentation for more details.
auth:
type: no_auth
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import subprocess
import sys
from datetime import datetime

from feast import FeatureStore


def run_demo():
store = FeatureStore(repo_path=".")

print("\n--- Run feast apply ---")
subprocess.run(["feast", "apply"])

print("\n--- Load features into online store ---")
store.materialize_incremental(end_date=datetime.now())

print("\n--- Product Recommendation Search ---")
try:
from sentence_transformers import SentenceTransformer
except ImportError:
print("sentence-transformers is required: pip install sentence-transformers")
sys.exit(1)

model = SentenceTransformer("all-MiniLM-L6-v2")

query = "gaming laptop accessories"
print(f"\n Query: '{query}'")
query_embedding = model.encode([query], normalize_embeddings=True)[0].tolist()

results = store.retrieve_online_documents_v2(
features=[
"product_embeddings:embedding",
"product_embeddings:product_name",
"product_embeddings:category",
"product_embeddings:price",
Comment thread
doc-johnson marked this conversation as resolved.
"product_embeddings:rating",
],
query=query_embedding,
top_k=5,
).to_dict()

if results and len(results.get("product_id", [])) > 0:
num_results = len(results["product_id"])
print(f" Top {num_results} recommendations:")
for i in range(num_results):
name = results["product_name"][i]
category = results["category"][i]
price = results["price"][i]
rating = results["rating"][i]
print(f" {i + 1}. {name} [{category}] - ${price:.2f} (rating: {rating})")
else:
print(" No results found.")

print("\n--- Run feast teardown ---")
subprocess.run(["feast", "teardown"])


if __name__ == "__main__":
run_demo()
Loading