Volver a Documentación

Ejemplos avanzados — Node.js

Patrones avanzados y casos de uso reales para integrar Rayuela en aplicaciones Node.js de producción.

Integración con Express.js
Middleware y rutas para aplicación e-commerce completa
const express = require('express');
const axios = require('axios');
const Redis = require('redis');

class RayuelaService {
  constructor(apiKey, redisClient) {
    this.apiKey = apiKey;
    this.baseURL = 'https://rayuela-backend-e7apihrdoa-uc.a.run.app/api/v1';
    this.redis = redisClient;
    this.client = axios.create({
      baseURL: this.baseURL,
      headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json'
      },
      timeout: 10000
    });
  }

  async getRecommendations(userId, options = {}) {
    const cacheKey = `recs:${userId}:${JSON.stringify(options)}`;
    
    // Intentar obtener del cache
    try {
      const cached = await this.redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }
    } catch (err) {
      console.warn('Redis cache miss:', err.message);
    }

    // Obtener de la API
    try {
      const response = await this.client.post('/recommendations/personalized/query', {
        external_user_id: userId,
        recommendation_goal: 'user_engagement',
        model_variant: 'standard',
        limit: options.limit || 10,
        filters: options.filters || { logic: 'and', filters: [] },
        context: options.context || {},
        strategy: options.strategy || 'hybrid',
        include_explanation: false
      });

      const recommendations = response.data;
      
      // Cachear por 10 minutos
      try {
        await this.redis.setex(cacheKey, 600, JSON.stringify(recommendations));
      } catch (err) {
        console.warn('Redis cache write failed:', err.message);
      }

      return recommendations;
    } catch (error) {
      console.error('Rayuela API error:', error.message);
      return this.getFallbackRecommendations();
    }
  }

  async trackInteraction(userId, productId, interactionType, value = 1.0) {
    try {
      await this.client.post('/interactions', {
        external_user_id: userId,
        external_product_id: productId,
        interaction_type: interactionType,
        value: value
      });
    } catch (error) {
      console.error('Failed to track interaction:', error.message);
      // No lanzar error - tracking no debe romper la experiencia
    }
  }

  getFallbackRecommendations() {
    // Retornar productos populares hardcodeados
    return {
      items: [
        { externalId: 'popular_1', score: 0.9 },
        { externalId: 'popular_2', score: 0.8 }
      ]
    };
  }
}

// Configuración
const app = express();
const redis = Redis.createClient({ url: process.env.REDIS_URL });
const rayuela = new RayuelaService(process.env.RAYUELA_API_KEY, redis);

app.use(express.json());
Middleware de Tracking
Middleware para tracking automático de interacciones
// Middleware para tracking automático
const trackingMiddleware = (interactionType) => {
  return async (req, res, next) => {
    const originalSend = res.send;
    
    res.send = function(data) {
      // Tracking asíncrono después de enviar respuesta
      setImmediate(async () => {
        try {
          const userId = req.user?.id;
          const productId = req.params.productId || req.body.productId;
          
          if (userId && productId) {
            await rayuela.trackInteraction(userId, productId, interactionType);
          }
        } catch (error) {
          console.error('Tracking error:', error.message);
        }
      });
      
      return originalSend.call(this, data);
    };
    
    next();
  };
};

// Rutas con tracking automático
app.get('/api/products/:productId', 
  trackingMiddleware('view'),
  async (req, res) => {
    const { productId } = req.params;
    
    // Obtener producto de la base de datos
    const product = await Product.findById(productId);
    
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    
    res.json(product);
  }
);

app.post('/api/cart/add',
  trackingMiddleware('cart_add'),
  async (req, res) => {
    const { productId, quantity } = req.body;
    const userId = req.user.id;
    
    // Lógica del carrito
    await Cart.addItem(userId, productId, quantity);
    
    res.json({ success: true });
  }
);

app.post('/api/orders',
  trackingMiddleware('purchase'),
  async (req, res) => {
    const { items } = req.body;
    const userId = req.user.id;
    
    // Procesar orden
    const order = await Order.create({ userId, items });
    
    res.json(order);
  }
);
Rutas de Recomendaciones
Endpoints optimizados para diferentes contextos
// Recomendaciones para homepage
app.get('/api/recommendations/homepage', async (req, res) => {
  try {
    const userId = req.user?.id;
    
    if (!userId) {
      // Usuario anónimo - mostrar productos populares
      const popularProducts = await Product.find({ featured: true })
        .limit(8)
        .sort({ sales_count: -1 });
      
      return res.json({
        items: popularProducts.map(p => ({
          externalId: p._id,
          score: p.sales_count / 1000,
          source: 'trending'
        }))
      });
    }
    
    const recommendations = await rayuela.getRecommendations(userId, {
      limit: 8,
      strategy: 'hybrid',
      filters: {
        logic: 'and',
        filters: [{ field: 'inStock', op: 'eq', value: true }]
      }
    });
    
    res.json(recommendations);
  } catch (error) {
    console.error('Homepage recommendations error:', error);
    res.status(500).json({ error: 'Failed to get recommendations' });
  }
});

