Przejdź do treści

Jak stworzyłam własny pseudojęzyk i pomogłam bibliotekarzom? (część I)



Stworzyć własny pseudojęzyk, przetłumaczyć go na zapytanie Elasticsearcha i zwrócić użytkownikowi wyniki… Brzmi tajemniczo? Zacznijmy od początku!

Za kulisami biblioteki, czyli zrozumieć MARC 21

Bibliotekarze operują codziennie na ogromnych zbiorach danych, w których zgromadzone są miliony rekordów bibliograficznych (książki, artykuły, audiobooki, e-booki, czasopisma itp.). Informacje na ich temat są zbierane i tworzone od kilkudziesięciu lat w różnych formatach i strukturach, a także na bieżąco aktualizowane.

Jednym z najbardziej rozpowszechnionych formatów zapisu rekordów bibliograficznych jest MARC 21, którego dokładny opis możecie znaleźć tutaj.

W największym uproszczeniu jest to zapis jednego rekordu w postaci pól (tagów) i wartości, gdzie nazwy pól (etykiety) to trzycyfrowe liczby przypisane do konkretnych informacji, np: pole 001 to numer kontrolny, 005data ostatniej modyfikacji, 020numer ISBN, 260 –adres wydawniczy, 651 – hasło przedmiotowe – nazwa geograficzna. Takich pól jest dokładnie 151 (!). Co więcej, większość z nich zawiera też podpola (oznaczone małymi literami alfabetu oraz cyframi od 1 do 9) doprecyzowujące daną informację, np. pole 245 zdefiniowane jako Tytuł i oznaczenie odpowiedzialności, zawiera w sobie zazwyczaj 3 podpola, przechowujące następujące dane: 245_a – Tytuł, 245_b – Ciąg dalszy tytułu, 245_c – Pozostałe elementy tytułu i oznaczenia odpowiedzialności. Pełną listę pól i podpól w wersji angielskiej znajdziecie tutaj.

Pliki w formacie MARC 21 nie wyglądają ani ładnie, ani czytelnie. Przykładowy plik zawierający pierwszą pozycję pod tytułem Bajki Robotów wygląda tak. Na szczęście te same dane możemy zwizualizować w nieco bardziej przyjaznych formatach, np. marcxml. Ten sam rekord prezentuje się dokładnie w ten sposób.

Trochę lepiej, ale nadal nie jest to najczytelniejszy plik, jaki w życiu widzieliście, prawda?

Niestety bibliotekarze codziennie mają do czynienia z danymi w takiej postaci. Muszą oni nie tylko przeglądać, czy tworzyć nowe rekordy, ale też wyszukiwać wśród nich te, których w danej chwili potrzebują, np. wszystkie książki wydane w 2022 roku. Oczywiście mają do tego różne programy, takie jak formularz na stronie data.bn.org.pl, jednak zwracane przez niego wyniki to nadal dane w formatach marc/json/xm, a nie np. przejrzysta tabela lub plik csv.

Wyobraźmy sobie sytuację, kiedy pani Krysia potrzebuje sprawdzić, w ilu rekordach pole 245_a zawiera frazę Pan Tadeusz? Z kolei pani Zosia chce zweryfikować, ile rekordów w ogóle nie zawiera pola 245_c? A jeśli pola się powtarzają?

Niestety elastyczność formatu MARC 21 jest też jego ogromną wadą, gdyż umożliwia wielokrotne powtarzanie się tych samych pól i podpól. W rezultacie rekordy zawierają np. dwa pola 380, określające formę/rodzaj dokumentu, jedno z wartością Książki, a drugie Proza. Stopień złożoności tej struktury danych jest niestety wysoki i bardzo utrudnia codzienną pracę.

Jak pomóc bibliotekarzom?

W ramach projektu, w którym mam przyjemność pracować powstał pomysł zaimplementowania wyszukiwarki, która pozwoli użytkownikom – bibliotekarzom – napisać dowolne, nawet najbardziej skomplikowane zapytanie, aby finalnie otrzymać wyniki w postaci jak najbardziej czytelnej i zrozumiałej tabeli oraz możliwość eksportu uzyskanych danych do pliku csv.

