Elasticsearch lietuviško teksto paieška

Kodėl lietuviška paieška yra iššūkis

Kai pradedi kurti paieškos sistemą lietuviškam turiniui, greitai supranti, kad tai nėra toks paprastas darbas kaip anglų kalbai. Elasticsearch iš dėžės puikiai veikia su anglų kalba – viskas sukonfigūruota, optimizuota ir veikia kaip šviesiai. Bet kai imi diegti lietuvišką paieškos funkciją, iškart susiduri su kalbos ypatumais: linksniais, galūnėmis, kirčiavimo taisyklėmis ir kitais dalykais, kurie daro lietuvių kalbą gražią, bet techniškai sudėtingą.

Pagrindinė problema – žodžių formos. Pavyzdžiui, žodis „namas” gali turėti daugybę formų: namo, namui, namą, namu ir taip toliau. Jei vartotojas ieško „namai”, o dokumente yra „namų”, standartinė paieška tiesiog nieko neras. Čia ir prasideda tikrasis darbas.

Dar vienas aspektas – lietuviški diakritiniai ženklai. Elasticsearch turi suprasti, kad „ė” ir „e” kartais turėtų būti laikomi panašiais simboliais, bet ne visada. Reikia rasti balansą tarp paieškos tikslumo ir lankstumo.

Analizatorių konfigūravimas lietuvių kalbai

Elasticsearch širdis – tai analizatoriai (analyzers). Jie nulemia, kaip tekstas bus apdorojamas prieš indeksuojant ir kaip bus apdorojama paieškos užklausa. Lietuvių kalbai reikia sukurti specialų analizatorių, kuris suprastų mūsų kalbos specifiką.

Paprasčiausias variantas – naudoti ICU (International Components for Unicode) pluginą. Jis leidžia normaliai dirbti su Unicode simboliais ir turi įvairių tokenizatorių bei filtrų. Štai kaip galėtų atrodyti bazinė konfigūracija:

PUT /lietuviu_tekstai
{
  "settings": {
    "analysis": {
      "analyzer": {
        "lietuviu_analizatorius": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "asciifolding",
            "lietuviu_stop_zodziai",
            "lietuviu_stemmer"
          ]
        }
      },
      "filter": {
        "lietuviu_stop_zodziai": {
          "type": "stop",
          "stopwords": ["ir", "bei", "ar", "o", "kad", "kaip"]
        }
      }
    }
  }
}

Čia matome kelis svarbius komponentus. Lowercase filtras paverčia viską mažosiomis raidėmis, asciifolding pašalina diakritikus (ą → a, ė → e), o stop žodžiai filtruoja dažniausius, bet mažai reikšmingus žodžius.

Tačiau tai tik pradžia. Tikrai veikiančiai sistemai reikia daugiau.

Stemming ir lemmatizacija – kas geriau

Čia prasideda įdomiausia dalis. Stemming – tai proceso, kai nuo žodžio „nukerpamos” galūnės, siekiant gauti šaknį. Pavyzdžiui, „bėgimas”, „bėgioti”, „bėga” visi taptų „bėg”. Lemmatizacija eina toliau – ji bando rasti žodžio bazinę formą (lemą), atsižvelgdama į kontekstą ir kalbos taisykles.

Lietuvių kalbai stemming nėra idealus sprendimas. Mūsų kalba per daug sudėtinga, per daug išimčių. Paprastas stemmer gali pridaryti daugiau žalos nei naudos. Pavyzdžiui, žodžiai „mėsa” ir „mėšlas” po agresyvaus stemmingo gali tapti per daug panašūs.

Geresnė alternatyva – naudoti morfologinį analizatorių. Yra keletas variantų:

Lemuoklis – tai lietuvių kalbos morfologinis analizatorius, kurį sukūrė Vytauto Didžiojo universiteto mokslininkai. Jis gali identifikuoti žodžio formą ir pateikti bazinę formą. Problema ta, kad jį integruoti į Elasticsearch nėra trivialus dalykas.

Hunspell – tai atviro kodo rašybos tikrinimo biblioteka, kuri turi ir lietuvių kalbos žodyną. Elasticsearch turi integruotą Hunspell palaikymą, todėl tai gali būti praktiškesnis variantas.

Štai kaip galima sukonfigūruoti Hunspell lietuvių kalbai:

