← Tous les SDK

SDK Rust

Le client officiel Rust de l’API LIKYLY : envoyez votre catalogue et ce que font vos visiteurs, recevez des recommandations personnalisées.

catalogue → événements → recommandations → impression → clic → achat

Installation

cargo add likyly tokio --features tokio/macros,tokio/rt-multi-thread

Asynchrone (Tokio). Construit sur reqwest avec rustls, types serde pour chaque requête et réponse. Le client Likyly est Clone et se partage à moindre coût.

Démarrage rapide

Chaque appel est asynchrone : les extraits ci-dessous s'exécutent dans un #[tokio::main] async fn main() -> Result<(), likyly::Error>.

1. Initialiser

Créez un client et réutilisez-le. Utilisez votre clé secrète côté serveur (voir Quelle clé ?).

use std::time::Duration;

use likyly::{
    Error, EventInput, HybridOptions, ItemImport, Likyly, Properties, RecommendationRequest, SessionOptions, SimilarOptions, TypedEvent,
    UserInput,
};
use serde_json::json;

// Backend only: the secret key gives access to your catalog and your users.
let likyly = Likyly::builder(std::env::var("LIKYLY_SECRET_KEY").unwrap())
    .build()?;

2. Envoyer votre catalogue

// Send your catalog once (and whenever it changes). Ids are YOUR ids: SKUs, UUIDs, Shopify gids...
let shoe = |id: &str, title: &str, description: &str, brand: &str, price: f64| ItemImport {
    item_id: id.into(),
    title: title.into(),
    description: Some(description.into()),
    properties: Some(props(json!({ "category": "shoes", "brand": brand, "price": price }))),
};
likyly
    .items()
    .upsert_many(&[
        shoe("SKU-1", "Nike Air Max", "Running shoe with air cushioning", "Nike", 129.9),
        shoe("SKU-2", "Adidas Ultraboost", "Responsive running shoe", "Adidas", 149.0),
        shoe("SKU-3", "Nike Pegasus", "Everyday running shoe", "Nike", 119.0),
    ])
    .await?;

// Users are optional: describe them if you want the profile to travel with their events.
likyly.users().upsert("user_123", UserInput { properties: Some(props(json!({ "country": "FR", "segment": "premium" }))) }).await?;