Główne założenia projektu

Pierwszą myślą, jaka zrodziła się w naszych głowach podczas analizy wymagań było stworzenie maksymalnie prostego języka zapytań, tak aby mogły go używać osoby bez wiedzy programistycznej. Jednocześnie musiał być na tyle ustrukturyzowany, aby dało się zaimplementować jego dynamiczny parser.

Drugi ważny aspekt stanowiła odpowiednia baza danych, w której chcieliśmy przechowywać cały zbiór rekordów. Biorąc pod uwagę potrzebę wykonywania wielu zaawansowanych zapytań i przeszukiwań pełnotekstowych, baza powinna zapewniać możliwie najkrótszy czas dostępu i wyszukiwania.

Po wielu dyskusjach i analizach zdecydowaliśmy się na użycie poniższych narzędzi.

Wykorzystane narzędzia

  • Jako bazę danych wykorzystaliśmy Elasticsearch, umożliwiający szybkie wyszukiwanie pełnotekstowe oraz inne skomplikowane operacje na danych. Dodatkowo ES udostepnia własny język zapytań DSL, do postaci którego miały być w rezultacie parsowane nasze zapytania.
  • API napisaliśmy oczywiście w Pythonie, do którego podłączyliśmy genialne biblioteki obsługujące zarówno format MARC 21: pymarc oraz połączenie z Elasticsearchem: elasticsearch-py oraz elasticsearch-dsl.
  • Składnia naszego pseudojęzyka była zainspirowana SQL-em oraz schematem trójek RDF, ale uprościliśmy ją w możliwie maksymalnym stopniu.

Całe flow aplikacji wygląda w ten sposób:

Schemat działania aplikacji

Pseudojęzyk zapytań w pigułce

Największe wyzwania

Oczywiście największym wyzwaniem okazało się stworzenie takiego pseudojęzyka, który będzie jednocześnie intuicyjny i prosty w obsłudze dla użytkowników, a także możliwy do sparsowania na elasticowy DSL. Zainspirowani SQL-em i trójkami RDF postanowiliśmy wyciągnąć z obu tych konceptów to, co najlepsze.

Zasada “mniej znaczy więcej” była w naszym przypadku kluczowa i wracała jak bumerang.

Zdecydowaliśmy się więc zrezygnować z większości słów kluczowych, znanych z SQL-a (SELECT, FROM, WHERE, itp.). W ostatecznej wersji naszego pseudojęzyka zostały jedynie 4 słowa kluczowe (2 definiujące operatory oraz 2 definiujące warunki), ale o tym za chwilę.

Wszystko sprowadza sie do “trójek”…

Fundamentem każdego prostego (niezagnieżdzonego) zapytania jest połączenie nazwy pola z wyszukiwaną wartością oraz warunkiem porównania, np.

  • [pole 005 musi być większe od 2020],
  • [pole 380 musi być równe frazie E-booki],
  • [pole 245 musi zawierać frazę Pan Tadeusz], itd…

Przyjrzyj się tym przykładom… Czy widzisz już pewien schemat?

Każde proste zapytanie to “trójka“: [pole – warunek – wartość]. Spróbujmy więc przekształcić powyższe przykłady.

  • 005 > 2020,
  • 380 = 'E-booki',
  • 245 * 'Pan Tadeusz' (Skąd ta gwiazdka? Wyjaśnię za chwilę ;)).

Jak widać, wszystko sprowadza się do porównania zawartości pola z konkretną wartością, na podstawie wybranego warunku.

Po analizie wymagań stworzyliśmy listę wszystkich możliwych warunków:

  • = – równa się,
  • != – nie równa się,
  • * – zawiera,
  • !* – nie zawiera,
  • > – większe (dla pól zawierających liczby i daty),
  • >= – większe lub równe (dla pól zawierających liczby i daty),
  • < mniejsze (dla pól zawierających liczby i daty),
  • <= mniejsze lub równe (dla pól zawierających liczby i daty).

