Migrering af Elasticsearch indexes med C#

Dette skulle være så nemt: NoSQL databaserne og deres skemaløse tilgang til verden. Pist væk med tabeller, primærnøgler, fremmednøgler og vigtigst af alt – Migrering! Billedet er desværre ikke helt så sort og hvidt. Jeg har arbejdet med NoSQL databaser som Elasticsearch, RavenDB, MongoDB og CouchDB mere eller mindre konstant de sidste fem år. Det sidste års tid mere intenst under udviklingen af mit cloud logging projekt elmah.io. Jeg er endnu ikke stødt på en NoSQL database som ikke har behov for en eller anden form for data-migrering.

I dette blog post vil jeg grave mig ned i at lave migrering af Elasticsearch, der er en fantastisk søgemotor som vi benytter på elmah.io. Eksemplerne er skrevet i C# med den officielle Elasticsearch klient NEST, men fremgangsmåden vil være den samme fra andre programmeringssprog.

NoSQL databaser som Elasticsearch siges at være skemaløse, men i praksis forholder det sig ikke altid sådan. Forestil dig en relationel database hvor du skal ændre postnummer kolonnen fra en int til en string. Her vil du typisk skrive et migreringsscript og eksekvere det mod din database enten manuelt eller via et migreringsværktøj som DbUp. I scriptet skal du tage stilling til selve ændringen af datatype, men også hvordan de eksisterende rækker skal migreres. Du kender sikkert processen.

I et produkt som Elasticsearch, kan du vælge mellem to forskellige løsninger når du skal lave ændringer:

  1. Lade Elasticsearch selv udvide skemaet ved at tilføje et postnummer felt i det dokument du indekserer.
  2. Lave et PutMapping kald på Elasticsearch med et opdateret “skema”.

I praksis bruger jeg typisk løsning 2, da det giver mig fuld kontrol over hvordan og hvornår nye felter oprettes. Elasticsearch vælger som standard at indeksere alle nye felter og det er langt fra altid, at dette er det ønskede resultat.

Elasticsearch har et glimrende forslag til at versionere indexes, hvilket jeg vil tage udgangspunkt i i det efterfølgende C# eksempler. Kort fortalt går det ud på at tilføje et versionsnummer til dit indexnavn og så oprette nye versioner af indexet når der skal ske ændringer som kræver reindeksering. For ikke at skulle release ny version af dine software der udpeger det aktive index, oprettes et alias (typisk indexets navn uden versionsnummeret). Alias vil således altid udpege det aktive index. For en detaljeret beskrivelse af fremgangsmåde, vil jeg anbefale dig at læse indholdet på ovenstående link.

Lad os starte med at oprette et nyt index med mapping og data til vores migreringseksempel:

var connectionSettings = new ConnectionSettings(new Uri("http://localhost:9200/"));
connectionSettings.SetDefaultIndex("customers");
var elasticClient = new ElasticClient(connectionSettings);

elasticClient.CreateIndex("customers-v1");

elasticClient.Alias(x => x
  .Add(a => a
    .Alias("customers")
    .Index("customers-v1")));

elasticClient.Map<Customer>(d => d
  .Properties(p => p
    .Number(n => n
      .Name(name => name.Zipcode)
      .Index(NonStringIndexOption.not_analyzed))));

I eksemplet opretter jeg et nyt index ved navn customers-v1 og tilføjer en mapping (skema) for C# typen Customer med et enkelt felt Zipcode. Resultatet ses på følgende screenshot:

screenshot1 Thomas Ardal

Herefter kan jeg indsætte en Customer:

elasticClient.Index(new Customer { Zipcode = 8000 });

Nu forestiller vi os at jeg har behov for at ændre typen af Zipcode fra Number til String og samtidigt migrere eksisterende dokumenter. Jeg kan ikke umiddelbart ændre på typen af et eksisterende felt i Elasticsearch, hvorfor jeg skriver en migrering vha. Reindex metoden i NEST:

var reindex = elasticClient.Reindex<Customer>(r => r
  .FromIndex("customers-v1")
  .ToIndex("customers-v2")
  .Query(q => q.MatchAll())
  .Scroll("10s")
  .CreateIndex(i => i
    .AddMapping<Customer>(m => m
      .Properties(p => p
        .String(n => n
          .Name(name => name.Zipcode)
          .Index(FieldIndexOption.not_analyzed))))));

var o = new ReindexObserver<Customer>(onError: e => { });
reindex.Subscribe(o);

Til Reindex metoden giver jeg et fra og til index navn og fortæller samtidigt Elasticsearch hvilke dokumenter som jeg ønsker migrereret fra customers-v1 til customers-v2. I dette tilfælde er det alle dokumenter. Som en del af Reindex kaldet opretter jeg også v2 indexet og tilføjer en ny mapping. Dette svarer til CreateIndex og Map kaldende i eksemplet fra før.

Sidst, men ikke mindst, skal vi have ændret pegepinden – vores alias – til at pege på det nye index:


elasticClient.DeleteIndex(d => d
  .Index("customers-v1"));

elasticClient.Alias(x => x
  .Add(a => a
    .Alias("customers")
    .Index("customers-v2")));

Først vælger jeg at slette det gamle index, hvilket også sletter det eksisterende alias. Derefter opretter jeg aliaset på ny og sætter det til at pege på customers-v2 indexet.

Når du lægger kode eksemplerne sammen bliver det tydeligt, at data migrering langt fra er et overstået kapitel i NoSQL databaser som Elasticsearch. Det er dog efter min mening blevet en helt del nemmere, da mange ændringer vil kunne laves uden at eksekvere scripts mod databasen. Ydermere er der kommet rigtig god support for re-indeksering i Elasticsearch, hvilket gør det nemt i de situationer hvor der er behov for at genopbygge et index.

Hele kode eksemplet kan findes på https://gist.github.com/ThomasArdal/1f69720da208f1863ae7

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *