Обзор на Elasticsearch?
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
https://www.udemy.com/course/elasticsearch-complete-guide/learn/lecture/11704736#overview
Обзор на начините за инсталиране
Компанията разработчик на Elastic Satck – Elastic NV, предлага безплатен trial период, подходящ за учебни цели, за използване на Elastic Stack като cloud service.
https://www.udemy.com/course/elasticsearch-complete-guide/learn/lecture/16287928#overview
За МакОС/Линукс – смъква се един *.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" : "people", "_type" : "_doc", "_id" : "123", "_score" : 1.0, "_source" : { "first_name" : "John", "last_name" : "Doe", "country_name" : "Nepal" } }
Всеки документ се намира в т.н. 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
Примерен отговор:
{ "cluster_name" : "872b578bae4e48d0866419204f42f5c2", "status" : "green", "timed_out" : false, "number_of_nodes" : 9, "number_of_data_nodes" : 3, "active_primary_shards" : 685, "active_shards" : 1370, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 }
За да видим 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 инстанция, като зададем в командния ред съответните настройки:
- cluster.name
- node.name
- path.data
- path.logs
$ bin/elasticsearch -Enode.name=node-3 -Epath.data=… -Epath.logs=…
Snapshots
Това е механизъм, аналогичен на 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 обект.
PUT /indx_edno
{
settings: {
"number_of_shards" : 2,
"number_of_replicas" : 2
}
}
Добавяне на документ в индекс
POST /indx_edno/_doc
{
....
}
Примерен резултат:
Защо обаче документът е запазен в 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“ слагаме самият скрипт, например:
POST /products/_update/101 { "script" : { "source" : "ctx._source.pole_edno--" } }
Тук, „ctx“ (от context) е обект, който в полето си „_source“, съдържа полетата от избраният документ, който ще ъпдейтваме. Аз лично си го обяснявам като аналог на this в някои програмни езици като PHP или JavaScript.
Ако например изпращаме заявка от някакво приложение и трябва да използваме параметри, както примерно мапваме параметри в PHP. Можем и в Elasticsearch да го направим, като използваме полето „params“, което е „key:value“ обект, съдържащ въпросните параметри, които искаме да намапнем. Горната заявка придобива следният вид:
POST /products/_update/101 { "script" : { "source" : "ctx._source.pole_edno -= params.quantity", "params" : { "quantity" : 4 } } }
Ако например искаме в даден случай да не направим нищо, тоест, например да не ъпдейтнем, трябва да „ctx.op = ‘noop'“ ето така:
POST /products/_update/101 { "script" : { "source" : """ if (ctx._source.pole_edno == 0) { ctx.op = 'noop'; } ... """ } }
Забележете, как когато имаме multiline script, го заграждаме с тройни кавички.
Вместо ‘noop’ може да използваме ‘delete’ за да изтрием намереният доцумент при определин случай:
POST /products/_update/101 { "script" : { "source" : """ if (ctx._source.pole_edno == 0) { ctx.op = 'delete'; } ... """ } }
Upserts
Или още, ако търсеният документ съществува – 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 например много документ, избрани по дадено условие?
Синтаксисът е приблизително този:
POST /products/_update_by_query { "script" : { "source" : "ctx._source.pole_edno--" }, "query" : { "match_all" : { ... } } }
Интересно е, че когато стартираме такава заявка, 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.
Delete by query
POST /products/_delete_by_query { "query" : { "match_all" : { ... } } }
И общо взето всичко е горе-долу същото.
Batch processing
До тук говорихме как да извършваме отделни single операции. Но можем ли да извършваме multiple операции?
Това става с помоща на т.н. Bulk API. Tози API използва NDJSON (Newline Delimeted JSON) – стандарт, почти същият като JSON само дето в JSON имаме един обект, в NDJSON имаме много JSON обекти, разграничени с нов ред.
Ето така можем да индексираме нов документ:
POST _bulk { "index" : { "_index" : "test", "_id" : "101" } } { "field1" : "value1", "field2" : "value2" }
С първият ред задаваме действието (action) и мета инфото, с втория – самият документ.
POST _bulk { "index" : { "_index" : "test", "_id" : "202" } } { "field1" : "value1", "field2" : "value2" } { "create" : { "_index" : "test", "_id" : "303" } } { "field11" : "value33", "field44" : "value55" }
Tук първо индексираме документ, след това създаваме документ. Разликата между index и create е, че create командата ще е неуспешна ако такъв документ вече съществува. За index няма значение дали вече съществува. Ако същетвува, ще бъде заменен.
Eто пример за update операция:
POST _bulk { "update" : {"_id" : "101", "_index" : "test"} } { "doc" : {"field2" : "value2", "field222" : "value222"} }
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 операцията е един, а не два реда:
POST _bulk { "delete" : { "_index" : "test", "_id" : "202" } }
Важна отметка!
В горните примери се вижда, че можем да използваме различни индекси, но можем да използваме един и същ индекс, който да зададем като част от 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 като се проверява подадената стойност за даденото поле.
Data types
Основните са: object, , integer, long, double, short, float, date, text, boolean, ip, nested, keywords.
Тип масив по принцип няма, но от друга страна всяка стойност може да се състои от една или повече такива, което на практика е масив. Всички стойности трабва да са от един и същ тип. Също, може да имаме вложени масиви и масиви от обекти.
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, само трябва да се има предвид, че при индексиране такива масиви се „флатват“.
Как ръчно да създадем mapping
Има два варианта на командата:
PUT /twitter/_mapping { "properties": { "first_name" : { type : "text" }, "email": { "type": "keyword" }, "personal_interests" : { "properties" : { "interest_name" : { type : "text" }, "interest_first_year" : { type : "int" } } } } }
PUT /twitter { "mappings" : { "properties": { "first_name" : { type : "text" }, "email": { "type": "keyword" }, "personal_interests" : { "properties" : { "interest_name" : { type : "text" }, "interest_first_year" : { type : "int" } } } } } }
Може и обектите да са „флатнати“ с т.н. „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 е доста неохотлив за такива промени в мапинга.
Какво може да се направи? Създавате нов индекс с нов мапинг и прехвърляне на данните там.
Реиндексиране на документи
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html
Казахме, че промяна на мапинга на индекс обикновено води до нуждата от реиндексиране на данните в този индекс.
Първата стъпка е да създадем индеск-копие на стария, като промените в мапинга ги зададем там, и например със скрипт да вземем и налеем наново данните в новия индекс. Добре, но идеята със скриптна не е добра, защото може да отнеме много време при голям обем от данни.
Затова Elasticsearch има специално API – Reindex API.
POST /_reindex { "source": { "index": "old_twitter" }, "dest": { "index": "new_twitter" } }
Field aliases
Идеята е да имаме например ново поле, което да сочи към вече съществуващо.
PUT trips { "mappings": { "properties": { "route_length_miles": { "type": "alias", "path": "distance" } } } }
В този случай „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 са думи, които не биват вземани под внимание когато се прави анализ на текста. Напр. предлози, съюзи…
Build-in analyzers
https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-analyzers.html
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, а създаваме нов, който го разширява.