… ale czasem wystarczy “para”

Dodatkowo pojawiła się potrzeba weryfikacji, czy rekordy zawierają konkretne pola (tzn. czy jest w nich jakakolwiek wartość różna od nulla lub pustego stringa). W tym przypadku struktura “trójki” okazała się za szeroka, gdyż nie mamy tu żadnej wartości do porównania.

Dla dwóch nowych warunków zaimplementowaliśmy więc strukturę “pary”, czyli połączenie nazwy pola ze słowem kluczowym exists lub not_exists, np.: [246 not_exists], [800 exists], gdzie:

  • exists – pole istnieje w danym rekordzie,
  • not_exists – pole nie istnieje w danym rekordzie.

Łączenie warunków

Teraz pozostała jeszcze kwestia łączenia ze sobą kilku warunków. Aby ułatwić późniejszą walidację, wprowadziliśmy założenie, że każda para lub trójka musi być “opakowana” w nawiasy klamrowe rozdzielone spacjami: { }, np.:

  • { 245 = 'Pan Tadeusz' },
  • { 246 exists }.

Do łączenia warunków istnieją tylko 2 logiczne operatory: oraz i lub, więc zastosowaliśmy odpowiadające im symbole:

  • & – oraz,
  • | – lub.

Przykładowe zapytania łączone (złożone) mogą wyglądać następująco:

  • { 245 = 'Pan Tadeusz' } & { 246 exists }
  • { 245 = 'Pan Tadeusz' } | { 245 = 'Kordian' }

Zapytanie w zapytaniu? Proszę bardzo!

A jak dodać zagnieżdzenia? Więcej klamerek!

Spójrz na poniższy przykład:

{ { 245 * 'Lem' } | { 245 * 'Szymborska' } } & { 380 = 'Artykuły' }

Udało Ci się go rozszyfrować?

Pole 245 musi zawierać przynajmniej jedną z fraz: “Lem” / “Szymborska”, a pole 380 musi równać się frazie “Artykuły”. Innymi słowy, chcemy znaleźć wszystkie artykuły, które mają w tytule nazwisko Lema lub Szymborskiej 🙂 Proste, prawda?

Zasady są po to, żeby… kod nie sypał wyjątków 😉

Na koniec jeszcze kilka zasad dotyczących typów danych:

  • Ciągi znaków zawsze muszą być otoczone pojedynczymi cudzysłowami.
  • Liczby muszą być zapisane normalnie, bez cudzysłowu.
  • Daty muszą być zapisane w formacie RRRR-MM-DD, bez cudzysłowu, np: 2022-01-12.
  • Każda para i trójka musi być opakowana w klamry, tak samo każda grupa zapytań łączona operatorem & lub | musi znajdować się w klamrach.

Wydawać by sie mogło, że stworzenie własnego pseudojęzyka było największą trudnością tego projektu… Nic bardziej mylnego!

To był tylko wierzchołek góry lodowej, a jej pokonanie okazało się dużo bardziej problematyczne. Na szczęście podczas tego rejsu nie ucierpiał ani jeden programista ;).

Czy jest na sali tłumacz? Jak przetłumaczyć własny pseudojęzyk na DSL

Dzięki opisanym wyżej założeniem udało mi się zaimplementować dynamiczny parser, który umożliwia przetłumaczenie dowolnego zapytania napisanego w naszym pseudojęzyku na zapytanie DSL wysyłane bezpośrednio do Elasticsearcha.

Opisane poniżej kroki algorytmu oznaczyłam kolejnymi liczbami, w celu ich późniejszej wizualizacji.

#1 Bez kamizelki ratunkowej wstęp wzbroniony, czyli początkowa walidacja

