Pewnie się zdziwi Cie ta informacja. Elasticsearch służy do… szukania. Tak. To prawda. Okazuje się, że można go wykorzystać również do indeksowania zawartości plików typu doc, docx, pdf itp. W tym wpisie przyjrzymy się jak to zrobić, jak zmienić analizator oraz jak “zgubić” plik jeśli i tak trzymamy go np. na S3.
Psst! Link do repo na dole artykułu.
Po co?
Nie zawsze fraza, której szukamy, znajduje się w nazwie pliku, tytule i innych dostarczonych metadanych. Wyobraź sobie portal umożliwiający gromadzenie i wyszukiwanie artykułów naukowych. Każdy artykuł to osobny plik PDF. Dodanie do obszaru wyszukiwania abstraktów już zauważalnie zwiększyłoby satysfakcję korzystania z portalu. Nie bez powodu ScienceDirect korzysta z Elasticsearch.
Środowisko
Jak tylko mogę, korzystam z Docker. W tym przypadku nie jest inaczej. Ułatwia to Wam, czytelnikom, w łatwy sposób powtórzyć to co udało mi się pokazać w artykule.
Aby umożliwić analizę plików w Elasticsearch, potrzebny jest Ingest Attachment Processor Plugin. W przypadku Docker moglibyśmy ręcznie włączyć terminal wewnątrz kontenera poniższą komendą.
sudo docker exec -it nazwa-kontenera bash
I zainstalować plugin, ale jest to kiepski pomysł. W przypadku usunięcia kontenera będziemy musieli tę czynność powtórzyć. Aby tego uniknąć, lekko zmodyfikowałem Docker Compose z ELK-iem z wpisu o Dockerze i dodałem prosty Dockerfile.
FROM docker.elastic.co/elasticsearch/elasticsearch:7.6.0
RUN bin/elasticsearch-plugin install --batch ingest-attachment
version: '2.2'
services:
  elasticsearch:
    build: ./custom-elasticsearch/
#    image: docker.elastic.co/elasticsearch/elasticsearch:7.6.0
    restart: unless-stopped
...
Wszystkie operacje na Elasticsearch (oprócz dodania pliku) wykonałem w Dev Tools na Kibana.
Przygotowanie Pipeline
Co to w ogóle Pipeline? Jest to definicja serii procesorów, które będą wykonywane w tej samej kolejności, w jakiej zostały zadeklarowane. Innymi słowy, dokument, który wrzucamy do bazy, zostanie przepuszczony przez każdy zdefiniowany procesor. Możemy w ten sposób wzbogacać dokument o nowe pola, przekształcać, a nawet usuwać jeśli spełniony zostanie zdefiniowany warunek.
Dodany wcześniej plugin zawiera pipeline, który rozpakuje i przeanalizuje dodawany plik. Pliki przekazywane są w postaci Base64. Poniżej deklaracja prostego pipeline.
PUT _ingest/pipeline/attachment
{
  "description" : "What did you hide in this file? (¬‿¬)",
  "processors" : [
    {
      "attachment" : {
        "field" : "data"
      }
    }
  ]
}
Dodanie pliku
Przygotowałem pliki doc, docx, pdf. Wrzuciłem treść piosenki U2 – I Still Haven’t Found What I’m Looking For. Jeden z pdf składa się z obrazka zrzutu ekranu tekstu. Czy plugin ma w sobie OCR? Dowiemy się.

