**Goal:** Implement item-based collaborative filtering using vector similarity. Store item feature vectors (or learned item embeddings) in Turso, then given a target item, retrieve the *k* most similar items as recommendations. 🚧 Full content tracked in [nbd:e8be9a].
**Goal:** Build an item-based recommendation engine. Store item feature vectors in Turso, then given a target item, find the *k* most similar items using KNN and exclude the query item from the results.
We will use hand-crafted 5-dimensional feature vectors for a product catalogue (no fastembed dependency — this keeps the focus on the recommendation logic itself). The five dimensions represent affinity scores for: **[electronics, clothing, sports, food, books]**.
// 1. Get the query item's embedding as a JSON string.
let mut stmt = conn
.prepare("SELECT vector_extract(embedding) FROM products WHERE id = ?")
.await?;
let mut rows = stmt.query(libsql::params![item_id]).await?;
let row = rows
.next()
.await?
.ok_or("item not found")?;
let query_vec: String = row.get(0)?;
// 2. Use vector_top_k with k+1 to leave room for the query item itself.
let sql = format!(
"SELECT products.id, products.name,
vector_distance_cos(products.embedding, vector(?1)) AS distance
FROM vector_top_k('products_idx', ?1, {limit})
JOIN products ON products.rowid = id
WHERE products.id != ?2
ORDER BY distance
LIMIT ?3",
limit = k + 1
);
let mut stmt = conn.prepare(&sql).await?;
let mut rows = stmt
.query(libsql::params![query_vec.as_str(), item_id, k as i64])
.await?;
// 3. Collect (name, distance) pairs.
let mut results = Vec::new();
while let Some(row) = rows.next().await? {
let name: String = row.get(1)?;
let distance: f64 = row.get(2)?;
results.push((name, distance));
}
Ok(results)
}
```
The key ideas:
1. **Retrieve the query vector** — `vector_extract` returns the stored embedding as a JSON string that can be passed straight back to `vector_top_k`.
2. **Over-fetch by one** — request `k + 1` candidates because `vector_top_k` will return the query item itself (distance ≈ 0). The `WHERE products.id != ?2` clause filters it out.
3. **Cosine distance** — `vector_distance_cos` returns a value between 0 (identical) and 2 (opposite). Lower means more similar.
#### Step 4 — Print recommendations
Request recommendations for three representative items and verify the clusters make sense:
```rust
let queries = vec![
(1, "Laptop"),
(4, "Running Shoes"),
(8, "Cookbook"),
];
for (id, name) in &queries {
let recs = recommend(&conn, *id, 2).await?;
let rec_str: Vec<String> = recs
.iter()
.map(|(n, d)| format!("{n} ({d:.3})"))
.collect();
println!(
"Customers who liked {name} also liked: {}",
rec_str.join(", ")
);
}
```
**Expected output (distances are approximate):**
```text
Customers who liked Laptop also liked: Mechanical Keyboard (0.023), USB-C Hub (0.041)
Customers who liked Running Shoes also liked: Yoga Mat (0.019), Water Bottle (0.063)
Customers who liked Cookbook also liked: Novel (0.168), Protein Bar (0.397)