Av Anders Ekdahl

Synka produktdata från e-handeln till sök/relevans-motor med Nexus

2024-05-25

Arkitektur, Composable Commerce & Tips

Att ha en sökmotor som inte är inbyggd och färdig-integrerad i e-handelssystemet är vardag för många e-handlare. Dessutom används sökmotorn ofta till mer än bara sök, den används även för kategori-listningar och rekommendationer. På många sätt blir sökmotorn en lika kritisk komponent som e-handelssystemet i sig.

En utmaning som alla ställs inför är att synka produkt-data från e-handelssystemet eller PIM:et till sökmotorn. I många fall vill man även synka data från andra system in till sökmotorn. Man vill skicka in lager-information och pris, man vill skicka in rena CMS-sidor för att göra dem sökbara, och man vill skicka in produkt-bilder och produkt-data.

Detta är en svårare utmaning än vad det först kan verka, särskilt i de fall där sökmotorn används för mer än bara sök. När man skickar pris och lager-information till sökmotorn blir det väldigt viktigt att uppdateringar av dessa skickas till sökmotorn inom sekunder efter att de skett. Det är lätt att ta för givet att det bara ska funka, men många integrationer bygger fortfarande på nattliga körningar eller schemalagda körningar ett par gånger om dagen.

Vanliga fallgropar

En vanlig fallgrop för att lösa detta är att man hoppas att en färdig plugin mellan e-handelssystemet och sökmotorn ska räcka och fungera. Problemet med dessa plugins som ofta byggs av antingen leverantören av e-handelssystemet eller sökmotorn inte är deras core business. Det är något de måste bygga för att kunna säga i säljmöten att det finns en integration. De funkar ofta helt okej för att komma igång med, men så fort man har önskemål som inte passar inom ett sälj-demo så stöter man på begränsningar. Platsen mellan e-handelssystemet och sökmotorn är lite av ett ingenmansland som ingen av leverantörerna egentligen vill befinna sig i, så resultatet blir oftast därefter.

En annan vanlig fallgrop är att man tänker att man kan använda serverless-teknik med cloud events så som Azure Functions eller AWS Lambda. I teorin låter det som en perfekt approach. E-handelssystemet genererar ett event som säger att en produkt uppdateras, man lyssnar på ett sånt event i sin serverless function och uppdaterar sökmotorn. Problemet är att ofta sker svepande uppdateringar på en stor mängd produkter som genererar en stor mängd events på en gång. Att lyssna på och behandla dessa events ett och ett kommer att ta betydligt längre tid än ifall man behandlar dem i batch. Det är heller inte ovanligt att flera events sker mer eller mindre samtidigt på samma produkt. Behandlas dessa ett och ett gör det att integrationen tar mycket längre tid än nödvändigt.

I andra fall behövs det synkronisering av olika events innan det kan skickas till sökmotorn. Man vill inte skicka produkten till sökmotorn förrän den har fått ett pris t.ex. Behandlar man dessa events ett och ett i en serverless arkitektur blir det en växande komplexitet i att avgöra vad för extra saker som behöver ske för att hantera denna typen av synkronsering.

Nexus

Nexus är ett gratis-för-alltid verktyg som Commerce Mind tagit fram med fokus på att förenkla och säkerställa de integrationer och data-flöden som är vardag för en e-handlare. Vi har kokat ner vår erfarenhet och kunskap i ett verktyg som vi erbjuder för att man ska kunna sova lite bättre och slippa oroa sig för att ens data flödar på rätt sätt mellan system.

Det vi börjar med att göra i Nexus är att skapa en . Den första fråga vi ställs inför är vilken data vi vill lagra i kö-meddelandet. Generellt sett finns det två sätt. Det första är att meddelandet innehåller ett id för t.ex. produkten som vi sen använder för att hämta produkt-datan när meddelandet behandlas. Det andra sättet är att låta meddelandet innehålla produkt-datan som vi vill skicka till sökmotorn. Båda dessa sätten har sina för- och nackdelar, men du får mer ut av Nexus om du lagrar datan i meddelandet. T.ex. kan Nexus hoppa över uppdateringar av produkten som inte ändrar datan som skickas in till sökmotorn. En annan fördel är att det går snabbare att behandla meddelandet eftersom vi inte först måste hämta produkt-data.