PUT /tekstai_su_hunspell
{
  "settings": {
    "analysis": {
      "analyzer": {
        "lt_hunspell": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "lt_hunspell_filter"
          ]
        }
      },
      "filter": {
        "lt_hunspell_filter": {
          "type": "hunspell",
          "locale": "lt_LT",
          "dedup": true
        }
      }
    }
  }
}

Žinoma, tam reikia turėti įdiegtus lietuviškus Hunspell žodynus Elasticsearch serveryje. Tai papildomas darbas, bet rezultatas gerokai geresnis nei be jokios morfologinės analizės.

N-gramų magija sudėtingesnei paieškai

Kartais morfologinė analizė nepakanka. Ypač kai vartotojai daro rašybos klaidų arba ieško dalinio atitikmens. Čia į pagalbą ateina n-gramai – tai teksto skaidymas į mažesnius gabalėlius.

Pavyzdžiui, žodis „Vilnius” su trigram (3 simbolių n-gramais) būtų suskaidytas taip: „Vil”, „iln”, „lni”, „niu”, „ius”. Jei vartotojas ieško „Vilniaus” arba net padaro klaidą ir rašo „Vilnuis”, sistema vis tiek ras atitikmenų, nes dauguma n-gramų sutaps.

Štai praktinė konfigūracija su edge n-gramais (kurie prasideda nuo žodžio pradžios):

PUT /tekstai_su_ngramais
{
  "settings": {
    "analysis": {
      "analyzer": {
        "lt_edge_ngram_analyzer": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "asciifolding",
            "edge_ngram_filter"
          ]
        },
        "lt_search_analyzer": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "asciifolding"
          ]
        }
      },
      "filter": {
        "edge_ngram_filter": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 15
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "pavadinimas": {
        "type": "text",
        "analyzer": "lt_edge_ngram_analyzer",
        "search_analyzer": "lt_search_analyzer"
      }
    }
  }
}

Atkreipk dėmesį, kad indeksavimui ir paieškai naudojami skirtingi analizatoriai. Tai svarbu – indeksuojant naudojame n-gramus, o ieškant – ne. Priešingu atveju paieška taptų per lėta ir per netiksli.

Sinonimų valdymas ir alternatyvūs rašymo būdai

Lietuvių kalboje yra daug žodžių, kurie reiškia tą patį arba gali būti rašomi keliais būdais. Pavyzdžiui, „kompiuteris” ir „kompas” (neformaliai), arba „elektroninis paštas” ir „el. paštas” ir „email”.

Elasticsearch leidžia apibrėžti sinonimų sąrašus, kurie bus naudojami paieškos metu. Tai daroma per synonym filter:

PUT /tekstai_su_sinonimais
{
  "settings": {
    "analysis": {
      "analyzer": {
        "lt_su_sinonimais": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "lt_sinonimų_filtras"
          ]
        }
      },
      "filter": {
        "lt_sinonimų_filtras": {
          "type": "synonym",
          "synonyms": [
            "kompiuteris, kompas, PC",
            "elektroninis paštas, el. paštas, email, e-mail",
            "telefonas, tel., mob.",
            "automobilis, auto, mašina"
          ]
        }
      }
    }
  }
}

Sinonimų sąrašą galima saugoti ir atskirame faile, jei jų daug. Tai patogiau administruoti ir atnaujinti nekeičiant indekso konfigūracijos.

Dar vienas svarbus aspektas – skirtingi rašymo būdai su brūkšneliais ir tarpais. Pavyzdžiui, „e-komercija”, „ekomercija”, „e komercija”. Čia gali padėti word_delimiter filtras:

"word_delimiter_filter": {
  "type": "word_delimiter",
  "preserve_original": true,
  "catenate_all": true
}

Šis filtras skaido žodžius pagal specialius simbolius, bet išsaugo ir originalą, todėl paieška veiks visiems variantams.

Relevancijos tobulinimas lietuviškam turiniui

Surasti dokumentus – tai tik pusė darbo. Kita pusė – juos surikiuoti tinkama tvarka. Elasticsearch naudoja TF-IDF ir BM25 algoritmus relevancijos skaičiavimui, bet kartais reikia papildomų patobulinimų.

Lietuviškam turiniui svarbu atsižvelgti į keletą dalykų:

Laukų svoriai (boosting) – ne visi laukai vienodai svarbūs. Jei paieškos frazė rasta pavadinime, tai svarbiaus nei jei ji rasta aprašymo gale. Tai galima kontroliuoti per multi-match užklausą:

GET /tekstai/_search
{
  "query": {
    "multi_match": {
      "query": "Vilnius",
      "fields": [
        "pavadinimas^3",
        "santrauka^2",
        "turinys"
      ],
      "type": "best_fields"
    }
  }
}

Čia pavadinimas turi 3x didesnį svorį nei turinys.

Frazių atitikimas – jei vartotojas ieško „Vilniaus senamiestis”, geriau, kad dokumentuose šie žodžiai būtų šalia vienas kito, o ne išsibarstę. Tam naudojamas phrase matching arba phrase_prefix:

GET /tekstai/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match_phrase": {
            "turinys": {
              "query": "Vilniaus senamiestis",
              "boost": 2
            }
          }
        },
        {
          "match": {
            "turinys": "Vilniaus senamiestis"
          }
        }
      ]
    }
  }
}

Ši užklausa ieško ir tikslios frazės (su didesniu svoriu), ir atskirų žodžių.

Function score – kartais reikia atsižvelgti į kitus faktorius, ne tik tekstinį atitikimą. Pavyzdžiui, naujesnės naujienos turėtų būti aukščiau, arba populiaresni straipsniai:

GET /naujienos/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "turinys": "technologijos"
        }
      },
      "functions": [
        {
          "gauss": {
            "data": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          }
        },
        {
          "field_value_factor": {
            "field": "perziuros",
            "modifier": "log1p",
            "factor": 0.1
          }
        }
      ],
      "boost_mode": "multiply"
    }
  }
}

Ši užklausa kelia aukštyn naujesnius straipsnius (per gauss funkciją) ir populiaresnius (per peržiūrų skaičių).

Praktiniai patarimai diegiant produkcinę sistemą

Teorija teorija, bet kai reikia paleisti realią sistemą su lietuvišku turiniu, iškyla papildomų klausimų. Štai keletas patarimų iš patirties:

Testuok su realiais duomenimis – sukurk reprezentatyvų duomenų rinkinį su tipiškomis paieškos užklausomis. Matuok precision ir recall metrikas. Elasticsearch turi explain API, kuris parodo, kodėl konkretus dokumentas gavo tam tikrą relevancijos balą – tai neįtikėtinai naudinga derinant.

Naudok kelis analizatorius – vienas indeksas gali turėti kelis to paties lauko variantus su skirtingais analizatoriais. Pavyzdžiui:

"pavadinimas": {
  "type": "text",
  "fields": {
    "exact": {
      "type": "keyword"
    },
    "stemmed": {
      "type": "text",
      "analyzer": "lt_hunspell"
    },
    "ngram": {
      "type": "text",
      "analyzer": "lt_edge_ngram_analyzer"
    }
  }
}

Tada paieškos užklausoje gali kombinuoti visus variantus pagal poreikį.

Monitorink našumą – lietuviški analizatoriai, ypač su morfologine analize ir n-gramais, gali būti resursų ėdrūs. Naudok Elasticsearch monitoring įrankius, stebėk indeksavimo ir paieškos laiką. Jei sistema lėtėja, galbūt reikia optimizuoti analizatorių konfigūraciją arba pridėti daugiau resursų.

Atnaujink žodynus – kalba gyvena ir keičiasi. Atsiranda naujų žodžių, ypač technologijų srityje. Periodiškai peržiūrėk stop žodžių sąrašus, sinonimus, morfologinius žodynus. Tai turėtų būti dalis sistemos priežiūros proceso.

Apsvarstyk hibridinį požiūrį – kartais geriausi rezultatai pasiekiami kombinuojant kelis metodus. Pavyzdžiui, pagrindinė paieška su morfologine analize, bet jei rezultatų per mažai – papildoma paieška su n-gramais. Arba naudok machine learning modelius (Elasticsearch turi Learning to Rank funkcionalumą) papildomam relevancijos tobulinimui.

Kai paieška tampa išmaniąja

Šiuolaikinė paieška – tai ne tik teksto atitikimas. Vartotojai tikisi, kad sistema supras jų ketinimus, pasiūlys alternatyvas, pataisys klaidas. Elasticsearch turi įrankių ir tam.

Suggestion API leidžia implementuoti „ar turėjote omenyje” funkciją, kuri pasiūlo pataisymus, jei vartotojas padarė rašybos klaidą:

