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