{"id":364085,"date":"2024-05-21T02:09:48","date_gmt":"2024-05-21T02:09:48","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=364085"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=364085","title":{"rendered":"<span>\u0420\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0442\u043e\u0432\u0430\u0440\u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043e\u0446\u0435\u043d\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0441 KafkaStreams \u0438 OpenSearch<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442! \u041c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u0410\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440. \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u0433\u043e\u0434 \u0442\u0440\u0443\u0436\u0443\u0441\u044c \u0444\u0440\u0438\u043b\u0430\u043d\u0441\u0435\u0440\u043e\u043c \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043f\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430. \u041c\u043e\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u043c\u0438\u043c\u043e \u0432\u0441\u0435\u0433\u043e \u043f\u0440\u043e\u0447\u0435\u0433\u043e \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442 \u0432 \u0441\u0435\u0431\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0441\u043a\u043e\u0440\u0438\u043d\u0433\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u044b \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u043b\u0430 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432 \u043f\u043e \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0441\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0435\u0439. \u041e\u0434\u043d\u0438\u043c \u0438\u0437 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0435\u0439 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0446\u0435\u043d\u043a\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043f\u043e \u0448\u043a\u0430\u043b\u0435 \u043e\u0442 1 \u0434\u043e 5. \u0414\u0443\u043c\u0430\u044e, \u0447\u0442\u043e \u043d\u0435 \u0441\u0442\u0430\u043d\u0435\u0442 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0435\u043c \u0410\u043c\u0435\u0440\u0438\u043a\u0438 \u0442\u043e, \u0447\u0442\u043e \u0440\u0430\u0441\u0447\u0435\u0442 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u043f\u043e \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u0430\u0440\u0438\u0444\u043c\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c\u0443 \u0432\u0441\u0435\u0445 \u043e\u0446\u0435\u043d\u043e\u043a \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043d\u0435 \u0441\u043e\u0432\u0441\u0435\u043c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043c \u0432\u044b\u0431\u043e\u0440\u043a\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0442\u0430\u043a\u043e\u043c \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u0442\u043e\u0432\u0430\u0440 &#171;\u0410&#187; \u0441 \u043e\u0434\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u043e\u0439 5 \u0432 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0448\u0435 \u0442\u043e\u0432\u0430\u0440\u0430 &#171;\u0411&#187;, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u043e\u0442\u043d\u044f \u043e\u0446\u0435\u043d\u043e\u043a 5 \u0438 \u043e\u0434\u043d\u0430 \u043e\u0446\u0435\u043d\u043a\u0430 4. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0434\u0430\u0432\u043d\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043e  &#8212; \u0434\u043b\u044f \u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u0434\u043e\u0432\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0431\u0438\u043d\u043e\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u00a0\u043c\u0435\u0442\u043e\u0434\u0443 \u0423\u0438\u043b\u0441\u043e\u043d\u0430 (Wilson Score Confidence Interval). \u0424\u043e\u0440\u043c\u0443\u043b\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/980\/576\/9e9\/9805769e9a0b5eb9a1f5491d77909c56.png\" alt=\"\u0424\u043e\u0440\u043c\u0443\u043b\u0430 Wilson Score Confidence Interval\" title=\"\u0424\u043e\u0440\u043c\u0443\u043b\u0430 Wilson Score Confidence Interval\" width=\"1570\" height=\"352\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/980\/576\/9e9\/9805769e9a0b5eb9a1f5491d77909c56.png\"\/><\/p>\n<div><figcaption>\u0424\u043e\u0440\u043c\u0443\u043b\u0430 Wilson Score Confidence Interval<\/figcaption><\/div>\n<\/figure>\n<p>\u0413\u0434\u0435:<\/p>\n<ol>\n<li>\n<p>p &#8212;  \u044d\u0442\u043e \u043e\u0446\u0435\u043d\u043a\u0430 \u043f\u0440\u043e\u043f\u043e\u0440\u0446\u0438\u0438 \u0443\u0441\u043f\u0435\u0445\u043e\u0432 \u0432 \u0432\u044b\u0431\u043e\u0440\u043a\u0435 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u043d\u0430 \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0442\u0437\u044b\u0432\u043e\u0432).<\/p>\n<\/li>\n<li>\n<p>n &#8212; \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0439 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0442\u0437\u044b\u0432\u043e\u0432).<\/p>\n<\/li>\n<li>\n<p>z &#8212; Z-\u0441\u0447\u0435\u0442, \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u0443\u0440\u043e\u0432\u043d\u044e \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0434\u043b\u044f \u0443\u0440\u043e\u0432\u043d\u044f \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438 95%, z \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0440\u0430\u0432\u043d\u043e 1.96.<\/p>\n<\/li>\n<\/ol>\n<p>\u0412 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u044e\u0441\u044c \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u0438\u043d \u0438\u0437 \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u0432 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u043a \u0440\u0430\u0441\u0447\u0435\u0442\u0443 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0438 \u043a\u0430\u043a Kafka Streams \u0438 OpenSearch. \u041a\u0430\u043a \u0438 \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u043f\u043e\u0441\u0442\u0430\u0445, \u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u043b \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 <a href=\"https:\/\/github.com\/aasmc\/product-rating-system\" rel=\"noopener noreferrer nofollow\">\u0434\u0435\u043c\u043e \u043f\u0440\u043e\u0435\u043a\u0442<\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b\u043b\u043e\u0436\u0438\u043b \u043d\u0430 Github. <\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u043e\u0438\u0441\u043a\u043e\u0432\u043e\u0433\u043e \u0434\u0432\u0438\u0436\u043a\u0430 \u044f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0432\u044b\u0431\u0440\u0430\u043b OpenSearch, \u0430 \u043d\u0435 ElasticSearch, \u0442\u0430\u043a \u043a\u0430\u043a \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0433\u0434\u0435 \u044f \u0441\u0435\u0439\u0447\u0430\u0441 \u0442\u0440\u0443\u0436\u0443\u0441\u044c. \u041a\u0440\u043e\u043c\u0435 \u0442\u043e\u0433\u043e, \u043d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 20 \u0438\u044e\u043b\u044f 2023 \u0433\u043e\u0434\u0430 Yandex Cloud <a href=\"https:\/\/cloud.yandex.com\/en\/services\/managed-elasticsearch\" rel=\"noopener noreferrer nofollow\">\u0431\u043e\u043b\u0435\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442<\/a> \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u0432\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u044b ElasticSearch, \u0430 \u0441 11 \u0430\u043f\u0440\u0435\u043b\u044f 2024 \u0433\u043e\u0434\u0430 \u0438 \u0432\u043e\u0432\u0441\u0435 \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u0442 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0443\u0441\u043b\u0443\u0433\u0438 \u043f\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043a ElasticSearch. <\/p>\n<h3>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u041f\u0440\u043e\u0435\u043a\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 2-\u0445 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0445 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432:<\/p>\n<ol>\n<li>\n<p>backend-service<\/p>\n<\/li>\n<li>\n<p>review-aggregator-service<\/p>\n<\/li>\n<\/ol>\n<p>\u0410 \u0442\u0430\u043a\u0436\u0435 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0443\u043a\u0440\u0443\u0442\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p> PostgreSQL<\/p>\n<\/li>\n<li>\n<p>Apache Kafka + Zookeeper<\/p>\n<\/li>\n<li>\n<p>Confluent Schema Registry<\/p>\n<\/li>\n<li>\n<p>Kafka Connect<\/p>\n<\/li>\n<li>\n<p>Debezium Postgres Connector<\/p>\n<\/li>\n<li>\n<p>\u041a\u043b\u0430\u0441\u0442\u0435\u0440 OpenSearch \u0438\u0437 \u0434\u0432\u0443\u0445 \u043d\u043e\u0434<\/p>\n<\/li>\n<\/ol>\n<h3>Backend Service<\/h3>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 REST API \u0434\u043b\u044f:<\/p>\n<ul>\n<li>\n<p>\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/p>\n<\/li>\n<li>\n<p>\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u0443\u0447\u0435\u043d\u0438\u0435 \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438: <code>AppUser<\/code>, <code>Product<\/code>, <code>ProductReview<\/code> \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 Postgres. \u0421\u0445\u0435\u043c\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 Postgres \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"sql\">CREATE TABLE IF NOT EXISTS app_users(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     USERNAME TEXT NOT NULL,     FIRST_NAME TEXT NOT NULL,     LAST_NAME TEXT NOT NULL );  CREATE TABLE IF NOT EXISTS products(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     NAME TEXT NOT NULL,     DESCRIPTION TEXT,     PRICE NUMERIC(20, 5) NOT NULL );  CREATE TABLE IF NOT EXISTS product_reviews(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     USER_ID BIGINT NOT NULL,     PRODUCT_ID BIGINT NOT NULL,     RATING INTEGER NOT NULL,     CREATED_AT TIMESTAMP NOT NULL,     COMMENT TEXT,     CONSTRAINT product_reviews_app_users_fk FOREIGN KEY (USER_ID) REFERENCES app_users(ID) ON DELETE NO ACTION,     CONSTRAINT product_reviews_products_fk FOREIGN KEY (PRODUCT_ID) REFERENCES products(ID) ON DELETE NO ACTION );  CREATE UNIQUE INDEX IF NOT EXISTS product_reviews_user_id_product_id_idx ON product_reviews(USER_ID, PRODUCT_ID);<\/code><\/pre>\n<p>\u0422\u0430\u0431\u043b\u0438\u0446\u0430 <code>product_reviews<\/code>, \u043a\u0443\u0434\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431 \u043e\u0442\u0437\u044b\u0432\u0430\u0445 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f Debezium Postgres Connector, \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043d\u0435\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 \u0442\u043e\u043f\u0438\u043a Kafka. \u0417\u0430 \u0440\u0430\u0431\u043e\u0442\u0443 \u0441 Kafka \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 <strong>Review Aggregator Service<\/strong>. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0440\u0430\u0431\u043e\u0442\u044b Debezium Postgres Connector \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 <a href=\"https:\/\/habr.com\/ru\/articles\/779620\/\" rel=\"noopener noreferrer nofollow\">\u043c\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a>.<\/p>\n<p><strong>Backend Service<\/strong> \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0442\u0432\u0430\u0440\u043e\u0432 \u0438\u0437 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0430 OpenSearch, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <code>org.opensearch.data.client.orhlc.OpenSearchRestTemplate<\/code>:<\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0430\u043a\u0446\u0435\u043d\u0442 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u043d\u0435 \u043d\u0430 \u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u043c \u043f\u043e\u0438\u0441\u043a\u0435, \u0430 \u043d\u0430 \u0440\u0430\u043d\u0436\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u043e\u0446\u0435\u043d\u043a\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 OpenSearch \u043e\u0447\u0435\u043d\u044c \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u043d\u0430\u044f \u0438 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"kotlin\">@Document(indexName = \"products\") class OpenSearchProduct(     @Id     var id: String? = null,     @Field(type = FieldType.Text, analyzer = \"russian\")     var name: String? = null,     @Field(type = FieldType.Text, analyzer = \"russian\")     var description: String? = null,     @Field(type = FieldType.Double)     var price: BigDecimal? = null,     @Field(name = \"wilson-score\", type = FieldType.Double)     var wilsonScore: Double = 0.0,     @Field(type = FieldType.Object)     var ratings: Map&lt;String, Int> = hashMapOf(         \"1\" to 0,         \"2\" to 0,         \"3\" to 0,         \"4\" to 0,         \"5\" to 0,     ),     @Field(type = FieldType.Long, name = \"rating_update_idempotency_key\")     var ratingIdempotencyKey: Long? = null )<\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0441\u0442\u043e\u0438\u0442 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p><code>ratings<\/code> \u0438\u043c\u0435\u0435\u0442 \u0442\u0430\u043a\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u043a\u0430\u0436\u0434\u0443\u044e \u043e\u0446\u0435\u043d\u043a\u0443 \u043f\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u0441\u0447\u0435\u0442\u0430 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430. <\/p>\n<\/li>\n<li>\n<p> <code>wilson-score<\/code> \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c \u0447\u0435\u0440\u0435\u0437 <a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/reference\/8.11\/docs-update.html#_scripted_updates\" rel=\"noopener noreferrer nofollow\">scripted updates<\/a> <br \/>\u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430, \u0447\u0442\u043e \u0434\u0430\u0435\u0442 \u043d\u0430\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u043a\u043e\u0440\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0438 \u0443\u0436\u0435 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u043e. <\/p>\n<\/li>\n<li>\n<p><code>ratingIdempotencyKey<\/code> \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0438\u0437-\u0437\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0445 \u0441\u0431\u043e\u0435\u0432 \u0432 \u0440\u0430\u0431\u043e\u0442\u0435 Kafka, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u0440\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u043c\u0438 Kafka \u043d\u0435 \u0434\u0430\u0435\u0442 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438 <code>exactly_once<\/code>, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e <code>at_least_once<\/code>.<\/p>\n<\/li>\n<\/ol>\n<h3>Review Aggregator Service<\/h3>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0433\u043e\u0432\u043e\u0440\u044f, \u0438\u043c\u0435\u043d\u043d\u043e \u044d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0438\u0441 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044e \u0434\u0430\u043d\u043d\u044b\u0445 \u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043e\u0442\u0437\u044b\u0432\u0430\u0445 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0435 OpenSearch.<\/p>\n<p>\u0410\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044f \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Kafka Streams. \u0414\u0430\u043b\u0435\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0443 \u043a\u043e\u0434 \u0441 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u043c\u0438 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u044f\u043c\u0438, \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430 Kafka Streams \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043d\u0430 \u0432 \u043c\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 <a href=\"https:\/\/habr.com\/ru\/articles\/775900\/\" rel=\"noopener noreferrer nofollow\">&#171;\u041c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0441 Kafka Streams \u0438 Spring Boot&#187;<\/a>.<\/p>\n<pre><code class=\"kotlin\">@Service class KafkaReviewAggregatorService(     private val topicProps: TopicProps,     private val kafkaProps: KafkaProps ) {      @Autowired     fun processStreams(builder: StreamsBuilder) {         val reviewValueSerde = SpecificAvroSerde&lt;AvroReview>()         configureSerde(reviewValueSerde, false)         val reviewKeySerde = Serdes.String()          val productRatingValueSerde = SpecificAvroSerde&lt;AvroProductRating>()         configureSerde(productRatingValueSerde, false)         val productRatingKeySerde = Serdes.String()          val productReviews: KStream&lt;String, AvroReview> = builder.stream(             topicProps.reviewTopic,             Consumed.with(reviewKeySerde, reviewValueSerde)         )          val aggregatedStore = Stores.keyValueStoreBuilder(             Stores.persistentKeyValueStore(topicProps.aggregatedReviewsStore),             Serdes.String(),             productRatingValueSerde         )          builder.addStateStore(aggregatedStore)          productReviews             .peek { key, value ->                 log.info(\"Processing AvroReview: {}. Message Key: {}\", value, key)             }             .mapValues(::toAvroProductRating)             .process(                 ProcessorSupplier {                     AvroReviewPunctuator(topicProps)                 },                 topicProps.aggregatedReviewsStore             )             .peek { key, value ->                 log.info(\"Processing AvroProductRating: {}. Message Key: {}\", value, key)             }             .to(                 topicProps.aggregatedReviewsTopic,                 Produced.with(productRatingKeySerde, productRatingValueSerde)             )     }       private fun configureSerde(serde: Serde&lt;*>, isKey: Boolean) {         if (serde is SpecificAvroSerde) {             val config = hashMapOf&lt;String, Any>(                 AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to kafkaProps.schemaRegistryUrl             )             serde.configure(config, isKey)         }     }       private fun toAvroProductRating(record: AvroReview): AvroProductRating =         AvroProductRating(             record.productId,             hashMapOf(                 convertDigitToString(record.rating) to 1             ),             record.`lsn$1`         )      private fun convertDigitToString(digit: Int): String {         return when(digit) {             1 -> \"one\"             2 -> \"two\"             3 -> \"three\"             4 -> \"four\"             5 -> \"five\"             else -> throw RuntimeException(\"Unsupported Rating: $digit\")         }     } }<\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0441\u0435 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e:<\/p>\n<ol>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c KStream \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0442\u043e\u043f\u0438\u043a\u0430, \u043a\u0443\u0434\u0430 \u043e\u0442\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431 \u043e\u0442\u0437\u044b\u0432\u0430\u0445<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  val productReviews: KStream&lt;String, AvroReview> = builder.stream(       topicProps.reviewTopic,       Consumed.with(reviewKeySerde, reviewValueSerde)   )<\/code><\/pre>\n<ol start=\"2\">\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c KeyValue \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  val aggregatedStore = Stores.keyValueStoreBuilder(       Stores.persistentKeyValueStore(topicProps.aggregatedReviewsStore),       Serdes.String(),       productRatingValueSerde   )    builder.addStateStore(aggregatedStore)<\/code><\/pre>\n<ol start=\"3\">\n<li>\n<p>\u041c\u0430\u043f\u0438\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u0441\u0442\u0440\u0438\u043c\u0435 \u0432 \u0443\u0434\u043e\u0431\u043d\u0443\u044e \u0434\u043b\u044f \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  productReviews       .peek { key, value ->           log.info(\"Processing AvroReview: {}. Message Key: {}\", value, key)       }       .mapValues(::toAvroProductRating)<\/code><\/pre>\n<p> <code>AvroProductRating<\/code> \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 .avsc \u0444\u0430\u0439\u043b\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Gradle \u043f\u043b\u0430\u0433\u0438\u043d\u0430:<\/p>\n<pre><code class=\"json\">{   \"type\": \"record\",   \"name\": \"AvroProductRating\",   \"namespace\": \"ru.aasmc.avro\",   \"fields\": [     {       \"name\": \"productId\",       \"type\": \"long\"     },     {       \"name\": \"ratings\",       \"type\": {         \"type\": \"map\",         \"values\": \"int\"       }     },     {       \"name\": \"idempotencyKey\",       \"type\": [         \"null\",         \"long\"       ],       \"default\": null     }   ] }<\/code><\/pre>\n<ol start=\"4\">\n<li>\n<p>\u0410\u0433\u0440\u0435\u0433\u0438\u0440\u0443\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u043e\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0435 (\u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u043e \u0442\u0435\u043a\u0441\u0442\u0443):<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">.process(     ProcessorSupplier {         AvroReviewPunctuator(topicProps)      },     topicProps.aggregatedReviewsStore )<\/code><\/pre>\n<ol start=\"5\">\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043f\u0438\u043a Kafka:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">.to(     topicProps.aggregatedReviewsTopic,     Produced.with(productRatingKeySerde, productRatingValueSerde) )<\/code><\/pre>\n<p>\u041f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 Kafka Streams \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u044b \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0435 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u043e\u043c, \u043e\u0434\u043d\u0430\u043a\u043e \u0442\u0443\u0442 \u0435\u0441\u0442\u044c \u043e\u0434\u0438\u043d \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043d\u044e\u0430\u043d\u0441\u043e\u0432:<\/p>\n<ul>\n<li>\n<p>\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0438\u0440\u0443\u044e\u0449\u0438\u0439 \u0442\u043e\u043f\u0438\u043a \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043f\u0430\u0434\u0430\u0442\u044c \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u0438 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0430 \u0432 \u043d\u0430\u0448\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u044d\u0442\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u043a\u043b\u0430\u0441\u0442\u0435\u0440 OpenSearch, \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u043c\u044b \u043f\u043e\u0437\u0430\u0431\u043e\u0442\u0438\u043c\u0441\u044f \u043e \u0434\u0435\u0434\u0443\u043f\u043b\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0438 <a href=\"https:\/\/developer.confluent.io\/patterns\/stream-processing\/suppressed-event-aggregator\/\" rel=\"noopener noreferrer nofollow\">\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430<\/a> <code>suppress<\/code>,  \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0433\u043e\u0432\u043e\u0440\u0438\u0442 Kafka Streams \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0430\u0433\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0438\u0440\u0443\u044e\u0449\u0438\u0439 \u0442\u043e\u043f\u0438\u043a, \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u043f\u0440\u0438 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u0438 \u043e\u043a\u043d\u0430 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438, \u0430 \u043e\u043d\u043e \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u043e\u043a\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u044b\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439. \u042d\u0442\u043e \u043f\u043e\u0442\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u043e\u0439. \u041f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u043c \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439, \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0442\u043e\u0432\u0430\u0440\u0430 \u0432 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0435 OpenSearch \u043a\u0430\u0436\u0434\u044b\u0435 30 \u043c\u0438\u043d\u0443\u0442. \u0421\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u043e\u043a\u043d\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0432 30 \u043c\u0438\u043d\u0443\u0442. \u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0442\u043e\u0432\u0430\u0440\u0443 \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u043b\u0438 10 \u043e\u0442\u0437\u044b\u0432\u043e\u0432. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u0441\u0443\u0442\u043e\u043a \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438, \u0430 \u0447\u0435\u0440\u0435\u0437 24 \u0447\u0430\u0441\u0430 \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u043b\u0438 \u0435\u0449\u0435 5 \u043e\u0446\u0435\u043d\u043e\u043a. \u041f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u0430 <code>suppress<\/code> \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0438\u0440\u0443\u044e\u0449\u0438\u0439 \u0442\u043e\u043f\u0438\u043a \u043f\u043e\u043f\u0430\u0434\u0443\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 24 \u0447\u0430\u0441\u0430, \u0430 \u043d\u0435 \u0447\u0435\u0440\u0435\u0437 30 \u043c\u0438\u043d\u0443\u0442, \u043a\u0430\u043a \u043c\u044b \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0442\u0435\u043b\u0438, \u0438\u0437-\u0437\u0430 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u043a\u043d\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u0440\u043e\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u043e\u043a\u043d\u0430. \u0427\u0442\u043e\u0431\u044b \u0440\u0435\u0448\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u0438\u0441\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u043b\u043e\u0433\u0438\u043a\u0443 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0438 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/kafka.apache.org\/10\/documentation\/streams\/developer-guide\/processor-api.html\" rel=\"noopener noreferrer nofollow\">Processor API<\/a>. <\/p>\n<\/li>\n<\/ul>\n<p>\u041a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"kotlin\">class AvroReviewPunctuator(     private val topicProps: TopicProps ): Processor&lt;String, AvroProductRating, String, AvroProductRating> {      private lateinit var context: ProcessorContext&lt;String, AvroProductRating>     private lateinit var store: KeyValueStore&lt;String, AvroProductRating>      override fun init(context: ProcessorContext&lt;String, AvroProductRating>) {         this.context = context         this.store = context.getStateStore(topicProps.aggregatedReviewsStore) as KeyValueStore&lt;String, AvroProductRating>         context.schedule(topicProps.punctuationInterval, PunctuationType.WALL_CLOCK_TIME, this::punctuate)     }       private fun punctuate(to: Long) {         log.debug(\"Enter punctuate method.\")         store.all().forEachRemaining { entry ->             log.info(\"Sending new aggregated value from punctuate: {}\", entry.value)             context.forward(Record(entry.key, entry.value, to))             store.delete(entry.key)         }     }      override fun process(record: Record&lt;String, AvroProductRating>) {         val key = record.key()         val newRating = record.value()         log.debug(\"Processing record with value: {}\", newRating)         val currentAggregatedRating = store.get(key)         if (currentAggregatedRating != null) {             val currentRatings = currentAggregatedRating.ratings             newRating.ratings.forEach { (ratingKey, ratingValue) ->                 currentRatings.merge(ratingKey, ratingValue, Int::plus)             }             val idempotencyKey = maxOf(newRating.idempotencyKey, currentAggregatedRating.idempotencyKey)             store.put(key, AvroProductRating(newRating.productId, currentRatings, idempotencyKey))         } else {             store.put(key, newRating)         }     } }<\/code><\/pre>\n<ol>\n<li>\n<p>\u041c\u0435\u0442\u043e\u0434 <code>init<\/code> \u043e\u0442\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0430. \u0422\u0443\u0442 \u043c\u044b \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u0440\u0430\u043d\u0435\u0435 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0435 \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0435 \u0432 \u0442\u043e\u043f\u043e\u043b\u043e\u0433\u0438\u044e KeyValue \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">this.store = context.getStateStore(topicProps.aggregatedReviewsStore) as KeyValueStore&lt;String, AvroProductRating><\/code><\/pre>\n<ol start=\"2\">\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c \u043f\u043b\u0430\u043d\u0438\u0440\u043e\u0432\u0449\u0438\u043a, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0442\u044c 1 \u0440\u0430\u0437 \u0437\u0430 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0440\u0435\u043c\u0435\u043d\u0438 <code>topicProps.punctuationInterval<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u044b \u0445\u0440\u0430\u043d\u0438\u043c \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">context.schedule(topicProps.punctuationInterval, PunctuationType.WALL_CLOCK_TIME, this::punctuate)<\/code><\/pre>\n<ol start=\"3\">\n<li>\n<p>\u0412 \u043c\u0435\u0442\u043e\u0434\u0435 <code>punctuate<\/code>, \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043b\u043e\u0433\u0438\u043a\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 \u043d\u0438\u0436\u0435 \u043f\u043e \u0441\u0442\u0440\u0438\u043c\u0443, \u0438 \u043e\u0447\u0438\u0441\u0442\u043a\u0435 \u0445\u0440\u0430\u043d\u044f\u0449\u0438\u0445\u0441\u044f \u0432 KeyValue \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435 \u0434\u0430\u043d\u043d\u044b\u0445:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  private fun punctuate(to: Long) {       log.debug(\"Enter punctuate method.\")       store.all().forEachRemaining { entry ->           log.info(\"Sending new aggregated value from punctuate: {}\", entry.value)           context.forward(Record(entry.key, entry.value, to))           store.delete(entry.key)       }   }<\/code><\/pre>\n<ol start=\"4\">\n<li>\n<p>\u041c\u0435\u0442\u043e\u0434 <code>process(record: Record&lt;String, AvroProductRating>)<\/code> \u043e\u0442\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u043a\u0430\u0436\u0434\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0438\u0437 \u0432\u044b\u0448\u0435\u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0441\u0442\u0440\u0438\u043c\u0430. \u0422\u0443\u0442 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 KeyValue \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430, \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0441\u043b\u0438\u044f\u043d\u0438\u0435 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u043e\u0432  \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u0430\u043f\u0438\u0441\u044c \u0432 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435. \u0421\u0442\u043e\u0438\u0442 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 <code>idempotencyKey<\/code> \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f LSN (Log Sequence Number) \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0439 Debezium Postgres Connector \u0432 Kafka \u043f\u0440\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <code>product_reviews<\/code>. \u0422\u0430\u043a \u043a\u0430\u043a LSN &#8212; \u043c\u043e\u043d\u043e\u0442\u043e\u043d\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u044e\u0449\u0435\u0435\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435, \u0432 \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u0443\u0435\u043c\u043e\u0439 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u043c\u044b \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u043c \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u0442\u0430\u0440\u043e\u0439 \u0438 \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u0412 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0435\u043c LSN \u0431\u0443\u0434\u0435\u0442     \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u0432 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0435 OpenSearch. <\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">override fun process(record: Record&lt;String, AvroProductRating>) {     val key = record.key()     val newRating = record.value()     log.debug(\"Processing record with value: {}\", newRating)     val currentAggregatedRating = store.get(key)     if (currentAggregatedRating != null) {         val currentRatings = currentAggregatedRating.ratings         newRating.ratings.forEach { (ratingKey, ratingValue) ->             currentRatings.merge(ratingKey, ratingValue, Int::plus)         }         val idempotencyKey = maxOf(newRating.idempotencyKey, currentAggregatedRating.idempotencyKey)         store.put(key, AvroProductRating(newRating.productId, currentRatings, idempotencyKey))     } else {         store.put(key, newRating)     } }<\/code><\/pre>\n<h3>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 OpenSearch<\/h3>\n<p>Kafka Consumer \u0432\u044b\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0442\u043e\u043f\u0438\u043a\u0430 \u0441 \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0442\u043e\u0432\u0430\u0440\u0430 \u0432 OpenSearch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e painless \u0441\u043a\u0440\u0438\u043f\u0442\u0430:<\/p>\n<pre><code class=\"kotlin\">private const val SCRIPT_SOURCE = \"\"\"     if (ctx._source['rating_update_idempotency_key'] == null ||      ctx._source['rating_update_idempotency_key'] &lt; params.idempotency_key) {         if (params.containsKey('one')) {             ctx._source.ratings['1'] = (ctx._source.ratings['1'] ?: 0) + params.one;         }         if (params.containsKey('two')) {             ctx._source.ratings['2'] = (ctx._source.ratings['2'] ?: 0) + params.two;         }         if (params.containsKey('three')) {             ctx._source.ratings['3'] = (ctx._source.ratings['3'] ?: 0) + params.three;         }         if (params.containsKey('four')) {             ctx._source.ratings['4'] = (ctx._source.ratings['4'] ?: 0) + params.four;         }         if (params.containsKey('five')) {             ctx._source.ratings['5'] = (ctx._source.ratings['5'] ?: 0) + params.five;         }         long s1 = ctx._source.ratings.containsKey('1') ? ctx._source.ratings['1'] : 0;         long s2 = ctx._source.ratings.containsKey('2') ? ctx._source.ratings['2'] : 0;         long s3 = ctx._source.ratings.containsKey('3') ? ctx._source.ratings['3'] : 0;         long s4 = ctx._source.ratings.containsKey('4') ? ctx._source.ratings['4'] : 0;         long s5 = ctx._source.ratings.containsKey('5') ? ctx._source.ratings['5'] : 0;         double p = (s1 * 0.0) + (s2 * 0.25) + (s3 * 0.5) + (s4 * 0.75) + (s5 * 1.0);         double n = (s1 * 1.0) + (s2 * 0.75) + (s3 * 0.5) + (s4 * 0.25) + (s5 * 0.0);         double wilsonScore = p + n > 0 ? ((p + 1.9208) \/ (p + n) - 1.96 * Math.sqrt((p * n) \/ (p + n) + 0.9604) \/ (p + n)) \/ (1 + 3.8416 \/ (p + n)) : 0;         ctx._source['wilson-score'] = wilsonScore;         ctx._source['rating_update_idempotency_key'] = params.idempotency_key;     } \"\"\"  private const val IDEMPOTENCY_KEY = \"idempotency_key\"  @Service class OpenSearchKafkaConsumer(     private val openSearchRestTemplate: OpenSearchRestTemplate,     private val props: OpenSearchProps ) {      @KafkaListener(topics = [\"\\${topicprops.aggregatedReviewsTopic}\"], concurrency = \"3\")     fun consumeAndSend(record: AvroProductRating) {         updateProductInOpenSearch(record)         log.info(\"Successfully updated rating of product with id = {}\", record.productId)     }      private fun updateProductInOpenSearch(record: AvroProductRating) {         val params = mutableMapOf&lt;String, Any>()         record.ratings.forEach { (key, value) ->             params[key] = value         }         params[IDEMPOTENCY_KEY] = record.idempotencyKey         val updateQuery = UpdateQuery.builder(record.productId.toString())             .withScriptType(ScriptType.INLINE)             .withLang(\"painless\")             .withScript(SCRIPT_SOURCE)             .withParams(params)             .build()         openSearchRestTemplate.update(updateQuery, IndexCoordinates.of(props.productIndex))     } }<\/code><\/pre>\n<p>\u0420\u0430\u0437\u0431\u0435\u0440\u0435\u043c \u0441\u043a\u0440\u0438\u043f\u0442: <\/p>\n<ol>\n<li>\n<p><code>ctx._source<\/code> \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043f\u043e\u043b\u044f\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c\u043e\u0433\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<li>\n<p><code>params<\/code> &#8212; \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0432 \u0441\u043a\u0440\u0438\u043f\u0442<\/p>\n<\/li>\n<li>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 <code>rating_update_idempotency_key<\/code> == <code>null<\/code>&#8212; \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0447\u0442\u043e \u043c\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0432\u043f\u0435\u0440\u0432\u044b\u0435, \u0438\u043b\u0438 \u0436\u0435 <code>lsn<\/code> \u0442\u043e\u0432\u0430\u0440\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043a\u043b\u044e\u0447\u0430 \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u0438, \u043c\u0435\u043d\u044c\u0448\u0435 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043f\u0440\u0438\u0448\u0435\u043b \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445 \u0441\u043a\u0440\u0438\u043f\u0442\u0430, \u0442\u043e \u0435\u0441\u0442\u044c \u043c\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043d\u043e\u0432\u044b\u0445 \u043e\u0446\u0435\u043d\u043e\u043a \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0430 \u043d\u0435 \u0442\u0435\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 Kafka Consumer \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0438\u0437-\u0437\u0430 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043e\u043d \u043f\u043e \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 \u043d\u0435 \u0437\u0430\u043a\u043e\u043c\u0438\u0442\u0438\u043b \u043e\u0444\u0444\u0441\u0435\u0442 \u0438 \u043f\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u043b \u0437\u0430\u043f\u0438\u0441\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u043f\u0440\u0438\u0448\u043b\u043e \u0432 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445. <\/p>\n<\/li>\n<li>\n<p><code>double p = (s1 * 0.0) + (s2 * 0.25) + (s3 * 0.5) + (s4 * 0.75) + (s5 * 1.0);<\/code> \u0420\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0438 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0435\u0433\u043e \u043e\u0442 0 \u0434\u043e 1, \u043e\u0442\u0434\u0430\u0432\u0430\u044f \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u0431\u043e\u043b\u0435\u0435 \u0432\u044b\u0441\u043e\u043a\u0438\u043c \u043e\u0446\u0435\u043d\u043a\u0430\u043c.<\/p>\n<\/li>\n<li>\n<p><code>double n = (s1 * 1.0) + (s2 * 0.75) + (s3 * 0.5) + (s4 * 0.25) + (s5 * 0.0);<\/code> \u0420\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u0438 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0435\u0433\u043e \u043e\u0442 0 \u0434\u043e 1, \u043e\u0442\u0434\u0430\u0432\u0430\u044f \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u0431\u043e\u043b\u0435\u0435 \u043d\u0438\u0437\u043a\u0438\u043c \u043e\u0446\u0435\u043d\u043a\u0430\u043c.<\/p>\n<\/li>\n<li>\n<p><code>double wilsonScore = p + n > 0 ? ((p + 1.9208) \/ (p + n) - 1.96 * Math.sqrt((p * n) \/ (p + n) + 0.9604) \/ (p + n)) \/ (1 + 3.8416 \/ (p + n)) : 0; <\/code>\u0420\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u043c wilson score \u043f\u043e \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u043d\u043e\u0439 \u0444\u043e\u0440\u043c\u0443\u043b\u0435. \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 Z-\u0441\u0447\u0435\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c 1.96, \u0447\u0442\u043e\u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 95% \u0443\u0440\u043e\u0432\u043d\u044e \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0432 \u043f\u043e\u043b\u0435 <code>wilson-score<\/code>, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u043a\u043b\u044e\u0447 \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e\u0441\u0442\u0438.<\/p>\n<\/li>\n<\/ol>\n<p>\u041c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0443\u044e \u0444\u043e\u0440\u043c\u0443\u043b\u0443 \u0434\u043b\u044f \u0440\u0430\u0441\u0447\u0435\u0442\u0430 Wilson Score Confidence Interval, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0434\u0445\u043e\u0434 \u0441 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u043c painless \u0441\u043a\u0440\u0438\u043f\u0442\u0430 \u0432\u0437\u044f\u043b \u0438\u0437 \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/www.elastic.co\/blog\/better-than-average-sort-by-best-rating-with-elasticsearch\" rel=\"noopener noreferrer nofollow\">Better than Average: Sort by Best Rating with Elasticsearch<\/a>.<\/p>\n<h3>\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u0438<\/h3>\n<p>\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u043e\u0441\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u043b Postman \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u044e,\u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u0442\u043e\u0432\u0430\u0440\u043e\u0432, \u0430 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441\u0442\u0430\u0432\u044f\u0442 \u043e\u0446\u0435\u043d\u043a\u0438 \u0442\u043e\u0432\u0430\u0440\u0430\u043c:<\/p>\n<ul>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 1. &#171;1&#187; &#8212; 3 \u043e\u0446\u0435\u043d\u043a\u0438, &#171;2&#187; &#8212; 2 \u043e\u0446\u0435\u043d\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 2. &#171;2&#187; &#8212; 3 \u043e\u0446\u0435\u043d\u043a\u0438, &#171;3&#187; &#8212; 2 \u043e\u0446\u0435\u043d\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 3. &#171;3&#187; &#8212; 2 \u043e\u0446\u0435\u043d\u043a\u0438, &#171;4&#187; &#8212; 2 \u043e\u0446\u0435\u043d\u043a\u0438, &#171;5&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 4. &#171;4&#187; &#8212; 3 \u043e\u0446\u0435\u043d\u043a\u0438, &#171;5&#187; &#8212; 2 \u043e\u0446\u0435\u043d\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 5. &#171;4&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430, &#171;5&#187; &#8212; 4 \u043e\u0446\u0435\u043d\u043a\u0438<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 6. &#171;4&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430, &#171;5&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 7. &#171;4&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 8. &#171;3&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 9. &#171;5&#187; &#8212; 1 \u043e\u0446\u0435\u043d\u043a\u0430<\/p>\n<\/li>\n<li>\n<p>\u0422\u043e\u0432\u0430\u0440 10. \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u043e\u0446\u0435\u043d\u043e\u043a.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043d\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c:<\/p>\n<pre><code class=\"json\">[   {     \"id\": 5,     \"name\": \"Product Five\",     \"price\": 14.0,     \"description\": \"Description Five\",     \"wilsonScore\": 0.5118538596631975,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 1,       \"5\": 4     }   },   {     \"id\": 4,     \"name\": \"Product Four\",     \"price\": 13.0,     \"description\": \"Description Four\",     \"wilsonScore\": 0.41770741047904003,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 3,       \"5\": 2     }   },   {     \"id\": 3,     \"name\": \"Product Three\",     \"price\": 12.0,     \"description\": \"Description Three\",     \"wilsonScore\": 0.2987857322245606,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 2,       \"4\": 2,       \"5\": 1     }   },   {     \"id\": 6,     \"name\": \"Product Six\",     \"price\": 15.0,     \"description\": \"Description Six\",     \"wilsonScore\": 0.26404786368925837,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 1,       \"5\": 1     }   },   {     \"id\": 9,     \"name\": \"Product Nine\",     \"price\": 18.0,     \"description\": \"Description Nine\",     \"wilsonScore\": 0.20654329147389294,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 0,       \"5\": 1     }   },   {     \"id\": 7,     \"name\": \"Product Seven\",     \"price\": 16.0,     \"description\": \"Description Seven\",     \"wilsonScore\": 0.11790609179425604,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 1,       \"5\": 0     }   },   {     \"id\": 2,     \"name\": \"Product Two\",     \"price\": 11.0,     \"description\": \"Description Two\",     \"wilsonScore\": 0.09409051165901611,     \"ratings\": {       \"1\": 0,       \"2\": 3,       \"3\": 2,       \"4\": 0,       \"5\": 0     }   },   {     \"id\": 8,     \"name\": \"Product Eight\",     \"price\": 17.0,     \"description\": \"Description Eight\",     \"wilsonScore\": 0.0546190651458835,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 1,       \"4\": 0,       \"5\": 0     }   },   {     \"id\": 1,     \"name\": \"Product One\",     \"price\": 10.0,     \"description\": \"Description One\",     \"wilsonScore\": 0.010529638390925532,     \"ratings\": {       \"1\": 3,       \"2\": 2,       \"3\": 0,       \"4\": 0,       \"5\": 0     }   },   {     \"id\": 10,     \"name\": \"Product Ten\",     \"price\": 19.0,     \"description\": \"Description Ten\",     \"wilsonScore\": 0.0,     \"ratings\": {       \"1\": 0,       \"2\": 0,       \"3\": 0,       \"4\": 0,       \"5\": 0     }   } ]<\/code><\/pre>\n<p>\u041a\u0430\u043a \u0432\u0438\u0434\u043d\u043e, \u0442\u043e\u0432\u0430\u0440\u044b \u0441 \u0431<strong><em>\u043e<\/em><\/strong>\u043b\u044c\u0448\u0438\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043e\u0446\u0435\u043d\u043e\u043a \u043d\u0430\u0445\u043e\u0434\u044f\u0442\u0441\u044f \u0432\u0432\u0435\u0440\u0445\u0443 \u0441\u043f\u0438\u0441\u043a\u0430, \u0430 \u0442\u043e\u0432\u0430\u0440\u044b \u0432\u043e\u043e\u0431\u0449\u0435 \u0431\u0435\u0437 \u043e\u0446\u0435\u043d\u043e\u043a &#8212; \u0432 \u0441\u0430\u043c\u043e\u043c \u043d\u0438\u0437\u0443. <\/p>\n<h4>\u041a\u0430\u043a \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u044b<\/h4>\n<ol>\n<li>\n<p>\u041f\u043e\u0434\u043d\u0438\u043c\u0430\u0435\u043c \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"bash\">cd docker docker-compose up -d<\/code><\/pre>\n<ol start=\"2\">\n<li>\n<p>\u0416\u0434\u0435\u043c, \u043a\u043e\u0433\u0434\u0430 \u043f\u043e\u0434\u043d\u0438\u043c\u0443\u0442\u0441\u044f \u0432\u0441\u0435 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u044b. \u0414\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0445 \u0437\u0430\u043a\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f Kafka Connect.<\/p>\n<\/li>\n<li>\n<p>\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c Avro \u0441\u0445\u0435\u043c\u0443 \u0430\u0433\u0440\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430\u0445 \u0432 Confluent Schema Registry \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Gradle \u043f\u043b\u0430\u0433\u0438\u043d\u0430 <code>com.github.imflog.kafka-schema-registry-gradle-plugin<\/code>. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432 <strong>\u043a\u043e\u0440\u043d\u0435\u0432\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438<\/strong> \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"bash\">.\/gradlew registerSchemaTask<\/code><\/pre>\n<ol start=\"4\">\n<li>\n<p>\u0414\u0435\u043b\u0430\u0435\u043c \u0447\u0438\u0441\u0442\u044b\u0439 \u0431\u0438\u043b\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043b\u0438\u0441\u044c Avro \u043a\u043b\u0430\u0441\u0441\u044b \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0441\u0445\u0435\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043b\u0435\u0436\u0430\u0442 \u0432 \u043f\u0430\u043f\u043a\u0435 <code>review-aggregator-service\/src\/main\/avro<\/code>. \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432 <strong>\u043a\u043e\u0440\u043d\u0435\u0432\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438<\/strong> \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"bash\">.\/gradlew clean build -x test<\/code><\/pre>\n<ol start=\"5\">\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Debezium Postgres Connector \u0432 Kafka Connect.<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"bash\">cd connector curl -s -S -XPOST -H Accept:application\/json -H Content-Type:application\/json http:\/\/localhost:8083\/connectors\/ -d @debezium-config.json<\/code><\/pre>\n<ol start=\"6\">\n<li>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u043e\u0432\u0430\u0442\u044c \u0441\u0430\u043c\u0438 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u044b <code>backend-service<\/code> \u0438 <code>review-aggregator-service<\/code>. \u041f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 <code>backend-service<\/code> \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c JVM \u043e\u043f\u0446\u0438\u044e <code>--add-opens java.base\/java.math=ALL-UNNAMED<\/code>. \u041f\u043e\u043a\u0430 \u043d\u0435 \u0432\u044b\u044f\u0441\u043d\u0438\u043b, \u043f\u043e \u043a\u0430\u043a\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435 Spring Data OpenSearch \u0432\u044b\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0430, \u0435\u0441\u043b\u0438 \u0432 \u043c\u043e\u0434\u0435\u043b\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0442\u0438\u043f BigDecimal.<\/p>\n<\/li>\n<\/ol>\n<p>\u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u044d\u0441\u043a\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430\u043c\u0438. \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438, \u043c\u043e\u0436\u043d\u043e \u0432\u043e\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0435\u0439 Postman, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0442\u0430\u043a\u0436\u0435 \u0435\u0441\u0442\u044c \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 <code>postman<\/code>. <\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/783360\/\"> https:\/\/habr.com\/ru\/articles\/783360\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442! \u041c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u0410\u043b\u0435\u043a\u0441\u0430\u043d\u0434\u0440. \u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u0433\u043e\u0434 \u0442\u0440\u0443\u0436\u0443\u0441\u044c \u0444\u0440\u0438\u043b\u0430\u043d\u0441\u0435\u0440\u043e\u043c \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043f\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u043d\u043e\u0433\u043e \u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430. \u041c\u043e\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u043f\u043e\u043c\u0438\u043c\u043e \u0432\u0441\u0435\u0433\u043e \u043f\u0440\u043e\u0447\u0435\u0433\u043e \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442 \u0432 \u0441\u0435\u0431\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0443 \u0441\u043a\u043e\u0440\u0438\u043d\u0433\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0431\u044b \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u043b\u0430 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432 \u043f\u043e \u043f\u043e\u043f\u0443\u043b\u044f\u0440\u043d\u043e\u0441\u0442\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0435\u0439. \u041e\u0434\u043d\u0438\u043c \u0438\u0437 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0435\u0439 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043e\u0446\u0435\u043d\u043a\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043f\u043e \u0448\u043a\u0430\u043b\u0435 \u043e\u0442 1 \u0434\u043e 5. \u0414\u0443\u043c\u0430\u044e, \u0447\u0442\u043e \u043d\u0435 \u0441\u0442\u0430\u043d\u0435\u0442 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0435\u043c \u0410\u043c\u0435\u0440\u0438\u043a\u0438 \u0442\u043e, \u0447\u0442\u043e \u0440\u0430\u0441\u0447\u0435\u0442 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u043f\u043e \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u0430\u0440\u0438\u0444\u043c\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u043c\u0443 \u0432\u0441\u0435\u0445 \u043e\u0446\u0435\u043d\u043e\u043a \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u043d\u0435 \u0441\u043e\u0432\u0441\u0435\u043c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u043c \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430\u043c \u0432\u044b\u0431\u043e\u0440\u043a\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u0442\u0430\u043a\u043e\u043c \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u0442\u043e\u0432\u0430\u0440 &#171;\u0410&#187; \u0441 \u043e\u0434\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u043e\u0439 5 \u0432 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u0448\u0435 \u0442\u043e\u0432\u0430\u0440\u0430 &#171;\u0411&#187;, \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u043e\u0442\u043d\u044f \u043e\u0446\u0435\u043d\u043e\u043a 5 \u0438 \u043e\u0434\u043d\u0430 \u043e\u0446\u0435\u043d\u043a\u0430 4. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0434\u0430\u0432\u043d\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043e  &#8212; \u0434\u043b\u044f \u0440\u0430\u0441\u0447\u0435\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u0434\u043e\u0432\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0431\u0438\u043d\u043e\u043c\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u00a0\u043c\u0435\u0442\u043e\u0434\u0443 \u0423\u0438\u043b\u0441\u043e\u043d\u0430 (Wilson Score Confidence Interval). \u0424\u043e\u0440\u043c\u0443\u043b\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0424\u043e\u0440\u043c\u0443\u043b\u0430 Wilson Score Confidence Interval<\/figcaption><\/div>\n<\/figure>\n<p>\u0413\u0434\u0435:<\/p>\n<ol>\n<li>\n<p>p &#8212;  \u044d\u0442\u043e \u043e\u0446\u0435\u043d\u043a\u0430 \u043f\u0440\u043e\u043f\u043e\u0440\u0446\u0438\u0438 \u0443\u0441\u043f\u0435\u0445\u043e\u0432 \u0432 \u0432\u044b\u0431\u043e\u0440\u043a\u0435 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u043d\u0430 \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0442\u0437\u044b\u0432\u043e\u0432).<\/p>\n<\/li>\n<li>\n<p>n &#8212; \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0439 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0431\u0449\u0435\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0442\u0437\u044b\u0432\u043e\u0432).<\/p>\n<\/li>\n<li>\n<p>z &#8212; Z-\u0441\u0447\u0435\u0442, \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c\u0443 \u0443\u0440\u043e\u0432\u043d\u044e \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0434\u043b\u044f \u0443\u0440\u043e\u0432\u043d\u044f \u0443\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0441\u0442\u0438 95%, z \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0440\u0430\u0432\u043d\u043e 1.96.<\/p>\n<\/li>\n<\/ol>\n<p>\u0412 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u0441\u0442\u0430\u0440\u0430\u044e\u0441\u044c \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0434\u0438\u043d \u0438\u0437 \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u0432 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0430 \u043a \u0440\u0430\u0441\u0447\u0435\u0442\u0443 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0430\u043a\u0438\u0435 \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0438 \u043a\u0430\u043a Kafka Streams \u0438 OpenSearch. \u041a\u0430\u043a \u0438 \u0432 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u043f\u043e\u0441\u0442\u0430\u0445, \u044f \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u0438\u043b \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 <a href=\"https:\/\/github.com\/aasmc\/product-rating-system\" rel=\"noopener noreferrer nofollow\">\u0434\u0435\u043c\u043e \u043f\u0440\u043e\u0435\u043a\u0442<\/a>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u044b\u043b\u043e\u0436\u0438\u043b \u043d\u0430 Github. <\/p>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u043e\u0438\u0441\u043a\u043e\u0432\u043e\u0433\u043e \u0434\u0432\u0438\u0436\u043a\u0430 \u044f \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043d\u043e \u0432\u044b\u0431\u0440\u0430\u043b OpenSearch, \u0430 \u043d\u0435 ElasticSearch, \u0442\u0430\u043a \u043a\u0430\u043a \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u043d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0433\u0434\u0435 \u044f \u0441\u0435\u0439\u0447\u0430\u0441 \u0442\u0440\u0443\u0436\u0443\u0441\u044c. \u041a\u0440\u043e\u043c\u0435 \u0442\u043e\u0433\u043e, \u043d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 20 \u0438\u044e\u043b\u044f 2023 \u0433\u043e\u0434\u0430 Yandex Cloud <a href=\"https:\/\/cloud.yandex.com\/en\/services\/managed-elasticsearch\" rel=\"noopener noreferrer nofollow\">\u0431\u043e\u043b\u0435\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442<\/a> \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u0432\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u044b ElasticSearch, \u0430 \u0441 11 \u0430\u043f\u0440\u0435\u043b\u044f 2024 \u0433\u043e\u0434\u0430 \u0438 \u0432\u043e\u0432\u0441\u0435 \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u0442 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0443\u0441\u043b\u0443\u0433\u0438 \u043f\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043a ElasticSearch. <\/p>\n<h3>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h3>\n<p>\u041f\u0440\u043e\u0435\u043a\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 2-\u0445 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b\u0445 \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432:<\/p>\n<ol>\n<li>\n<p>backend-service<\/p>\n<\/li>\n<li>\n<p>review-aggregator-service<\/p>\n<\/li>\n<\/ol>\n<p>\u0410 \u0442\u0430\u043a\u0436\u0435 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0443\u043a\u0440\u0443\u0442\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p> PostgreSQL<\/p>\n<\/li>\n<li>\n<p>Apache Kafka + Zookeeper<\/p>\n<\/li>\n<li>\n<p>Confluent Schema Registry<\/p>\n<\/li>\n<li>\n<p>Kafka Connect<\/p>\n<\/li>\n<li>\n<p>Debezium Postgres Connector<\/p>\n<\/li>\n<li>\n<p>\u041a\u043b\u0430\u0441\u0442\u0435\u0440 OpenSearch \u0438\u0437 \u0434\u0432\u0443\u0445 \u043d\u043e\u0434<\/p>\n<\/li>\n<\/ol>\n<h3>Backend Service<\/h3>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 REST API \u0434\u043b\u044f:<\/p>\n<ul>\n<li>\n<p>\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/p>\n<\/li>\n<li>\n<p>\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0441\u043f\u0438\u0441\u043a\u0430 \u043e\u0442\u0437\u044b\u0432\u043e\u0432 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c<\/p>\n<\/li>\n<li>\n<p>\u043f\u043e\u0443\u0447\u0435\u043d\u0438\u0435 \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0441\u043f\u0438\u0441\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438: <code>AppUser<\/code>, <code>Product<\/code>, <code>ProductReview<\/code> \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 Postgres. \u0421\u0445\u0435\u043c\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 Postgres \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"sql\">CREATE TABLE IF NOT EXISTS app_users(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     USERNAME TEXT NOT NULL,     FIRST_NAME TEXT NOT NULL,     LAST_NAME TEXT NOT NULL );  CREATE TABLE IF NOT EXISTS products(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     NAME TEXT NOT NULL,     DESCRIPTION TEXT,     PRICE NUMERIC(20, 5) NOT NULL );  CREATE TABLE IF NOT EXISTS product_reviews(     ID BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,     USER_ID BIGINT NOT NULL,     PRODUCT_ID BIGINT NOT NULL,     RATING INTEGER NOT NULL,     CREATED_AT TIMESTAMP NOT NULL,     COMMENT TEXT,     CONSTRAINT product_reviews_app_users_fk FOREIGN KEY (USER_ID) REFERENCES app_users(ID) ON DELETE NO ACTION,     CONSTRAINT product_reviews_products_fk FOREIGN KEY (PRODUCT_ID) REFERENCES products(ID) ON DELETE NO ACTION );  CREATE UNIQUE INDEX IF NOT EXISTS product_reviews_user_id_product_id_idx ON product_reviews(USER_ID, PRODUCT_ID);<\/code><\/pre>\n<p>\u0422\u0430\u0431\u043b\u0438\u0446\u0430 <code>product_reviews<\/code>, \u043a\u0443\u0434\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431 \u043e\u0442\u0437\u044b\u0432\u0430\u0445 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f Debezium Postgres Connector, \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043d\u0435\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0432 \u0442\u043e\u043f\u0438\u043a Kafka. \u0417\u0430 \u0440\u0430\u0431\u043e\u0442\u0443 \u0441 Kafka \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 <strong>Review Aggregator Service<\/strong>. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0440\u0430\u0431\u043e\u0442\u044b Debezium Postgres Connector \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0432 <a href=\"https:\/\/habr.com\/ru\/articles\/779620\/\" rel=\"noopener noreferrer nofollow\">\u043c\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435<\/a>.<\/p>\n<p><strong>Backend Service<\/strong> \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0443 \u0442\u0432\u0430\u0440\u043e\u0432 \u0438\u0437 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0430 OpenSearch, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <code>org.opensearch.data.client.orhlc.OpenSearchRestTemplate<\/code>:<\/p>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u0430\u043a\u0446\u0435\u043d\u0442 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 \u043d\u0435 \u043d\u0430 \u043f\u043e\u043b\u043d\u043e\u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u043c \u043f\u043e\u0438\u0441\u043a\u0435, \u0430 \u043d\u0430 \u0440\u0430\u043d\u0436\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u0441\u043e\u0433\u043b\u0430\u0441\u043d\u043e \u043e\u0446\u0435\u043d\u043a\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 OpenSearch \u043e\u0447\u0435\u043d\u044c \u0443\u043f\u0440\u043e\u0449\u0435\u043d\u043d\u0430\u044f \u0438 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"kotlin\">@Document(indexName = \"products\") class OpenSearchProduct(     @Id     var id: String? = null,     @Field(type = FieldType.Text, analyzer = \"russian\")     var name: String? = null,     @Field(type = FieldType.Text, analyzer = \"russian\")     var description: String? = null,     @Field(type = FieldType.Double)     var price: BigDecimal? = null,     @Field(name = \"wilson-score\", type = FieldType.Double)     var wilsonScore: Double = 0.0,     @Field(type = FieldType.Object)     var ratings: Map&lt;String, Int> = hashMapOf(         \"1\" to 0,         \"2\" to 0,         \"3\" to 0,         \"4\" to 0,         \"5\" to 0,     ),     @Field(type = FieldType.Long, name = \"rating_update_idempotency_key\")     var ratingIdempotencyKey: Long? = null )<\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0441\u0442\u043e\u0438\u0442 \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p><code>ratings<\/code> \u0438\u043c\u0435\u0435\u0442 \u0442\u0430\u043a\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u043a\u0430\u0436\u0434\u0443\u044e \u043e\u0446\u0435\u043d\u043a\u0443 \u043f\u043e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u042d\u0442\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u0441\u0447\u0435\u0442\u0430 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430. <\/p>\n<\/li>\n<li>\n<p> <code>wilson-score<\/code> \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c \u0447\u0435\u0440\u0435\u0437 <a href=\"https:\/\/www.elastic.co\/guide\/en\/elasticsearch\/reference\/8.11\/docs-update.html#_scripted_updates\" rel=\"noopener noreferrer nofollow\">scripted updates<\/a> <br \/>\u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0442\u043e\u0432\u0430\u0440\u0430, \u0447\u0442\u043e \u0434\u0430\u0435\u0442 \u043d\u0430\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u043a\u043e\u0440\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0432\u044b\u0431\u043e\u0440\u043a\u0443 \u0442\u043e\u0432\u0430\u0440\u043e\u0432, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u043e\u043b\u0435 \u0434\u043b\u044f \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0438 \u0443\u0436\u0435 \u043f\u043e\u0434\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u043e. <\/p>\n<\/li>\n<li>\n<p><code>ratingIdempotencyKey<\/code> \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432 \u043d\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0439\u0442\u0438\u043d\u0433\u0430 \u0438\u0437-\u0437\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0445 \u0441\u0431\u043e\u0435\u0432 \u0432 \u0440\u0430\u0431\u043e\u0442\u0435 Kafka, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u0440\u0438 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u043d\u0435\u0448\u043d\u0438\u043c\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u043c\u0438 Kafka \u043d\u0435 \u0434\u0430\u0435\u0442 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438 <code>exactly_once<\/code>, \u0430 \u0442\u043e\u043b\u044c\u043a\u043e <code>at_least_once<\/code>.<\/p>\n<\/li>\n<\/ol>\n<h3>Review Aggregator Service<\/h3>\n<p>\u0421\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e \u0433\u043e\u0432\u043e\u0440\u044f, \u0438\u043c\u0435\u043d\u043d\u043e \u044d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0438\u0441 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0437\u0430 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044e \u0434\u0430\u043d\u043d\u044b\u0445 \u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u043e\u0442\u0437\u044b\u0432\u0430\u0445 \u043a \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0432 \u043a\u043b\u0430\u0441\u0442\u0435\u0440\u0435 OpenSearch.<\/p>\n<p>\u0410\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044f \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Kafka Streams. \u0414\u0430\u043b\u0435\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0443 \u043a\u043e\u0434 \u0441 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u043c\u0438 \u043f\u043e\u044f\u0441\u043d\u0435\u043d\u0438\u044f\u043c\u0438, \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430 Kafka Streams \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u043d\u0430 \u0432 \u043c\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 <a href=\"https:\/\/habr.com\/ru\/articles\/775900\/\" rel=\"noopener noreferrer nofollow\">&#171;\u041c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0441 Kafka Streams \u0438 Spring Boot&#187;<\/a>.<\/p>\n<pre><code class=\"kotlin\">@Service class KafkaReviewAggregatorService(     private val topicProps: TopicProps,     private val kafkaProps: KafkaProps ) {      @Autowired     fun processStreams(builder: StreamsBuilder) {         val reviewValueSerde = SpecificAvroSerde&lt;AvroReview>()         configureSerde(reviewValueSerde, false)         val reviewKeySerde = Serdes.String()          val productRatingValueSerde = SpecificAvroSerde&lt;AvroProductRating>()         configureSerde(productRatingValueSerde, false)         val productRatingKeySerde = Serdes.String()          val productReviews: KStream&lt;String, AvroReview> = builder.stream(             topicProps.reviewTopic,             Consumed.with(reviewKeySerde, reviewValueSerde)         )          val aggregatedStore = Stores.keyValueStoreBuilder(             Stores.persistentKeyValueStore(topicProps.aggregatedReviewsStore),             Serdes.String(),             productRatingValueSerde         )          builder.addStateStore(aggregatedStore)          productReviews             .peek { key, value ->                 log.info(\"Processing AvroReview: {}. Message Key: {}\", value, key)             }             .mapValues(::toAvroProductRating)             .process(                 ProcessorSupplier {                     AvroReviewPunctuator(topicProps)                 },                 topicProps.aggregatedReviewsStore             )             .peek { key, value ->                 log.info(\"Processing AvroProductRating: {}. Message Key: {}\", value, key)             }             .to(                 topicProps.aggregatedReviewsTopic,                 Produced.with(productRatingKeySerde, productRatingValueSerde)             )     }       private fun configureSerde(serde: Serde&lt;*>, isKey: Boolean) {         if (serde is SpecificAvroSerde) {             val config = hashMapOf&lt;String, Any>(                 AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG to kafkaProps.schemaRegistryUrl             )             serde.configure(config, isKey)         }     }       private fun toAvroProductRating(record: AvroReview): AvroProductRating =         AvroProductRating(             record.productId,             hashMapOf(                 convertDigitToString(record.rating) to 1             ),             record.`lsn$1`         )      private fun convertDigitToString(digit: Int): String {         return when(digit) {             1 -> \"one\"             2 -> \"two\"             3 -> \"three\"             4 -> \"four\"             5 -> \"five\"             else -> throw RuntimeException(\"Unsupported Rating: $digit\")         }     } }<\/code><\/pre>\n<p>\u0422\u0443\u0442 \u0432\u0441\u0435 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u043e:<\/p>\n<ol>\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c KStream \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0442\u043e\u043f\u0438\u043a\u0430, \u043a\u0443\u0434\u0430 \u043e\u0442\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431 \u043e\u0442\u0437\u044b\u0432\u0430\u0445<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  val productReviews: KStream&lt;String, AvroReview> = builder.stream(       topicProps.reviewTopic,       Consumed.with(reviewKeySerde, reviewValueSerde)   )<\/code><\/pre>\n<ol start=\"2\">\n<li>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c KeyValue \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  val aggregatedStore = Stores.keyValueStoreBuilder(       Stores.persistentKeyValueStore(topicProps.aggregatedReviewsStore),       Serdes.String(),       productRatingValueSerde   )    builder.addStateStore(aggregatedStore)<\/code><\/pre>\n<ol start=\"3\">\n<li>\n<p>\u041c\u0430\u043f\u0438\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u0441\u0442\u0440\u0438\u043c\u0435 \u0432 \u0443\u0434\u043e\u0431\u043d\u0443\u044e \u0434\u043b\u044f \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">  productReviews       .peek { key, value ->           log.info(\"Processing AvroReview: {}. Message Key: {}\", value, key)       }       .mapValues(::toAvroProductRating)<\/code><\/pre>\n<p> <code>AvroProductRating<\/code> \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 .avsc \u0444\u0430\u0439\u043b\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Gradle \u043f\u043b\u0430\u0433\u0438\u043d\u0430:<\/p>\n<pre><code class=\"json\">{   \"type\": \"record\",   \"name\": \"AvroProductRating\",   \"namespace\": \"ru.aasmc.avro\",   \"fields\": [     {       \"name\": \"productId\",       \"type\": \"long\"     },     {       \"name\": \"ratings\",       \"type\": {         \"type\": \"map\",         \"values\": \"int\"       }     },     {       \"name\": \"idempotencyKey\",       \"type\": [         \"null\",         \"long\"       ],       \"default\": null     }   ] }<\/code><\/pre>\n<ol start=\"4\">\n<li>\n<p>\u0410\u0433\u0440\u0435\u0433\u0438\u0440\u0443\u0435\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u043e\u043c \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0435 (\u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u043e \u0442\u0435\u043a\u0441\u0442\u0443):<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">.process(     ProcessorSupplier {         AvroReviewPunctuator(topicProps)      },     topicProps.aggregatedReviewsStore )<\/code><\/pre>\n<ol start=\"5\">\n<li>\n<p>\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043f\u0438\u043a Kafka:<\/p>\n<\/li>\n<\/ol>\n<pre><code class=\"kotlin\">.to(     topicProps.aggregatedReviewsTopic,<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-364085","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364085","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=364085"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/364085\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=364085"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=364085"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=364085"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}