GET /tekstai/_search
{
  "suggest": {
    "teksto_pasiulymas": {
      "text": "Vilnuis",
      "term": {
        "field": "pavadinimas"
      }
    }
  }
}

Dar galingesnė – completion suggester, kuri leidžia implementuoti autocomplete funkciją. Tai ypač naudinga lietuviškai paieškai, kur žodžiai ilgi ir sudėtingi:

"pavadinimas_autocomplete": {
  "type": "completion",
  "analyzer": "simple",
  "preserve_separators": true,
  "preserve_position_increments": true,
  "max_input_length": 50
}

Percolate queries – tai atvirkštinė paieška. Vietoj to, kad ieškotum dokumentų pagal užklausą, saugai užklausas ir tikrini, ar naujas dokumentas atitinka jas. Tai naudinga, pavyzdžiui, sukuriant įspėjimų sistemą: „pranešk man, kai pasirodys straipsnis apie Elasticsearch ir lietuvių kalbą”.

Agregacijos – tai ne tik paieška, bet ir duomenų analizė. Gali sukurti faceted navigation (filtravimą pagal kategorijas), rodyti populiariausius terminus, analizuoti tendencijas. Lietuviškam turiniui tai gali būti sudėtingiau dėl žodžių formų įvairovės, bet teisingai sukonfigūravus analizatorius, veikia puikiai.

Ką daryti, kai standartinių sprendimų nepakanka

Kartais net su visais šiais įrankiais nepavyksta pasiekti norimo rezultato. Lietuvių kalba tikrai sudėtinga, ir ne viskas veikia iš karto. Štai keletas krypčių, kur galima eiti toliau:

Galima integruoti išorinius morfologinius analizatorius per Elasticsearch ingest pipelines. Tai leidžia apdoroti tekstą prieš jį indeksuojant, naudojant sudėtingesnius algoritmus. Pavyzdžiui, galima siųsti tekstą į atskirą servisą, kuris naudoja Lemuoklį ar kitą specializuotą įrankį, ir grąžina lemmatizuotą tekstą.

Jei turi daug duomenų ir resursų, verta pažiūrėti į machine learning sprendimus. Elasticsearch turi integruotą ML funkcionalumą, bet galima naudoti ir išorinius modelius, pavyzdžiui, transformer-based modelius (BERT ir panašius), pritaikytus lietuvių kalbai. Yra keletas tokių modelių, sukurtų lietuvių mokslininkų.

Dar viena kryptis – semantic search, kur ieškoma ne pagal žodžių atitikimą, bet pagal prasmę. Tai reikalauja vector embeddings ir dense vector search, kurį Elasticsearch palaiko. Lietuvių kalbai yra keletas embedding modelių, kuriuos galima panaudoti.

Kelias į geresnę lietuvišką paieškos patirtį

Elasticsearch lietuviško teksto paieškai – tai ne vienkartinis projektas, o nuolatinis procesas. Pradedi nuo paprastos konfigūracijos, testuoji, matai, kas veikia ir kas ne, tobulini. Svarbu suprasti, kad nėra vieno idealaus sprendimo – viskas priklauso nuo konkretaus use case, duomenų pobūdžio, vartotojų lūkesčių.

Pradėk nuo pagrindų: tinkamas tokenizavimas, lowercase, asciifolding. Tada pridėk stop žodžius, sinonimus. Jei reikia geresnių rezultatų – įdiegk morfologinę analizę per Hunspell arba kitą įrankį. Jei vartotojai daro daug klaidų – pridėk n-gramus ir suggestion API. Jei reikia labai tikslios paieškos – naudok phrase matching ir boosting.

Visada testuok su realiais duomenimis ir realiomis užklausomis. Stebėk, kaip vartotojai naudoja paiešką, kokių rezultatų tikisi, kur susiduria su problemomis. Elasticsearch turi puikias monitoring galimybes – naudok jas. Matuok ne tik technines metrikas (atsakymo laikas, resursų naudojimas), bet ir verslo metrikas (ar vartotojai randa tai, ko ieško, ar spaudžia į rezultatus).

Lietuvių kalba sudėtinga, bet tai nereiškia, kad neįmanoma sukurti geros paieškos. Reikia tiesiog daugiau pastangų nei anglų kalbai, geresnio supratimo apie kalbos ypatybes ir noro eksperimentuoti. Elasticsearch suteikia visus reikalingus įrankius – belieka juos teisingai panaudoti.

Daugiau

Bun, Node ir Deno: vykdymo laiko karas