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