Na początek ulubione słowo każdego programisty – walidacja danych! Nie chcemy przecież mieć w naszym Titanicu dziur, przez które dostanie się woda. Warto więc zabezpieczyć się przed kilkoma błędami ze strony użytkownika końcowego. Na samym wstępie sprawdzamy jedynie 2 sytuacje (kilka innych będzie weryfikowanych w dalszej części działania parsera).

  • Niedomknięte cudzysłowy
    Liczba cudzysłowów jest nieparzysta? Oj, chyba użytkownik musiał być głodny, bo zjadł jeden z nich. Oczywiście informujemy go o tym, zwracając odpowiedni komunikat.
  • Puste klamry
    Ktoś chciał napisać dłuższe zapytanie, ale zmienił zdanie i usunął jego część, zostawiając puste klamry {} ? Nie ze mną takie numery, Bruner!

Logika parsera zdefiniowana jest w klasie ElasticSearchParser, a zawarte w niej metody odpowiadają za poszczególne kroki algorytmu.

A tak prezentuje się funkcja realizująca powyższe zadania:

class ElasticSearchParser:    
...
    def validate_query(self):
        quotes = self.query.count("'")
        if quotes % 2 == 1:
            raise InvalidQuery('Missing quote')

        empty_brackets = self.query.count('{}')
        if empty_brackets:
            raise InvalidQuery('Empty brackets')

Zmienna self.query to wewnętrzna zmienna klasy obsługującej parsowanie, której na starcie przypisane zostaje oryginalne zapytanie wysłane do API z frontendu.

#2 Dziel i rządź

Po wstępnej walidacji zakładamy, że zapytanie jest poprawne i dzielimy je po spacjach, aby łatwiej operować na jego kolejnych “wyrazach”.

#2
self.query = self.query.split()

#3 Ups, a co jeśli ciąg znaków zawierał spacje?

No właśnie, ups… W pierwszej wersji kodu faktycznie nie uwzględiłam tego aspektu, ale prędzej czy później musiałam to naprawić… Jak?

Znajdując wyrażenia rozpoczynające się od cudzysłowu, a następnie doklejając do niego kolejne wyrazy, aż do znalezienia drugiego cudzysłowu, zamykającego ciąg znaków…
I tak w kółko:

...
STRING_QUOTE = "'"
...
def concat_strings_delimited_by_spaces(self):

    tmp_query = []
    in_string = False
    current_string_start = None
    current_string_end = None

    for idx, term in enumerate(list(self.query)):

        # 1-word string without spaces
        if term[0] == STRING_QUOTE and term[-1] == STRING_QUOTE and not in_string:  
            tmp_query.append(term.replace(STRING_QUOTE, ''))
        
       # beginning of a longer string
        elif term[0] == STRING_QUOTE and not in_string:  
            in_string = True
            current_string_start = idx

        # end of a longer string
        elif term[-1] == STRING_QUOTE and in_string: 
                in_string = False
                current_string_end = idx
                tmp_query.append(' '.join(self.query[current_string_start: current_string_end + 1]).replace(STRING_QUOTE,''))
            elif not in_string:
                tmp_query.append(term.replace(STRING_QUOTE, ''))
        self.query = tmp_query

#4 Bo klamry zawsze chodzą parami…

Wspominałam wcześniej, że nasz pseudojęzyk wymaga “opakowania” każdej trójki lub pary w nawiasy klamrowe. Ten pomysł nie spadł z księżyca. Dzięki niemu można teraz łatwo wyciągnąć wszystkie podzapytania i przeanalizować je.

Podczas operacji szukania klamerek, nadajemy im kolejne numery, co pozwoli w późniejszych krokach odwoływać się do odpowiednich podzapytań głównego zapytania.

Zanim spojrzymy na kod, zwizualizujmy obecną sytuację:

Wizualizacja kolejnych kroków parsera (1-4)

Jak widać każda klamra otwierająca { oznaczona jest kolejnym numerem licząc od 1, a każdej klamrze zamykającej } przypisywany jest numer ostatniej otwierającej (ostatniego niedomkniętego podzapytania).

Ten mechanizm pozwala jednoznacznie zidentyfikować wszystkie podzapytania, które w kolejnym kroku zaczniemy dzielić na 2 grupy. Dodatkowo zliczana jest też ilość wszystkich podzapytań.

A tak prezentuje się funkcja odpowiedzialna za numerowanie podzapytań:

START_BRACKET = '{'
END_BRACKET = '}'

def add_brackets_to_query(self):

    counter = 0
    with_start_brackets = []

    for term in self.query:
       if term == START_BRACKET:
           counter += 1
           term = f'{{{counter}'
       with_start_brackets.append(term)

    if counter == 0:
       raise InvalidQuery('Missing bracket')

    with_end_brackets = []
    last_start_brackets = []
 
   for term in with_start_brackets:
        if START_BRACKET in term:
            number = list(term)[1]
            last_start_brackets.append(number)

        elif term == END_BRACKET:
            term = f'}}{last_start_brackets[-1]}'
            last_start_brackets.pop()
        with_end_brackets.append(term)

    self.query = with_end_brackets
    self.clauses_counter = counter

#5 Proste czy zagnieżdzone?

Teraz czas na małe grupowanie. Iterując po kolejnych podzapytaniach, będziemy weryfikować, z ilu elementów się składają – jeśli zawierają tylko 2 wyrazy – mamy “parę”, 3 wyrazy oznaczają “trójkę”, w każdym innym przypadku podzapytanie będzie złożone.

Na początek skupimy się na tych prostych (“parach” i “trójkach”) i każde z nich przetłumaczymy od razu na zapytanie DSL – w ten sposób ułatwimy sobie późniejszą konwersję.

W kodzie posłużyłam się dodatkową klasą SimpleClauseObject, która przechowuje informacje o identyfikatorze podzapytania oraz jego treści, a także tworzy jego odpowiednik DSL. Spójrzmy, jak to wygląda:

 def prepare_simple_clauses(self):

        try:
            for i in range(1, self.clauses_counter + 1):
                clause_start = self.query.index(f'{{{i}')
                clause_end = self.query.index(f'}}{i}')
                clause_str = self.query[clause_start: clause_end + 1]

                number = int(clause_str[-1][-1])
                valid_clause = clause_str[1:-1]

                if len(valid_clause) == 2:
                    self.validate_short_clause(valid_clause)
                    clause_obj = SimpleClauseObject(number=number, clause=valid_clause)
                    self.simple_clauses[number] = clause_obj
                    self.query[clause_start: clause_end + 1] = [f'{{{i}}}']

                elif len(valid_clause) == 3:  # creating simple query
                    clause_obj = SimpleClauseObject(number=number, clause=valid_clause)
                    self.simple_clauses[number] = clause_obj

                    # replacing clause with number placeholder in self.query
                    self.query[clause_start: clause_end + 1] = [f'{{{i}}}']
                else:
                    self.complex_clauses_numbers.append(i)

        except ValueError as e:
            print('error', e)
            raise InvalidQuery('Missing bracket')

Odnalezione pary i trójki są zapisywane na liście simple_clauses jako obiekty klasy SimpleClauseObject, a w zmiennej query ich zawartość jest podmieniana 3-znakowym wyrazem – identyfikatorem objętym dwoma klamrami (np. {2}) . Z kolei identyfikatory wszystkich pozostałych zapytań zapisywane są na liście complex_clasues_numbers.

Zajrzyjmy jeszcze do definicji klasy SimpleClauseObject:

class SimpleClauseObject:

    def __init__(self, number: int, clause: str):
        self.number = number
        self.clause = clause
        self.validate_date_field()
        self.es_simple_query = self.prepare_simple_es_query()

    def validate_date_field(self):
        field, condition, *value = self.clause
        if field in DATE_FIELDS:
            if value[0] == STRING_QUOTE or value[-1] == STRING_QUOTE:
                raise InvalidQuery('Dates should not be wrapped in quotes. Remove them.')
            if len(value[0]) != 10:
                raise InvalidQuery('Invalid date format')

    def prepare_simple_es_query(self):
        field, condition, *value = self.clause
        if value:
            value = value[0]

        if condition == Conditions.GREATER:
            return Q({'range': {field: {
                'gt': value
            }}})
        elif condition == Conditions.GREATER_EQUAL:
            return Q({'range': {field: {
                'gte': value
            }}})
        elif condition == Conditions.LOWER:
            return Q({'range': {field: {
                'lt': value
            }}})

        elif condition == Conditions.LOWER_EQUAL:
            return Q({'range': {field: {
                'lte': value
            }}})
        elif condition == Conditions.EQUALS:
            return Q('term', **{f'{field}.keyword': value})

        elif condition == Conditions.NOT_EQUAL:
            return Q({'query_string': {'query': f'NOT ({field}: {value})'}})

        elif condition == Conditions.NOT_CONTAINS:
            return Q({'bool': {'must_not': {'match': {field: value}}}})

        elif condition == Conditions.NOT_EXISTS:
            return Q({'bool': {'must_not': {'exists': {'field': field}}}})

        elif condition == Conditions.EXISTS:
            return Q({'exists': {'field': field}})

        return Q({'match_phrase': {field: value}})

W konstruktorze następuje dodatkowa walidacja dat oraz tworzenie odpowiedniego obiektu Q – zgodnego ze składnią DSL – w zależnosci od przekazanego warunku. Struktura tych obiektów wynika z dokumentacji Elasticsearch oraz biblioteki elasticsearch_dsl.

Jak można się domyślić enum Conditions przechowuje opisane wcześniej symbole warunków.

class Conditions(str, Enum):
    EQUALS = '='
    LOWER = '<'
    LOWER_EQUAL = '<='
    GREATER = '>'
    GREATER_EQUAL = '>='
    CONTAINS = '*'
    NOT_EQUAL = '!='
    NOT_EXISTS = 'not_exists'
    EXISTS = 'exists'
    NOT_CONTAINS = '!*'

Jak teraz prezentuje się nasze zapytanie?

Wizualizacja kolejnych kroków parsera (1-5)

Oho, czyżby coś nam tu ubyło?

Zawartość wszystkich prostych podzapytań wylądowała w liście simple_clauses, zaś w zmiennej przechowującej główne zapytanie zostały jedynie identyfikatory podzapytań oraz łączące je operatory.

Czas połączyć wszystko w całość!

#6 Kolejność ma znaczenie

Tym razem zacznę od kodu:

class Operators(str, Enum):
    OR = '|'
    AND = '&'


class ElasticSearchParser:    
...
    def translate_query(self):

       i = 0
       q = None

       # iterating over query as long as there are no complex clauses left
       while len(self.complex_clauses_numbers) > 0:
          number = self.complex_clauses_numbers[i]
          clause_start = self.query.index(f'{{{number}')
          clause_end = self.query.index(f'}}{number}')
          clause_str = self.query[clause_start + 1: clause_end]
          nested = [c for c in clause_str if len(c) == 2]

          if len(nested) == 0:  # no more nested inside
              operators = [o for o in clause_str if o in (Operators.AND, Operators.OR)]

              if not self.is_operators_list_valid(operators):
                  raise InvalidQuery('Operators in the same level are not identical')
              operator = operators[0]
              clauses = [int(x[1]) for x in clause_str if len(x) == 3]


              # looking for clauses in simple and complex lists
              clauses_obj = [value.es_simple_query for (key, value) in s elf.simple_clauses.items() if
                             key in clauses] + [value.clause for (key, value) in self.complex_clauses.items() if
                                                key in clauses]
              q = clauses_obj[0]

              if operator == Operators.AND:
                  for c in clauses_obj[1:]:
                      q &= c
              else:
                  for c in clauses_obj[1:]:
                      q |= c

              complex_clause_obj = ComplexClauseObject(number, q)
              self.complex_clauses[number] = complex_clause_obj
              self.complex_clauses_numbers.remove(number)
              self.query[clause_start:clause_end + 1] = [f'{{{number}}}']
             
              i = 0

          else:
              i += 1

       return q
class ComplexClauseObject:
    def __init__(self, number, clause):
        self.number = number
        self.clause = clause

Finalna translacja zapytania to (nie)kończąca się pętla while, weryfikująca czy zostały jeszcze jakieś nieobsłużone podzapytania.

