MySQL: Migrering til ElasticSearch

Vi flyttede fritekst-søgningen ud af MySQL og opnåede mange fordele med denne migrering til ElasticSearch. Hastighed og stabilitet var bare to af gevinsterne ved migreringen.

Søgning i store mængder data udvikler sig – både for hvad der kvalificerer sig som store mængder, men også hvilke værktøjer der er mest velegnede til opgaven. ElasticSearch har set en stor opblomstring, så det er naturligt at undersøge hvad platformen kan gøre for os.

Det kan sagtens lade sig gøre at komme i gang uden at skulle investere andet end din egen tid (meget af platformen er Open Source). Når det er sagt er det også godt at vide, at bag teknologien står en virksomhed der både udbyder kurser, hosting og betalt adgang til bl.a. en række analyse og visualiseringsværktøjer, hvis du har brug for det.

Hos Overskrift begynder vi at gribe ud efter nye værktøjer, når vi ser et begyndende behov, men også når omtalen af teknologi nærmer sig områder vi opererer i. I dette tilfælde lå en stor del af vores data på en ældre MySQL-platform og de seneste 3 måneders data var fuld-tekst indekseret i en MyISAM tabel. Den ældre version af MySQL begyndte at knage, og selvom databasen havde masser af dejlig RAM, kunne vi, pga. versionen af MySQL, ikke anvende Fuldtekst-indexes på den nyere InnoDB tabel-type. Vi så altså på et database-opdateringsprojekt, men tænkte også om vi evt. kunne finde noget teknologi der skalerede bedre; Her faldt vores opmærksomhed på ElasticSearch.

Hvad kan ElasticSearch, som MySQL ikke kan?

ElasticSearch er meget anderledes, for udviklere der kommer fra primært at arbejde med MySQL. Tingene hedder noget andet: Du skal f.eks. vende dig til, at hvad du tidligere kendte som en tabel i MySQL nu hedder et index, og data skal indsættes og fremsøges ved at sende og modtage JSON-dokumenter til databasen istedet for SQL. Det er forholdsvis ligetil at abstrahere simple tabelopslag ud i funktionskald.

Den rigtig god nyhed for os var, at søgesproget i MySQL fulltext-indexes og ElasticSearch ligner hinanden utroligt meget, så kundernes søgestrenge skulle ikke skrives om. Vi kunne omskrive blot omskrive applikationens model-kode, for at bibeholde interfacet til søgninger i de relevante data.

Migrering til ElasticSearch betyder også indbygget skalering

ElasticSearch er bygget til skalérbarhed. Hvis du sætter en server op på dit netværk, har du hurtigt database-storage, men sætter du endnu en server (node) op, begynder den at aflaste den første server og du har et cluster – det giver dog mest mening når dit cluster består af mindst 3 noder. Kan en node “se” en anden node på netværket, kan den videresende opslag fra din applikation til den anden node. Typisk vil en node være master – og fungere som load-balancer for andre noder i clusteret, men den opgave kan altså også overtages af andre noder. Skaléringen kan altså både give større sikkerhed mod hardware-nedbrud, men også bedre performance.

Migrering fra SQL til JSON

Arbejder du med MySQL (eller andre relationelle databaser) ved du, at data gemmes i rækker i tabeller, og for at gøre søgning i disse data hurtigere oprettes et index, der ligesom et index i en bog gør, at du hurtigt kan finde information baseret på udvalgte kolonner eller værdier i din tabels rækker af data. Skal du hente alle records fra en tabel i MySQL, og se dem med det højeste id først, kan det f.eks. se således ud:

SELECT * FROM overskrift_2019_11
ORDER BY id DESC;

Samme slags opslag i ElasticSearch kunne se således ud:

GET overskrift_2019_11/_search
{
"query": {
"match_all": {}
},
"sort": {
"id": "desc"
}
}

Altså et JSON dokument, der angiver at vi gerne vil se på tabellen indexet overskrift_2019_11. JSON-dokumentet sendes med et GET-request til ElasticSearch, som svarer noget a’la:

{
   "took" : 28,
   "timed_out" : false,
   "_shards" : {
     "total" : 5,
     "successful" : 5,
     "skipped" : 0,
     "failed" : 0
   },
   "hits" : {
     "total" : 3095276,
     "max_score" : null,
     "hits" : [
       {
         "_index" : "overskrift_2019_11",
         "_type" : "posts",
         "_id" : "486678534",
         "_score" : null,
         "_source" : {
           "id" : 486678534,
           "item_url" : "http://www.example.com/test",
           "title" : "Kommunen er blandt de første til at matche 6. klasser med SOSU-uddannelse",
           "description" : "Som de første i landet har Kommunen lavet et 4-dages brobygningsforløb, hvor alle 6. klasser prøver kræfter med omsorgsfagene. Opstarten har været så stor en succes, at forløbet fortsætter. I et nyt samarbejde mellem ...

Altså også et JSON-dokument som svar. Udover data fortæller ElasticSearch heri bl.a. også at opslaget tog 28 millisekunder, og at der findes 3.095.276 “records” der matcher. Skal man søge efter en specifik tekst, bliver forespørgslen lidt mere kompliceret.

GET overskrift_2019_11/_search
{
"query": {
"bool": {
"must": {
"query_string": {
"query": "+banjo +guitar*",
"fields": ["title","description","author"],
"type": "cross_fields"
}
}
}
},
"sort": {
"id": "desc"
}
}

Når man ser svartider falde fra flere sekunder til få milisekunder uden tabel-låse på hvert opslag, og først har abstraheret JSON-formatet væk, begynder det altså at give ekstremt god mening – især som afløser for et monolitisk MySQL fuldtekst-index. Og du har måske fornemmet her, at vores tilgang til ordet migrering ikke helt følger den gængse opfattelse.

Pragmatisk migrering

ElasticSearch er nemlig ekstremt god til skaléring og lynhurtig tekstsøgning. Vi har dog en applikation, som har mange data-afhængigheder mellem forskellige entiteter. Skulle alle tabeller og data med i denne migrering til ElasticSearch, ville vi se på en meget stor kodeomskrivning af vores applikation, så vi valgte i stedet at afhjælpe det mest presserende problem: Svartiderne på tekstsøgninger fra den mest aktive tabel (med ca. 150.000 INSERT’s pr. dag og mange opslag).

For at få svartider ned på tværs af hele applikationen, eliminerede vi altså fulltext-indekset på vores indholds tabel og kopierer nye data til ElasticSearch så snart de er indlæst i MySQL. Alle MySQL-opslag, der brugte fuld-tekst indekset blev skrevet om til ElasticSearch queries.

Planlægning af strukturer

De data vi arbejder med, er naturligt periode-opdelte. Når kunderne skal bruge dem, er de som regel interesserede i data i månedsintervaller. I andre tilfælde foretages udtræk ofte pr. år. Meget gamle data er ikke super interessante, og vi vil gerne kunne offloade “gamle” data (læs slette indexet) når de ikke længere er relevante. På MySQL ligger disse data opdelt i:

  • en tabel for de seneste 3 måneder med fulltext index, og
  • en stor arkiv-tabel (uden indexes) for alt ældre indhold.

På ElasticSearch opretter vi et index pr. måned med månedens data, og afgrænser adgangen til f.eks. de seneste 3 måneders data med et alias, der altid peger på de indexes, der dækker de seneste 3 måneder. Alias’et hedder altså posts_recent og er et alias for de seneste 3 måneder’s indexes:

overskrift_2019_10
overskrift_2019_11
overskrift_2019_12

Når vi træder ind i en ny måned omdefineres indexet til at pege på de seneste 3 måneder. I MySQL ville det svare til at gemme data i en tabel pr. måned og benytte et view, som en SELECT … UNION … på de 3 seneste måneders tabeller. Det eneste vi skal være opmærksomme på er, at gen-definere vores alias’er, når vi træder ind i en ny måned.