This demo shows how to use the onnx_predict scalar UDF to run ONNX model inference directly inside SQL queries. Skardi loads ONNX models lazily on first use and caches them in memory — no pre-registration or configuration required.
onnx_predict is a DataFusion scalar UDF with the following signature:
onnx_predict('path/to/model.onnx', input1, input2, ...) -> FLOAT- First argument: A string literal path to an
.onnxfile (relative to the working directory) - Remaining arguments: Model inputs — their types and count must match what the ONNX model expects
- Returns:
FLOAT(scalar prediction per row), orLIST(FLOAT)when inputs are aggregated lists
- Lazy loading: The ONNX runtime (ORT) is only initialized when the first model is loaded. Models are cached by file path after the first call.
- Automatic type detection: Input/output types and shapes are introspected from the ONNX model metadata — no manual configuration needed.
- Integer input models: Models that expect integer inputs (e.g., NCF-style user/item ID models) are automatically detected and processed row-by-row.
- Float input models: Models with float inputs (e.g., time-series, embedding models) are batched into ndarrays.
- List mode: When any input is a
ListArray(e.g., fromarray_agg), the output is wrapped inLIST(FLOAT)to preserve row cardinality.
- An ONNX model file (
.onnx) accessible from the server's working directory - The Skardi server built with model support:
cargo build --release -p skardi-server
This example combines Lance vector search with ONNX model inference to build a movie recommendation system. It finds similar movies via KNN, then ranks them using a Neural Collaborative Filtering (NCF) model.
The context file (ctx_movie_recommendation.yaml) defines two data sources:
data_sources:
- name: "movies"
type: "postgres"
connection_string: "postgresql://localhost:5432/test?sslmode=disable"
options:
table: "movies"
schema: "public"
user_env: "PG_USER"
pass_env: "PG_PASSWORD"
description: "Movies table with movie_id, title, genres, year"
- name: "movie_embeddings"
type: "lance"
path: "data/movie_embeddings.lance"
description: "128-dimensional movie embeddings"The pipeline (pipeline_movie_recommendation.yaml) chains three steps in a single SQL query:
-- Step 1: Find the movie by title
WITH last_watched AS (
SELECT movie_id, title
FROM movies
WHERE title = {last_watched_movie}
LIMIT 1
),
-- Step 2: Find 10 similar movies via Lance KNN
knn_results AS (
SELECT knn.movie_id
FROM lance_knn(
'movie_embeddings',
'embedding',
(SELECT embedding FROM movie_embeddings
WHERE movie_id = (SELECT movie_id FROM last_watched)),
10
) knn
WHERE knn.movie_id != (SELECT movie_id FROM last_watched)
),
-- Step 3: Score each candidate with the NCF ONNX model
ranked_recommendations AS (
SELECT
kr.movie_id,
onnx_predict('models/ncf.onnx',
CAST({user_id} AS BIGINT),
CAST(kr.movie_id AS BIGINT)
) AS prediction_score
FROM knn_results kr
)
-- Step 4: Join with movie metadata and return top results
SELECT
m.movie_id, m.title, m.genres, m.year, m.genres_list,
rr.prediction_score
FROM ranked_recommendations rr
JOIN movies m ON rr.movie_id = m.movie_id
ORDER BY rr.prediction_score DESC
LIMIT {top_n}# Set PostgreSQL credentials
export PG_USER="your_user"
export PG_PASSWORD="your_password"
# Start the server
cargo run --bin skardi-server -- \
--ctx demo/onnx_predict/ctx_movie_recommendation.yaml \
--pipeline demo/onnx_predict/pipeline_movie_recommendation.yaml \
--port 8080curl -X POST http://localhost:8080/movie-recommendation-pipeline/execute \
-H "Content-Type: application/json" \
-d '{
"last_watched_movie": "Toy Story (1995)",
"user_id": 42,
"top_n": 5
}'Expected Response:
{
"success": true,
"data": [
{
"movie_id": 356,
"title": "Forrest Gump (1994)",
"genres": "Comedy|Drama|Romance|War",
"year": 1994,
"genres_list": ["Comedy", "Drama", "Romance", "War"],
"prediction_score": 0.923
}
],
"rows": 5,
"execution_time_ms": 45
}| Parameter | Type | Required | Description |
|---|---|---|---|
last_watched_movie |
string | Yes | Title of the movie to find recommendations for |
user_id |
integer | Yes | User ID for personalized scoring |
top_n |
integer | Yes | Number of recommendations to return |
Models like NCF that take user/item IDs:
SELECT
item_id,
onnx_predict('models/ncf.onnx',
CAST({user_id} AS BIGINT),
CAST(item_id AS BIGINT)
) AS score
FROM candidates
ORDER BY score DESC
LIMIT 10Models that take numeric features:
SELECT
id,
onnx_predict('models/regressor.onnx',
CAST(feature_1 AS FLOAT),
CAST(feature_2 AS FLOAT),
CAST(feature_3 AS FLOAT)
) AS prediction
FROM data_tableWhen inputs come from aggregation (e.g., time-series), the output is automatically wrapped in a list:
SELECT
group_id,
onnx_predict('models/forecaster.onnx',
array_agg(value ORDER BY timestamp)
) AS forecast
FROM time_series
GROUP BY group_idThe models/ directory includes pre-built ONNX models:
| Model | Description | Inputs |
|---|---|---|
ncf.onnx |
Neural Collaborative Filtering | user_id (INT64), item_id (INT64) |
TinyTimeMixer.onnx |
Time-series forecasting | aggregated float sequences |
Ensure the model path is correct relative to the server's working directory (where you run cargo run or skardi-server).
The UDF needs at minimum the model path and one input. Check your SQL syntax.
For integer-input models, ensure inputs are cast to BIGINT or INT:
onnx_predict('model.onnx', CAST(col AS BIGINT), ...)This usually means the input shape or type doesn't match what the model expects. Enable debug logging to see the model's expected inputs:
RUST_LOG=debug cargo run --bin skardi-server -- ...The logs will show:
Model input 'user_id': type=Int64, shape=[-1, 1]
Model input 'item_id': type=Int64, shape=[-1, 1]