Wklejanie Base64 do Postmana lub Dev Tools w Kibanie jest słabe. Zabiera za dużo miejsca. Dlatego posłużymy się CURL-em. Dodamy dokument z parametrami filename i data do indeksu songs. Zwróć uwagę na ?pipeline=attachment w którym wskazujemy wcześniej zdefiniowany Pipeline.
(echo -n '{"filename":"U2.docx", "data": "'; base64 ./U2.docx; echo '"}') |
curl -H "Content-Type: application/json" -d @-  http://192.168.114.128:9200/songs/_doc/1?pipeline=attachment
Efektem jest dokument w bazie, który wygląda tak (pozwoliłem sobie usunąć base64 oraz część piosenki):
GET /songs/_doc/1
{
  "_index" : "songs",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "filename" : "U2.docx",
    "data" : "UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAACAAAAA...AsyoAAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAAAAsACwDBAgAAXy0AAAAA",
    "attachment" : {
      "date" : "2020-02-21T19:14:00Z",
      "content_type" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      "author" : "Maciej Szymczyk",
      "language" : "en",
      "title" : "U2 - I Still Haven't Found What I'm Looking For",
      "content" : """I have climbed highest mountain
I have run through the fields
Only to be with you
Only to be with you
...
But I still haven't found
What I'm looking for
But I still haven't found
What I'm looking for""",
      "content_length" : 931
    }
  }
}
Oprócz treści w attachment.content, dostaliśmy również metadane jakimi są tytuł, język, autor, data.
Wyszukiwanie
Okazuje się, że z wyszukiwaniem nie jest tak kolorowo. O ile szukanie słowa “kissed” zwraca nam dodany plik
POST /songs/_search
{
  "query":{
    "term":{
      "attachment.content": "kissed"
    }
  }
}
POST /songs/_search
{
  "query": {
    "query_string": {
      "default_field": "attachment.content", 
      "query": "kissed"
    }
  }
}
To słowo “kiss” nie zwraca nam żadnych wyników.
POST /songs/_search
{
  "query":{
    "term":{
      "attachment.content": "kiss"
    }
  }
}
POST /songs/_search
{
  "query": {
    "query_string": {
      "default_field": "attachment.content", 
      "query": "kiss"
    }
  }
}
Dzieje się tak, ponieważ nie przygotowaliśmy indeksu przed dodaniem pierwszego rekordu. Chodzi mi tu głównie o analizator dla języka angielskiego. Oprócz usunięcia znaków przystankowych i łączników, przekształci czasowniki do podstawowej formy ( kissed -> kiss ). Możemy sprawdzić jak taki analizator działa poniższym zapytaniem.
GET /_analyze
{
  "analyzer": "english",
  "text": """I have climbed highest mountain
              I have run through the fields"""
}
Poprawmy ten błąd. Usuńmy indeks, dodajmy definicje indeksu i plik z piosenką.
DELETE /songs
PUT /songs
{
  "mappings": {
    "properties": {
      "attachment.content": {
        "type": "text",
        "analyzer": "english"
      }
    }
  }
}
Teraz użycie query_string zwraca nam rekord niezależnie czy wpiszemy kiss, czy kissed. Natomiast użycie zapytania z term odwróciło się (kiss działa, a kissed nie). Czemu się tak dzieje? Pamiętaj, że w query_string podana fraza przechodzi przez analizator użyty w danym polu. Więc kissed jest i tak przekształcony w kiss. Jeśli chcesz, bym bardziej szczegółowo opisał analizatory. Daj znać w komentarzu.
Ale ja nie potrzebuję analizować całego pliku
Pliki bywają różne, kwadratowe i podłużne, a na pewno duże i małe. Aby nie tracić czasu na cały plik, możemy ograniczyć treści do przeanalizowania. To samo tyczy się metadanych pliku.
PUT _ingest/pipeline/better_attachment
{
  "description" : "What did you hide in this file? better version (¬‿¬)",
  "processors" : [
    {
      "attachment" : {
        "field" : "data",
        "properties": [ "content", "title" ],
        "indexed_chars" : 20,
        "indexed_chars_field" : "max_size"
      }
    }
  ]
}
Teraz rekord wygląda jak poniżej (pamietaj zmienić ?pipeline= przy dodawaniu).
{
  "_index" : "songs",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "filename" : "U2.docx",
    "data" : "UEsDBBQABgAIAAAAIQDfpNJsWgEAACAFAAATAAgCW0NvbnRlbnRfVHlwZXNdLnhtbCCiBAIooAACAAAAA...AsyoAAGRvY1Byb3BzL2FwcC54bWxQSwUGAAAAAAsACwDBAgAAXy0AAAAA",
    "attachment" : {
      "title" : "U2 - I Still Haven't Found What I'm Looking For",
      "content" : "I have climbed highe"
    }
  }
}
Wystarczy mi treść. Pliki trzymam na S3
W takim przypadku możemy dodać kolejny procesor, a dokładnie Remove Processor.
PUT _ingest/pipeline/even_better_attachment
{
  "description" : "What did you hide in this file? even better version (¬‿¬)",
  "processors" : [
    {
      "attachment" : {
        "field" : "data",
        "properties": [ "content", "title" ],
        "indexed_chars" : 20,
        "indexed_chars_field" : "max_size"
      },
      "remove":{
        "field":"data"
      }
    }
  ]
}
Teraz dokument wygląda tak:
{
  "_index" : "songs",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "filename" : "U2.docx",
    "attachment" : {
      "title" : "U2 - I Still Haven't Found What I'm Looking For",
      "content" : "I have climbed highe"
    }
  }
}
To co z tym OCR-em?
Niestety pdf z obrazkiem nie zadziałał ?
A co z językiem polskim?
Dostępne są wtyczki dostarczające analizatory języka polskiego. O tym jak działają, przeczytasz na blogu Marcina Lewandowskiego (Cztery Tygodnie).
Repozytorium
https://github.com/zorteran/wiadro-danych-elasticsearch-ingest-attachment

 
	