Les identifiants sont les vôtres : n’importe quelle chaîne (SKU-123, un UUID, gid://shopify/Product/123). properties est libre ; category et description améliorent la similarité, le reste est stocké tel quel.

3. Suivre les événements

// Tell LIKYLY what your visitors do.
likyly.events().view(EventInput::for_user("user_123", "SKU-1")).await?;

// Purchases carry an event_id: replaying the call can never count the sale twice.
likyly
    .events()
    .purchase(EventInput {
        event_id: Some("purchase_order_9281_SKU-1".into()),
        quantity: Some(1),
        properties: Some(props(json!({ "price": 129.9, "currency": "EUR", "orderId": "order_9281" }))),
        ..EventInput::for_user("user_123", "SKU-1")
    })
    .await?;

4. Obtenir des recommandations

// Ask what to show. You never pick an algorithm: LIKYLY chooses the best strategy for what it knows.
let recs = likyly
    .recommendations()
    .get(RecommendationRequest {
        user_id: Some("user_123".into()),
        placement: Some("homepage".into()),
        limit: Some(3),
        ..Default::default()
    })
    .await?;

println!("strategy: {}", recs.strategy);
for item in &recs.items {
    println!("{} {}", item.item_id, item.title.as_deref().unwrap_or(""));
}

Vous ne choisissez jamais d’algorithme : LIKYLY sélectionne la meilleure stratégie selon ce qu’il sait du visiteur et vous dit laquelle il a utilisée (strategy : hybrid, content, collaborative, session ou popular). Sans utilisateur, sans session et sans article, vous obtenez tout de même une réponse utile : ce qui est populaire.

5. Boucler : l’attribution

// Send the recommendation_id back: LIKYLY measures which recommendations get seen, clicked and bought.
let shown = EventInput {
    recommendation_id: Some(recs.recommendation_id.clone()),
    placement: Some("homepage".into()),
    ..EventInput::for_user("user_123", recs.items[0].item_id.clone())
};
likyly.events().impression(shown.clone()).await?;
likyly.events().click(shown).await?;

Renvoyez l’identifiant de recommandation sur les événements suivants (impression à l’affichage, click, add_to_cart, purchase) : LIKYLY mesure ainsi quelles recommandations convertissent réellement.

Quelle clé ?

Clé secrèteClé publique
Où l'utilisersur votre serveur uniquementn'importe où, y compris une page web
Recommandationsouioui
Enregistrer des événementsouioui
Articles et utilisateurs (créer, lire, supprimer)ouinon (Error::PermissionDenied)
Explications debugouinon

La clé publique ne peut que lire des recommandations et écrire des événements : elle peut donc être exposée. Ne mettez jamais la clé secrète dans du code livré à un navigateur ou à une application mobile.

// Where the code is exposed to visitors, use the PUBLIC key: it can only read recommendations and record events.
let front = Likyly::builder(std::env::var("LIKYLY_PUBLIC_KEY").unwrap())
    .build()?;

// A visitor who is not logged in: use an anonymous session id (any string you generate and keep in a cookie).
front.events().view(EventInput::for_session("sess_abc", "SKU-2")).await?;
let for_visitor = front
    .recommendations()
    .get(RecommendationRequest {
        session_id: Some("sess_abc".into()),
        viewed_item_ids: Some(vec!["SKU-2".into()]),
        placement: Some("product_page".into()),
        limit: Some(3),
        ..Default::default()
    })
    .await?;

Événements

track est le mécanisme unique ; impression, view, click, add_to_cart, remove_from_cart et purchase sont des raccourcis vers celui-ci. Les types d’événements sont des chaînes libres : utilisez les vôtres (favorite, share, video_played...) aussi librement que les officiels.

// Any string is a valid event type: track what matters to your business.
likyly
    .events()
    .track(
        "favorite",
        EventInput { properties: Some(props(json!({ "list": "wishlist" }))), ..EventInput::for_user("user_123", "SKU-2") },
    )
    .await?;

// Up to 1000 events per call, each with its own type.
likyly
    .events()
    .track_many(&[
        TypedEvent::new("view", EventInput::for_user("user_123", "SKU-3")),
        TypedEvent::new("add_to_cart", EventInput { quantity: Some(1), ..EventInput::for_user("user_123", "SKU-3") }),
    ])
    .await?;
  • Un événement porte un article et un utilisateur, une session, ou les deux (les deux relient la session anonyme à l’utilisateur une fois connecté).
  • Visiteurs anonymes : générez un identifiant de session (n’importe quelle chaîne) et gardez-le dans un cookie ; aucune connexion requise.
  • properties est un objet libre (prix, devise, numéro de commande...). Ses clés sont stockées telles que vous les envoyez et jamais renommées par le SDK.
  • L’identifiant d’événement (clé d’idempotence) : rejouer un événement avec le même identifiant n’enregistre rien et renvoie duplicate: true. Renseignez-le sur les achats, par exemple purchase_<commande>_<article>.
  • placement est un libellé libre de l’endroit où quelque chose s’est produit ou sera affiché (homepage, product_page, cart...).

Recommandations avancées

recommendations.get est la voie normale. Pour choisir vous-même la stratégie :

// Advanced Recommendations: one strategy at a time, when you want to choose.
let similar =
    likyly.recommendations().similar(SimilarOptions { item_id: "SKU-1".into(), limit: Some(3), ..Default::default() }).await?;
let hybrid = likyly
    .recommendations()
    .hybrid(HybridOptions {
        user_id: "user_123".into(),
        item_id: "SKU-1".into(),
        alpha: Some(0.7),
        limit: Some(3),
        ..Default::default()
    })
    .await?;
let session = likyly
    .recommendations()
    .session(SessionOptions { viewed_item_ids: Some(vec!["SKU-1".into(), "SKU-2".into()]), limit: Some(3), ..Default::default() })
    .await?;
StratégieCe qu'elle fait
popularLes articles les plus populaires : le repli pour un visiteur sans historique
similarArticles similaires à un article (similarité de contenu), sans utilisateur
collaborativeCe qu'ont aimé les utilisateurs aux historiques proches. Nécessite un modèle entraîné, sinon une erreur « introuvable »
hybridArticles similaires personnalisés pour un utilisateur ; alpha (0 à 1) pondère le collaboratif face au contenu
sessionPondéré par la récence, à partir des articles vus ou de l'historique d'un utilisateur (l'un des deux, exactement)

Ces endpoints portent les identifiants dans le chemin de l’URL : un identifiant contenant / (comme gid://shopify/Product/1) ou, pour la liste de session, ,, est refusé par une erreur de validation. Utilisez alors recommendations.get, qui prend les identifiants dans le corps de la requête.

Erreurs

Les erreurs sont typées (toutes sont des likyly::Error). Interceptez celles qui vous concernent :

ErreurQuand
Error::ValidationRequête invalide : détectée par le SDK avant tout envoi, ou 422 de l'API (le message nomme les champs fautifs)
Error::Authentication401 : clé API absente, invalide ou révoquée
Error::PermissionDenied403 : par exemple une clé publique utilisée pour une opération réservée à la clé secrète, ou une limite de plan
Error::NotFound404
Error::RateLimit429 : voir la valeur de retry-after
Error::ApiTout autre statut d'erreur de l'API (les quatre lignes précédentes en sont des sous-types)
Error::NetworkAucune réponse HTTP : DNS, connexion coupée, TLS
Error::TimeoutLa requête a expiré

Les erreurs issues de l’API portent : status_code(), request_id(), retry_after() (Duration), body(). Communiquez l’identifiant de requête lorsque vous contactez le support.

match likyly.items().get("does-not-exist").await {
    Ok(_) => {}
    Err(Error::NotFound(e)) => println!("no such item, request {:?}", e.request_id),
    Err(Error::RateLimit(e)) => println!("slow down, retry in {:?}", e.retry_after),
    Err(other) => return Err(other),
}

Tentatives et délais

Le SDK relance pour vous les échecs transitoires (2 tentatives par défaut, backoff exponentiel à partir de 0,5 s plafonné à 8 s, avec jitter complet) :

  • 429 : toujours relancé, en respectant Retry-After. Un Retry-After supérieur à 60 s n’est pas attendu : l’erreur de limite de débit est renvoyée.
  • 502, 503, 504, connexions coupées et délais dépassés : relancés uniquement quand rejouer l’appel est sans danger : lectures, upserts, suppressions, événements portant un identifiant d’événement, et lots dont chaque événement en a un.
  • Un événement sans identifiant d’événement n’est jamais relancé après un échec ambigu : un réseau capricieux ne peut donc jamais enregistrer un achat deux fois. Renseignez l’identifiant sur les événements qui comptent et ils deviennent relançables.
  • Les autres erreurs 4xx ne sont jamais relancées.

Configuration

RéglageOptionPar défaut
Clé APILikyly::new(key) / Likyly::builder(key)obligatoire
URL de base.base_url(..)https://api.likyly.com
Catalogue.catalog(..)votre seul catalogue
Délai d'attente.timeout(Duration)10 s
Nouvelles tentatives.max_retries(..)2
Suffixe User-Agent.user_agent(..)aucun
Client HTTP personnalisé.transport(Arc<dyn Transport>), or ReqwestTransport::with_client(..)reqwest
let tuned = Likyly::builder(std::env::var("LIKYLY_SECRET_KEY").unwrap())
    .timeout(Duration::from_secs(5)) // per attempt (default 10 s)
    .max_retries(3) // automatic retries on 429 / 502 / 503 / 504 / network errors (default 2, 0 disables)
    .user_agent("my-shop/1.4") // appended to the SDK's User-Agent
    .build()?;

client.with_options(RequestOptions { timeout, max_retries }) renvoie une copie du client avec d'autres limites ; abandonner un future annule l'appel.

Référence

Clé : S = clé secrète uniquement, P = clé publique ou secrète.

MéthodeEffetClé
likyly.items().getUn article par votre identifiantS
likyly.items().listUne page du catalogue (limit / offset), avec le totalS
likyly.items().upsertCrée ou remplace un article (idempotent)S
likyly.items().deleteSupprime un article (les événements déjà enregistrés sont conservés)S
likyly.items().upsert_manyUpsert par lot, 1 à 1000 articlesS
likyly.items().importAlias de l'upsert par lot ci-dessus (lot JSON)S
likyly.items().delete_manySuppression par lot, 1 à 1000 identifiants ; les inconnus sont signalés, pas levésS
likyly.users().getUn profil utilisateurS
likyly.users().listUne page d'utilisateurs (limit / offset)S
likyly.users().upsertCrée ou remplace un profil (properties libres)S
likyly.users().deleteEfface le profil et tous les événements enregistrés pour cet utilisateurS
likyly.users().importUpsert d'utilisateurs par lot, 1 à 1000S
likyly.events().trackEnregistre un événement de n'importe quel type (chaîne libre)P
likyly.events().track_manyJusqu'à 1000 événements par appel, chacun avec son propre typeP
likyly.events().impressionL'article a été affiché au visiteurP
likyly.events().viewLe visiteur a consulté l'articleP
likyly.events().clickLe visiteur a cliqué sur l'articleP
likyly.events().add_to_cartLe visiteur a ajouté l'article au panierP
likyly.events().remove_from_cartLe visiteur a retiré l'article du panierP
likyly.events().purchaseLe visiteur a acheté l'article (renseignez un identifiant d'événement)P
likyly.recommendations().getRecommandations avec stratégie automatique : utilisateur, session, article ou articles vusP
likyly.recommendations().popularAvancé : les articles les plus populairesP
likyly.recommendations().similarAvancé : articles similaires à un articleP
likyly.recommendations().collaborativeAvancé : ce qu'ont aimé les utilisateurs similaires (modèle entraîné requis)P
likyly.recommendations().hybridAvancé : articles similaires personnalisés pour un utilisateur (alpha)P
likyly.recommendations().sessionAvancé : à partir des articles vus ou de l'historique d'un utilisateurP

Les listes sont paginées avec limit et offset (la page par défaut de l’API compte 100 articles) ; le total est fourni avec la page. Les utilisateurs ont un import par lot mais pas de suppression par lot : l’API n’a pas de tel endpoint. L’import des articles est un alias de l’upsert par lot (l’import CSV est une fonction du tableau de bord).

Notes Rust

  • Error est #[non_exhaustive] : gardez un bras générique. Les variantes issues de l'API portent un ApiError boxé (e.status_code, e.request_id, e.retry_after).
  • Les champs des requêtes et des réponses sont en snake_case ; les champs optionnels sont des Option<_>, et ..Default::default() complète le reste. Les clés de Properties (une serde_json::Map) vous appartiennent et ne sont jamais renommées.

Paquet : likyly sur crates.io. Référence REST complète : api.likyly.com/docs.