Vårt till en början tomma kö-meddelande ser alltså ut så här:

[QueueMessage("product", IdempotentMessages = true)]
public class ProductChangedQueueMessage : IQueueMessageWithId
{
    public string? Id { get; set; }
}

Eftersom varje meddelande har ett id och vi vet att vi har ett begränsat antal produkter så slår vi på att Nexus sparar behandlade meddelanden. Även om vi har miljontals produkter så är det ändå ett begränsat antal som en Nexus-kö utan problem kan hantera.

Det intressanta med att spara meddelanden istället för att slänga dem när de är behandlade är att det låter oss göra saker som att jämföra mot förra meddelandet för att ta smartare beslut. En vanlig sak i e-handelsvärlden är att url:en för en produkt bestäms av produktens namn, och om produktens namn ändras behöver det t.ex. skapas upp redirects från gamla till nya namnet. Eller att hantera lagkraven kring prishistorik genom att spara priserna i en Nexus-kö och automatiskt veta vad det föregående priset var.

Så låt oss utgå från att vi lagrar data som produkt-namn, beskrivning, bilder och pris i själva meddelandet. Nästa fråga vi ställs inför är vilket id vi ska använda för kö-meddelandet. En kandidat är det interna produkt-id:t från e-handelssystemet. Men det är en ganska dålig kandidat eftersom det bara är e-handelssystemet som känner till det. Om vi i nästa steg vill lyssna på förändringar i ERP som inte har tillgång till detta id så kommer vi inte kunna slå samman dessa uppdateringar. Istället behöver vi använda en identifierare som är genensam mellan system, typiskt produktnummer eller artikelnummer.

Vårt kö-meddelande ser nu ut ungefär så här:

[QueueMessage("product")]
public class ProductChangedQueueMessage : IQueueMessageWithId
{
    public string? Id { get; set; }
    public ProductInformation? ProductInformation { get; set; }
    public ProductPrice? Price { get; set; }
}

public class ProductInformation
{
    public required string? Name { get; set; }
    public required string? Description { get; set; }
}

public class ProductPrice
{
    public required decimal Price { get; set; }
    public required string Currency { get; set; }
}

Det intressanta med att dela upp produkt-informationen som namn och beskrivning till en egen del av meddelandet och att priset är en egen del är att de då kan skickas in till kön oberoende av varandra genom att utnyttja att en kö kan ta emot ofullständiga meddelanden. Dvs, vi kan skicka in produkt-information och pris från olika ställen och Nexus garanterar att datan från de olika källorna slås ihop korrekt i samma meddelande.

Nästa steg är att börja fylla våra köer med data. Nexus kommer automatiskt att göra en IEnqueuer<TMessage> tillgänglig som vi kan använda för att lägga till meddelanden:

class ProductService(IEnqueuer<ProductChangedQueueMessage> enqueuer)
{
    public async Task EnqueueProductChangedAsync(string productNumber, string name, string description)
    {
        await enqueuer.EnqueueAsync(
            new ProductChangedQueueMessage 
            {
                Id = productNumber,
                ProductInformation = new ProductInformation
                {
                    Name = name,
                    Description = description,
                }
            }, 
            new EnqueueContext { PartialMessage = true }
        );
    }
    
    public async Task EnqueuePriceChangedAsync(string productNumber, decimal price, string currency)
    {
        await enqueuer.EnqueueAsync(
            new ProductChangedQueueMessage 
            {
                Id = productNumber,
                Price = new ProductPrice
                {
                    Price = price,
                    Currency = currency,
                }
            }, 
            new EnqueueContext { PartialMessage = true }
        );
    }
}

När vi nu har fått in data i kön är det dags att skapa ett jobb som kan behandla kön:

public class ProductQueueJob(IPreviousMessageProvider previousMessageProvider) : IScheduledQueueJob<ProductChangedQueueMessage>
{
    public string DefaultSchedule => CronSchedule.TimesPerMinute(10);

    public async Task<ProcessResults> ProcessMessageAsync(ProductChangedQueueMessage message, CancellationToken cancellationToken)
    {
        var previousMessage = previousMessageProvider.GetPreviousMessage(message);
        if (previousMessage?.ProductInformation?.Name != null &&
            message.ProductInformation?.Name != null &&
            previousMessage.ProductInformation.Name != message.ProductInformation.Name)
        {
            await CreateRedirectAsync(from: previousMessage?.ProductInformation?.Name, to: message.ProductInformation?.Name);
        }
        
        if (message.ProductInformation == null || message.Price == null)
        {
            // Once the missing data comes in the processing job will run again so we mark the
            // message as processed
            return ProcessResults.Processed("Skipping processing of incomplete message");
        }
        
        await SendProductToSearchEngineAsync(message);

        return ProcessResults.Processed();
    }
}

Här ser vi hur vi enkelt kan läsa upp föregående meddelande och jämföra vad som ändrats för att agera på specifika förändringar. Föregående meddelande är alltid det senaste meddelande som behandlades av Nexus-jobbet. Dvs, man riskerar inte att missa en förändring ifall jobbet var pausat och flera förändringar kom in under tiden.

I det här fallet behandlar vi varje meddelande ett och ett, vilket är det enklaste sättet att göra det på, men för stora mängder produkter kommer det ta mycket längre tid än att behandla flera meddelanden åt gången. Med Nexus är det förstås enkelt att behandla meddelanden i batch istället:

public class ProductQueueJob : IScheduledBatchQueueJob<ProductChangedQueueMessage>
{
    public string DefaultSchedule => CronSchedule.TimesPerMinute(10);
    public int BatchSize => 100;

    public async Task ProcessMessagesAsync(QueueMessageBatch<ProductChangedQueueMessage> batch, CancellationToken cancellationToken)
    {
        await SendProductsToSearchEngineAsync(batch.Messages);
    }
}

Nu lyssnar vi på ändringar på produkt-information och priser och lägger i Nexus-kön och vi har ett jobb som behandlar kön 10 gånger i minuten. Anledningen till att kö-jobb i Nexus är schemalagda snarare än att de körs så fort ett kö-meddelande kommer in är för att kunna invänta en större mängd meddelanden och utnyttja att det går snabbare att behandla många åt gången. Men det finns inget som hindrar en att schemalägga jobbet att köra varje sekund.

Det vi har kvar är ett sätt att köra en full sync. Att ta all produkt-data och alla priser och köra igen.

public class EnqueueAllProductsJob(IEnqueuer<ProductChangedQueueMessage> enqueuer, 
                                   ErpService erpService, 
                                   ProductDataService productDataService) : IScheduledJob
{
    public string DefaultSchedule => CronSchedule.NotScheduled();
    
    public async Task<JobResults> ExecuteAsync(CancellationToken cancellationToken)
    {
        await enqueuer.EnqueueAsync(
            (await erpService.GetAllPricesAsync())
                .Select(price => new ProductChangedQueueMessage 
                {
                    Id = price.ProductNumber,
                    Price = new ProductPrice 
                    { 
                        Price = price.Price, 
                        Currency = price.Currency,
                    }
                }),
                new EnqueueContext { PartialMessage = true }
        );
        
        await enqueuer.EnqueueAsync(
            (await productDataService.GetAllProductsAsync())
                .Select(product => new ProductChangedQueueMessage 
                {
                    Id = product.ProductNumber,
                    ProductInformation = new ProductInformation 
                    { 
                        Name = product.Name, 
                        Description = product.Description,
                    }
                }),
                new EnqueueContext { PartialMessage = true }
        );
    }
}

Även här utnyttjar vi att Nexus kan ta emot ofullständiga meddelanden eftersom ERP kan ha priser för produkter som vi inte har produkt-information för än och vice versa. Även om vi inte kan behandla dessa meddelanden förrän den andra datan kommer in vill vi ändå ha in datan i kön.

