Първа разлика: При B-Tree родителският елемент може да има N на брой наследника, а не само 2 като при Binary tree.
Втора разлика: При B-Tree всички последни елементи (листата) са винаги на едно ниво, за разлика от Binary tree. И по този начин при B-Tree за да намерим дадено „листо“ и тръгнем от root елементът, ще ни трябват винаги еднакъв брой стъпки. И всички „листа“при B-Tree са сортирани. И има връзка (т.н. double linked list) между отделните „групички“ (т.н. sets of leafnodes) листа на отделните „родители“, за да не се налага да се връщаме едно ниво нагоре когато търсим, която я няма в даденият set of leafnodes.
Junction tables are a standard practice in relational database design.
If you have a many-to-many relationship between two entities, the standard way to represent them is with three tables.
Two of the tables are entity tables, with a primary key. A junction table lies between them (logically) and contains two foreign keys, one that references each entity table. Often, these two foreign keys will be the only two columns in the junction table.
Демек, това е таблица, която осигурява логическата свързаност между други две таблици, и обикновно съдържа само техните Primary keys.
Има три типа релации в релационните бази данни:
One to one, когато на даден запис от т.н. parent таблица отговаря само един запис от т.н. child таблица.
One to mаny – когато на даден запис от т.н. parent таблица отговаряш N на брой записа от т.н. child таблица
Many to many – когато на даден запис от т.н. parent таблица съответстват както N на брой записи от т.н. child таблица. Но също и даден запис от т.н. child таблица съответстват N на брой записи от т.н. parent таблица. За какъв практически пример се сещам… Имаме таблица orders и таблица order_items. Ясно е коя какво съдържа – за дадена поръчка – N на брой items. Но всеко от тези items на дадената поръчка, могат да са част и от друга поръчка.
По принцип, many to many релация става с точно такава таблица. Не знам как можеш да обединиш две таблици в many to many релация директно. То по принцип тази релация не на практика директно непостижима в релационните бази, всичко се разлага винаги до one to many така или иначе.
Tова е ключ, който е изкуствено добавен, за да служи като най-често Първичен ключ. Тоест, на които самата RDBMS им задава стойността автоматично, например AUTO_INCREMENT или различни GUID стойности (като при MSSQL), sequences в PostgreSQL…
Всеки със сигурност е използвал Сурогатен ключ и то не веднъж. Всеки път като създадеш ново поле, което служи за ID и е целочислен, положителен AUTO_INCREMENT, и го зададем за Първичен ключ, това е на практика Сурогатен ключ.
Идеята е да служат за изцяло служебна цел и по принцип нямат стойност извън Базата Данни, и не носят никаква информация чисто за приложението, което използва дадената таблица.
Има голямо застъпване на понятията Първичен и Сурогатен ключ. Първичният ключ е вид Сурогатен ключ.
Представяме си един UNIQUE ключ. Ако махнем едно поле и уникалността се запази – това е Супер ключ. Ако се счупи – това е Потенциален ключ.
Супер ключ е нещо като лошо дефиниран UNIQUE ключ, тоест – има излишни атрибути, които и да махнем от този UNIQUE ключ, няма да счупим начинът, по който можем уникално да определим даденият запис.
Разликата между Супер ключ и Потенциален ключ е, че единият удовлетворява само изискването за уникалност, другият – и за минималност.
Потенциално, всеки от Потенциалните ключове може да е Първичен ключ, принципна разлика между двете няма.
Просто си задай въпроса: „Кое поле, или минимална комбинация от полета, може да ми гарантира уникалността на реда“, и това е.
Eто пример, това са възможните Супер ключове: {p_id} {p_id, p_firstname} {p_id, p_surname} {p_id, p_firstname, p_surname}
Koй е Потенциалният ключ? {p_id} защото отговаря и на изискването за уникалност, и за минималност.
Prime attribute са тези полета, които са част от някои от Потенциалните ключове.
Съответно, Non-prime attributes са такива, които дори и да са част от някой от Супер ключовете, не са част от никой от Потенциалните ключове.
Composite key е такъв ключ, който е съставен от повече от едно поле.
Compound key е такъв ключ, чиито отделни полета са от своя страна Foreign keys към други таблици.
Типичен пример за Compound key имаме в т.н. junction tables, където обединяваме други 2 таблици например, и имаме Потенциален ключ от две полета, всяко от което „гледа“ към своята си таблица.
За Алтернативен ключ се смята Потенциален ключ, който не се използва на практика за поддържане на уникалността. Образни казано – Потенциален ключ, който си е останал потенциален, но за него може и да има, може и да няма дефиниран например UNIQUE ключ, това не е от значение, просто идеята е, че като определим всички Потенциални ключове и „короноваме“ един от тях за Първичен, останалите ще наричаме Алтернативни ключове.
Добър пример би бил – аристокрацията на една държава, трябва да короноваме крал – един от аристократите. Останалите остават Алтернативни ключове – такива, които могат да бъдат крале, но не са.
If a table has more than one candidate key, one of them will become the primary key and rest of all are called alternate keys.
Селективността на даден индекс е понятие, близко свързано с неговата кардиналност. В смисъл такъв, че селективността е по-скоро коефициент, равен на отношението между кардиналността на даденият индекс, към кардиналността на таблицата.
Selectivity = Index cardinality / Table cardinality
Колкото селективността е по-близо до 1, толкова по-добре, толкова повече търсенето по даденият индекс ще е по-конкретно и недвусмислено.
Index selectivity is how tightly a database index helps narrow the search for specific values in a table.
Понятия, като index cardinality и index selectivity не са просто суха статистика, защото query planner-ът ги използва за да прецени предварително колко резултата биха съвпаднали с търсеното. RDBMS системата винаги трябва да има нещо като „хистограма“ или мета информация за самата информация, която управляваната от нея БД съдържа.
A superkey is a set of one or more attributes that, taken collectively, allow us to identify uniquely a tuple in the relation. For example, the ID attribute of the relation instructor is sufficient to distinguish one instructor tuple from another.
Написваш всички възможни комбинации от колони – по единично, по двойки, по-тройки и т.н…
Трябва и едни празен вариaнт да има – {}
Oт тези варианти, махаш всички, които имат повтярящи се стойности.
Тези, които остават са Супер ключовете.
Например:
Monarch Name
Monarch Number
Royal House
Edward
II
Plantagenet
Edward
III
Plantagenet
Richard
III
Plantagenet
Henry
IV
Lancaster
{} {Monarch Name} {Monarch Number} {Royal House} {Monarch Name, Monarch Number} {Monarch Name, Royal House} {Monarch Number, Royal House} {Monarch Name, Monarch Number, Royal House}
Виждаме обаче, че за {Monarch Name, Royal House} се получават повтарящи се кортежи – Edward, Plantagenet – следователно, махаме комбинацията {Monarch Name, Royal House}, тя не може да бъде Суперключ.
За {Monarch Number, Royal House} също може да се получат повтарящи се кортежи, за конкретният случай – не, затова може да е Суперключ, но в перспектива – не.
Тази комбинация {Monarch Name, Monarch Number} може да е Суперключ, защото няма как да има два краля с еднакво име и номер едновремено.
Така, получихме тези две комбинации, които могат да са Суперключ: {Monarch Name, Monarch Number} {Monarch Name, Monarch Number, Royal House}
Но в същото време, само първата е напълно достатъчна за да гарантира уникалността на кортежите. Това е вече Candidate Key (Потенциален ключ).
Следователно, в релация с n атрибута, максималният брой възможни Суперключа са 2^n
На практика Суперключовете нямат голямо значение, те са по-скоро за улесняване на DB дизайна – да преценим от кои полета имаме нужда за да карантираме уникалността на всеки запис. И ако например, няма такова/такива полета – да добавим ново поле, което да свърши тази работа.
След това, на тази база, да изберем и Потенциалният ключ, който да е всъщност минималният Суперключ. Защо се казва „потенциален“? Защото, подобно на Суперключа, можем да имаме повече от един, и просто трябва да изберем един.
Много е важно, при DB design да се разсъждава в перспектива – например, с оглед на това, какво би нарушило уникалността на записите. Затова Суперключове и Потенциални ключове трябва да се избират не само на база вече съществуваща таблица, а и в перспектива.
SQLite е релационна база данни, поддържаща SQL стандарта, работеща като библиотека към приложенията, а не като отделна програма. Затова, обикновено се използва като DB добавка към други програми.
SQLite не е server-client тип софуер, а по-скоро библиотека, която може да се вгражда в други програми.
Самата концепция на SQLite е всяка база данни да е съредоточена в един файл, което я прави подходяща за поддържане от други програми, без да е необходим сървърен процес.
Домейн (data domain) – множество от всички допустими, а не текущо налични стойности, което дадена колона може да приема. Условно казано, домените са на ниво ДП. Те са на практика отделните множества, от които се образува да даденото ДП, от което е част дадената релация. Домейн на градовете, домейн на хората, домен на рождените дати… Когато говорим за релация, домейнът е все едно даденият атрибут, а когато говорим за таблица – все едно колоната.
Each of these domains is, in effect, a pool of values, some or all of which may be represented in the data bank at any instant.
We shall call the set of values represented at some instant the active domain at that instant.
Домейн може да е например възможните стойности като при ENUM полетата – чисто изброени стойност.
Може да е типът на полети + евнтуално някакъв constraint, например varchar(100). Toест, казваме, „в тази колона всички възможни стойности са стрингове и не по-дълги от 100 символа, това е домейнът на това поле.
Домейнът на поле Заплата e например „всички положителни числа от 0 до 100 000 лв.“.
Ограниченията (constraints) позволяват в още по-голяма степен да се специфицират стойностите, които атрибутите от даден домейн могат да приемат.
–––––––––––––––––––––––––––––––––––––––-
Декартовото произведение (ДП) не притежава свойството комутативност, тоест ако разменим местата на участващите операнди, резултатът ще е различен. S1 x S2 ≠ S2 x S1
Също, не притежава и асоциативност – тоес ако имаме ДП на 3 и повече множества и един път ги групираме по един начин, дръг път по друг, ще получим различни резултати. (S1 x S2) x S3 ≠ (S1 x S3) x S2
Комутативност – поредността на операндите няма значение, напр. при умножение, събиране, но не и изваждане и деление. 5 x 6 = 6 x 5
Асоциативност – как групираме операндите на операцията. Напр. при умножение няма значение (5 x 6) x 7 = (7 x 5) x 6
При множествата асоциативна операция е дизюнкция („обединение“, „OR“) S1 ∨ S2 = S2 ∨ S1
Но не и разлика – резултантно множество, чиито елементи са елементи на S1, но не са елементи на S2
Aко един множител от ДП е празно множество, полученото ДП е празно множество. S1 x Ф = Ф
Що е то релация има ли тя почва у нас?
Вземаме например две множества S1 и S2, като S1 ще съдържа населени места, S2 – хора. Правим ДП от двете множества и получаваме всички възможни комбинации от хора и населени места. Но на практика само по една от комбинациите за човек ще са носещи информация и са истинни, защото човек може да е роден само в едно населено място. Това е релация – тази част от ДП, това подмножество от ДП, което показва реалните отношения между ЧОВЕК и НАСЕЛЕНО МЯСТО, и което носи инфомация.
Релацията между множествата (Иван, Стоян, Петър….) и (Русе, Варна, София…) е само тази част от ДП, която показва наистина кой къде е роден. Затова се казва „релация“ (отношение), защото показва отнешията между отделните елементи на двете множества – отножението между Иван и София, Петър и Русе,… и т.н.
Казва се релация, защото отразява връзките между отделните елементи в отделните множества. „Иван“ от множество „Хора“ е роден в „Каспичан“ от множеството „Населени места“ и на „1978-11-12“ от множество „Дата на раждане“… Така се отразява връзката между „Иван“, „Каспичан“, „1978-11-12″… Връзката, /релацията/ между тези три или повече конкретни елемента в отделните 3 или повече множества. Иван е роден конкретно в Каспичан и конкретно на тази дата…
Това се има предвид под „релация“ – отношението между какво? Между ОТДЕЛНИТЕ ЕЛЕМЕНТИ на множествата, не между множествата като цяло. За второто си има сечения, обединение, разлика…
Разликата с ДП е, че там Иван щеше да е роден във всички „Нас. места“ и на всички налични дати от „DOB“.
A relation is nothing but a table of values. Every row in the table represents a collection of related data values. These rows in the table denote a real-world entity or relationship.
Една релация е множество, принадлежащо на ДП на участващите множества: R ⊆ S1 x S2 x …. Sn;
Relations of degree 1 are often called unary, degree 2 – binary, degree 3 – ternary, and degree n – n-ary.
Всеки стълб от табличното представяне на релацията, която има уникално име, се нарича атрибут на релацията: R = (A1, A2, …. An);
Разликата между релация и таблица е, че когато релацията има име и набор от атрибути, отношенияте между които биват истинни, и всеки атрибут има уникално име, тогава вече можем да представим релацията в табличен вид и тогава имаме таблица. Релацията е по-скоро математически термин, който когато се „превъплъти“ в табличен вид, се нарича таблица.
Aко се замислим, всичко около нас множество от безкрайно много вероятни възможности, но се е случила само тази, в която сме.
Базова релация – тези релации, в които се съхраняват данните, а не се получават чрез различни операции като обединение и т.н… По-просто казано, това е самата таблица, а не разултатът от например SELECT операция.
Производни релации – такива релации, които не съхраняват данни, а се получават чрез прилагане на операции над други релации.
Схема на релация – името и и множеството от атрибутите и – R(A1, A2….An)
Ключ на релация – множество от атрибути, явяващо се подмножество от всички атрибути на релацията К = (A1, A3, A58… Am) ⊆ (A1, A2, A3… An) за които няма два или повече кортежа t, в които всички тези атрибути са еднакви. t1(K) ≠ t2(K) ≠ … tn(K)
Първичен ключ на релация – понякога една релация може да има повече от един ключа, тогава решаваме и приемаме един от тях за първичен. На практика, почти винаги се добавя допълнителен атрибут на име ID, който изкуствено да служи за първичен ключ. Първичен ключ (primary key) е атрибут (по-рядко група атрибути), който служи да идентифицира по уникален начин всеки запис (екземпляр) на релацията. Когато измежду атрибутите на релацията няма един подходящ за първичен ключ атрибут, вариантите са: – да се прибегне към множество от два и повече атрибути, които заедно идентифицират записите еднозначно, т.нар. сложен първичен ключ (composite primary key), или – да се добави нов атрибут, по който да се прави идентификацията на записите.
Чужд (външен) ключ на релация – ключ на дадената релация, който не е Първичен ключ за своята релация но е Първичен ключ за друга релация. Така на практика се реализира логическа свързаност между информацията на двете релации. Външният ключ (foreign key) е необходим, когато налице е отношение между две таблици (релации). Отношението се създава, като копие от първичния ключ на едната таблица се включи в структурата на втората таблица, за която той е външен (понеже тя вече си има свой собствен първичен ключ). Освен да помогне в установяването на отношение между двете таблици, външният ключ помага да се осигури и интегритета (целостта) на ниво отношение.
Текущ екземпляр на релация – релациите са динамични, във всеки момент всяка релация е набор от определени кортежи. „Моментната снимка“ от този набор от кортежи е текущият екземпляр на релацията.
Проекция – унарна операция, която на релация съпоставя нова, в която участват същите кортежи като в началната, но без определени атрибути (колони). П A1,A3(R(A1,A2,A3)) = R(A1, A3)
Relation universe – релациите с един и същ header.
Relation header – наборът от именованите атрибути на дадена релация, като за всеки атрибут имаме и типа му. Тоест – двойки от име и тип за всяка колона. Отговаря на схемата на таблицата. Аналогията с ООП би била, че хедъра е класът, а бодито е масив от обекти от този клас.
Relation degree – броят двойки (име – тип) в хедъра на дадената релация.
Relation cardinality – броят кортежи в дадената релация.
Relation body – time-varying set of tuples, tied to the header.
A relation R can be considered as a variable. The heading of a relation is the „type“ of the variable R The type of R is (D1, D2, D3, … Dn) The body of a relation is the „value“ of the variable R The value of R is a subset of the Cartesian Product of D1, D2, …, Dn
Свойства на релацията:
There are no duplicate tuples. Tuples are unordered. Attributes are unordered. All attribute values are atomic.
Difference between Tuple Relational Calculus (TRC) and Domain Relational Calculus (DRC) – при първото имаме резултат цели кортежи, при второто – проекция на избраните кортежи, съдържаща само някои атрибути. По-просто казано, едното селектва цели редове, другото само определени полета от намерените редове. Двете заедно съставят т.н. Relational calculus.
TRC имаме когато нямаме Проекция, a само Селекция, тоест трябват ни всички атрибути на кортежа, целият кортеж. {t | t ∈ Employee ^ t[Salary] > 5000} Чете се така: имаме кортеж(и) t, такива че принадлежат на релацията Employee и атрибутът им Salary е по-голям от 5000
TRC имаме когато имаме и Проекция, и Селекция {<f,s,a>|<f,l,s,b,a> Employee ^ Salary > 50000}
Какво е База данни – информация за дадена област от живота, систематизирана така, че да бъде удобно управлението и използването и.
Това се постига например (при релационните бази данни) с помощта на таблици, където всеки ред (кортеж) представлява един вид отделен обект, за който съхраняваме информация, а всяка колона е отделно свойство (property) на този обект.
Има няколко модела за организация на Базите данни – Релационен, Нерелационен, Йерархичен, Мрежов и др.
При релационният метод, информацията е организирана така, че да се избегнат т.н. „аномалии“ и да се пести място. Всъщност, второто е следствие на първото, в смисъл такъв, че постигне ли се първото, ще имаме налице и второто.
При Релационният модел това разпределяне и организиране на информацията означава, тя да бъде разпределена по отделни таблици, като запазването на цялостната свързаност се осъществява с т.н. „отношения“ или „релации“ между отделните таблици.
Първо, първичните ключове имат за цел да гарантират уникалността на реда, уникалните ключове – на полето. Не че уникалните също не могат да гарантират уникалността на реда, но трябва да са NOT NULL.
Кое е подмножество на кое?
Ключовете са за гарантиране на уникалността на стойностите в поле или на целия ред. Индексите – само за повишаване бързодействието на SELECT заявките, или за други функционалности като FULLTEXT търсене например.
Ключовете прилагат т.н. Integrity Constraints, индексите – не.
Kлючовете не трябва да се променят без да се внимава, защото може да са FOREIGN KEYs. Индексите – могат свободно.
Първо, да не се бърка с table cardinality, което значи „броят записи избщо в таблицата“.
Index cardinality refers to the uniqueness of values stored in a specified column within an index.
Явно затова кардиналността на UNIQUE индексите е равна на броя редове, върху които е даденият UNIQUE индекс, просто всички са уникални.
Aми ако има NULL стойности? Те как се броят за кардиналността? Групират се и се броят като уникални, като отделни, уникални стойности? Тоест ако имаме 20 уникални not null стойности и отделно още примерно 3 NULL-а, (следователно – 23 реда в таблицата), кардиналността ще е 23!
Тоест, броя уникални + броя NULL стойности.
Колкото кардиналността на индекса е по-малка от броя редове на таблицата (table cardinality), толкова по-малко са уникалните стойности на това поле.
A unique index would have cardinality equal to the number of rows in the table. But a non-unique index can have a cardinality of anywhere from 1 to the number of rows in the table, depending on how many times each index key appears in the table.
Каква е принципната полза от кардиналността?
Една полза би била за да се подобри т.н. „selectivity“ на даденото поле/полета. Колкото по-голяма е кардиналността (тоест броя уникални стойности на базата на общият брой стойности), това значи, че дадената SELECT заявка ще използва даеният индекс по-оптимално, за намиране на търсеният резултат, с по-малко търсене.
Селективността може лесно да се намери като разделим броя уникални стойности на общият брой стойности. Идеалната селективност е 1. Демек, идеалният индекс е този който не съдържа повтарящи се стойности.
Колкото селективността е по-близо до 1, толкова повече има смисъл от индекс за това поле/полета. Както и обратното – колкото по-малко на брой уникални стойности имаме, в отношение с целият брой стойности – толкова повече индексът би бил безсмислен.
Колкото селективността на един индекс е по-близо до 1, толкова и ползата му е по-голяма. Поне за B-tree индексите. За да си го представим по-нагледно, спомнете си за онази игра, дето пускаш едно топче по едни възли и на всеки възел може да отиде или на ляво, или на дясно…
Под Integrity Constraints се има предвид набор от правила, които съхраняваната в дадена БД информация трябва да спазва, за да се запазва качеството на информацията. Под „качество на информацията“ нека имаме предвид нейната полезност според случая, според нуждите, за които тя се използва. В този смисъл, може да се каже, че Integrity Constraints отговарят и поддържат семантичната стойност на информацията.
Integrity Constraints биват следните типове:
1. Domain constraints
Тук изхождаме от това, че дадено поле може да съдържа стойности само от предварително зададено множество – домейн. Елементите на предварително зададеното множество (домейн) на даденото поле, трябва да са от един и същ тип (това в случаят е домейна).
2. Entity integrity constraints
Тук изискването е относо Първичният ключ, и се състои в това, той да не е NULL, нито никоя част от него, ако е съставен, защото това би нарушило достъпността до даденият запис и би нарушило Първа нормалана форма.
Това ограничение изисква всяка таблица да има Супер ключ, бил той и Първичен ключ, и също, никое поле от тях да не може да има NULL стойност.
Също, ако някое от полетата може по принцип да има NULL стойности, то не трябва да се задава като Първичен ключ.
3. Referential Integrity Constraints
Това ограничение трябва да се спазва при релациите между от отделните таблици, при т.н. Foreign Keys.
4. Key constraints
Tук ограничението се прилага за да се запази уникалността на дадено поле или група полета ако имаме съставен или несъставен UNIQUE ключ.
5. Enterprise (semantic) constraints
Допълнителни, специфични за стойността на полето ограничения и изисквания към информацията, свързани с например – максимална дължина на символните низове (CHAR, VARCHAR…), максимална и минимална стойност на числените стойности и т.н…
Използват се за по-финна настройка на семантичната стойност на информацията.
Open source engine for analytics and full-text search. Поради бързината си и мощната си full-text search функционалност много често се използва например за т.н. „search as you type“, „autocompletion“, „correcting typos“, „search by synonyms“, „ordering by relevance“ и други подобни.
Как Elasticsearch организира съхраняването на информацията?
В Elasticsearch информацията се съхранява в т.н. документи, които са JSON обекти, като можем да си ги представяме аналогично като редове в таблица в някоя от RDBMS.
От своя страна, документът е съставен от полета, които са аналог на колоните в таблица.
Как взаимодействаме с Elastisearch?
Посредством REST Api. Заявките, които изпращаме са също JSON обекти.
Elasticsearch е написан на JAVA и е един вид „надстройка“ на Apache Lucene. Може да се scale-ва много добре.
Какво е т.н. Elastic Stack?
Допълнителни инструменти, разработени от компанията, разработила Elasticsearch.
Kibana
Използва се за визуализиране на информацията от Elasticsearch (графики, piecharts и др.), пускане на заявки, forecasting, anomality detection и други подобни. Kibana е нещо като dashboard на Elasticsearch. С Kibana може да се управлява и например аутентикацията към Elasticsearch. Oбщо взето, Kibana е web-interface към информацията в Elasticsearch.
Logstash
Използва се традиционно за управление на логовете от различни програми, които се запазват в Elasticsearch. Например, когато се запише нещо в логфайл, това за Logstash е едно събитие. Което събитие се обработва от Logstash и информацията се изпраща на Elasticsearch, но може и например да се изпрати e-mail, да се изпрати HTTP заявка към зададен URL и други подобни.
Затова се казва, че Logstash е „data processing pipeline“ или още, система от плъгини, разделени на три условни групи – input, filtering и output плъгини, които работят като pipeline. Има различни input plugins, например за четене от файл, за приемане на HTTP заявки, четене от друга база данни… Filtering plugins отговарят за обработката на постъпилата информация. Можем например да парсираме XML, CSV, JSON…, можем например по IP на източника да намираме геолокацията му… Output plugin задават къде да изпратим обработената информация, най-често към Elasticsearch. Такива дестинации се наричат stashes.
Една така „поточна линия“ от плъгини представлява един pipeline. Може да има повече от един паралелни pipelines.
В горният пример, задаваме на Logstash да третира всяко добавяне на ред в даденият лог файл като събитие. Тук това става с input plugin на име file, но за тази цел има и инструмент, наречен Beats.
След това идва ред на filter plugin, който обработва подаденият ред, парсира го на части и създава един JSON обект с отделни полета.
Накрая, output plugin изпраща горният JSON обект към Elasticsearch.
X-Pack
Пакет от функционалности, добавени към Elastisearch и Kibana. Например:
Security – добавя authentication и authorization към Elasticsearch и Kibana с например създаване на потребители и роли.
Monitoring – служи за наблюдение работата на Elastic Stack, като например memory usage, CPU, disk space и други подобни.
Alerting – когато искаш да знаеш дали не е настъпило нещо критично, като прекалено натоварване например. В такъв случай може да бъдеш уведомен по e-mail, Slack…
Reporting – служи например когато трбва да експортираш различните визуализации от Kibana например като картинка или PDF.
Machine Learning – за задаване на различни machine learning jobs, които например да следят за спайкове в натоварването, да правят прогнози за бъдещето и т.н…
Graph – служи за определяне на релевантността междо отделните документи. Демек, ако търсиш AC/DC много е вероятно да харесваш и Metallica. Или ако намерим даден продукт, да можем да намерим и подобни на него.
SQL – това просто казано, ни помага да пишем SQL заявки към Elasticsearch, вместо да използвме Query DSL (езикът за заявки на Elasticsearch).
Beats – колекция от т.н. „data shippers“. Data shipper може да се разбира като сървис или демон, който събира и изпраща информация към Logstash или Elasticsearch.
Важно е да се отбележи, че запазването на информация в Elasticsearch може да стане както през Logstash или Beats, така и директно, използвайки Elasticsearch API.
Kaквo е ELK stack? Преди да е имало Beats и X-Pack, стакът е бил само от Elasticsearch, Logstash и Kibana. С тях двете, вече се казва Elastic Stack. Демек, Elаstic Stack е надстройка на ELK.
Обзор на някои примерни начини на използване на Elasticsearch
За МакОС/Линукс – смъква се един *.tar.gz файл, разархивира се с: tar -zxf thefile.tar.gz Toзи файл основно се състои от jar файлове. Взлизаш в екстрактнатата директория и стартираш elasticsearch в bin директорията. Би трябвало да зареди, може да се спре с Ctrl+C, може да се изпращат заявки или с curl през командният ред или с HTTP клиент като Postman.
Тоест, реално нищо не инсталираме, просто смъкваме, разархивираме и стартираме.
Elasticsearch може или да използва инсталираната вече на компютъра Java, или да използва OpenJDK, който идва с гореспоменатият файл.
Kibana се инсталира по абсолютно аналогичен начин.
Обзор на директорията, съдържаща Elasticsearch
bin/ – тук има основно изпълними файлове, като самият сървис на Elasticsearch, също и разни helper програми, като elasticsearch-plugin и elasticsearch-sql-cli с които се инсталират плъгини и се изпълняват SQL заявки към Elasticsearch.
config/ – разни конфиг файлове, като: elasticsearch.yml, който е главният конфиг файл на Elasticsearch. Всички сетинги са коментирани, тоест, използват се дефолтните такива, освен ако не разкоментираме някоя от тях. Сетинги като: къде да съхранява информацията, какъв IP и порт да използва и т.н. jvm.options – настройки, свързани с Java Virtual Machine, в която работи Elasticsearch log4j2.properties – „log4j2“ е популярен логинг фреймуърк за Java и Elasticsearch го използва за запазване на различна лог информация.
Има и разни конфиг файлове за конфигуриране на потребители и роли.
/jdk – съдържа OpenJDK, с което Elasticsearch си идва, ако нямате Java.
/lib – тук са разлини библиотеки, които Elasticsearch използва като log4j2, Apache Lucene
/modules – различни модули, които добавят повече функционалност на Elasaticsearch. Тук е например X-Pack
/plugins – където са плъгините, като за начало няма нищо, защото не сме добавяли плъгини. Разликата между модул и плъгин е, че модулите идват наготово, а плъгините можем ние да си напишем или готови да смъкнем от някъде. Също и, че плъгините могат да се махат, модулите – не.
Обзор на архитектурата на Elasticsearch
Какво е node – една работеща инстанция на Elasticsearch, не компютър или виртуална машина, защото на един компютър може да има много отделни инстанции. Това, което инсталирахме и стартирахме е точно един node. Moже да имаме N на брой nodes, които да ни позволят scalability. Tогава данните ще са разпределени между отделните nodes.
Как тогава всички тези nodes биват управлявани и информацията – разпределяна между тях, как Elasticsearch знае къде какво да търси? Над nodes в йерархията се намира т.н. cluster, който обединява един или повече nodes. В нашият случай имаме един cluster с един node в него. Cluster е колекция от свързани nodes, която колекция съдържа и управлява цялата информацията. Clusters са напълно независими един от друг, въпреки че е възможно да се прави cross-cluster search.
Когато правим HTTP заявки към Elasticsearch, всъщност комуникираме с REST Api на дадения клъстер.
Kак се стартира cluster? Като стартираш node, той или се добавя към cluster, към който е настроен да принадлежи, или сам стартира cluster.
Няма node без cluster.
Всеки node съдържа documents. Koгато добавяш document, оригиналната му стойност се запазва в полето _source, отделно се добавят и някои служебни полета за „вътрешно“ ползване от Elasticsearch. Tоест, ето един примерен document:
Всеки документ се намира в т.н. index. Представяме си индексите като таблици. Индексите не само логически обединяват докоментите, но и позволяват различни per-index инастройки.
Обобщение: cluster -> 1..N nodes -> 0..N index -> 0..N document -> 0..N fields Но трябва да се знае, че даден индекс не принадлежи към конкретен node, индексите принадлежат към клъстерите, просто са разхвърляни по отделните nodes.
Обзор на cluster
Форматът на HTTP заявка към Elasticsearch: [HTTP verb] /[api][/command]
Например за да видим състоянието на клъстера, изпращаме команда „health“ към API „_cluster“: GET /_cluster/health Примерен отговор:
За да видим nodes в един cluster, изпращаме команда „nodes“ към API „_cat“. GET /_cat/nodes Примерен отговор:
10.46.64.66 57 64 1 1.01 1.11 1.23 l - instance-0000000013 10.46.79.252 64 48 2 1.69 1.32 1.14 l - instance-0000000014 10.46.64.65 53 60 2 0.40 0.84 1.12 l - instance-0000000012 10.46.79.246 38 98 11 2.11 2.20 2.59 m - instance-0000000015 10.46.79.250 51 97 12 1.93 2.36 2.64 m - instance-0000000004 10.46.79.230 53 99 23 3.49 3.03 3.12 di - instance-0000000002 10.46.64.73 73 99 34 5.03 4.18 4.00 di - instance-0000000000 10.46.64.96 41 99 11 2.56 2.50 2.73 di - instance-0000000016 10.46.79.249 34 97 12 1.05 2.01 2.33 m * instance-0000000003
Aко добавим параметър „v“, ще получим и заглавен ред за да знаем коя колона какво съдържа: GET /_cat/nodes?v Примерен отговор:
Забележете, че един от nodes е маркиран като master node.
Горното може да стане и с още по-подробна команда, която връща JSON: GET _nodes
Aко искаме да видим индексите в даденият cluster: GET _cat/indices?v Примерен отговор:
Ще забележим, че освен останалите индекси, Kibana също има свои служебни индекси. Тези индекси започват с „.“ за да не ги показва Кибана заедно с другите, подобно на скритите файлове в Линукс.
Koлоната „pri“ или „primary shards“ е броят shards към всеки индекс.
Sharding
Sharding е способ за разделяне на даден индекс на части, наречени shards. Shards са на ниво индекс, не клъстер или node.
Имаме например индекс, който не можем да поберем на никой от двата си nodes. Затова – разделяме го на два shards и ги пазим във всеки от nodes.
Общо взето, доста гъвкаво може да се разпределят shards по различните nodes.
Друг смисъл от shards е, че дадена заявка отправена към повече от един shard, се изпълнява паралелно за всеки от shards и така се печели скорост.
Oт Elasticsearch 7 всеки индекс се създава с един shard, ако ти трябват повече, има т.н. Split API за тази цел. Както и Shrink API ако трбва да се намали броят shards.
Replication
В Elasticsearch, репликацията е настроена по подразбиране.
Репликацията е начин за избягване на загуба на данни при различни, най-често хардуерни проблеми, като например повреда на харддиск и т.н…
Репликацията съществува на ниво индекс и това, което прави реално е да създава реплики (копия) на отделните shards в индекса – replicas или replica shards, които са пълно копие на оригинала си. Оригиналът се нарича „primary shard„. Primary shard и неговите replicas се наричат „replication group„.
При създаване на индекс може да се зададе всеки shard колко реплики може да има, по подразбиране – една.
И идеята е репликите НИКОГА да не се съхраняват на същият node, на които е и техният primary shard. Затова и репликацията има смисъл само за клъстери, които имат повече от един node.
Репликация не се използва само с идеята да се подсигури информацията, а и да се повиши бързината при select заявки, защото това извличане на данни няма да е само от един shard, а от N, бидейки напълно огледални. Тоест, разпределяме натоварването паралелно на отделните shards и всеки от тези shards могат да обработват заявки паралелно, по едно и също време.
Нека създадем един нов индекс „pages“, без никакви настройки, и да видим отговора от Elasticsearch на долната фигура.
Защо новият индекс е с health „жълто“? Защото в нашият случай имаме един node, a при създаване на нов индекс, по подразбиране се създава и един primary shard и една негова реплика. Но знаем, че те двете не могат да съществуват на един node. Затова Elasticsearch ни подсказва, че трябва да добавим още един node. А иначе, индексът ни е напълно наред.
Как да видим всичките shards в клъстера?
GET /_cat/shards?v Примерен отговор:
Виждаме, че индексът „pages“ има два shards – primary и replica, като вторият е UNASSIGNED, защото както казахме, няма втори node за тази цел.
Има нещо друго интересно, защо на Kibana индексите имат само primary shard, а не и реплики? Защото те са конфигурирани с настройка „auto_expand_replicas“, която настройка задава, че когато добавим нов node, автоматично ще им бъде създадена реплика на primary shard-овете. Тоест, тази настройка динамично променя броя на репликите на primary shard-овете, на базата на това колко nodes имаме.
Добавяне на нов node към клъстера
Един от начините да добавим нов node е например като смъкнем нов Elasticsearch и го инсталираме напълно отделно но с някои специфични настройки. Но това е само за development цели, не и за production.
Първо, задайте име на новия node в elasticsearch.yml – настройката node.name. По подразбиране е зададено името на компютъра, но то вече е заето от първата ни инсталация. Също, може да зададем името на клъстера, към който да присъединим такущата инстанция (node) – cluster.name.
Стартираме ли новият node, той автоматично би трябвало да се добави към клъстера.
Това е единият начин, но има и по-интелгентен – да не смъкваме нов Elasticsearch или да копираме директорията му под ново име, а да стартираме нова Elasticsearch инстанция, като зададем в командния ред съответните настройки:
Това е механизъм, аналогичен на backups и ни позволява да възстановим информацията към дадена дата и време.
Може да се правят snapshots на отделни индекси или на целият клъстър.
Видове nodes
master node на клъстъра, отговорен за т.н. cluster wide actions – създаване и изтриване на индекси, управление на останалите nodes, както и техните shards. Такъв master node може да е един от всички, може и да е dedicated.
data nodes – както и името му подсказва, такива служат за съхранение на информацията на клъстъра.
ingest nodes – за изпълняване на ingest pipelines – серия от действия, през които трябва да мине една информация за да бъде индексирана в документ.
machine learning nodes – позволяват на даденият node да стартира machine learning jobs.
coordination nodes – управляват как Elasticsearch си разпределя заявките вътрешно.
voting-only nodes – master node се „избира“ с процес, наречен voting process
dim значи data, ingest and master – това са ролите по подразбиране на всички nodes.
Managing documents
Създаване и триене на индекс
PUT /indx_edno DELETE /indx_edno
Aко трябва да добавим различни настройки, използваме JSON обект.
Защо обаче документът е запазен в 3 shards? Защото имаме 1 главен shard и 2 replica shards. Също, _id полето в случая е autogenerated, но ако искаме можем и ние да зададем стойността му като част от URL-a
POST /indx_edno/_doc/101
{
...
}
Извличане на документ по ID
Използваме горният URL, но по GET:
GET /indx_edno/_doc/101
Примерен резултат:
Aко няма такъв документ, „found“ ще е false.
Ъпдейтване на документ
POST /indx_edno/_update/101
{
"doc" : {
....
}
}
Важно е да се знае, че вътрешно Elasticsearch не прави update, a replace на документ. Това, което реално прави update операцията e, да извлече документа, да го промени, да изтрие сегашния, и да запази и индексира новия.
Scripted updates
В Elasticsearch е въможно да влагаме определена, по-сложна логика, от това просто да отправяме заявки. Например, да обядиняваме повече от една заявка в прости скриптове и т.н…
Ето примерен синтаксис:
POST /products/_update/101
{
"script" : {
...
}
}
И съответно, в „script“ слагаме самият скрипт, например:
Тук, „ctx“ (от context) е обект, който в полето си „_source“, съдържа полетата от избраният документ, който ще ъпдейтваме. Аз лично си го обяснявам като аналог на this в някои програмни езици като PHP или JavaScript.
Ако например изпращаме заявка от някакво приложение и трябва да използваме параметри, както примерно мапваме параметри в PHP. Можем и в Elasticsearch да го направим, като използваме полето „params“, което е „key:value“ обект, съдържащ въпросните параметри, които искаме да намапнем. Горната заявка придобива следният вид:
Или още, ако търсеният документ съществува – update, ако не – insert.
POST /indx_edno/_update/101
{
"upsert" : {
....
}
}
Replacing documents
PUT /indx_edno/_doc/101 { … }
Ако правилно разбирам, разликата с update на документ е, че при update променяш част от документа, а при replace целият документ все едно се изтрива и запазва и индексира наново по ID. Демек, вместо „на документ с това ID му промени това и това“, все едно казваш „ето ти напълно нов документ за това ID“.
Тоест, сегашният пример с PUT ще изтрие каквото има по това ID и ще запази новият документ. С replace не можеш да променяш само част(и) от даден документ, защото то изтрива и запазва+индексира каквото му подадеш наново.
Delete document by its ID
DELETE /indx_edno/_doc/101
Introducing routing
Дотук добре, но аз пак се чудя за неща като например, като добавяме нов документ, как Elasticsearch решава в кой shard да го запази? И после, как знае от кой shard да го извлече?
Тук идва понятието routing, което най-просто се свежда точно до това.
Когато запазва документ, Elasticsearch използва проста формула за да изчисля в кой точно shard да го запази.
shard_num = hash(_routing) % num_primary_shards
Тук, по дефолт _routing е ID-то на документа, но може да се задава изрично като част от т.н. meta info на всеки документ. Демек, ако в JSON-a на документа, наравно с метаполета като _id, _source, _index… няма _routing, значи използваме _id за целта.
Помним, че броя shards не може да се променя след като веднъж индекса е създаден. Защо? Защото за нови документи, които тепърва запазваме не е проблем, но не и за вече създадените. Ако създадем документ и му изчислим номера на shard-a, и после променим броя shard-ове, когато по същата тази формула се опитаме да го намерим и извлечем, ще имаме друг резултат, защото num_primary_shards ще е друго.
How Elasticsearch reads data
Самият процес по извличане на даден документ по ID е по-сложен.
Първо, т.н. coordinating node като получи дадената GET заявка, използва горната формула за да намери primary shard-а по ID-то. Подчертавам primary shard, не репликите. Не знам дали въпросният „coordinating node“ е някакъв специален node, или просто е този, към който дойде заявката в даденото време.
А вече, от дали Elasticsearch ще извлече търсеният документ от primary shard-а или от някоя от репликите му в дадената replication group, се решава допълнително на база на това, от къде performance ще е най-добър. Съгласете се, че няма смисъл всички рекуести да отиват само към primary shard-овете, щом имаме и реплики, не е добре за производителността.
За това, от кой точно shard в дадената replica group да се извлече дoкумента, се използва техника, наречена ARS – Adaptive Replica Search – нещо като load balancer за отделните shards.
Но пак подчертавам, тук говорим само за извличано по ID, не за т.н. search queries.
How Elasticsearch writes data
Тук имаме напълно същият процес по рутиране но само докато се намери primary shard, в който да запишем информацията. Write requests винаги се рутират към primary shard-a. Там става валидирането на информацията и запазването, след което вече автоматично Elasticsearch запазва копие на информацията и по останалите реплики.
Важно е, че операцията ще се счита за успешна дори и информацията да се запази само в primary shard-a.
Интересно стaва например ако фейлне primary shard-a. Тогава някой от репликите става primary.
Също, може например едни реплики да се ъпдейтнат с новата информация, други – не.
За такива сценарии, Elasticsearch има техника, наречена Primary terms and sequence numbers.
Document versioning
Полето „_version“ представлява цяло число, увеличаващо се с 1 всеки път, когато променим документа, включително и изтрием. Ако изтрием документ и в рамките на 60 секунди запазим нов документ със същото ID, _version номера се увеличава с 1 от там, където е бил. Иначе – започва пак от 1.
Този тип versioning се нарича internal versioning.
Има и external versioning, при който ние задаваме номера на версията. Може да се използва например ако основно използваме релационна база, а Elasticsearch използваме заради search функционалността му и за backup на данните.
PUT /products/_doc/101?version=123445412&version_type=external
{
...
}
Optimistic concurrency control
По принцип, има опасност докато някой е извлякъл документ, променил го е, и го е запазил, друг да е сторил същото и да се получи „замазване“ на информацията „един върху друг“.
Например, два клиента извлякат един и същ документ, направят промяна (например наличността в склада) и решат да запазят промените си. Вторият ще „замаже“ промените на първият, който е запазил.
В такъв случай втората ни заявка не трябва да бъде отказана.
Единият начин е да се използва полето „_version“. И двата клиента извличат даден документ с един и същ _version, например 1. Първият запазва промяната си и _version се увеличава на 2. Вторият опитва и той да запази неговите промени но неговата стойност на _version е статата 1 и затова Elasticsearch отказва да запази.
Има и по-нов начин, с използването на полетата „_primary_term“ и „_seq_no“, които са част от документа, когато го извлечем, тоест, получаваме ги от Elasticsearch заедно с документа и другата информация.
Когато опитаме да запазим документа обратно, трябва да добавим стойнностите на тези две полета като част от POST заявката, под имена „_if_primary_term“ и „_if_seq_no“.
Elasticsearch ще използва тези стойности за да провери дали документът вече не е бил променен от друг клиент, в промеждутъка от извличането му до запазването му.
Идеята е, че ако веднъж направиш update, Elasticsearch освен, че ще запази самият документ, ще промени и стойността на „_seq_no“. Така, ако ние пак или който и да е друг, опита пак да направи update, Elasticsearch ще откаже, защото стойността на „_if_seq_no“, която му подаваме, ще е старата.
Интересно е, че „_primary_term“ няма да се промени както „_seq_no“.
Имаме ли такава ситуация, при която междувременно друг е променил документ, който и ние искаме да запазим, трябва да извлчем актуалният документ наново. Тоест, наша работа е да вземем нужните действия.
Update by query
Как например да променим не един документ по неговото ID, a например много документ, избрани по дадено условие?
Интересно е, че когато стартираме такава заявка, Elasticsearch първо прави „моментна снимка“ на индекса.
След това, заявката се изпраща т.н. „search query“ към всички shards на индекса, понеже няма как да знаем в кой/кой shards се наимрат турсените документи.
Ако има намерени резултати, се изпраща т.н. „bulk request“ за ъпдейтване на намерените документи.
Отделните т.н. „двойки от search and bulk query“ се изпълняват последователно, а не паралелно. Ако има грешка при изпълнение или на search query или на bulk query за даден shard, Elasticsearch ще направи 10 опита. Ако и 10-те са неуспешни, цялата update операция се прекратява до там, като успешните промени (ако има такива) не водят до rollback на цялата операция. Тоест, възможно е някои shards да бъдат ъпдейтнати, други не. Тоест, цялата операция по ъпдейт не се изпълнява в транзаксия, всеки от отделните shards е за себе си.
Важно е, че цялата операция по update се порекратява при първи неуспешен update на shard.
На горната фигура виждаме примерна ситуация, в която shard (или още replication group) А e ъпдейтнат успешно, shard B – не, дори и след 10 опита, a за shard C изобщо няма и да има опит за ъпдейтване, заради несполучливият shard B.
Aко се получи така, че когато ти ъпдейтваш, преди това друг е вече ъпдейтнал, и фактически ти пренабиваш стара информация. Това се нарича „version conflict“ и по принцип спира изпълнението на твоя ъпдейт. Но ако все пак искаш да ъпдейтнеш, замазвайки предишните промени, трябва да добавиш елемент „conflicts“: „proceed“ в бодито на заявката или в query string.
До тук говорихме как да извършваме отделни single операции. Но можем ли да извършваме multiple операции?
Това става с помоща на т.н. Bulk API. Tози API използва NDJSON (Newline Delimeted JSON) – стандарт, почти същият като JSON само дето в JSON имаме един обект, в NDJSON имаме много JSON обекти, разграничени с нов ред.
Tук първо индексираме документ, след това създаваме документ. Разликата между index и create е, че create командата ще е неуспешна ако такъв документ вече съществува. За index няма значение дали вече съществува. Ако същетвува, ще бъде заменен.
Eто пример за bulk операция за индексиране на N на брой документа:
{"index":{"_id":1}}
{"name":"Wine - Maipo Valle Cabernet","price":152,"in_stock":38,"sold":47,"tags":["Alcohol","Wine"],"description":"Morbi porttitor lorem id ligula.","is_active":true,"created":"2004\/05\/13"}
{"index":{"_id":2}}
{"name":"Tart Shells - Savory","price":99,"in_stock":10,"sold":430,"tags":[],"description":"Etiam vel augue. Vestibulum rutrum rutrum neque. Aenean auctor gravida sem.","is_active":true,"created":"2007\/10\/14"}
{"index":{"_id":3}}
{"name":"Kirsch - Schloss","price":25,"in_stock":24,"sold":215,"tags":[],"description":"In eleifend quam a odio.","is_active":true,"created":"2000\/11\/17"}
{"index":{"_id":4}}
{"name":"Coffee - Colombian Portioned","price":37,"in_stock":37,"sold":477,"tags":["Coffee"],"description":"Lorem ipsum dolor sit amet, consectetuer","is_active":false,"created":"2008\/08\/17"}
Общо взето, забелязва се, че index, create и update операциите вървят по двойки – първо мета информацията, после самата информация. Защото например delete операцията е един, а не два реда:
В горните примери се вижда, че можем да използваме различни индекси, но можем да използваме един и същ индекс, който да зададем като част от URI.
POST /products/_bulk
...
Досещате се, че така можем да премахнем параметъра „_index“: … от горните рекуести.
Други важни отметки:
1. Content-type хедъра трябва да е application/x-ndjson
2. Всяка операция трябва да е разбита на редове, завършващи на \n или \r\n и винаги трябва да има един празен, последен ред.
3. Ако една операция е неуспешна, това не спира изпълнението на останалите, всяко операция е за себе си. Целият bulk процес няма да бъде прекратен.
Mapping and analysis
По принцип силата на Elasticsearch и на подобните бази като Solr, е най вече не в самото сурово съхранение на данните като една класическа база даннни, а по-скоро в search функционалността, която предлагат. Но за да я предложат е ясно, че постъпващите данни не трябва само да се запазват, но и да преминат през определени допълнителни обработки. Някъде това се нарича „digesting“ или „смилане“ – процес по създаване на т.н. inverted index.
Важно е да се знае, че това което е съхранено в _source не е това, което Elasticsearch използва за търсене. Това в _search е просто суровата информация. Просто няма как дълъг текст например, да бъде използван в търсене, различно от буквалното, ако не бъде предварително обработен.
С тази задача се заема т.н. analyzer, който се състои от три части – character filter, tokenizer и token filters. Крайната цел на тези трите е от подадената информация да се произведе т.н. searchable datastructure.
Какво прави character filter? Действа на ниво отделни символи, като премахва, заменя и т.н…. например заменя HTML entitites с техните еквиваленити и т.н. Също, премахване на HTML тагове например.
Зад всеки analyzer строи точно един tokenizer, отговорен за разбиването на тескта на токени. За самото „токенизиране“ отговаря т.н. Unicode Segmentation Algoritm,който приемаме за сега, че просто сплитва по интервал. Също, тук става и премахване на пунктуационните знаци.
След това идват token filters, чиято работа най-просто казано е да обработят различните токени, получени от tokenizer-a, като например премахване на съюзи, междуметия и други, несъществени за търсенето токени.
Ето как може да се провери как Elasticsearch би анализирал даден текст:
POST /_analyze
{
"text" : "Lorem ipsum ala bala портокала 1, 2 ,3 ..."
}
Може да се задава каквъв анализатор да се използва, какъв токенизатор, филтри… и други подобни.
Inverted indexes
Най-общо казано, inverted index е мапинг между термините на даден текст и това в кой документ се намират, или с други думи „кое къде е“.
Също можем да си го представим като индекс на термините в края на някои книги.
What is mapping
Mожем да направим аналогия със структурата на таблицата в релационните бази данни.
Има два начина за създаване на мапинг – explicit и dynamic.
При explicit задаваме полетата и типовете им още при създаване на индекса.
При dynamic това се върши от Elasticsearch като се проверява подадената стойност за даденото поле.
Тип масив по принцип няма, но от друга страна всяка стойност може да се състои от една или повече такива, което на практика е масив. Всички стойности трабва да са от един и същ тип. Също, може да имаме вложени масиви и масиви от обекти.
Object – за съхранение на JSON обекти.
Може да имаме nested обекти, просто за даденият ключ се задава стойността му да е „properties“ и тогава, стойноста на „properties“ се задава да е самият nested обект.
Nested – за да съхраним масив от обекти, имаме специален тип „nested“, чиято цел е да запази отношенията между отделните обекти в масива и всеки обект в масива да е независим от другите.
Вътрешно такива обекти се „флатват“ по време на индексирането, кdакто се вижда от долната фигура, защото Apache Lucene не поддържа обекти.
keyword – най-често се използва за exact searching, филтриране, сортиране, агрегиране.
text – обратното на keyword, използва се за full-text търсене.
Как работи keyword типът
Преди видяхме, че Standard analyzer е използван за text полетата. За keywords се използва Keyword analyzer, който на практика се явява „noop analyzer“, тоест нищо не анализира. Разбираемо, щом такива полета се използват за „exact matching“.
Дори и препинателните знаци биват запазени.
Можем да видим това със заявката:
POST /_analyze
{
text: "Some example text bla bla bla",
analyzer: "keyword"
}
Type coersion
Идеята е: когато например създаваш нов индекс и добавиш документ, или към празен индекс добавиш документ, Elasticsearch създава mapping с помощта на т.н. „dynamic mapping“. Tова е аналог на създаване на таблица в релационните ДБ.
Например, запазваме поле със стойност – integer. Elasticsearch ще очаква следващите стойности на това поле да са пак от тип integer. Aко например опитаме да запазим целочислена стойност но предтавена като стринг („101“), Elasticsearch ще преобразува string стойността в integer такава (101) и доцументът ще бъде индексиран все дно сме подали integer. Това е „type coercion“.
Но какво ако подадената стойност не може да се преобразува в необходимият тип, например ако е „m101“? Tогава заявката няма да бъде успешна.
Tук има нещо много важно. Ако погледнем полето „_source“ на даденият документ, ще видим, че там стойността на въпросното поле е оригиналната. Ще рече, че type coercion важи само при индексиране.
Както се вижда type coercion е механизъм за „замазване на ситуацията“ и е препоръчително да се изключи, за да сме сигурни, че винаги подаваме превилните типове данни на Elasticsearch.
Масиви
Taкъв тип всъщност не съществува, но въпреки това можем да съхраняваме съвкупност от стойности в дадено поле, без да е нужно това да се завада предварително. Тоест, ако едно поле е от тип „text“, в него можеш да съхраняваш и скаларен текст, и повече от един текста. Демек, array тип няма, защото няма нужда специално да има, и без това всяко поле може да е array.
Ако имаме например поле със стойност [„Some example“, „text bla bla“] той ще бъде индексиран сякаш е едно изречение, като конкатениран с интервал – „Some example text bla bla“. Но това конкатениране е само за текстовите типове данни.
Относно правилото, че всички елементи трябва да са от един тип, това е до толкова вярно, до колкото тези стойности могат да бъдат преобразувани за да отговарят на mapping-a (при условие, че type coercion е разрешено разбира се).
Можем да имаме и nested arrays, само трябва да се има предвид, че при индексиране такива масиви се „флатват“.
Може и обектите да са „флатнати“ с т.н. „dot notation“:
PUT /twitter
{
"mappings" : {
"properties": {
"first_name" : { type : "text" },
"email": { "type": "keyword" },
"personal_interests.interest_name" : { type : "text" },
"personal_interests.interest_first_year" : { type : "int" }
}
}
}
И важното е да се знае, че щом вече имаме mapping за даденият индекс, той вече ще задава какъв да е типът на стойностите по полетата. А това дали можем да запазим например int в text поле, зависи дали сме включили или не „type coercion“.
Друго важно е, че mapping-ът по принцип не задължава документите да имат всички полета, които той задава. Т.е. не е както при релационните бази данни, където трябва да има или стойност или NULL. В Elasticsearch всички полета са опционални. Adding a field mapping does not make the field required. All fields in a document are optional.
Mapping retrieval
Щом можем за задаваме mapping, трябва да може и да показваме такъв, например в случаите на dynamic mapping. За даден индекс командата е:
GET /twitter/_mapping
За конкретно поле:
GET /twitter/_mapping/field/[the name of the field]
Или ако е пропърти на обект:
GET /twitter/_mapping/[obj_name].[property_name]
Add new field to mapping
Aко искаме да добавим ново поле/полета къ вече съществуващ индекс, това става по много подобен начин:
PUT /twitter/_mapping
{
"properties": {
"created_at" : { type : "date" }
}
}
Date type
Този тип може да бъде съхраняван по три начина:
стринг във формат ISO 8601
милисекунди от 1 Яну 1970
custom data format
Вътрешно, date стойностите се съхраняват като цяло число с милисекунди от 1 Яну 1970г.
Когато използваме стринг, може да имаме и само дата, без време, например „1978-04-15“, тогава Elasticsearch ще приеме, че времето е 00:00 в полунощ.
Много важно е ако имате формат на „date“ полето да е „epoch_millis“, а не „epoch_second“ и таймстампът ви е в секунди, а не в милисекунди, да го умножите по 1000 преди да го запазите, защото за Elasticsearch това поле е за съхранение в милисекунди, и ще си мисли, че това което му подавате също е милисекунди, и ще ги запази успешно, а вие ще си мислите, че Elasticsearch знае, че са секунди и…
Overview of mapping parameters
Toва са някои допълнителни параметри, с които се настройва как да се създаде mapping освен например само като зададем типа на полето.
format – за задаване на формата на поле от тип data, например „dd/MM/yyyy“, „epoch_millis“…
properties – задава полетата на обект или nested поле
coerce – дали да се опита да преобразува подадената за това поле стойност в правилният тип. По подразбиране е разрешено. Може да се задава както на ниво поле, така и на ниво индекс.
doc_values – задава друг начин на индексиране на полето, а не с inverted index, който не е дъбър вариант когато трвябва да можем да филтрираме или сортираме по това поле. При inverted index първо вземахме дадената стойност и тогава намирахме в кои документи се намира тя. Тук имаме обратното – вземаме документа и тогава вземаме стойността на даденото поле от него. Това е най-просто doc_values структурата – обратното на inverted index, но не като заместител, а по-скоро като допълнител. А кое от двете ще бъде използвано, зависи от заявката, която използваме, защото doc_values по принцип се използва за сортиране, филтриране, aggregation и др…
Кога да използваме doc_values и кога не? Неизползването му пести дисково пространство но за сметка на скоростта, защото вместо да имаме тази data structure готова, трябва да я създаваме временно при всяка заявка.
norms – за изчисляване на т.н. „relevance score“, с които полученият резултат от дадена заявка се сортира допълнително по това, кои стойности са по-близо до търсеният резултат. Подобно на doc_values и тези, т.н. „norms“ заемат доста място, затова трябва да се забраняват за полета, за които няма да имаме нужда от „relevance score“.
index – задава дали дадена стойност да бъде индексирана или просто запазена (в _source). Ако не бъде индексирана, тя не може да участва в search заявки.
null_value – тази настройка е свързана с това как се запазват NULL стойности. По принцип NULL стойностите, както и празни масиви или масиви от NULL-ве не се индексират и съответно, по тях не може да се търси. Да обаче с null_value може да се зададе стойност по подразбиране за такива полета. И когато Elasticsearch попадне на такова празно поле, той ще индексира тази стойност, по този начин правейки полето searchable. Важно – това работи замо за explicit NULL values, демек, когато изрично му подадеш NULL, а не когато изобщо не го използваш. Второ – стойността по подразбиране на такова поле трябва да е от същият тип като типа на полето. Трето – всичко това важи само за това, което Elasticsearch индексира, демек, в _source пак ще имаме NULL.
copy_to – нещо като calculated fields, може да се задава автоматично стойността на друго/други полета да се копира в зададеното поле. Например ако имаме полета за първо име и фамилия, може да си зададем трето поле, където автоматично да имаме цялото име. Важно: въпросното „копирано“ няма да е част от _source обекта, а ще бъде само индексирано.
Update existing mapping
Понякога се налага да се промени мапингът на даден индекс. Но това не винаги е възможно поради причини като например, че вече може да има индексирани документи с текущият мапинг, и ако го променим, ше трябва да реиндексираме въпросните документи.
Специално за добавяне на нови полета към мапинга проблеми няма, но не и за редактиране или премахване на съществуващи.
Почти винаги промяна в мапинга значи реиндексиране, затова Elasticsearch е доста неохотлив за такива промени в мапинга.
Какво може да се направи? Създавате нов индекс с нов мапинг и прехвърляне на данните там.
Казахме, че промяна на мапинга на индекс обикновено води до нуждата от реиндексиране на данните в този индекс.
Първата стъпка е да създадем индеск-копие на стария, като промените в мапинга ги зададем там, и например със скрипт да вземем и налеем наново данните в новия индекс. Добре, но идеята със скриптна не е добра, защото може да отнеме много време при голям обем от данни.
Затова Elasticsearch има специално API – Reindex API.
В този случай „distance“ е оригиналът, към който сочи „route_length_miles“.
Multi-field mapping
Едно поле може да има повече от един маппинг, тоест може да има повече от един тип. Например text поле може да се мапне и като keyword поле. Има случаи, в които се налага едно поле да може и да се индексира например (за да го използваме за full-text search), и да можем да го намираме в буквално търсене. Или да използваме т.н. aggregations, които могат да вървят само с keyword тип.
Ето как се налага да имаме два мапинга за едно поле.
Index patterns
Идеята е да дефинираме предварително шаблвклюон за създаване на индекси, включващ настройките и мапинга, за да можем по-лесно и автоматизирано да създаваме подобни като структура индекси.
Един пример – често се създават индекси, които съхраняват например различна лог информация и са много на брой но се различават по име, което име е подобно, но завърша на датата. Например когато тази лог информация е по дни.
Как тогава, като имаме дефиниран индекс темплейт, да го използваме при създаване на нов индекс, демек, как да кажем на индекса да използва този даден темплейт?
Просто при създаване на индекс, като зададем името на индекса да съвпада с името на темплейта или на шаблон/шаблони от темплейта, Elasticsearch сам ще разбере кой темплейт да използва. По името.
Тоест, опитаме ли да създадем индекс с име: access-logs-2020-06-21 Elasticsearch автоматично ще използва горният темплейт.
Интересно, ако например при създаване на индекса, зададем допълнителни настройки. Тогава тези, които са нови, тоест ги няма в темплейта, ще бъдат използвани, а тези, които се препокриват – ще използва тези, зададени от заявката за създаване, т.е. те ще презапишат тези от темплейта.
Индекс темплейти може да бъдат редактирани, просто се използва същата заявка. Както и
PUT /_template/access-logs
GET /_template/access-logs
DELETE /_template/access-logs
Elastic Common Schema (ECS)
Набор от често използвани полета и от кой тип са. Защо е нужно да има такива полета?
Нека имаме ситуация, в която различни web сървъри като Apache и Nginx пълнят своите логове в Elasticsearch. Също и различни DB сървъри и т.н… Всеки от тях изпраща освен другата информация и поле за timestamp.
ECS казва, че такова поле трбва да се казва @timestamp, независимо от източника.
Освен за timestamp, ECS задава общ стандарт и за други полета като IP, geolocations, operating system…
Идеята е да добави общи полета, не за замести такива от източника, тоест, оригиналът пак трябва да ги запазим 1 към 1, но отделно да имаме и такива общи полета.
Dynamic mapping
Освен че може ние изрично да зададем мапинга на индекса, също е и възможно да оставим Elasticsearch сам да определи същия, според това, какви са типовете данни на първията запазен документ.
The first time Elasticsearch meets a field it will automatically create a field mapping for it.
Elasticsearch има своя специфика, когато сам прави mapping, нека погледнем следното изображение:
Полето „created_at“ е разпознато като тип date с помощта на т.н. „date detection“.
Полето „in_stock“ е било разпознато като long, а не int, защото няма как предварително да се знае колко големи числа ще запазваме в него. Просто Elasticsearch се е застраховал.
За полето „tags“ Elasticsearch разпознава цялото като „text“, което значи, че може да участва в fulltextsearch, a отделните стойности – като „keyword“, което значи, че могат да се използват за exact matches, aggregation и сортиране.
Важно е да се знае, че dynamic mapping не е добра идея, защото Elasticsearch няма да направи mapping, добър откъм performance, а по-скоро откъм универсалност.
Eто някои прости правила, по които Elasticsearch прави mapping:
Ако срещне стрингова стойност – мапва я като text, ако може да я разпознае като дата – като date, ако може да я разпознае като число – като float или long.
Integer стойностите – като long.
Floating point values – като float и т.н…
Objects are mapped to objects но понеже такъв тъп няма, това го прииемаме като загатнат тип.
За масивите – както знаем в Elasticsearch една стойност може да е както скаларна, така и съдържаща повече от една стойност, подобно на натурален масив. Затова масивите нямат специален тип за mapping, просто отделните им стойности се мапват поотделно като независими стойности.
Комбиниране на изрично и на динамично мапване
Основната идея е, че и двете могат добре да се допълват.
Конфигуриране на динамичното мапване
Можем да деактивираме динамичното мапване и тогава, ако не зададем изрично такова за дадено поле, което запазваме, то няма да бъде индексирано, а само ще бъде запазено в _source обекта. Демек, ще можем да го извличаме но не и да търсим по него. Все пак, ако нямаме мапинг за едно поле, то не може да бъде индексирано.
Ако имаме динамичният мапинг активиран, тогава първо ще създаде мапинг за такова поле, и тогава ще го индексира.
Следователно, щом нямаме активен динамик мапинг, новите полета трябва изрично да им се задава мапинг ако искаме да бъдат индексирани.
Ако динмаик маппинг е зададен не true или false, а „strict“?
Toгава казваме на Elasticsearch изрично да не запазва документи с полета без мапинг. Демек, ако опитаме да запазим горният документ, ще получим грешка от рода на : „mapping set to strict, dynamic introduction of [field_name] within [_doc] is not allowed“.
Това се прави по принцип за да имаме пълен контрол върху мапинга на един индекс.
numeric_detection: true – тази настройка казва, че когато поле има стойност стринг приличащ на число („234“, „45.3“…) то ще бъде мапнато като int или float.
Dynamic mapping
Друг начин за конфигуриране на динамичният мапинг. Най-просто – това са набор от условия и за всяко – набор от мапинги за полета, когато това условие е удовлетворено.
В примерът на снимката създаваме нов индекс dynamic_template_test, към който може да добавяме много mapping templates с полето dynamic_templates. Например един такъв темплейт кръщаваме integers или както искаме.
С match_mapping_type задаваме кога този темплейт ще се прилага – в случая ни интересуват дадения JSON тип – цели числа (long) и видим ли такива, ще ги мапваме като integer.
Tаблицата долу-дясно показва с примери каква да е стойността на match_mapping_type при съответният пример.
Mоже би ви е интересно защо long, а не integer? Защото long събира по-големи числа и са предпочели един вид… по-общият случай. Since we cannot distinguish between an integer and a long and as a result the wider datatype will always be chosen.
По-горе казахме, че имаме набор от условия, за които даденият темплейт ще се прилага. Има доста параметри, които да задават такива условия.
match – задава шаблон за името на полето, за което да се прилага даденият темплейт
unmatch – задава шаблон за това за кои полета от вече съвпадналите с match да не се прилага темплета.
На снимката се вижда как да използваме първият темплейт за полета с имена, започващи с text_ но не завършващи с _keyword, a вторият темплейт за такива с имена завършващи с _keyword.
Нещо подобно можем да постигнем и с параметрите path_match и path_unmatch. Te търсят по целят field path т.е. не само името на полето, а и пътят (разделен с точки) до него.
Stemming & stop words
Stemming е превръщането на производна дума в нейният оригинал, напр. „ябълките“ в „ябълка“.
Stop words са думи, които не биват вземани под внимание когато се прави анализ на текста. Напр. предлози, съюзи…
standard – разбива текста на думи и маха пункуацията, прави думите lowercase, премахва stop words.
This is Peter’s cute-looking dog? [„this“, „is“, „peter’s“, „cute“, „looking“, „dog“]
simple – почти същият като standard, само дето сплитва не само на думи, а на всичко, което не е буква. В долният пример, освен на думи, ще сплитне и по апострофа.
This is Peter’s cute-looking dog? [„this“, „is“, „peter“, „s“, „cute“, „looking“, „dog“]
whitespace – сплитва само по интервали, не обръща в lowercase.
This is Peter’s cute-looking dog? [„This“, „is“, „Peter’s“, „cute-looking“, „dog“]
keyword – тъй наречен No-op analyzer, защото не променя нищо по подаденият текст и просто го втъща като единичен токен.
This is Peter’s cute-looking dog? [„This is Peter’s cute-looking dog?“]
pattern – дава възможност ние да си напишем regular expression patterns за да дефинираме как да обработи текста. По подразбиране шаблонът е \W+ – всички non word characters – такива, които не са буква, цифра или подчертавка.
По подразбиране обръща в lowercase но това може и да се забрани.
Освен тези основни има и доста language analyzers.
Отделно, build-in анализаторите могат и да се конфигурират. Например:
Така може да зададем на standard анализатора да премахва stop words, нещо което той не прави по принцип. Всъщност, не променяме standard, а създаваме нов, който го разширява.
Eager Loading може да се обясни най-просто с „Дай ми всичко за Иван Иванов, не само основната информация от таблицата users, но и всичките му поръчки от таблицата user_orders“.
Lazy Loading e все едно „Дай ми основната информация за Иван Иванов oт таблицата users, по нея аз когато ми трябва ще се извлека и останалата информация за Иван Иванов от таблицата user_orders“.
Имаш например две parent-child таблици. Например cars и models.
В cars ще имаме BMW, Opel, Mercedes, WV….
В models ще имаме моделите, закачени за всяка марка. Например за Опел ще имаме Кадет, Вектра, Омега и т.н…. За WV ще имаме Golf, Passat… и т.н….
Проблемът N + 1 се състои в това, че за да изциклим всички коли с техните модели, трябва да изпълним заявка за Cars, и за всеки от резултатите и да изпълним по една заявка за даден модел.
N e броят на итерациите на резултсета (броят модели).
1-то е главната, която е нужна, за да вземем тези N резултата (модели).
Струва ми се, че този проблем може да се реши с една заявка с LEFT JOIN.
Referential Integrity е термин от теорията на БД, и може просто да се обясни така: Знаем, че от теорията на БД когато имаме релационни отношения между две или повече таблици, това значи, че на даден ред от едната таблица, трябва да съответства един или повече записа от друга или други таблици.
Следвайки тази, нека я наречем „философия“ или правило, значи, че можем да разпределим данните с които работим, по N на брой таблици, като програмно пресъздадем логиката във формата на релации (нормализиране).
Referential Integrity е това, към което образно казано, се стремим. То е тази „цялостност“, която означава правилното разпределение на данните по отделни релации (таблици), който гарантира и осигурява, че няма да има загуба на данни и нарушаване на цялостната логика.
Referential Integrity е това, за което отговярят програмно foreign keys във всяка БД и също реализира т.н. „Database normalization“.