// Recomendaciones para página de producto
app.get('/api/recommendations/product/:productId', async (req, res) => {
  try {
    const { productId } = req.params;
    const userId = req.user?.id;
    
    if (!userId) {
      // Usuario anónimo - productos relacionados por categoría
      const product = await Product.findById(productId);
      const related = await Product.find({
        category: product.category,
        _id: { $ne: productId }
      }).limit(4);
      
      return res.json({
        items: related.map(p => ({
          externalId: p._id,
          score: 0.8,
          source: 'curated'
        }))
      });
    }
    
    const recommendations = await rayuela.getRecommendations(userId, {
      limit: 4,
      strategy: 'content_based',
      filters: { 
        logic: 'and',
        filters: [{ field: 'inStock', op: 'eq', value: true }]
      },
      context: {
        page_type: 'product_detail',
        source_external_product_id: productId
      }
    });
    
    res.json(recommendations);
  } catch (error) {
    console.error('Product recommendations error:', error);
    res.status(500).json({ error: 'Failed to get recommendations' });
  }
});

// Recomendaciones para carrito
app.get('/api/recommendations/cart', async (req, res) => {
  try {
    const userId = req.user.id;
    const cart = await Cart.findOne({ userId }).populate('items.product');
    
    // Obtener categorías del carrito
    const cartCategories = [...new Set(
      cart.items.map(item => item.product.category)
    )];
    
    const recommendations = await rayuela.getRecommendations(userId, {
      limit: 6,
      strategy: 'hybrid',
      filters: {
        logic: 'and',
        filters: [{
          field: 'price',
          op: 'lt',
          value: Math.max(...cart.items.map(item => item.product.price)) * 1.5
        }]
      }
    });
    
    res.json(recommendations);
  } catch (error) {
    console.error('Cart recommendations error:', error);
    res.status(500).json({ error: 'Failed to get recommendations' });
  }
});
Jobs en Background
Sincronización de datos con Bull Queue
const Bull = require('bull');
const syncQueue = new Bull('rayuela sync', process.env.REDIS_URL);

// Job para sincronizar productos
syncQueue.process('sync-products', async (job) => {
  const { batchSize = 1000 } = job.data;
  
  try {
    let offset = 0;
    let hasMore = true;
    
    while (hasMore) {
      const products = await Product.find({ is_active: true })
        .skip(offset)
        .limit(batchSize)
        .lean();
      
      if (products.length === 0) {
        hasMore = false;
        break;
      }
      
      // Transformar para Rayuela
      const rayuelaProducts = products.map(p => ({
        externalId: p._id.toString(),
        name: p.name,
        description: p.description,
        price: p.price,
        category: p.category,
        brand: p.brand,
        inStock: p.stock > 0,
        attributes: p.attributes || {}
      }));
      
      // Enviar a Rayuela
      await rayuela.client.post('/ingestion/batch', {
        products: rayuelaProducts
      });
      
      offset += batchSize;
      
      // Actualizar progreso
      job.progress(Math.min(100, (offset / await Product.countDocuments()) * 100));
    }
    
    return { synced: offset };
  } catch (error) {
    console.error('Product sync failed:', error);
    throw error;
  }
});

// Job para sincronizar interacciones recientes
syncQueue.process('sync-interactions', async (job) => {
  const { hoursBack = 24 } = job.data;
  const cutoff = new Date(Date.now() - hoursBack * 60 * 60 * 1000);
  
  try {
    const interactions = await Interaction.find({
      created_at: { $gte: cutoff }
    }).lean();
    
    const rayuelaInteractions = interactions.map(i => ({
      external_user_id: i.user_id.toString(),
      external_product_id: i.product_id.toString(),
      interaction_type: i.type,
      value: i.value || 1.0,
      timestamp: i.created_at.toISOString()
    }));
    
    // Enviar en lotes
    const batchSize = 1000;
    for (let i = 0; i < rayuelaInteractions.length; i += batchSize) {
      const batch = rayuelaInteractions.slice(i, i + batchSize);
      
      await rayuela.client.post('/ingestion/batch', {
        interactions: batch
      });
    }
    
    return { synced: rayuelaInteractions.length };
  } catch (error) {
    console.error('Interaction sync failed:', error);
    throw error;
  }
});

// Programar jobs
syncQueue.add('sync-products', {}, {
  repeat: { cron: '0 2 * * *' }, // Diario a las 2 AM
  removeOnComplete: 5,
  removeOnFail: 10
});

syncQueue.add('sync-interactions', { hoursBack: 1 }, {
  repeat: { every: 60000 }, // Cada minuto
  removeOnComplete: 10,
  removeOnFail: 5
});

// Endpoint para trigger manual
app.post('/api/admin/sync-rayuela', async (req, res) => {
  try {
    const productJob = await syncQueue.add('sync-products', {});
    const interactionJob = await syncQueue.add('sync-interactions', { hoursBack: 24 });
    
    res.json({
      productJobId: productJob.id,
      interactionJobId: interactionJob.id
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
Optimización de Rendimiento

Mejores prácticas

  • • Usa Redis para cachear recomendaciones (5-15 minutos)
  • • Implementa circuit breaker para fallos de API
  • • Tracking asíncrono con setImmediate() o queues
  • • Connection pooling con axios.create()
  • • Fallbacks para alta disponibilidad

⚠️ Consideraciones

  • • Timeouts apropiados (10-30 segundos)
  • • Rate limiting en endpoints públicos
  • • Monitoreo de errores con Sentry/similar
  • • Logs estructurados para debugging