Hur lång tid detta jobb tar är helt beroende på hur snabbt det går att läsa ut data från de andra systemen. Nexus kan utan problem ta emot tio-tusentals meddelanden i sekunden.

Effekten av detta jobb är att bara de produkter vars data har ändrats sedan Nexus behandlade dem sist kommer att synkas eftersom vi håller reda på tidigare version av meddelandet. Men vill man tvinga en fullständig sync ändå är det bara att uppdatera alla kö-meddelandens status via admin-gränssnittet som ingår i Nexus:

Varför syns inte min produkt på siten!?

Innan produkt-datan skickas till sök- och relevans-motorn vill vi validera att datan är korrekt. Vi har redan lagt in validering som säkerställer att produkten har ett pris innan den skickas, men ofta behövs det mer logik än så. Vi vill säkerställa att produkten har en eller flera bilder, en eller flera kategorier, etc. Denna logik kan enkelt läggas till i jobbet.

En vanlig fråga från verksamheten på många e-handelsföretag är "Varför syns inte produkt X på siten?" och ofta beror det på att den fastnat i någon validering. Låt oss göra det enkelt för verksamheten att själv kunna se detta:

public class ProductQueueJob : IScheduledQueueJob<ProductChangedQueueMessage>
{
    public string DefaultSchedule => CronSchedule.TimesPerMinute(10);

    public async Task<ProcessResults> ProcessMessageAsync(ProductChangedQueueMessage message, CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(message.ProductInformation?.Description))
        {
            return ProcessResults.CustomStatus("Invalid", "Missing description");
        }
        
        await SendProductToSearchEngineAsync(message);

        return ProcessResults.Processed();
    }
}

I detta fall kontrollerar vi ifall det finns en beskrivning och om den saknas så hamnar meddelandet i statusen "Invalid" med beskrivningen "Missing description". Det blir sen lätt för vem som helst att i admin-grässnittet filtera fram alla meddelanden i statusen Invalid för att se vad som blivit fel:

Summering

Även om det är ett bra use-case för Nexus att synca produkt-data till en sökmotor är det knappast det enda. I den här artikeln har vi gått igenom ett antal av de features som gör Nexus speciellt och kanske framförallt hur enkelt Nexus gör det att slippa de vanliga fallgroparna. Vill du automatiskt synca om alla produkter i en kategori när namnet på kategorin ändras? För de flesta krävs det en manuell full sync. Med Nexus blir det enkelt att automatisera.

Det finns mycket mer funktionalitet att utforska utöver den här artikeln så som att låta jobb ha parametrar, att använda virtuella köer, att utnyttja Nexus Functions när det behövs något enklare än en kö eller att se alla tidigare version av ett kö-meddelande.

Låter det som att det skulle kunna hjälpa dig med dina data-flöden? Hör av dig, Nexus är gratis för alltid och du får full tillgång till koden via en open source-licens.

Passa på att följa oss på LinkedIn för att en notis om när vi släpper artikeln om hur Nexus hanterar att synka om produkten när framtida, datum-styrda priser aktiveras!

Anders Ekdahl

Anders är hjärnan bakom de tekniska e-handels-ramverken som tagit Nordic Nest, Lyko, NA-KD, Filippa K, Kicks, m.fl. från lovande digitala initiativ till ledande e-handelsaktörer.

I sin roll som CTO och chefsarkitekt för Sveriges främsta e-handelskonsult har han lett över 200 utvecklare till framgång. Med sin förmåga att kombinera teknik, strategi och affärsvärde har Anders en djup insikt om vad du som e-handlare behöver för att nå och överträffa dina mål.

Epost: epost

Commerce Mind

Commerce Mind är ett oberoende specialistföretag inom e-handel. Vi hjälper dig med allt från KPIer till teknik och arkitektur till processutveckling och upphandling.

Läs mer om vårt erbjudande här

Tveka inte på att kontakta oss ifall du vill röra dig snabbare.