Z każdą iteracją algorytm analizuje kolejne podzapytanie i sprawdza, czy zawiera ono w sobie jakieś zagnieżdzenia (taka sytuacja wystąpi wtedy, gdy wewnątrz znajdą się dwuznakowe wyrazy – czyli klamerki otwierające / zamykające wraz z identyfiaktorami). Jeśli tak, pętla przeskakuje do analizy kolejnego podzapytania, wiedząc, że do tego wróci jeszcze w późniejszym czasie, gdy zostanie ono rozłożone na części.

Z kolei w sytuacji, gdy podzapytanie nie ma w sobie zagnieżdzeń, dzieją się kolejne operacje:

  • znajdowane są wszystkie zawarte w nim operatory (& oraz |) i weryfikowana jest ich poprawność – na jednym poziomie operacji logicznej możemy mieć tylko jeden typ operatora,
  • wyciągane są identyfikatory zawartych w nim podzapytań (prostych oraz złożonych), a także odpowiadające im zbudowane wcześniej obiekty Q.
  • w zależności od operatora tworzona jest suma lub iloraz logiczny wszystkich podzapytań,
  • tworzony jest obiekt klasy ComplexClauseObject przechowujący identyfikator analizowanego podzapytania oraz zaktualizowaną wersję Q-struktury,
  • identyfikator podzapytania jest usuwany z listy complex_clauses_numbers,
  • zawartość podzapytania w zmiennej query zastąpywana jest opisanym wcześniej placeholderem – identyfikatorem objętym dwoma klamrami,
  • wartość zmiennej kontrolującej pętlę jest zerowana.

Kosmos, co nie?

Ale ten kosmos naprawdę się kręci i działa! I jest w stanie przetłumaczyć każde zapytanie pseudojęzyka na poprawne zapytanie DSL, zachowując przy tym oryginalne zagnieżdzenia i poprawne zasady logiki Boole’a.

A zupełnie nieświadomy niczego Elasticsearch dostaje piękne zapytania, takie jak np. to:

"es_query": {
        "_params": {
            "should": [
                {
                    "_params": {
                        "245": "Lem"
                    }
                },
                {
                    "_params": {
                        "245": "Szymborska"
                    }
                }
            ]
        }
    }

No i oczywiście zwraca wyniki 🙂

Przyznam się, że wymyślenie tego algorytmu nie było proste, co więcej – w chwilach zwątpienia miałam wrażenie, że jest to wręcz nierealne, ale jednak… udało się. Z pewnością ten kod nie jest idealny, lecz niech pierwszy rzuci kamieniem, kto zawsze tworzy idealny kod :p

Zostawiam tutaj pole do popisu… samej sobie i szansę na jego udoskonalenie (bez zepsucia tego, co działa!).

A jak to wygląda po stronie Elasticsearcha?

Bardzo dobre pytanie!

Jeśli jesteście zainteresowani, odpowiedź znajdziecie w kolejnym artykule!

Zaś wszystkim, którzy dotarli aż tutaj dziękuję za wytrwałość i poświęcony czas 🙂

Dajcie znać w komentarzach, co sądzicie o takim customowym rozwiązaniu? Może macie jakieś pomysły na jego udoskonalenie? Czekam na Wasze opinie!

P.S. Pozdrawiam Maćka S. – twórcę tej kosmicznej idei, którą (sama nie wiem, jakim cudem) udało mi się przełożyć na DZIAŁAJĄCY kod! Jeśli masz w głowie jeszcze jakieś programistyczne “mission impossible” – wiesz, gdzie mnie szukać! 🙂

5 4 votes
Article Rating
guest
1 Komentarz
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Daniel
Daniel
4 miesięcy temu

O naiwności 🙂

Przepraszam, nie odbierz tego źle. Pomysł dobry, ale nierealny 🙂

Zakładasz, że książki są poprawnie skatalogowane.

A nie są.

Nie znam bardziej bałaganiarskich baz, niż właśnie te katalogi w MARC21.